github.com/nicgrayson/terraform@v0.4.3-0.20150415203910-c4de50829380/helper/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 default 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/script.sh" 29 30 // DefaultTimeout is used if there is no timeout given 31 DefaultTimeout = 5 * time.Minute 32 ) 33 34 // SSHConfig is decoded from the ConnInfo of the resource. These 35 // are the only keys we look at. If a KeyFile is given, that is used 36 // instead of a password. 37 type SSHConfig 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 49 // VerifySSH is used to verify the ConnInfo is usable by remote-exec 50 func VerifySSH(s *terraform.InstanceState) error { 51 connType := s.Ephemeral.ConnInfo["type"] 52 switch connType { 53 case "": 54 case "ssh": 55 default: 56 return fmt.Errorf("Connection type '%s' not supported", connType) 57 } 58 return nil 59 } 60 61 // ParseSSHConfig is used to convert the ConnInfo of the InstanceState into 62 // a SSHConfig struct 63 func ParseSSHConfig(s *terraform.InstanceState) (*SSHConfig, error) { 64 sshConf := &SSHConfig{} 65 decConf := &mapstructure.DecoderConfig{ 66 WeaklyTypedInput: true, 67 Result: sshConf, 68 } 69 dec, err := mapstructure.NewDecoder(decConf) 70 if err != nil { 71 return nil, err 72 } 73 if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil { 74 return nil, err 75 } 76 if sshConf.User == "" { 77 sshConf.User = DefaultUser 78 } 79 if sshConf.Port == 0 { 80 sshConf.Port = DefaultPort 81 } 82 if sshConf.ScriptPath == "" { 83 sshConf.ScriptPath = DefaultScriptPath 84 } 85 if sshConf.Timeout != "" { 86 sshConf.TimeoutVal = safeDuration(sshConf.Timeout, DefaultTimeout) 87 } else { 88 sshConf.TimeoutVal = DefaultTimeout 89 } 90 return sshConf, nil 91 } 92 93 // safeDuration returns either the parsed duration or a default value 94 func safeDuration(dur string, defaultDur time.Duration) time.Duration { 95 d, err := time.ParseDuration(dur) 96 if err != nil { 97 log.Printf("Invalid duration '%s', using default of %s", dur, defaultDur) 98 return defaultDur 99 } 100 return d 101 } 102 103 // PrepareConfig is used to turn the *SSHConfig provided into a 104 // usable *Config for client initialization. 105 func PrepareConfig(conf *SSHConfig) (*Config, error) { 106 var conn net.Conn 107 var err error 108 109 sshConf := &ssh.ClientConfig{ 110 User: conf.User, 111 } 112 if conf.Agent { 113 sshAuthSock := os.Getenv("SSH_AUTH_SOCK") 114 115 if sshAuthSock == "" { 116 return nil, fmt.Errorf("SSH Requested but SSH_AUTH_SOCK not-specified") 117 } 118 119 conn, err = net.Dial("unix", sshAuthSock) 120 if err != nil { 121 return nil, fmt.Errorf("Error connecting to SSH_AUTH_SOCK: %v", err) 122 } 123 // I need to close this but, later after all connections have been made 124 // defer conn.Close() 125 signers, err := agent.NewClient(conn).Signers() 126 if err != nil { 127 return nil, fmt.Errorf("Error getting keys from ssh agent: %v", err) 128 } 129 130 sshConf.Auth = append(sshConf.Auth, ssh.PublicKeys(signers...)) 131 } 132 if conf.KeyFile != "" { 133 fullPath, err := homedir.Expand(conf.KeyFile) 134 if err != nil { 135 return nil, fmt.Errorf("Failed to expand home directory: %v", err) 136 } 137 key, err := ioutil.ReadFile(fullPath) 138 if err != nil { 139 return nil, fmt.Errorf("Failed to read key file '%s': %v", conf.KeyFile, err) 140 } 141 142 // We parse the private key on our own first so that we can 143 // show a nicer error if the private key has a password. 144 block, _ := pem.Decode(key) 145 if block == nil { 146 return nil, fmt.Errorf( 147 "Failed to read key '%s': no key found", conf.KeyFile) 148 } 149 if block.Headers["Proc-Type"] == "4,ENCRYPTED" { 150 return nil, fmt.Errorf( 151 "Failed to read key '%s': password protected keys are\n"+ 152 "not supported. Please decrypt the key prior to use.", conf.KeyFile) 153 } 154 155 signer, err := ssh.ParsePrivateKey(key) 156 if err != nil { 157 return nil, fmt.Errorf("Failed to parse key file '%s': %v", conf.KeyFile, err) 158 } 159 160 sshConf.Auth = append(sshConf.Auth, ssh.PublicKeys(signer)) 161 } 162 if conf.Password != "" { 163 sshConf.Auth = append(sshConf.Auth, 164 ssh.Password(conf.Password)) 165 sshConf.Auth = append(sshConf.Auth, 166 ssh.KeyboardInteractive(PasswordKeyboardInteractive(conf.Password))) 167 } 168 host := fmt.Sprintf("%s:%d", conf.Host, conf.Port) 169 config := &Config{ 170 SSHConfig: sshConf, 171 Connection: ConnectFunc("tcp", host), 172 SSHAgentConn: conn, 173 } 174 return config, nil 175 } 176 177 func (c *Config) CleanupConfig() error { 178 if c.SSHAgentConn != nil { 179 return c.SSHAgentConn.Close() 180 } 181 182 return nil 183 }