helper/communicator
This commit is contained in:
parent
7eff6b117d
commit
4b3ed5d7e2
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue