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  }