From 193de1f5d311fad6e8ee4b24391b031bbf02466a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 15 Jul 2013 11:41:48 +0900 Subject: [PATCH 1/7] builder/common: add common StepConnectSSH for builders --- builder/common/step_connect_ssh.go | 146 ++++++++++++++++++++++++ builder/common/step_connect_ssh_test.go | 14 +++ 2 files changed, 160 insertions(+) create mode 100644 builder/common/step_connect_ssh.go create mode 100644 builder/common/step_connect_ssh_test.go diff --git a/builder/common/step_connect_ssh.go b/builder/common/step_connect_ssh.go new file mode 100644 index 000000000..881b5067c --- /dev/null +++ b/builder/common/step_connect_ssh.go @@ -0,0 +1,146 @@ +package common + +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" +) + +// StepConnectSSH is a multistep Step implementation that waits for SSH +// to become available. It gets the connection information from a single +// configuration when creating the step. +// +// Uses: +// ui packer.Ui +// +// Produces: +// communicator packer.Communicator +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() (string, error) + + // SSHConfig is a function that returns the proper client configuration + // for SSH access. + SSHConfig func() (*gossh.ClientConfig, error) + + // SSHWaitTimeout is the total timeout to wait for SSH to become available. + SSHWaitTimeout time.Duration + + cancel bool + comm packer.Communicator +} + +func (s *StepConnectSSH) Run(state map[string]interface{}) multistep.StepAction { + ui := state["ui"].(packer.Ui) + + var comm packer.Communicator + var err error + + waitDone := make(chan bool, 1) + go func() { + ui.Say("Waiting for SSH to become available...") + comm, err = s.waitForSSH() + waitDone <- true + }() + + log.Printf("Waiting for SSH, up to timeout: %s", s.SSHWaitTimeout) + timeout := time.After(s.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 + } + + ui.Say("Connected to SSH!") + 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{}) { +} + +func (s *StepConnectSSH) waitForSSH() (packer.Communicator, error) { + handshakeAttempts := 0 + + 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 request the TCP connection information + address, err := s.SSHAddress() + if err != nil { + log.Printf("Error getting SSH address: %s", err) + continue + } + + // Retrieve the SSH configuration + sshConfig, err := s.SSHConfig() + if err != nil { + log.Printf("Error getting SSH config: %s", err) + continue + } + + // Attempt to connect to SSH port + connFunc := ssh.ConnectFunc("tcp", address, 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: sshConfig, + } + + 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 + } + + break + } + + return comm, nil +} diff --git a/builder/common/step_connect_ssh_test.go b/builder/common/step_connect_ssh_test.go new file mode 100644 index 000000000..49b0e52b4 --- /dev/null +++ b/builder/common/step_connect_ssh_test.go @@ -0,0 +1,14 @@ +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") + } +} From 29bfab06310c6beb03d48bcd6472c2cd6fcf6333 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 15 Jul 2013 14:06:41 +0900 Subject: [PATCH 2/7] builder/amazonebs: switch to common SSH connect step --- builder/amazonebs/builder.go | 6 +- builder/amazonebs/ssh.go | 31 ++++++ builder/amazonebs/step_connect_ssh.go | 149 -------------------------- builder/common/step_connect_ssh.go | 12 +-- 4 files changed, 42 insertions(+), 156 deletions(-) create mode 100644 builder/amazonebs/ssh.go delete mode 100644 builder/amazonebs/step_connect_ssh.go diff --git a/builder/amazonebs/builder.go b/builder/amazonebs/builder.go index 2d93399ec..83e3b0a91 100644 --- a/builder/amazonebs/builder.go +++ b/builder/amazonebs/builder.go @@ -159,7 +159,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &stepKeyPair{}, &stepSecurityGroup{}, &stepRunSourceInstance{}, - &stepConnectSSH{}, + &common.StepConnectSSH{ + SSHAddress: sshAddress, + SSHConfig: sshConfig, + SSHWaitTimeout: b.config.sshTimeout, + }, &stepProvision{}, &stepStopInstance{}, &stepCreateAMI{}, diff --git a/builder/amazonebs/ssh.go b/builder/amazonebs/ssh.go new file mode 100644 index 000000000..95bbda4cb --- /dev/null +++ b/builder/amazonebs/ssh.go @@ -0,0 +1,31 @@ +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 +} diff --git a/builder/amazonebs/step_connect_ssh.go b/builder/amazonebs/step_connect_ssh.go deleted file mode 100644 index 84e511a87..000000000 --- a/builder/amazonebs/step_connect_ssh.go +++ /dev/null @@ -1,149 +0,0 @@ -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 -} diff --git a/builder/common/step_connect_ssh.go b/builder/common/step_connect_ssh.go index 881b5067c..6c05db935 100644 --- a/builder/common/step_connect_ssh.go +++ b/builder/common/step_connect_ssh.go @@ -24,11 +24,11 @@ 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() (string, error) + SSHAddress func(map[string]interface{}) (string, error) // SSHConfig is a function that returns the proper client configuration // for SSH access. - SSHConfig func() (*gossh.ClientConfig, error) + SSHConfig func(map[string]interface{}) (*gossh.ClientConfig, error) // SSHWaitTimeout is the total timeout to wait for SSH to become available. SSHWaitTimeout time.Duration @@ -46,7 +46,7 @@ func (s *StepConnectSSH) Run(state map[string]interface{}) multistep.StepAction waitDone := make(chan bool, 1) go func() { ui.Say("Waiting for SSH to become available...") - comm, err = s.waitForSSH() + comm, err = s.waitForSSH(state) waitDone <- true }() @@ -85,7 +85,7 @@ WaitLoop: func (s *StepConnectSSH) Cleanup(map[string]interface{}) { } -func (s *StepConnectSSH) waitForSSH() (packer.Communicator, error) { +func (s *StepConnectSSH) waitForSSH(state map[string]interface{}) (packer.Communicator, error) { handshakeAttempts := 0 var comm packer.Communicator @@ -98,14 +98,14 @@ func (s *StepConnectSSH) waitForSSH() (packer.Communicator, error) { } // First we request the TCP connection information - address, err := s.SSHAddress() + 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() + sshConfig, err := s.SSHConfig(state) if err != nil { log.Printf("Error getting SSH config: %s", err) continue From ac7807e7e5bb782c271c1ab2db3014da9398a79e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 15 Jul 2013 14:10:30 +0900 Subject: [PATCH 3/7] builder/common: if cancel during SSH, cancel the attempts --- builder/common/step_connect_ssh.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/builder/common/step_connect_ssh.go b/builder/common/step_connect_ssh.go index 6c05db935..2f32b7c0f 100644 --- a/builder/common/step_connect_ssh.go +++ b/builder/common/step_connect_ssh.go @@ -73,6 +73,9 @@ WaitLoop: return multistep.ActionHalt case <-time.After(1 * time.Second): 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.") return multistep.ActionHalt } From 3bf49d1c66038acc0bc89f69b3a3b4736dc8696f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 15 Jul 2013 14:14:10 +0900 Subject: [PATCH 4/7] builder/digitalocean: use common connect ssh --- builder/digitalocean/builder.go | 6 +- builder/digitalocean/ssh.go | 30 +++++ builder/digitalocean/step_connect_ssh.go | 141 ----------------------- 3 files changed, 35 insertions(+), 142 deletions(-) create mode 100644 builder/digitalocean/ssh.go delete mode 100644 builder/digitalocean/step_connect_ssh.go diff --git a/builder/digitalocean/builder.go b/builder/digitalocean/builder.go index d03459987..df6f4750c 100644 --- a/builder/digitalocean/builder.go +++ b/builder/digitalocean/builder.go @@ -216,7 +216,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe new(stepCreateSSHKey), new(stepCreateDroplet), new(stepDropletInfo), - new(stepConnectSSH), + &common.StepConnectSSH{ + SSHAddress: sshAddress, + SSHConfig: sshConfig, + SSHWaitTimeout: 5 * time.Minute, + }, new(stepProvision), new(stepPowerOff), new(stepSnapshot), diff --git a/builder/digitalocean/ssh.go b/builder/digitalocean/ssh.go new file mode 100644 index 000000000..bc35407c2 --- /dev/null +++ b/builder/digitalocean/ssh.go @@ -0,0 +1,30 @@ +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 +} diff --git a/builder/digitalocean/step_connect_ssh.go b/builder/digitalocean/step_connect_ssh.go deleted file mode 100644 index c25923201..000000000 --- a/builder/digitalocean/step_connect_ssh.go +++ /dev/null @@ -1,141 +0,0 @@ -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 - } -} From 979bfc412e2f7d08b47af2370bf46f11fdcc2bf8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 15 Jul 2013 14:17:09 +0900 Subject: [PATCH 5/7] builder/virtualbox: use common SSH connect step --- builder/virtualbox/builder.go | 6 +- builder/virtualbox/ssh.go | 25 ++++ builder/virtualbox/step_wait_for_ssh.go | 148 ------------------------ 3 files changed, 30 insertions(+), 149 deletions(-) create mode 100644 builder/virtualbox/ssh.go delete mode 100644 builder/virtualbox/step_wait_for_ssh.go diff --git a/builder/virtualbox/builder.go b/builder/virtualbox/builder.go index a925f4a65..0cc6df57e 100644 --- a/builder/virtualbox/builder.go +++ b/builder/virtualbox/builder.go @@ -323,7 +323,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe new(stepVBoxManage), new(stepRun), new(stepTypeBootCommand), - new(stepWaitForSSH), + &common.StepConnectSSH{ + SSHAddress: sshAddress, + SSHConfig: sshConfig, + SSHWaitTimeout: b.config.sshWaitTimeout, + }, new(stepUploadVersion), new(stepUploadGuestAdditions), new(stepProvision), diff --git a/builder/virtualbox/ssh.go b/builder/virtualbox/ssh.go new file mode 100644 index 000000000..f67133a70 --- /dev/null +++ b/builder/virtualbox/ssh.go @@ -0,0 +1,25 @@ +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 +} diff --git a/builder/virtualbox/step_wait_for_ssh.go b/builder/virtualbox/step_wait_for_ssh.go deleted file mode 100644 index 8a2db4133..000000000 --- a/builder/virtualbox/step_wait_for_ssh.go +++ /dev/null @@ -1,148 +0,0 @@ -package virtualbox - -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" -) - -// This step waits for SSH to become available and establishes an SSH -// connection. -// -// Uses: -// config *config -// sshHostPort uint -// ui packer.Ui -// -// 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 - } -} - -// 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) - 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 - for { - time.Sleep(5 * time.Second) - - if s.cancel { - log.Println("SSH wait cancelled. Exiting loop.") - return nil, errors.New("SSH wait cancelled") - } - - // Attempt to connect to SSH port - 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)), - }, - }, - } - - 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 -} From fd58b33b38ccfe2ed104e529e0d69b82b60ad56d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 15 Jul 2013 14:22:13 +0900 Subject: [PATCH 6/7] builder/vmware: convert to cmmon ssh step --- builder/vmware/builder.go | 6 +- builder/vmware/ssh.go | 65 ++++++++++ builder/vmware/step_wait_for_ssh.go | 183 ---------------------------- 3 files changed, 70 insertions(+), 184 deletions(-) create mode 100644 builder/vmware/ssh.go delete mode 100644 builder/vmware/step_wait_for_ssh.go diff --git a/builder/vmware/builder.go b/builder/vmware/builder.go index ae44fb215..c4fbe0931 100644 --- a/builder/vmware/builder.go +++ b/builder/vmware/builder.go @@ -284,7 +284,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &stepConfigureVNC{}, &stepRun{}, &stepTypeBootCommand{}, - &stepWaitForSSH{}, + &common.StepConnectSSH{ + SSHAddress: sshAddress, + SSHConfig: sshConfig, + SSHWaitTimeout: b.config.sshWaitTimeout, + }, &stepUploadTools{}, &stepProvision{}, &stepShutdown{}, diff --git a/builder/vmware/ssh.go b/builder/vmware/ssh.go new file mode 100644 index 000000000..66d477e68 --- /dev/null +++ b/builder/vmware/ssh.go @@ -0,0 +1,65 @@ +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 +} diff --git a/builder/vmware/step_wait_for_ssh.go b/builder/vmware/step_wait_for_ssh.go deleted file mode 100644 index 84e475454..000000000 --- a/builder/vmware/step_wait_for_ssh.go +++ /dev/null @@ -1,183 +0,0 @@ -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 -} From 9e3885e19217e56719d3cf3a52e1b213b07c4c49 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 15 Jul 2013 14:22:55 +0900 Subject: [PATCH 7/7] CHANGELOG --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee6a71dda..cab28e1c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,8 +27,10 @@ FEATURES: IMPROVEMENTS: -* everything: invalid keys in configuration are now considered validation +* core: invalid keys in configuration are now considered validation 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: Verify the source AMI is EBS-backed before launching. [GH-169] * shell provisioner: the build name and builder type are available in