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