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