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  }