Commit 91c14347 authored by Mitchell Hashimoto's avatar Mitchell Hashimoto

Merge branch 'common-ssh'

This unifies all the builders so that they all wait for SSH using a common
step available in builder/common. This common step has the best practices of
every previously unique step, such as retrying SSH handshakes, limiting
the number of SSH handshakes, timeouts, and so on.
parents d422abfc 9e3885e1
...@@ -27,8 +27,10 @@ FEATURES: ...@@ -27,8 +27,10 @@ FEATURES:
IMPROVEMENTS: IMPROVEMENTS:
* everything: invalid keys in configuration are now considered validation * core: invalid keys in configuration are now considered validation
errors. [GH-104] errors. [GH-104]
* core: all builders now share a common SSH connection core, improving
SSH reliability over all the builders.
* amazon-ebs: Credentials will come from IAM role if available. [GH-160] * amazon-ebs: Credentials will come from IAM role if available. [GH-160]
* amazon-ebs: Verify the source AMI is EBS-backed before launching. [GH-169] * amazon-ebs: Verify the source AMI is EBS-backed before launching. [GH-169]
* shell provisioner: the build name and builder type are available in * shell provisioner: the build name and builder type are available in
......
...@@ -159,7 +159,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -159,7 +159,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&stepKeyPair{}, &stepKeyPair{},
&stepSecurityGroup{}, &stepSecurityGroup{},
&stepRunSourceInstance{}, &stepRunSourceInstance{},
&stepConnectSSH{}, &common.StepConnectSSH{
SSHAddress: sshAddress,
SSHConfig: sshConfig,
SSHWaitTimeout: b.config.sshTimeout,
},
&stepProvision{}, &stepProvision{},
&stepStopInstance{}, &stepStopInstance{},
&stepCreateAMI{}, &stepCreateAMI{},
......
package amazonebs
import (
gossh "code.google.com/p/go.crypto/ssh"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/packer/communicator/ssh"
)
func sshAddress(state map[string]interface{}) (string, error) {
config := state["config"].(config)
instance := state["instance"].(*ec2.Instance)
return fmt.Sprintf("%s:%d", instance.DNSName, config.SSHPort), nil
}
func sshConfig(state map[string]interface{}) (*gossh.ClientConfig, error) {
config := state["config"].(config)
privateKey := state["privateKey"].(string)
keyring := new(ssh.SimpleKeychain)
if err := keyring.AddPEMKey(privateKey); err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
return &gossh.ClientConfig{
User: config.SSHUsername,
Auth: []gossh.ClientAuth{
gossh.ClientAuthKeyring(keyring),
},
}, nil
}
package amazonebs
import (
gossh "code.google.com/p/go.crypto/ssh"
"errors"
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/communicator/ssh"
"github.com/mitchellh/packer/packer"
"log"
"time"
)
type stepConnectSSH struct {
cancel bool
comm packer.Communicator
}
func (s *stepConnectSSH) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(config)
ui := state["ui"].(packer.Ui)
var comm packer.Communicator
var err error
waitDone := make(chan bool, 1)
go func() {
comm, err = s.waitForSSH(state)
waitDone <- true
}()
log.Printf("Waiting for SSH, up to timeout: %s", config.sshTimeout.String())
timeout := time.After(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))
return multistep.ActionHalt
}
s.comm = comm
state["communicator"] = comm
break WaitLoop
case <-timeout:
ui.Error("Timeout waiting for SSH.")
s.cancel = true
return multistep.ActionHalt
case <-time.After(1 * time.Second):
if _, ok := state[multistep.StateCancelled]; ok {
log.Println("Interrupt detected, quitting waiting for SSH.")
return multistep.ActionHalt
}
}
}
return multistep.ActionContinue
}
func (s *stepConnectSSH) Cleanup(map[string]interface{}) {
if s.comm != nil {
// Close it TODO
s.comm = nil
}
}
// This blocks until SSH becomes available, and sends the communicator
// on the given channel.
func (s *stepConnectSSH) waitForSSH(state map[string]interface{}) (packer.Communicator, error) {
config := state["config"].(config)
instance := state["instance"].(*ec2.Instance)
privateKey := state["privateKey"].(string)
ui := state["ui"].(packer.Ui)
// Build the keyring for authentication. This stores the private key
// we'll use to authenticate.
keyring := &ssh.SimpleKeychain{}
err := keyring.AddPEMKey(privateKey)
if err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
// Create the function that will be used to create the connection
connFunc := ssh.ConnectFunc(
"tcp", fmt.Sprintf("%s:%d", instance.DNSName, config.SSHPort), 5*time.Minute)
ui.Say("Waiting for SSH to become available...")
var comm packer.Communicator
for {
time.Sleep(5 * time.Second)
if s.cancel {
log.Println("SSH wait cancelled. Exiting loop.")
return nil, errors.New("SSH wait cancelled")
}
// First just attempt a normal TCP connection that we close right
// away. We just test this in order to wait for the TCP port to be ready.
nc, err := connFunc()
if err != nil {
log.Printf("TCP connection to SSH ip/port failed: %s", err)
continue
}
nc.Close()
// Build the configuration to connect to SSH
config := &ssh.Config{
Connection: connFunc,
SSHConfig: &gossh.ClientConfig{
User: config.SSHUsername,
Auth: []gossh.ClientAuth{
gossh.ClientAuthKeyring(keyring),
},
},
}
sshConnectSuccess := make(chan bool, 1)
go func() {
comm, err = ssh.New(config)
if err != nil {
log.Printf("SSH connection fail: %s", err)
sshConnectSuccess <- false
return
}
sshConnectSuccess <- true
}()
select {
case success := <-sshConnectSuccess:
if !success {
continue
}
case <-time.After(5 * time.Second):
log.Printf("SSH handshake timeout. Trying again.")
continue
}
ui.Say("Connected via SSH!")
break
}
return comm, nil
}
package virtualbox package common
import ( import (
gossh "code.google.com/p/go.crypto/ssh" gossh "code.google.com/p/go.crypto/ssh"
...@@ -11,23 +11,33 @@ import ( ...@@ -11,23 +11,33 @@ import (
"time" "time"
) )
// This step waits for SSH to become available and establishes an SSH // StepConnectSSH is a multistep Step implementation that waits for SSH
// connection. // to become available. It gets the connection information from a single
// configuration when creating the step.
// //
// Uses: // Uses:
// config *config // ui packer.Ui
// sshHostPort uint
// ui packer.Ui
// //
// Produces: // Produces:
// communicator packer.Communicator // communicator packer.Communicator
type stepWaitForSSH struct { type StepConnectSSH struct {
// SSHAddress is a function that returns the TCP address to connect to
// for SSH. This is a function so that you can query information
// if necessary for this address.
SSHAddress func(map[string]interface{}) (string, error)
// SSHConfig is a function that returns the proper client configuration
// for SSH access.
SSHConfig func(map[string]interface{}) (*gossh.ClientConfig, error)
// SSHWaitTimeout is the total timeout to wait for SSH to become available.
SSHWaitTimeout time.Duration
cancel bool cancel bool
comm packer.Communicator comm packer.Communicator
} }
func (s *stepWaitForSSH) Run(state map[string]interface{}) multistep.StepAction { func (s *StepConnectSSH) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(*config)
ui := state["ui"].(packer.Ui) ui := state["ui"].(packer.Ui)
var comm packer.Communicator var comm packer.Communicator
...@@ -35,12 +45,13 @@ func (s *stepWaitForSSH) Run(state map[string]interface{}) multistep.StepAction ...@@ -35,12 +45,13 @@ func (s *stepWaitForSSH) Run(state map[string]interface{}) multistep.StepAction
waitDone := make(chan bool, 1) waitDone := make(chan bool, 1)
go func() { go func() {
ui.Say("Waiting for SSH to become available...")
comm, err = s.waitForSSH(state) comm, err = s.waitForSSH(state)
waitDone <- true waitDone <- true
}() }()
log.Printf("Waiting for SSH, up to timeout: %s", config.sshWaitTimeout.String()) log.Printf("Waiting for SSH, up to timeout: %s", s.SSHWaitTimeout)
timeout := time.After(config.sshWaitTimeout) timeout := time.After(s.SSHWaitTimeout)
WaitLoop: WaitLoop:
for { for {
// Wait for either SSH to become available, a timeout to occur, // Wait for either SSH to become available, a timeout to occur,
...@@ -52,6 +63,7 @@ WaitLoop: ...@@ -52,6 +63,7 @@ WaitLoop:
return multistep.ActionHalt return multistep.ActionHalt
} }
ui.Say("Connected to SSH!")
s.comm = comm s.comm = comm
state["communicator"] = comm state["communicator"] = comm
break WaitLoop break WaitLoop
...@@ -61,6 +73,9 @@ WaitLoop: ...@@ -61,6 +73,9 @@ WaitLoop:
return multistep.ActionHalt return multistep.ActionHalt
case <-time.After(1 * time.Second): case <-time.After(1 * time.Second):
if _, ok := state[multistep.StateCancelled]; ok { if _, ok := state[multistep.StateCancelled]; ok {
// The step sequence was cancelled, so cancel waiting for SSH
// and just start the halting process.
s.cancel = true
log.Println("Interrupt detected, quitting waiting for SSH.") log.Println("Interrupt detected, quitting waiting for SSH.")
return multistep.ActionHalt return multistep.ActionHalt
} }
...@@ -70,24 +85,12 @@ WaitLoop: ...@@ -70,24 +85,12 @@ WaitLoop:
return multistep.ActionContinue return multistep.ActionContinue
} }
func (s *stepWaitForSSH) Cleanup(map[string]interface{}) { func (s *StepConnectSSH) Cleanup(map[string]interface{}) {
if s.comm != nil {
// TODO: close
s.comm = nil
}
} }
// This blocks until SSH becomes available, and sends the communicator func (s *StepConnectSSH) waitForSSH(state map[string]interface{}) (packer.Communicator, error) {
// on the given channel. handshakeAttempts := 0
func (s *stepWaitForSSH) waitForSSH(state map[string]interface{}) (packer.Communicator, error) {
config := state["config"].(*config)
ui := state["ui"].(packer.Ui)
sshHostPort := state["sshHostPort"].(uint)
connFunc := ssh.ConnectFunc(
"tcp", fmt.Sprintf("127.0.0.1:%d", sshHostPort), 5*time.Minute)
ui.Say("Waiting for SSH to become available...")
var comm packer.Communicator var comm packer.Communicator
for { for {
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
...@@ -97,7 +100,22 @@ func (s *stepWaitForSSH) waitForSSH(state map[string]interface{}) (packer.Commun ...@@ -97,7 +100,22 @@ func (s *stepWaitForSSH) waitForSSH(state map[string]interface{}) (packer.Commun
return nil, errors.New("SSH wait cancelled") return nil, errors.New("SSH wait cancelled")
} }
// First we request the TCP connection information
address, err := s.SSHAddress(state)
if err != nil {
log.Printf("Error getting SSH address: %s", err)
continue
}
// Retrieve the SSH configuration
sshConfig, err := s.SSHConfig(state)
if err != nil {
log.Printf("Error getting SSH config: %s", err)
continue
}
// Attempt to connect to SSH port // Attempt to connect to SSH port
connFunc := ssh.ConnectFunc("tcp", address, 5*time.Minute)
nc, err := connFunc() nc, err := connFunc()
if err != nil { if err != nil {
log.Printf("TCP connection to SSH ip/port failed: %s", err) log.Printf("TCP connection to SSH ip/port failed: %s", err)
...@@ -108,39 +126,22 @@ func (s *stepWaitForSSH) waitForSSH(state map[string]interface{}) (packer.Commun ...@@ -108,39 +126,22 @@ func (s *stepWaitForSSH) waitForSSH(state map[string]interface{}) (packer.Commun
// Then we attempt to connect via SSH // Then we attempt to connect via SSH
config := &ssh.Config{ config := &ssh.Config{
Connection: connFunc, Connection: connFunc,
SSHConfig: &gossh.ClientConfig{ SSHConfig: sshConfig,
User: config.SSHUser,
Auth: []gossh.ClientAuth{
gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)),
gossh.ClientAuthKeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
},
},
} }
sshConnectSuccess := make(chan bool, 1) comm, err = ssh.New(config)
go func() { if err != nil {
comm, err = ssh.New(config) log.Printf("SSH handshake err: %s", err)
if err != nil {
log.Printf("SSH connection fail: %s", err)
sshConnectSuccess <- false
return
}
sshConnectSuccess <- true
}()
select { handshakeAttempts += 1
case success := <-sshConnectSuccess: if handshakeAttempts < 10 {
if !success { // Try to connect via SSH a handful of times
continue continue
} }
case <-time.After(5 * time.Second):
log.Printf("SSH handshake timeout. Trying again.") return nil, err
continue
} }
ui.Say("Connected via SSH!")
break break
} }
......
package common
import (
"github.com/mitchellh/multistep"
"testing"
)
func TestStepConnectSSH_Impl(t *testing.T) {
var raw interface{}
raw = new(StepConnectSSH)
if _, ok := raw.(multistep.Step); !ok {
t.Fatalf("connect ssh should be a step")
}
}
...@@ -216,7 +216,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -216,7 +216,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
new(stepCreateSSHKey), new(stepCreateSSHKey),
new(stepCreateDroplet), new(stepCreateDroplet),
new(stepDropletInfo), new(stepDropletInfo),
new(stepConnectSSH), &common.StepConnectSSH{
SSHAddress: sshAddress,
SSHConfig: sshConfig,
SSHWaitTimeout: 5 * time.Minute,
},
new(stepProvision), new(stepProvision),
new(stepPowerOff), new(stepPowerOff),
new(stepSnapshot), new(stepSnapshot),
......
package digitalocean
import (
gossh "code.google.com/p/go.crypto/ssh"
"fmt"
"github.com/mitchellh/packer/communicator/ssh"
)
func sshAddress(state map[string]interface{}) (string, error) {
config := state["config"].(config)
ipAddress := state["droplet_ip"].(string)
return fmt.Sprintf("%s:%d", ipAddress, config.SSHPort), nil
}
func sshConfig(state map[string]interface{}) (*gossh.ClientConfig, error) {
config := state["config"].(config)
privateKey := state["privateKey"].(string)
keyring := new(ssh.SimpleKeychain)
if err := keyring.AddPEMKey(privateKey); err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
return &gossh.ClientConfig{
User: config.SSHUsername,
Auth: []gossh.ClientAuth{
gossh.ClientAuthKeyring(keyring),
},
}, nil
}
package digitalocean
import (
gossh "code.google.com/p/go.crypto/ssh"
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/communicator/ssh"
"github.com/mitchellh/packer/packer"
"log"
"time"
)
type stepConnectSSH struct {
comm packer.Communicator
}
func (s *stepConnectSSH) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(config)
privateKey := state["privateKey"].(string)
ui := state["ui"].(packer.Ui)
ipAddress := state["droplet_ip"]
// Build the keyring for authentication. This stores the private key
// we'll use to authenticate.
keyring := &ssh.SimpleKeychain{}
err := keyring.AddPEMKey(privateKey)
if err != nil {
err := fmt.Errorf("Error setting up SSH config: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
connFunc := ssh.ConnectFunc(
"tcp",
fmt.Sprintf("%s:%d", ipAddress, config.SSHPort),
5*time.Minute)
// Build the actual SSH client configuration
sshConfig := &ssh.Config{
Connection: connFunc,
SSHConfig: &gossh.ClientConfig{
User: config.SSHUsername,
Auth: []gossh.ClientAuth{
gossh.ClientAuthKeyring(keyring),
},
},
}
// Start trying to connect to SSH
connected := make(chan error, 1)
connectQuit := make(chan bool, 1)
defer func() {
connectQuit <- true
}()
var comm packer.Communicator
go func() {
ui.Say("Connecting to the droplet via SSH...")
attempts := 0
handshakeAttempts := 0
for {
select {
case <-connectQuit:
return
default:
}
// A brief sleep so we're not being overly zealous attempting
// to connect to the instance.
time.Sleep(500 * time.Millisecond)
attempts += 1
nc, err := connFunc()
if err != nil {
continue
}
nc.Close()
log.Println("TCP connection made. Attempting SSH handshake.")
comm, err = ssh.New(sshConfig)
if err == nil {
log.Println("Connected to SSH!")
break
}
handshakeAttempts += 1
log.Printf("SSH handshake error: %s", err)
if handshakeAttempts > 5 {
connected <- err
return
}
}
connected <- nil
}()
log.Printf("Waiting up to %s for SSH connection", config.sshTimeout)
timeout := time.After(config.sshTimeout)
ConnectWaitLoop:
for {
select {
case err := <-connected:
if err != nil {
err := fmt.Errorf("Error connecting to SSH: %s", err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
// We connected. Just break the loop.
break ConnectWaitLoop
case <-timeout:
err := errors.New("Timeout waiting for SSH to become available.")
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
case <-time.After(1 * time.Second):
if _, ok := state[multistep.StateCancelled]; ok {
log.Println("Interrupt detected, quitting waiting for SSH.")
return multistep.ActionHalt
}
}
}
// Set the communicator on the state bag so it can be used later
s.comm = comm
state["communicator"] = comm
return multistep.ActionContinue
}
func (s *stepConnectSSH) Cleanup(map[string]interface{}) {
if s.comm != nil {
// TODO: close
s.comm = nil
}
}
...@@ -323,7 +323,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -323,7 +323,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
new(stepVBoxManage), new(stepVBoxManage),
new(stepRun), new(stepRun),
new(stepTypeBootCommand), new(stepTypeBootCommand),
new(stepWaitForSSH), &common.StepConnectSSH{
SSHAddress: sshAddress,
SSHConfig: sshConfig,
SSHWaitTimeout: b.config.sshWaitTimeout,
},
new(stepUploadVersion), new(stepUploadVersion),
new(stepUploadGuestAdditions), new(stepUploadGuestAdditions),
new(stepProvision), new(stepProvision),
......
package virtualbox
import (
gossh "code.google.com/p/go.crypto/ssh"
"fmt"
"github.com/mitchellh/packer/communicator/ssh"
)
func sshAddress(state map[string]interface{}) (string, error) {
sshHostPort := state["sshHostPort"].(uint)
return fmt.Sprintf("127.0.0.1:%d", sshHostPort), nil
}
func sshConfig(state map[string]interface{}) (*gossh.ClientConfig, error) {
config := state["config"].(*config)
return &gossh.ClientConfig{
User: config.SSHUser,
Auth: []gossh.ClientAuth{
gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)),
gossh.ClientAuthKeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
},
}, nil
}
...@@ -284,7 +284,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -284,7 +284,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&stepConfigureVNC{}, &stepConfigureVNC{},
&stepRun{}, &stepRun{},
&stepTypeBootCommand{}, &stepTypeBootCommand{},
&stepWaitForSSH{}, &common.StepConnectSSH{
SSHAddress: sshAddress,
SSHConfig: sshConfig,
SSHWaitTimeout: b.config.sshWaitTimeout,
},
&stepUploadTools{}, &stepUploadTools{},
&stepProvision{}, &stepProvision{},
&stepShutdown{}, &stepShutdown{},
......
package vmware
import (
gossh "code.google.com/p/go.crypto/ssh"
"errors"
"fmt"
"github.com/mitchellh/packer/communicator/ssh"
"io/ioutil"
"log"
"os"
)
func sshAddress(state map[string]interface{}) (string, error) {
config := state["config"].(*config)
vmxPath := state["vmx_path"].(string)
log.Println("Lookup up IP information...")
f, err := os.Open(vmxPath)
if err != nil {
return "", err
}
defer f.Close()
vmxBytes, err := ioutil.ReadAll(f)
if err != nil {
return "", err
}
vmxData := ParseVMX(string(vmxBytes))
var ok bool
macAddress := ""
if macAddress, ok = vmxData["ethernet0.address"]; !ok || macAddress == "" {
if macAddress, ok = vmxData["ethernet0.generatedAddress"]; !ok || macAddress == "" {
return "", errors.New("couldn't find MAC address in VMX")
}
}
ipLookup := &DHCPLeaseGuestLookup{
Device: "vmnet8",
MACAddress: macAddress,
}
ipAddress, err := ipLookup.GuestIP()
if err != nil {
log.Printf("IP lookup failed: %s", err)
return "", fmt.Errorf("IP lookup failed: %s", err)
}
log.Printf("Detected IP: %s", ipAddress)
return fmt.Sprintf("%s:%d", ipAddress, config.SSHPort), nil
}
func sshConfig(state map[string]interface{}) (*gossh.ClientConfig, error) {
config := state["config"].(*config)
return &gossh.ClientConfig{
User: config.SSHUser,
Auth: []gossh.ClientAuth{
gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)),
gossh.ClientAuthKeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
},
}, nil
}
package vmware
import (
gossh "code.google.com/p/go.crypto/ssh"
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/communicator/ssh"
"github.com/mitchellh/packer/packer"
"io/ioutil"
"log"
"os"
"time"
)
// This step waits for SSH to become available and establishes an SSH
// connection.
//
// Uses:
// config *config
// ui packer.Ui
// vmx_path string
//
// Produces:
// communicator packer.Communicator
type stepWaitForSSH struct {
cancel bool
comm packer.Communicator
}
func (s *stepWaitForSSH) Run(state map[string]interface{}) multistep.StepAction {
config := state["config"].(*config)
ui := state["ui"].(packer.Ui)
var comm packer.Communicator
var err error
waitDone := make(chan bool, 1)
go func() {
comm, err = s.waitForSSH(state)
waitDone <- true
}()
log.Printf("Waiting for SSH, up to timeout: %s", config.sshWaitTimeout.String())
timeout := time.After(config.sshWaitTimeout)
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))
return multistep.ActionHalt
}
s.comm = comm
state["communicator"] = comm
break WaitLoop
case <-timeout:
ui.Error("Timeout waiting for SSH.")
s.cancel = true
return multistep.ActionHalt
case <-time.After(1 * time.Second):
if _, ok := state[multistep.StateCancelled]; ok {
log.Println("Interrupt detected, quitting waiting for SSH.")
return multistep.ActionHalt
}
}
}
return multistep.ActionContinue
}
func (s *stepWaitForSSH) Cleanup(map[string]interface{}) {
if s.comm != nil {
// TODO: close
s.comm = nil
}
}
// Reads the network information for lookup via DHCP.
func (s *stepWaitForSSH) dhcpLeaseLookup(vmxPath string) (GuestIPFinder, error) {
f, err := os.Open(vmxPath)
if err != nil {
return nil, err
}
defer f.Close()
vmxBytes, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
vmxData := ParseVMX(string(vmxBytes))
var ok bool
macAddress := ""
if macAddress, ok = vmxData["ethernet0.address"]; !ok || macAddress == "" {
if macAddress, ok = vmxData["ethernet0.generatedAddress"]; !ok || macAddress == "" {
return nil, errors.New("couldn't find MAC address in VMX")
}
}
return &DHCPLeaseGuestLookup{"vmnet8", macAddress}, nil
}
// This blocks until SSH becomes available, and sends the communicator
// on the given channel.
func (s *stepWaitForSSH) waitForSSH(state map[string]interface{}) (packer.Communicator, error) {
config := state["config"].(*config)
ui := state["ui"].(packer.Ui)
vmxPath := state["vmx_path"].(string)
handshakeAttempts := 0
ui.Say("Waiting for SSH to become available...")
var comm packer.Communicator
for {
time.Sleep(5 * time.Second)
if s.cancel {
log.Println("SSH wait cancelled. Exiting loop.")
return nil, errors.New("SSH wait cancelled")
}
// First we wait for the IP to become available...
log.Println("Lookup up IP information...")
ipLookup, err := s.dhcpLeaseLookup(vmxPath)
if err != nil {
log.Printf("Can't lookup via DHCP lease: %s", err)
}
ip, err := ipLookup.GuestIP()
if err != nil {
log.Printf("IP lookup failed: %s", err)
continue
}
log.Printf("Detected IP: %s", ip)
// Attempt to connect to SSH port
connFunc := ssh.ConnectFunc(
"tcp", fmt.Sprintf("%s:%d", ip, config.SSHPort), 5*time.Minute)
nc, err := connFunc()
if err != nil {
log.Printf("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: &gossh.ClientConfig{
User: config.SSHUser,
Auth: []gossh.ClientAuth{
gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)),
gossh.ClientAuthKeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
},
},
}
comm, err = ssh.New(config)
if err != nil {
log.Printf("SSH handshake err: %s", err)
handshakeAttempts += 1
if handshakeAttempts < 10 {
// Try to connect via SSH a handful of times
continue
}
return nil, err
}
ui.Say("Connected via SSH!")
break
}
return comm, nil
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment