From 4b3ed5d7e2f61aaa1c02951aad97d5ec33c46005 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 13 Jun 2015 17:42:38 -0400 Subject: [PATCH] helper/communicator --- helper/communicator/config.go | 38 ++++++ helper/communicator/step_connect.go | 44 +++++++ helper/communicator/step_connect_ssh.go | 153 ++++++++++++++++++++++++ 3 files changed, 235 insertions(+) create mode 100644 helper/communicator/config.go create mode 100644 helper/communicator/step_connect.go create mode 100644 helper/communicator/step_connect_ssh.go diff --git a/helper/communicator/config.go b/helper/communicator/config.go new file mode 100644 index 000000000..2500a7d2d --- /dev/null +++ b/helper/communicator/config.go @@ -0,0 +1,38 @@ +package communicator + +import ( + "errors" + "time" + + "github.com/mitchellh/packer/template/interpolate" +) + +// Config is the common configuration that communicators allow within +// a builder. +type Config struct { + SSHHost string `mapstructure:"ssh_host"` + SSHPort int `mapstructure:"ssh_port"` + SSHUsername string `mapstructure:"ssh_username"` + SSHPassword string `mapstructure:"ssh_password"` + SSHPrivateKey string `mapstructure:"ssh_private_key_file"` + SSHPty bool `mapstructure:"ssh_pty"` + SSHTimeout time.Duration `mapstructure:"ssh_timeout"` +} + +func (c *Config) Prepare(ctx *interpolate.Context) []error { + if c.SSHPort == 0 { + c.SSHPort = 22 + } + + if c.SSHTimeout == 0 { + c.SSHTimeout = 5 * time.Minute + } + + // Validation + var errs []error + if c.SSHUsername == "" { + errs = append(errs, errors.New("An ssh_username must be specified")) + } + + return errs +} diff --git a/helper/communicator/step_connect.go b/helper/communicator/step_connect.go new file mode 100644 index 000000000..77feebfb9 --- /dev/null +++ b/helper/communicator/step_connect.go @@ -0,0 +1,44 @@ +package communicator + +import ( + "github.com/mitchellh/multistep" + gossh "golang.org/x/crypto/ssh" +) + +// StepConnect is a multistep Step implementation that connects to +// the proper communicator and stores it in the "communicator" key in the +// state bag. +type StepConnect struct { + // Config is the communicator config struct + Config *Config + + // The fields below are callbacks to assist with connecting to SSH. + // + // SSHAddress should return the default host to connect to for SSH. + // This is only called if ssh_host isn't specified in the config. + // + // SSHConfig should return the default configuration for + // connecting via SSH. + SSHAddress func(multistep.StateBag) (string, error) + SSHConfig func(multistep.StateBag) (*gossh.ClientConfig, error) + + substep multistep.Step +} + +func (s *StepConnect) Run(state multistep.StateBag) multistep.StepAction { + // Eventually we might switch between multiple of these depending + // on the communicator type. + s.substep = &StepConnectSSH{ + Config: s.Config, + SSHAddress: s.SSHAddress, + SSHConfig: s.SSHConfig, + } + + return s.substep.Run(state) +} + +func (s *StepConnect) Cleanup(state multistep.StateBag) { + if s.substep != nil { + s.substep.Cleanup(state) + } +} diff --git a/helper/communicator/step_connect_ssh.go b/helper/communicator/step_connect_ssh.go new file mode 100644 index 000000000..9be653c01 --- /dev/null +++ b/helper/communicator/step_connect_ssh.go @@ -0,0 +1,153 @@ +package communicator + +import ( + "errors" + "fmt" + "log" + "strings" + "time" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/communicator/ssh" + "github.com/mitchellh/packer/packer" + gossh "golang.org/x/crypto/ssh" +) + +// StepConnectSSH is a step that only connects to SSH. +// +// In general, you should use StepConnect. +type StepConnectSSH struct { + // All the fields below are documented on StepConnect + Config *Config + SSHAddress func(multistep.StateBag) (string, error) + SSHConfig func(multistep.StateBag) (*gossh.ClientConfig, error) +} + +func (s *StepConnectSSH) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + + var comm packer.Communicator + var err error + + cancel := make(chan struct{}) + waitDone := make(chan bool, 1) + go func() { + ui.Say("Waiting for SSH to become available...") + comm, err = s.waitForSSH(state, cancel) + waitDone <- true + }() + + log.Printf("[INFO] Waiting for SSH, up to timeout: %s", s.Config.SSHTimeout) + timeout := time.After(s.Config.SSHTimeout) +WaitLoop: + for { + // Wait for either SSH to become available, a timeout to occur, + // or an interrupt to come through. + select { + case <-waitDone: + if err != nil { + ui.Error(fmt.Sprintf("Error waiting for SSH: %s", err)) + state.Put("error", err) + return multistep.ActionHalt + } + + ui.Say("Connected to SSH!") + state.Put("communicator", comm) + break WaitLoop + case <-timeout: + err := fmt.Errorf("Timeout waiting for SSH.") + state.Put("error", err) + ui.Error(err.Error()) + close(cancel) + return multistep.ActionHalt + case <-time.After(1 * time.Second): + if _, ok := state.GetOk(multistep.StateCancelled); ok { + // The step sequence was cancelled, so cancel waiting for SSH + // and just start the halting process. + close(cancel) + log.Println("[WARN] Interrupt detected, quitting waiting for SSH.") + return multistep.ActionHalt + } + } + } + + return multistep.ActionContinue +} + +func (s *StepConnectSSH) Cleanup(multistep.StateBag) { +} + +func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan struct{}) (packer.Communicator, error) { + handshakeAttempts := 0 + + var comm packer.Communicator + first := true + for { + // Don't check for cancel or wait on first iteration + if !first { + select { + case <-cancel: + log.Println("[DEBUG] SSH wait cancelled. Exiting loop.") + return nil, errors.New("SSH wait cancelled") + case <-time.After(5 * time.Second): + } + } + first = false + + // First we request the TCP connection information + address, err := s.SSHAddress(state) + if err != nil { + log.Printf("[DEBUG] Error getting SSH address: %s", err) + continue + } + + // Retrieve the SSH configuration + sshConfig, err := s.SSHConfig(state) + if err != nil { + log.Printf("[DEBUG] Error getting SSH config: %s", err) + continue + } + + // Attempt to connect to SSH port + connFunc := ssh.ConnectFunc("tcp", address) + nc, err := connFunc() + if err != nil { + log.Printf("[DEBUG] TCP connection to SSH ip/port failed: %s", err) + continue + } + nc.Close() + + // Then we attempt to connect via SSH + config := &ssh.Config{ + Connection: connFunc, + SSHConfig: sshConfig, + Pty: s.Config.SSHPty, + } + + log.Println("[INFO] Attempting SSH connection...") + comm, err = ssh.New(address, config) + if err != nil { + log.Printf("[DEBUG] SSH handshake err: %s", err) + + // Only count this as an attempt if we were able to attempt + // to authenticate. Note this is very brittle since it depends + // on the string of the error... but I don't see any other way. + if strings.Contains(err.Error(), "authenticate") { + log.Printf( + "[DEBUG] Detected authentication error. Increasing handshake attempts.") + handshakeAttempts += 1 + } + + if handshakeAttempts < 10 { + // Try to connect via SSH a handful of times + continue + } + + return nil, err + } + + break + } + + return comm, nil +}