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