github.com/alexissmirnov/terraform@v0.4.3-0.20150423153700-1ef9731a2f14/helper/ssh/provisioner.go (about) 1 package ssh 2 3 import ( 4 "encoding/pem" 5 "fmt" 6 "io/ioutil" 7 "log" 8 "math/rand" 9 "net" 10 "os" 11 "strconv" 12 "strings" 13 "time" 14 15 "github.com/hashicorp/terraform/terraform" 16 "github.com/mitchellh/go-homedir" 17 "github.com/mitchellh/mapstructure" 18 "golang.org/x/crypto/ssh" 19 "golang.org/x/crypto/ssh/agent" 20 ) 21 22 const ( 23 // DefaultUser is used if there is no default user given 24 DefaultUser = "root" 25 26 // DefaultPort is used if there is no port given 27 DefaultPort = 22 28 29 // DefaultScriptPath is used as the path to copy the file to 30 // for remote execution if not provided otherwise. 31 DefaultScriptPath = "/tmp/script_%RAND%.sh" 32 33 // DefaultTimeout is used if there is no timeout given 34 DefaultTimeout = 5 * time.Minute 35 ) 36 37 // SSHConfig is decoded from the ConnInfo of the resource. These 38 // are the only keys we look at. If a KeyFile is given, that is used 39 // instead of a password. 40 type SSHConfig struct { 41 User string 42 Password string 43 KeyFile string `mapstructure:"key_file"` 44 Host string 45 Port int 46 Agent bool 47 Timeout string 48 ScriptPath string `mapstructure:"script_path"` 49 TimeoutVal time.Duration `mapstructure:"-"` 50 } 51 52 func (c *SSHConfig) RemotePath() string { 53 return strings.Replace( 54 c.ScriptPath, "%RAND%", 55 strconv.FormatInt(int64(rand.Int31()), 10), -1) 56 } 57 58 // VerifySSH is used to verify the ConnInfo is usable by remote-exec 59 func VerifySSH(s *terraform.InstanceState) error { 60 connType := s.Ephemeral.ConnInfo["type"] 61 switch connType { 62 case "": 63 case "ssh": 64 default: 65 return fmt.Errorf("Connection type '%s' not supported", connType) 66 } 67 return nil 68 } 69 70 // ParseSSHConfig is used to convert the ConnInfo of the InstanceState into 71 // a SSHConfig struct 72 func ParseSSHConfig(s *terraform.InstanceState) (*SSHConfig, error) { 73 sshConf := &SSHConfig{} 74 decConf := &mapstructure.DecoderConfig{ 75 WeaklyTypedInput: true, 76 Result: sshConf, 77 } 78 dec, err := mapstructure.NewDecoder(decConf) 79 if err != nil { 80 return nil, err 81 } 82 if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil { 83 return nil, err 84 } 85 if sshConf.User == "" { 86 sshConf.User = DefaultUser 87 } 88 if sshConf.Port == 0 { 89 sshConf.Port = DefaultPort 90 } 91 if sshConf.ScriptPath == "" { 92 sshConf.ScriptPath = DefaultScriptPath 93 } 94 if sshConf.Timeout != "" { 95 sshConf.TimeoutVal = safeDuration(sshConf.Timeout, DefaultTimeout) 96 } else { 97 sshConf.TimeoutVal = DefaultTimeout 98 } 99 return sshConf, nil 100 } 101 102 // safeDuration returns either the parsed duration or a default value 103 func safeDuration(dur string, defaultDur time.Duration) time.Duration { 104 d, err := time.ParseDuration(dur) 105 if err != nil { 106 log.Printf("Invalid duration '%s', using default of %s", dur, defaultDur) 107 return defaultDur 108 } 109 return d 110 } 111 112 // PrepareConfig is used to turn the *SSHConfig provided into a 113 // usable *Config for client initialization. 114 func PrepareConfig(conf *SSHConfig) (*Config, error) { 115 var conn net.Conn 116 var err error 117 118 sshConf := &ssh.ClientConfig{ 119 User: conf.User, 120 } 121 if conf.Agent { 122 sshAuthSock := os.Getenv("SSH_AUTH_SOCK") 123 124 if sshAuthSock == "" { 125 return nil, fmt.Errorf("SSH Requested but SSH_AUTH_SOCK not-specified") 126 } 127 128 conn, err = net.Dial("unix", sshAuthSock) 129 if err != nil { 130 return nil, fmt.Errorf("Error connecting to SSH_AUTH_SOCK: %v", err) 131 } 132 // I need to close this but, later after all connections have been made 133 // defer conn.Close() 134 signers, err := agent.NewClient(conn).Signers() 135 if err != nil { 136 return nil, fmt.Errorf("Error getting keys from ssh agent: %v", err) 137 } 138 139 sshConf.Auth = append(sshConf.Auth, ssh.PublicKeys(signers...)) 140 } 141 if conf.KeyFile != "" { 142 fullPath, err := homedir.Expand(conf.KeyFile) 143 if err != nil { 144 return nil, fmt.Errorf("Failed to expand home directory: %v", err) 145 } 146 key, err := ioutil.ReadFile(fullPath) 147 if err != nil { 148 return nil, fmt.Errorf("Failed to read key file '%s': %v", conf.KeyFile, err) 149 } 150 151 // We parse the private key on our own first so that we can 152 // show a nicer error if the private key has a password. 153 block, _ := pem.Decode(key) 154 if block == nil { 155 return nil, fmt.Errorf( 156 "Failed to read key '%s': no key found", conf.KeyFile) 157 } 158 if block.Headers["Proc-Type"] == "4,ENCRYPTED" { 159 return nil, fmt.Errorf( 160 "Failed to read key '%s': password protected keys are\n"+ 161 "not supported. Please decrypt the key prior to use.", conf.KeyFile) 162 } 163 164 signer, err := ssh.ParsePrivateKey(key) 165 if err != nil { 166 return nil, fmt.Errorf("Failed to parse key file '%s': %v", conf.KeyFile, err) 167 } 168 169 sshConf.Auth = append(sshConf.Auth, ssh.PublicKeys(signer)) 170 } 171 if conf.Password != "" { 172 sshConf.Auth = append(sshConf.Auth, 173 ssh.Password(conf.Password)) 174 sshConf.Auth = append(sshConf.Auth, 175 ssh.KeyboardInteractive(PasswordKeyboardInteractive(conf.Password))) 176 } 177 host := fmt.Sprintf("%s:%d", conf.Host, conf.Port) 178 config := &Config{ 179 SSHConfig: sshConf, 180 Connection: ConnectFunc("tcp", host), 181 SSHAgentConn: conn, 182 } 183 return config, nil 184 } 185 186 func (c *Config) CleanupConfig() error { 187 if c.SSHAgentConn != nil { 188 return c.SSHAgentConn.Close() 189 } 190 191 return nil 192 }