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