github.com/marksheahan/packer@v0.10.2-0.20160613200515-1acb2d6645a0/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 UseSftp: s.Config.SSHFileTransferMethod == "sftp", 166 } 167 168 log.Println("[INFO] Attempting SSH connection...") 169 comm, err = ssh.New(address, config) 170 if err != nil { 171 log.Printf("[DEBUG] SSH handshake err: %s", err) 172 173 // Only count this as an attempt if we were able to attempt 174 // to authenticate. Note this is very brittle since it depends 175 // on the string of the error... but I don't see any other way. 176 if strings.Contains(err.Error(), "authenticate") { 177 log.Printf( 178 "[DEBUG] Detected authentication error. Increasing handshake attempts.") 179 handshakeAttempts += 1 180 } 181 182 if handshakeAttempts < s.Config.SSHHandshakeAttempts { 183 // Try to connect via SSH a handful of times. We sleep here 184 // so we don't get a ton of authentication errors back to back. 185 time.Sleep(2 * time.Second) 186 continue 187 } 188 189 return nil, err 190 } 191 192 break 193 } 194 195 return comm, nil 196 } 197 198 func sshBastionConfig(config *Config) (*gossh.ClientConfig, error) { 199 auth := make([]gossh.AuthMethod, 0, 2) 200 if config.SSHBastionPassword != "" { 201 auth = append(auth, 202 gossh.Password(config.SSHBastionPassword), 203 gossh.KeyboardInteractive( 204 ssh.PasswordKeyboardInteractive(config.SSHBastionPassword))) 205 } 206 207 if config.SSHBastionPrivateKey != "" { 208 signer, err := commonssh.FileSigner(config.SSHBastionPrivateKey) 209 if err != nil { 210 return nil, err 211 } 212 213 auth = append(auth, gossh.PublicKeys(signer)) 214 } 215 216 return &gossh.ClientConfig{ 217 User: config.SSHBastionUsername, 218 Auth: auth, 219 }, nil 220 }