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 }