github.com/jsoriano/terraform@v0.6.7-0.20151026070445-8b70867fdd95/communicator/ssh/provisioner.go (about)

     1  package ssh
     2  
     3  import (
     4  	"encoding/pem"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"log"
     8  	"net"
     9  	"os"
    10  	"time"
    11  
    12  	"github.com/hashicorp/terraform/terraform"
    13  	"github.com/mitchellh/go-homedir"
    14  	"github.com/mitchellh/mapstructure"
    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  	KeyFile    string `mapstructure:"key_file"`
    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  	BastionKeyFile  string `mapstructure:"bastion_key_file"`
    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  	if connInfo.Port == 0 {
    84  		connInfo.Port = DefaultPort
    85  	}
    86  	if connInfo.ScriptPath == "" {
    87  		connInfo.ScriptPath = DefaultScriptPath
    88  	}
    89  	if connInfo.Timeout != "" {
    90  		connInfo.TimeoutVal = safeDuration(connInfo.Timeout, DefaultTimeout)
    91  	} else {
    92  		connInfo.TimeoutVal = DefaultTimeout
    93  	}
    94  
    95  	// Default all bastion config attrs to their non-bastion counterparts
    96  	if connInfo.BastionHost != "" {
    97  		if connInfo.BastionUser == "" {
    98  			connInfo.BastionUser = connInfo.User
    99  		}
   100  		if connInfo.BastionPassword == "" {
   101  			connInfo.BastionPassword = connInfo.Password
   102  		}
   103  		if connInfo.BastionKeyFile == "" {
   104  			connInfo.BastionKeyFile = connInfo.KeyFile
   105  		}
   106  		if connInfo.BastionPort == 0 {
   107  			connInfo.BastionPort = connInfo.Port
   108  		}
   109  	}
   110  
   111  	return connInfo, nil
   112  }
   113  
   114  // safeDuration returns either the parsed duration or a default value
   115  func safeDuration(dur string, defaultDur time.Duration) time.Duration {
   116  	d, err := time.ParseDuration(dur)
   117  	if err != nil {
   118  		log.Printf("Invalid duration '%s', using default of %s", dur, defaultDur)
   119  		return defaultDur
   120  	}
   121  	return d
   122  }
   123  
   124  // prepareSSHConfig is used to turn the *ConnectionInfo provided into a
   125  // usable *SSHConfig for client initialization.
   126  func prepareSSHConfig(connInfo *connectionInfo) (*sshConfig, error) {
   127  	sshAgent, err := connectToAgent(connInfo)
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	sshConf, err := buildSSHClientConfig(sshClientConfigOpts{
   133  		user:     connInfo.User,
   134  		keyFile:  connInfo.KeyFile,
   135  		password: connInfo.Password,
   136  		sshAgent: sshAgent,
   137  	})
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  
   142  	var bastionConf *ssh.ClientConfig
   143  	if connInfo.BastionHost != "" {
   144  		bastionConf, err = buildSSHClientConfig(sshClientConfigOpts{
   145  			user:     connInfo.BastionUser,
   146  			keyFile:  connInfo.BastionKeyFile,
   147  			password: connInfo.BastionPassword,
   148  			sshAgent: sshAgent,
   149  		})
   150  		if err != nil {
   151  			return nil, err
   152  		}
   153  	}
   154  
   155  	host := fmt.Sprintf("%s:%d", connInfo.Host, connInfo.Port)
   156  	connectFunc := ConnectFunc("tcp", host)
   157  
   158  	if bastionConf != nil {
   159  		bastionHost := fmt.Sprintf("%s:%d", connInfo.BastionHost, connInfo.BastionPort)
   160  		connectFunc = BastionConnectFunc("tcp", bastionHost, bastionConf, "tcp", host)
   161  	}
   162  
   163  	config := &sshConfig{
   164  		config:     sshConf,
   165  		connection: connectFunc,
   166  		sshAgent:   sshAgent,
   167  	}
   168  	return config, nil
   169  }
   170  
   171  type sshClientConfigOpts struct {
   172  	keyFile  string
   173  	password string
   174  	sshAgent *sshAgent
   175  	user     string
   176  }
   177  
   178  func buildSSHClientConfig(opts sshClientConfigOpts) (*ssh.ClientConfig, error) {
   179  	conf := &ssh.ClientConfig{
   180  		User: opts.user,
   181  	}
   182  
   183  	if opts.keyFile != "" {
   184  		pubKeyAuth, err := readPublicKeyFromPath(opts.keyFile)
   185  		if err != nil {
   186  			return nil, err
   187  		}
   188  		conf.Auth = append(conf.Auth, pubKeyAuth)
   189  	}
   190  
   191  	if opts.password != "" {
   192  		conf.Auth = append(conf.Auth, ssh.Password(opts.password))
   193  		conf.Auth = append(conf.Auth, ssh.KeyboardInteractive(
   194  			PasswordKeyboardInteractive(opts.password)))
   195  	}
   196  
   197  	if opts.sshAgent != nil {
   198  		conf.Auth = append(conf.Auth, opts.sshAgent.Auth())
   199  	}
   200  
   201  	return conf, nil
   202  }
   203  
   204  func readPublicKeyFromPath(path string) (ssh.AuthMethod, error) {
   205  	fullPath, err := homedir.Expand(path)
   206  	if err != nil {
   207  		return nil, fmt.Errorf("Failed to expand home directory: %s", err)
   208  	}
   209  	key, err := ioutil.ReadFile(fullPath)
   210  	if err != nil {
   211  		return nil, fmt.Errorf("Failed to read key file %q: %s", path, err)
   212  	}
   213  
   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(key)
   217  	if block == nil {
   218  		return nil, fmt.Errorf("Failed to read key %q: no key found", path)
   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.", path)
   224  	}
   225  
   226  	signer, err := ssh.ParsePrivateKey(key)
   227  	if err != nil {
   228  		return nil, fmt.Errorf("Failed to parse key file %q: %s", path, 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  	sshAuthSock := os.Getenv("SSH_AUTH_SOCK")
   241  
   242  	if sshAuthSock == "" {
   243  		return nil, fmt.Errorf("SSH Requested but SSH_AUTH_SOCK not-specified")
   244  	}
   245  
   246  	conn, err := net.Dial("unix", sshAuthSock)
   247  	if err != nil {
   248  		return nil, fmt.Errorf("Error connecting to SSH_AUTH_SOCK: %v", err)
   249  	}
   250  
   251  	// connection close is handled over in Communicator
   252  	return &sshAgent{
   253  		agent: agent.NewClient(conn),
   254  		conn:  conn,
   255  	}, nil
   256  }
   257  
   258  // A tiny wrapper around an agent.Agent to expose the ability to close its
   259  // associated connection on request.
   260  type sshAgent struct {
   261  	agent agent.Agent
   262  	conn  net.Conn
   263  }
   264  
   265  func (a *sshAgent) Close() error {
   266  	return a.conn.Close()
   267  }
   268  
   269  func (a *sshAgent) Auth() ssh.AuthMethod {
   270  	return ssh.PublicKeysCallback(a.agent.Signers)
   271  }
   272  
   273  func (a *sshAgent) ForwardToAgent(client *ssh.Client) error {
   274  	return agent.ForwardToAgent(client, a.agent)
   275  }