github.com/emate/packer@v0.8.1-0.20150625195101-fe0fde195dc6/helper/communicator/step_connect_ssh.go (about) 1 package communicator 2 3 import ( 4 "errors" 5 "fmt" 6 "log" 7 "net" 8 "strings" 9 "time" 10 11 "github.com/mitchellh/multistep" 12 commonssh "github.com/mitchellh/packer/common/ssh" 13 "github.com/mitchellh/packer/communicator/ssh" 14 "github.com/mitchellh/packer/packer" 15 gossh "golang.org/x/crypto/ssh" 16 ) 17 18 // StepConnectSSH is a step that only connects to SSH. 19 // 20 // In general, you should use StepConnect. 21 type StepConnectSSH struct { 22 // All the fields below are documented on StepConnect 23 Config *Config 24 Host func(multistep.StateBag) (string, error) 25 SSHConfig func(multistep.StateBag) (*gossh.ClientConfig, error) 26 SSHPort func(multistep.StateBag) (int, error) 27 } 28 29 func (s *StepConnectSSH) Run(state multistep.StateBag) multistep.StepAction { 30 ui := state.Get("ui").(packer.Ui) 31 32 var comm packer.Communicator 33 var err error 34 35 cancel := make(chan struct{}) 36 waitDone := make(chan bool, 1) 37 go func() { 38 ui.Say("Waiting for SSH to become available...") 39 comm, err = s.waitForSSH(state, cancel) 40 waitDone <- true 41 }() 42 43 log.Printf("[INFO] Waiting for SSH, up to timeout: %s", s.Config.SSHTimeout) 44 timeout := time.After(s.Config.SSHTimeout) 45 WaitLoop: 46 for { 47 // Wait for either SSH to become available, a timeout to occur, 48 // or an interrupt to come through. 49 select { 50 case <-waitDone: 51 if err != nil { 52 ui.Error(fmt.Sprintf("Error waiting for SSH: %s", err)) 53 state.Put("error", err) 54 return multistep.ActionHalt 55 } 56 57 ui.Say("Connected to SSH!") 58 state.Put("communicator", comm) 59 break WaitLoop 60 case <-timeout: 61 err := fmt.Errorf("Timeout waiting for SSH.") 62 state.Put("error", err) 63 ui.Error(err.Error()) 64 close(cancel) 65 return multistep.ActionHalt 66 case <-time.After(1 * time.Second): 67 if _, ok := state.GetOk(multistep.StateCancelled); ok { 68 // The step sequence was cancelled, so cancel waiting for SSH 69 // and just start the halting process. 70 close(cancel) 71 log.Println("[WARN] Interrupt detected, quitting waiting for SSH.") 72 return multistep.ActionHalt 73 } 74 } 75 } 76 77 return multistep.ActionContinue 78 } 79 80 func (s *StepConnectSSH) Cleanup(multistep.StateBag) { 81 } 82 83 func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan struct{}) (packer.Communicator, error) { 84 // Determine if we're using a bastion host, and if so, retrieve 85 // that configuration. This configuration doesn't change so we 86 // do this one before entering the retry loop. 87 var bProto, bAddr string 88 var bConf *gossh.ClientConfig 89 if s.Config.SSHBastionHost != "" { 90 // The protocol is hardcoded for now, but may be configurable one day 91 bProto = "tcp" 92 bAddr = fmt.Sprintf( 93 "%s:%d", s.Config.SSHBastionHost, s.Config.SSHBastionPort) 94 95 conf, err := sshBastionConfig(s.Config) 96 if err != nil { 97 return nil, fmt.Errorf("Error configuring bastion: %s", err) 98 } 99 bConf = conf 100 } 101 102 handshakeAttempts := 0 103 104 var comm packer.Communicator 105 first := true 106 for { 107 // Don't check for cancel or wait on first iteration 108 if !first { 109 select { 110 case <-cancel: 111 log.Println("[DEBUG] SSH wait cancelled. Exiting loop.") 112 return nil, errors.New("SSH wait cancelled") 113 case <-time.After(5 * time.Second): 114 } 115 } 116 first = false 117 118 // First we request the TCP connection information 119 host, err := s.Host(state) 120 if err != nil { 121 log.Printf("[DEBUG] Error getting SSH address: %s", err) 122 continue 123 } 124 port := s.Config.SSHPort 125 if s.SSHPort != nil { 126 port, err = s.SSHPort(state) 127 if err != nil { 128 log.Printf("[DEBUG] Error getting SSH port: %s", err) 129 continue 130 } 131 } 132 133 // Retrieve the SSH configuration 134 sshConfig, err := s.SSHConfig(state) 135 if err != nil { 136 log.Printf("[DEBUG] Error getting SSH config: %s", err) 137 continue 138 } 139 140 // Attempt to connect to SSH port 141 var connFunc func() (net.Conn, error) 142 address := fmt.Sprintf("%s:%d", host, port) 143 if bAddr != "" { 144 // We're using a bastion host, so use the bastion connfunc 145 connFunc = ssh.BastionConnectFunc( 146 bProto, bAddr, bConf, "tcp", address) 147 } else { 148 // No bastion host, connect directly 149 connFunc = ssh.ConnectFunc("tcp", address) 150 } 151 152 nc, err := connFunc() 153 if err != nil { 154 log.Printf("[DEBUG] TCP connection to SSH ip/port failed: %s", err) 155 continue 156 } 157 nc.Close() 158 159 // Then we attempt to connect via SSH 160 config := &ssh.Config{ 161 Connection: connFunc, 162 SSHConfig: sshConfig, 163 Pty: s.Config.SSHPty, 164 DisableAgent: s.Config.SSHDisableAgent, 165 } 166 167 log.Println("[INFO] Attempting SSH connection...") 168 comm, err = ssh.New(address, config) 169 if err != nil { 170 log.Printf("[DEBUG] SSH handshake err: %s", err) 171 172 // Only count this as an attempt if we were able to attempt 173 // to authenticate. Note this is very brittle since it depends 174 // on the string of the error... but I don't see any other way. 175 if strings.Contains(err.Error(), "authenticate") { 176 log.Printf( 177 "[DEBUG] Detected authentication error. Increasing handshake attempts.") 178 handshakeAttempts += 1 179 } 180 181 if handshakeAttempts < s.Config.SSHHandshakeAttempts { 182 // Try to connect via SSH a handful of times. We sleep here 183 // so we don't get a ton of authentication errors back to back. 184 time.Sleep(2 * time.Second) 185 continue 186 } 187 188 return nil, err 189 } 190 191 break 192 } 193 194 return comm, nil 195 } 196 197 func sshBastionConfig(config *Config) (*gossh.ClientConfig, error) { 198 auth := make([]gossh.AuthMethod, 0, 2) 199 if config.SSHBastionPassword != "" { 200 auth = append(auth, 201 gossh.Password(config.SSHBastionPassword), 202 gossh.KeyboardInteractive( 203 ssh.PasswordKeyboardInteractive(config.SSHBastionPassword))) 204 } 205 206 if config.SSHBastionPrivateKey != "" { 207 signer, err := commonssh.FileSigner(config.SSHBastionPrivateKey) 208 if err != nil { 209 return nil, err 210 } 211 212 auth = append(auth, gossh.PublicKeys(signer)) 213 } 214 215 return &gossh.ClientConfig{ 216 User: config.SSHBastionUsername, 217 Auth: auth, 218 }, nil 219 }