github.com/nicgrayson/terraform@v0.4.3-0.20150415203910-c4de50829380/helper/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 default 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/script.sh"
    29  
    30  	// DefaultTimeout is used if there is no timeout given
    31  	DefaultTimeout = 5 * time.Minute
    32  )
    33  
    34  // SSHConfig is decoded from the ConnInfo of the resource. These
    35  // are the only keys we look at. If a KeyFile is given, that is used
    36  // instead of a password.
    37  type SSHConfig 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  
    49  // VerifySSH is used to verify the ConnInfo is usable by remote-exec
    50  func VerifySSH(s *terraform.InstanceState) error {
    51  	connType := s.Ephemeral.ConnInfo["type"]
    52  	switch connType {
    53  	case "":
    54  	case "ssh":
    55  	default:
    56  		return fmt.Errorf("Connection type '%s' not supported", connType)
    57  	}
    58  	return nil
    59  }
    60  
    61  // ParseSSHConfig is used to convert the ConnInfo of the InstanceState into
    62  // a SSHConfig struct
    63  func ParseSSHConfig(s *terraform.InstanceState) (*SSHConfig, error) {
    64  	sshConf := &SSHConfig{}
    65  	decConf := &mapstructure.DecoderConfig{
    66  		WeaklyTypedInput: true,
    67  		Result:           sshConf,
    68  	}
    69  	dec, err := mapstructure.NewDecoder(decConf)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  	if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil {
    74  		return nil, err
    75  	}
    76  	if sshConf.User == "" {
    77  		sshConf.User = DefaultUser
    78  	}
    79  	if sshConf.Port == 0 {
    80  		sshConf.Port = DefaultPort
    81  	}
    82  	if sshConf.ScriptPath == "" {
    83  		sshConf.ScriptPath = DefaultScriptPath
    84  	}
    85  	if sshConf.Timeout != "" {
    86  		sshConf.TimeoutVal = safeDuration(sshConf.Timeout, DefaultTimeout)
    87  	} else {
    88  		sshConf.TimeoutVal = DefaultTimeout
    89  	}
    90  	return sshConf, nil
    91  }
    92  
    93  // safeDuration returns either the parsed duration or a default value
    94  func safeDuration(dur string, defaultDur time.Duration) time.Duration {
    95  	d, err := time.ParseDuration(dur)
    96  	if err != nil {
    97  		log.Printf("Invalid duration '%s', using default of %s", dur, defaultDur)
    98  		return defaultDur
    99  	}
   100  	return d
   101  }
   102  
   103  // PrepareConfig is used to turn the *SSHConfig provided into a
   104  // usable *Config for client initialization.
   105  func PrepareConfig(conf *SSHConfig) (*Config, error) {
   106  	var conn net.Conn
   107  	var err error
   108  
   109  	sshConf := &ssh.ClientConfig{
   110  		User: conf.User,
   111  	}
   112  	if conf.Agent {
   113  		sshAuthSock := os.Getenv("SSH_AUTH_SOCK")
   114  
   115  		if sshAuthSock == "" {
   116  			return nil, fmt.Errorf("SSH Requested but SSH_AUTH_SOCK not-specified")
   117  		}
   118  
   119  		conn, err = net.Dial("unix", sshAuthSock)
   120  		if err != nil {
   121  			return nil, fmt.Errorf("Error connecting to SSH_AUTH_SOCK: %v", err)
   122  		}
   123  		// I need to close this but, later after all connections have been made
   124  		// defer conn.Close()
   125  		signers, err := agent.NewClient(conn).Signers()
   126  		if err != nil {
   127  			return nil, fmt.Errorf("Error getting keys from ssh agent: %v", err)
   128  		}
   129  
   130  		sshConf.Auth = append(sshConf.Auth, ssh.PublicKeys(signers...))
   131  	}
   132  	if conf.KeyFile != "" {
   133  		fullPath, err := homedir.Expand(conf.KeyFile)
   134  		if err != nil {
   135  			return nil, fmt.Errorf("Failed to expand home directory: %v", err)
   136  		}
   137  		key, err := ioutil.ReadFile(fullPath)
   138  		if err != nil {
   139  			return nil, fmt.Errorf("Failed to read key file '%s': %v", conf.KeyFile, err)
   140  		}
   141  
   142  		// We parse the private key on our own first so that we can
   143  		// show a nicer error if the private key has a password.
   144  		block, _ := pem.Decode(key)
   145  		if block == nil {
   146  			return nil, fmt.Errorf(
   147  				"Failed to read key '%s': no key found", conf.KeyFile)
   148  		}
   149  		if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
   150  			return nil, fmt.Errorf(
   151  				"Failed to read key '%s': password protected keys are\n"+
   152  					"not supported. Please decrypt the key prior to use.", conf.KeyFile)
   153  		}
   154  
   155  		signer, err := ssh.ParsePrivateKey(key)
   156  		if err != nil {
   157  			return nil, fmt.Errorf("Failed to parse key file '%s': %v", conf.KeyFile, err)
   158  		}
   159  
   160  		sshConf.Auth = append(sshConf.Auth, ssh.PublicKeys(signer))
   161  	}
   162  	if conf.Password != "" {
   163  		sshConf.Auth = append(sshConf.Auth,
   164  			ssh.Password(conf.Password))
   165  		sshConf.Auth = append(sshConf.Auth,
   166  			ssh.KeyboardInteractive(PasswordKeyboardInteractive(conf.Password)))
   167  	}
   168  	host := fmt.Sprintf("%s:%d", conf.Host, conf.Port)
   169  	config := &Config{
   170  		SSHConfig:    sshConf,
   171  		Connection:   ConnectFunc("tcp", host),
   172  		SSHAgentConn: conn,
   173  	}
   174  	return config, nil
   175  }
   176  
   177  func (c *Config) CleanupConfig() error {
   178  	if c.SSHAgentConn != nil {
   179  		return c.SSHAgentConn.Close()
   180  	}
   181  
   182  	return nil
   183  }