github.com/nathanielks/terraform@v0.6.1-0.20170509030759-13e1a62319dc/communicator/ssh/provisioner.go (about)

     1  package ssh
     2  
     3  import (
     4  	"encoding/pem"
     5  	"fmt"
     6  	"log"
     7  	"net"
     8  	"os"
     9  	"time"
    10  
    11  	"github.com/hashicorp/terraform/communicator/shared"
    12  	"github.com/hashicorp/terraform/terraform"
    13  	"github.com/mitchellh/mapstructure"
    14  	"github.com/xanzy/ssh-agent"
    15  	"golang.org/x/crypto/ssh"
    16  	"golang.org/x/crypto/ssh/agent"
    17  )
    18  
    19  const (
    20  	// DefaultUser is used if there is no user given
    21  	DefaultUser = "root"
    22  
    23  	// DefaultPort is used if there is no port given
    24  	DefaultPort = 22
    25  
    26  	// DefaultScriptPath is used as the path to copy the file to
    27  	// for remote execution if not provided otherwise.
    28  	DefaultScriptPath = "/tmp/terraform_%RAND%.sh"
    29  
    30  	// DefaultTimeout is used if there is no timeout given
    31  	DefaultTimeout = 5 * time.Minute
    32  )
    33  
    34  // connectionInfo is decoded from the ConnInfo of the resource. These are the
    35  // only keys we look at. If a PrivateKey is given, that is used instead
    36  // of a password.
    37  type connectionInfo struct {
    38  	User       string
    39  	Password   string
    40  	PrivateKey string `mapstructure:"private_key"`
    41  	Host       string
    42  	Port       int
    43  	Agent      bool
    44  	Timeout    string
    45  	ScriptPath string        `mapstructure:"script_path"`
    46  	TimeoutVal time.Duration `mapstructure:"-"`
    47  
    48  	BastionUser       string `mapstructure:"bastion_user"`
    49  	BastionPassword   string `mapstructure:"bastion_password"`
    50  	BastionPrivateKey string `mapstructure:"bastion_private_key"`
    51  	BastionHost       string `mapstructure:"bastion_host"`
    52  	BastionPort       int    `mapstructure:"bastion_port"`
    53  }
    54  
    55  // parseConnectionInfo is used to convert the ConnInfo of the InstanceState into
    56  // a ConnectionInfo struct
    57  func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) {
    58  	connInfo := &connectionInfo{}
    59  	decConf := &mapstructure.DecoderConfig{
    60  		WeaklyTypedInput: true,
    61  		Result:           connInfo,
    62  	}
    63  	dec, err := mapstructure.NewDecoder(decConf)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil {
    68  		return nil, err
    69  	}
    70  
    71  	// To default Agent to true, we need to check the raw string, since the
    72  	// decoded boolean can't represent "absence of config".
    73  	//
    74  	// And if SSH_AUTH_SOCK is not set, there's no agent to connect to, so we
    75  	// shouldn't try.
    76  	if s.Ephemeral.ConnInfo["agent"] == "" && os.Getenv("SSH_AUTH_SOCK") != "" {
    77  		connInfo.Agent = true
    78  	}
    79  
    80  	if connInfo.User == "" {
    81  		connInfo.User = DefaultUser
    82  	}
    83  
    84  	// Format the host if needed.
    85  	// Needed for IPv6 support.
    86  	connInfo.Host = shared.IpFormat(connInfo.Host)
    87  
    88  	if connInfo.Port == 0 {
    89  		connInfo.Port = DefaultPort
    90  	}
    91  	if connInfo.ScriptPath == "" {
    92  		connInfo.ScriptPath = DefaultScriptPath
    93  	}
    94  	if connInfo.Timeout != "" {
    95  		connInfo.TimeoutVal = safeDuration(connInfo.Timeout, DefaultTimeout)
    96  	} else {
    97  		connInfo.TimeoutVal = DefaultTimeout
    98  	}
    99  
   100  	// Default all bastion config attrs to their non-bastion counterparts
   101  	if connInfo.BastionHost != "" {
   102  		// Format the bastion host if needed.
   103  		// Needed for IPv6 support.
   104  		connInfo.BastionHost = shared.IpFormat(connInfo.BastionHost)
   105  
   106  		if connInfo.BastionUser == "" {
   107  			connInfo.BastionUser = connInfo.User
   108  		}
   109  		if connInfo.BastionPassword == "" {
   110  			connInfo.BastionPassword = connInfo.Password
   111  		}
   112  		if connInfo.BastionPrivateKey == "" {
   113  			connInfo.BastionPrivateKey = connInfo.PrivateKey
   114  		}
   115  		if connInfo.BastionPort == 0 {
   116  			connInfo.BastionPort = connInfo.Port
   117  		}
   118  	}
   119  
   120  	return connInfo, nil
   121  }
   122  
   123  // safeDuration returns either the parsed duration or a default value
   124  func safeDuration(dur string, defaultDur time.Duration) time.Duration {
   125  	d, err := time.ParseDuration(dur)
   126  	if err != nil {
   127  		log.Printf("Invalid duration '%s', using default of %s", dur, defaultDur)
   128  		return defaultDur
   129  	}
   130  	return d
   131  }
   132  
   133  // prepareSSHConfig is used to turn the *ConnectionInfo provided into a
   134  // usable *SSHConfig for client initialization.
   135  func prepareSSHConfig(connInfo *connectionInfo) (*sshConfig, error) {
   136  	sshAgent, err := connectToAgent(connInfo)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  
   141  	sshConf, err := buildSSHClientConfig(sshClientConfigOpts{
   142  		user:       connInfo.User,
   143  		privateKey: connInfo.PrivateKey,
   144  		password:   connInfo.Password,
   145  		sshAgent:   sshAgent,
   146  	})
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	var bastionConf *ssh.ClientConfig
   152  	if connInfo.BastionHost != "" {
   153  		bastionConf, err = buildSSHClientConfig(sshClientConfigOpts{
   154  			user:       connInfo.BastionUser,
   155  			privateKey: connInfo.BastionPrivateKey,
   156  			password:   connInfo.BastionPassword,
   157  			sshAgent:   sshAgent,
   158  		})
   159  		if err != nil {
   160  			return nil, err
   161  		}
   162  	}
   163  
   164  	host := fmt.Sprintf("%s:%d", connInfo.Host, connInfo.Port)
   165  	connectFunc := ConnectFunc("tcp", host)
   166  
   167  	if bastionConf != nil {
   168  		bastionHost := fmt.Sprintf("%s:%d", connInfo.BastionHost, connInfo.BastionPort)
   169  		connectFunc = BastionConnectFunc("tcp", bastionHost, bastionConf, "tcp", host)
   170  	}
   171  
   172  	config := &sshConfig{
   173  		config:     sshConf,
   174  		connection: connectFunc,
   175  		sshAgent:   sshAgent,
   176  	}
   177  	return config, nil
   178  }
   179  
   180  type sshClientConfigOpts struct {
   181  	privateKey string
   182  	password   string
   183  	sshAgent   *sshAgent
   184  	user       string
   185  }
   186  
   187  func buildSSHClientConfig(opts sshClientConfigOpts) (*ssh.ClientConfig, error) {
   188  	conf := &ssh.ClientConfig{
   189  		User: opts.user,
   190  	}
   191  
   192  	if opts.privateKey != "" {
   193  		pubKeyAuth, err := readPrivateKey(opts.privateKey)
   194  		if err != nil {
   195  			return nil, err
   196  		}
   197  		conf.Auth = append(conf.Auth, pubKeyAuth)
   198  	}
   199  
   200  	if opts.password != "" {
   201  		conf.Auth = append(conf.Auth, ssh.Password(opts.password))
   202  		conf.Auth = append(conf.Auth, ssh.KeyboardInteractive(
   203  			PasswordKeyboardInteractive(opts.password)))
   204  	}
   205  
   206  	if opts.sshAgent != nil {
   207  		conf.Auth = append(conf.Auth, opts.sshAgent.Auth())
   208  	}
   209  
   210  	return conf, nil
   211  }
   212  
   213  func readPrivateKey(pk string) (ssh.AuthMethod, error) {
   214  	// We parse the private key on our own first so that we can
   215  	// show a nicer error if the private key has a password.
   216  	block, _ := pem.Decode([]byte(pk))
   217  	if block == nil {
   218  		return nil, fmt.Errorf("Failed to read key %q: no key found", pk)
   219  	}
   220  	if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
   221  		return nil, fmt.Errorf(
   222  			"Failed to read key %q: password protected keys are\n"+
   223  				"not supported. Please decrypt the key prior to use.", pk)
   224  	}
   225  
   226  	signer, err := ssh.ParsePrivateKey([]byte(pk))
   227  	if err != nil {
   228  		return nil, fmt.Errorf("Failed to parse key file %q: %s", pk, err)
   229  	}
   230  
   231  	return ssh.PublicKeys(signer), nil
   232  }
   233  
   234  func connectToAgent(connInfo *connectionInfo) (*sshAgent, error) {
   235  	if connInfo.Agent != true {
   236  		// No agent configured
   237  		return nil, nil
   238  	}
   239  
   240  	agent, conn, err := sshagent.New()
   241  	if err != nil {
   242  		return nil, err
   243  	}
   244  
   245  	// connection close is handled over in Communicator
   246  	return &sshAgent{
   247  		agent: agent,
   248  		conn:  conn,
   249  	}, nil
   250  
   251  }
   252  
   253  // A tiny wrapper around an agent.Agent to expose the ability to close its
   254  // associated connection on request.
   255  type sshAgent struct {
   256  	agent agent.Agent
   257  	conn  net.Conn
   258  }
   259  
   260  func (a *sshAgent) Close() error {
   261  	if a.conn == nil {
   262  		return nil
   263  	}
   264  
   265  	return a.conn.Close()
   266  }
   267  
   268  func (a *sshAgent) Auth() ssh.AuthMethod {
   269  	return ssh.PublicKeysCallback(a.agent.Signers)
   270  }
   271  
   272  func (a *sshAgent) ForwardToAgent(client *ssh.Client) error {
   273  	return agent.ForwardToAgent(client, a.agent)
   274  }