github.com/inge4pres/terraform@v0.7.5-0.20160930053151-bd083f84f376/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 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  
    88  	// Format the host if needed.
    89  	// Needed for IPv6 support.
    90  	connInfo.Host = shared.IpFormat(connInfo.Host)
    91  
    92  	if connInfo.Port == 0 {
    93  		connInfo.Port = DefaultPort
    94  	}
    95  	if connInfo.ScriptPath == "" {
    96  		connInfo.ScriptPath = DefaultScriptPath
    97  	}
    98  	if connInfo.Timeout != "" {
    99  		connInfo.TimeoutVal = safeDuration(connInfo.Timeout, DefaultTimeout)
   100  	} else {
   101  		connInfo.TimeoutVal = DefaultTimeout
   102  	}
   103  
   104  	// Load deprecated fields; we can handle either path or contents in
   105  	// underlying implementation.
   106  	if connInfo.PrivateKey == "" && connInfo.KeyFile != "" {
   107  		connInfo.PrivateKey = connInfo.KeyFile
   108  	}
   109  	if connInfo.BastionPrivateKey == "" && connInfo.BastionKeyFile != "" {
   110  		connInfo.BastionPrivateKey = connInfo.BastionKeyFile
   111  	}
   112  
   113  	// Default all bastion config attrs to their non-bastion counterparts
   114  	if connInfo.BastionHost != "" {
   115  		// Format the bastion host if needed.
   116  		// Needed for IPv6 support.
   117  		connInfo.BastionHost = shared.IpFormat(connInfo.BastionHost)
   118  
   119  		if connInfo.BastionUser == "" {
   120  			connInfo.BastionUser = connInfo.User
   121  		}
   122  		if connInfo.BastionPassword == "" {
   123  			connInfo.BastionPassword = connInfo.Password
   124  		}
   125  		if connInfo.BastionPrivateKey == "" {
   126  			connInfo.BastionPrivateKey = connInfo.PrivateKey
   127  		}
   128  		if connInfo.BastionPort == 0 {
   129  			connInfo.BastionPort = connInfo.Port
   130  		}
   131  	}
   132  
   133  	return connInfo, nil
   134  }
   135  
   136  // safeDuration returns either the parsed duration or a default value
   137  func safeDuration(dur string, defaultDur time.Duration) time.Duration {
   138  	d, err := time.ParseDuration(dur)
   139  	if err != nil {
   140  		log.Printf("Invalid duration '%s', using default of %s", dur, defaultDur)
   141  		return defaultDur
   142  	}
   143  	return d
   144  }
   145  
   146  // prepareSSHConfig is used to turn the *ConnectionInfo provided into a
   147  // usable *SSHConfig for client initialization.
   148  func prepareSSHConfig(connInfo *connectionInfo) (*sshConfig, error) {
   149  	sshAgent, err := connectToAgent(connInfo)
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  
   154  	sshConf, err := buildSSHClientConfig(sshClientConfigOpts{
   155  		user:       connInfo.User,
   156  		privateKey: connInfo.PrivateKey,
   157  		password:   connInfo.Password,
   158  		sshAgent:   sshAgent,
   159  	})
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  
   164  	var bastionConf *ssh.ClientConfig
   165  	if connInfo.BastionHost != "" {
   166  		bastionConf, err = buildSSHClientConfig(sshClientConfigOpts{
   167  			user:       connInfo.BastionUser,
   168  			privateKey: connInfo.BastionPrivateKey,
   169  			password:   connInfo.BastionPassword,
   170  			sshAgent:   sshAgent,
   171  		})
   172  		if err != nil {
   173  			return nil, err
   174  		}
   175  	}
   176  
   177  	host := fmt.Sprintf("%s:%d", connInfo.Host, connInfo.Port)
   178  	connectFunc := ConnectFunc("tcp", host)
   179  
   180  	if bastionConf != nil {
   181  		bastionHost := fmt.Sprintf("%s:%d", connInfo.BastionHost, connInfo.BastionPort)
   182  		connectFunc = BastionConnectFunc("tcp", bastionHost, bastionConf, "tcp", host)
   183  	}
   184  
   185  	config := &sshConfig{
   186  		config:     sshConf,
   187  		connection: connectFunc,
   188  		sshAgent:   sshAgent,
   189  	}
   190  	return config, nil
   191  }
   192  
   193  type sshClientConfigOpts struct {
   194  	privateKey string
   195  	password   string
   196  	sshAgent   *sshAgent
   197  	user       string
   198  }
   199  
   200  func buildSSHClientConfig(opts sshClientConfigOpts) (*ssh.ClientConfig, error) {
   201  	conf := &ssh.ClientConfig{
   202  		User: opts.user,
   203  	}
   204  
   205  	if opts.privateKey != "" {
   206  		pubKeyAuth, err := readPrivateKey(opts.privateKey)
   207  		if err != nil {
   208  			return nil, err
   209  		}
   210  		conf.Auth = append(conf.Auth, pubKeyAuth)
   211  	}
   212  
   213  	if opts.password != "" {
   214  		conf.Auth = append(conf.Auth, ssh.Password(opts.password))
   215  		conf.Auth = append(conf.Auth, ssh.KeyboardInteractive(
   216  			PasswordKeyboardInteractive(opts.password)))
   217  	}
   218  
   219  	if opts.sshAgent != nil {
   220  		conf.Auth = append(conf.Auth, opts.sshAgent.Auth())
   221  	}
   222  
   223  	return conf, nil
   224  }
   225  
   226  func readPrivateKey(pk string) (ssh.AuthMethod, error) {
   227  	// We parse the private key on our own first so that we can
   228  	// show a nicer error if the private key has a password.
   229  	block, _ := pem.Decode([]byte(pk))
   230  	if block == nil {
   231  		return nil, fmt.Errorf("Failed to read key %q: no key found", pk)
   232  	}
   233  	if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
   234  		return nil, fmt.Errorf(
   235  			"Failed to read key %q: password protected keys are\n"+
   236  				"not supported. Please decrypt the key prior to use.", pk)
   237  	}
   238  
   239  	signer, err := ssh.ParsePrivateKey([]byte(pk))
   240  	if err != nil {
   241  		return nil, fmt.Errorf("Failed to parse key file %q: %s", pk, err)
   242  	}
   243  
   244  	return ssh.PublicKeys(signer), nil
   245  }
   246  
   247  func connectToAgent(connInfo *connectionInfo) (*sshAgent, error) {
   248  	if connInfo.Agent != true {
   249  		// No agent configured
   250  		return nil, nil
   251  	}
   252  
   253  	agent, conn, err := sshagent.New()
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  
   258  	// connection close is handled over in Communicator
   259  	return &sshAgent{
   260  		agent: agent,
   261  		conn:  conn,
   262  	}, nil
   263  
   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  	if a.conn == nil {
   275  		return nil
   276  	}
   277  
   278  	return a.conn.Close()
   279  }
   280  
   281  func (a *sshAgent) Auth() ssh.AuthMethod {
   282  	return ssh.PublicKeysCallback(a.agent.Signers)
   283  }
   284  
   285  func (a *sshAgent) ForwardToAgent(client *ssh.Client) error {
   286  	return agent.ForwardToAgent(client, a.agent)
   287  }