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