github.com/erriapo/terraform@v0.6.12-0.20160203182612-0340ea72354f/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  	"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 KeyFile 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  	// Deprecated
    55  	KeyFile        string `mapstructure:"key_file"`
    56  	BastionKeyFile string `mapstructure:"bastion_key_file"`
    57  }
    58  
    59  // parseConnectionInfo is used to convert the ConnInfo of the InstanceState into
    60  // a ConnectionInfo struct
    61  func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) {
    62  	connInfo := &connectionInfo{}
    63  	decConf := &mapstructure.DecoderConfig{
    64  		WeaklyTypedInput: true,
    65  		Result:           connInfo,
    66  	}
    67  	dec, err := mapstructure.NewDecoder(decConf)
    68  	if err != nil {
    69  		return nil, err
    70  	}
    71  	if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil {
    72  		return nil, err
    73  	}
    74  
    75  	// To default Agent to true, we need to check the raw string, since the
    76  	// decoded boolean can't represent "absence of config".
    77  	//
    78  	// And if SSH_AUTH_SOCK is not set, there's no agent to connect to, so we
    79  	// shouldn't try.
    80  	if s.Ephemeral.ConnInfo["agent"] == "" && os.Getenv("SSH_AUTH_SOCK") != "" {
    81  		connInfo.Agent = true
    82  	}
    83  
    84  	if connInfo.User == "" {
    85  		connInfo.User = DefaultUser
    86  	}
    87  	if connInfo.Port == 0 {
    88  		connInfo.Port = DefaultPort
    89  	}
    90  	if connInfo.ScriptPath == "" {
    91  		connInfo.ScriptPath = DefaultScriptPath
    92  	}
    93  	if connInfo.Timeout != "" {
    94  		connInfo.TimeoutVal = safeDuration(connInfo.Timeout, DefaultTimeout)
    95  	} else {
    96  		connInfo.TimeoutVal = DefaultTimeout
    97  	}
    98  
    99  	// Load deprecated fields; we can handle either path or contents in
   100  	// underlying implementation.
   101  	if connInfo.PrivateKey == "" && connInfo.KeyFile != "" {
   102  		connInfo.PrivateKey = connInfo.KeyFile
   103  	}
   104  	if connInfo.BastionPrivateKey == "" && connInfo.BastionKeyFile != "" {
   105  		connInfo.BastionPrivateKey = connInfo.BastionKeyFile
   106  	}
   107  
   108  	// Default all bastion config attrs to their non-bastion counterparts
   109  	if connInfo.BastionHost != "" {
   110  		if connInfo.BastionUser == "" {
   111  			connInfo.BastionUser = connInfo.User
   112  		}
   113  		if connInfo.BastionPassword == "" {
   114  			connInfo.BastionPassword = connInfo.Password
   115  		}
   116  		if connInfo.BastionPrivateKey == "" {
   117  			connInfo.BastionPrivateKey = connInfo.PrivateKey
   118  		}
   119  		if connInfo.BastionPort == 0 {
   120  			connInfo.BastionPort = connInfo.Port
   121  		}
   122  	}
   123  
   124  	return connInfo, nil
   125  }
   126  
   127  // safeDuration returns either the parsed duration or a default value
   128  func safeDuration(dur string, defaultDur time.Duration) time.Duration {
   129  	d, err := time.ParseDuration(dur)
   130  	if err != nil {
   131  		log.Printf("Invalid duration '%s', using default of %s", dur, defaultDur)
   132  		return defaultDur
   133  	}
   134  	return d
   135  }
   136  
   137  // prepareSSHConfig is used to turn the *ConnectionInfo provided into a
   138  // usable *SSHConfig for client initialization.
   139  func prepareSSHConfig(connInfo *connectionInfo) (*sshConfig, error) {
   140  	sshAgent, err := connectToAgent(connInfo)
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  
   145  	sshConf, err := buildSSHClientConfig(sshClientConfigOpts{
   146  		user:       connInfo.User,
   147  		privateKey: connInfo.PrivateKey,
   148  		password:   connInfo.Password,
   149  		sshAgent:   sshAgent,
   150  	})
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  
   155  	var bastionConf *ssh.ClientConfig
   156  	if connInfo.BastionHost != "" {
   157  		bastionConf, err = buildSSHClientConfig(sshClientConfigOpts{
   158  			user:       connInfo.BastionUser,
   159  			privateKey: connInfo.BastionPrivateKey,
   160  			password:   connInfo.BastionPassword,
   161  			sshAgent:   sshAgent,
   162  		})
   163  		if err != nil {
   164  			return nil, err
   165  		}
   166  	}
   167  
   168  	host := fmt.Sprintf("%s:%d", connInfo.Host, connInfo.Port)
   169  	connectFunc := ConnectFunc("tcp", host)
   170  
   171  	if bastionConf != nil {
   172  		bastionHost := fmt.Sprintf("%s:%d", connInfo.BastionHost, connInfo.BastionPort)
   173  		connectFunc = BastionConnectFunc("tcp", bastionHost, bastionConf, "tcp", host)
   174  	}
   175  
   176  	config := &sshConfig{
   177  		config:     sshConf,
   178  		connection: connectFunc,
   179  		sshAgent:   sshAgent,
   180  	}
   181  	return config, nil
   182  }
   183  
   184  type sshClientConfigOpts struct {
   185  	privateKey string
   186  	password   string
   187  	sshAgent   *sshAgent
   188  	user       string
   189  }
   190  
   191  func buildSSHClientConfig(opts sshClientConfigOpts) (*ssh.ClientConfig, error) {
   192  	conf := &ssh.ClientConfig{
   193  		User: opts.user,
   194  	}
   195  
   196  	if opts.privateKey != "" {
   197  		pubKeyAuth, err := readPrivateKey(opts.privateKey)
   198  		if err != nil {
   199  			return nil, err
   200  		}
   201  		conf.Auth = append(conf.Auth, pubKeyAuth)
   202  	}
   203  
   204  	if opts.password != "" {
   205  		conf.Auth = append(conf.Auth, ssh.Password(opts.password))
   206  		conf.Auth = append(conf.Auth, ssh.KeyboardInteractive(
   207  			PasswordKeyboardInteractive(opts.password)))
   208  	}
   209  
   210  	if opts.sshAgent != nil {
   211  		conf.Auth = append(conf.Auth, opts.sshAgent.Auth())
   212  	}
   213  
   214  	return conf, nil
   215  }
   216  
   217  func readPrivateKey(pk string) (ssh.AuthMethod, error) {
   218  	key, _, err := pathorcontents.Read(pk)
   219  	if err != nil {
   220  		return nil, fmt.Errorf("Failed to read private key %q: %s", pk, err)
   221  	}
   222  
   223  	// We parse the private key on our own first so that we can
   224  	// show a nicer error if the private key has a password.
   225  	block, _ := pem.Decode([]byte(key))
   226  	if block == nil {
   227  		return nil, fmt.Errorf("Failed to read key %q: no key found", pk)
   228  	}
   229  	if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
   230  		return nil, fmt.Errorf(
   231  			"Failed to read key %q: password protected keys are\n"+
   232  				"not supported. Please decrypt the key prior to use.", pk)
   233  	}
   234  
   235  	signer, err := ssh.ParsePrivateKey([]byte(key))
   236  	if err != nil {
   237  		return nil, fmt.Errorf("Failed to parse key file %q: %s", pk, err)
   238  	}
   239  
   240  	return ssh.PublicKeys(signer), nil
   241  }
   242  
   243  func connectToAgent(connInfo *connectionInfo) (*sshAgent, error) {
   244  	if connInfo.Agent != true {
   245  		// No agent configured
   246  		return nil, nil
   247  	}
   248  
   249  	agent, conn, err := sshagent.New()
   250  	if err != nil {
   251  		return nil, err
   252  	}
   253  
   254  	// connection close is handled over in Communicator
   255  	return &sshAgent{
   256  		agent: agent,
   257  		conn:  conn,
   258  	}, nil
   259  
   260  }
   261  
   262  // A tiny wrapper around an agent.Agent to expose the ability to close its
   263  // associated connection on request.
   264  type sshAgent struct {
   265  	agent agent.Agent
   266  	conn  net.Conn
   267  }
   268  
   269  func (a *sshAgent) Close() error {
   270  	if a.conn == nil {
   271  		return nil
   272  	}
   273  
   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  }