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