github.com/adamar/terraform@v0.2.2-0.20141016210445-2e703afdad0e/helper/ssh/provisioner.go (about) 1 package ssh 2 3 import ( 4 "encoding/pem" 5 "fmt" 6 "io/ioutil" 7 "log" 8 "time" 9 10 "code.google.com/p/go.crypto/ssh" 11 "github.com/hashicorp/terraform/terraform" 12 "github.com/mitchellh/go-homedir" 13 "github.com/mitchellh/mapstructure" 14 ) 15 16 const ( 17 // DefaultUser is used if there is no default user given 18 DefaultUser = "root" 19 20 // DefaultPort is used if there is no port given 21 DefaultPort = 22 22 23 // DefaultScriptPath is used as the path to copy the file to 24 // for remote execution if not provided otherwise. 25 DefaultScriptPath = "/tmp/script.sh" 26 27 // DefaultTimeout is used if there is no timeout given 28 DefaultTimeout = 5 * time.Minute 29 ) 30 31 // SSHConfig is decoded from the ConnInfo of the resource. These 32 // are the only keys we look at. If a KeyFile is given, that is used 33 // instead of a password. 34 type SSHConfig struct { 35 User string 36 Password string 37 KeyFile string `mapstructure:"key_file"` 38 Host string 39 Port int 40 Timeout string 41 ScriptPath string `mapstructure:"script_path"` 42 TimeoutVal time.Duration `mapstructure:"-"` 43 } 44 45 // VerifySSH is used to verify the ConnInfo is usable by remote-exec 46 func VerifySSH(s *terraform.InstanceState) error { 47 connType := s.Ephemeral.ConnInfo["type"] 48 switch connType { 49 case "": 50 case "ssh": 51 default: 52 return fmt.Errorf("Connection type '%s' not supported", connType) 53 } 54 return nil 55 } 56 57 // ParseSSHConfig is used to convert the ConnInfo of the InstanceState into 58 // a SSHConfig struct 59 func ParseSSHConfig(s *terraform.InstanceState) (*SSHConfig, error) { 60 sshConf := &SSHConfig{} 61 decConf := &mapstructure.DecoderConfig{ 62 WeaklyTypedInput: true, 63 Result: sshConf, 64 } 65 dec, err := mapstructure.NewDecoder(decConf) 66 if err != nil { 67 return nil, err 68 } 69 if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil { 70 return nil, err 71 } 72 if sshConf.User == "" { 73 sshConf.User = DefaultUser 74 } 75 if sshConf.Port == 0 { 76 sshConf.Port = DefaultPort 77 } 78 if sshConf.ScriptPath == "" { 79 sshConf.ScriptPath = DefaultScriptPath 80 } 81 if sshConf.Timeout != "" { 82 sshConf.TimeoutVal = safeDuration(sshConf.Timeout, DefaultTimeout) 83 } else { 84 sshConf.TimeoutVal = DefaultTimeout 85 } 86 return sshConf, nil 87 } 88 89 // safeDuration returns either the parsed duration or a default value 90 func safeDuration(dur string, defaultDur time.Duration) time.Duration { 91 d, err := time.ParseDuration(dur) 92 if err != nil { 93 log.Printf("Invalid duration '%s', using default of %s", dur, defaultDur) 94 return defaultDur 95 } 96 return d 97 } 98 99 // PrepareConfig is used to turn the *SSHConfig provided into a 100 // usable *Config for client initialization. 101 func PrepareConfig(conf *SSHConfig) (*Config, error) { 102 sshConf := &ssh.ClientConfig{ 103 User: conf.User, 104 } 105 if conf.KeyFile != "" { 106 fullPath, err := homedir.Expand(conf.KeyFile) 107 if err != nil { 108 return nil, fmt.Errorf("Failed to expand home directory: %v", err) 109 } 110 key, err := ioutil.ReadFile(fullPath) 111 if err != nil { 112 return nil, fmt.Errorf("Failed to read key file '%s': %v", conf.KeyFile, err) 113 } 114 115 // We parse the private key on our own first so that we can 116 // show a nicer error if the private key has a password. 117 block, _ := pem.Decode(key) 118 if block == nil { 119 return nil, fmt.Errorf( 120 "Failed to read key '%s': no key found", conf.KeyFile) 121 } 122 if block.Headers["Proc-Type"] == "4,ENCRYPTED" { 123 return nil, fmt.Errorf( 124 "Failed to read key '%s': password protected keys are\n"+ 125 "not supported. Please decrypt the key prior to use.", conf.KeyFile) 126 } 127 128 signer, err := ssh.ParsePrivateKey(key) 129 if err != nil { 130 return nil, fmt.Errorf("Failed to parse key file '%s': %v", conf.KeyFile, err) 131 } 132 133 sshConf.Auth = append(sshConf.Auth, ssh.PublicKeys(signer)) 134 } 135 if conf.Password != "" { 136 sshConf.Auth = append(sshConf.Auth, 137 ssh.Password(conf.Password)) 138 sshConf.Auth = append(sshConf.Auth, 139 ssh.KeyboardInteractive(PasswordKeyboardInteractive(conf.Password))) 140 } 141 host := fmt.Sprintf("%s:%d", conf.Host, conf.Port) 142 config := &Config{ 143 SSHConfig: sshConf, 144 Connection: ConnectFunc("tcp", host), 145 } 146 return config, nil 147 }