github.com/HashDataInc/packer@v1.3.2/helper/communicator/step_connect_ssh.go (about)

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