From 5c0d8ecd72ff1b083d042b6d38e3ad35b4c2e118 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 12 Jun 2013 00:41:58 -0700 Subject: [PATCH] builder/virtualbox: Wait for SSH to become available --- builder/virtualbox/builder.go | 20 ++- builder/virtualbox/builder_test.go | 37 ++++++ builder/virtualbox/step_type_boot_command.go | 5 +- builder/virtualbox/step_wait_for_ssh.go | 132 +++++++++++++++++++ 4 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 builder/virtualbox/step_wait_for_ssh.go diff --git a/builder/virtualbox/builder.go b/builder/virtualbox/builder.go index e1f3bc1c2..86ca516f9 100644 --- a/builder/virtualbox/builder.go +++ b/builder/virtualbox/builder.go @@ -34,10 +34,14 @@ type config struct { OutputDir string `mapstructure:"output_directory"` SSHHostPortMin uint `mapstructure:"ssh_host_port_min"` SSHHostPortMax uint `mapstructure:"ssh_host_port_max"` + SSHPassword string `mapstructure:"ssh_password"` SSHPort uint `mapstructure:"ssh_port"` + SSHUser string `mapstructure:"ssh_username"` + SSHWaitTimeout time.Duration `` VMName string `mapstructure:"vm_name"` - RawBootWait string `mapstructure:"boot_wait"` + RawBootWait string `mapstructure:"boot_wait"` + RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"` } func (b *Builder) Prepare(raw interface{}) error { @@ -140,6 +144,19 @@ func (b *Builder) Prepare(raw interface{}) error { errs = append(errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max")) } + if b.config.SSHUser == "" { + errs = append(errs, errors.New("An ssh_username must be specified.")) + } + + if b.config.RawSSHWaitTimeout == "" { + b.config.RawSSHWaitTimeout = "20m" + } + + b.config.SSHWaitTimeout, err = time.ParseDuration(b.config.RawSSHWaitTimeout) + if err != nil { + errs = append(errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err)) + } + b.driver, err = b.newDriver() if err != nil { errs = append(errs, fmt.Errorf("Failed creating VirtualBox driver: %s", err)) @@ -164,6 +181,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) packer new(stepForwardSSH), new(stepRun), new(stepTypeBootCommand), + new(stepWaitForSSH), } // Setup the state bag diff --git a/builder/virtualbox/builder_test.go b/builder/virtualbox/builder_test.go index 99b140cfd..1682f0113 100644 --- a/builder/virtualbox/builder_test.go +++ b/builder/virtualbox/builder_test.go @@ -11,6 +11,7 @@ func testConfig() map[string]interface{} { return map[string]interface{}{ "iso_md5": "foo", "iso_url": "http://www.google.com/", + "ssh_username": "foo", } } @@ -197,3 +198,39 @@ func TestBuilderPrepare_SSHHostPort(t *testing.T) { t.Fatalf("should not have error: %s", err) } } + +func TestBuilderPrepare_SSHUser(t *testing.T) { + var b Builder + config := testConfig() + + config["ssh_username"] = "" + err := b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + config["ssh_username"] = "exists" + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) { + var b Builder + config := testConfig() + + // Test with a bad value + config["ssh_wait_timeout"] = "this is not good" + err := b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test with a good one + config["ssh_wait_timeout"] = "5s" + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} diff --git a/builder/virtualbox/step_type_boot_command.go b/builder/virtualbox/step_type_boot_command.go index 0cb816622..c5726088b 100644 --- a/builder/virtualbox/step_type_boot_command.go +++ b/builder/virtualbox/step_type_boot_command.go @@ -66,7 +66,6 @@ func (s *stepTypeBootCommand) Run(state map[string]interface{}) multistep.StepAc } } - time.Sleep(15 * time.Second) return multistep.ActionContinue } @@ -103,7 +102,7 @@ func scancodes(message string) []string { } } - result := make([]string, 0, len(message) * 2) + result := make([]string, 0, len(message)*2) for len(message) > 0 { var scancode []string @@ -141,7 +140,7 @@ func scancodes(message string) []string { scancode = append(scancode, "aa") } - scancode = append(scancode, fmt.Sprintf("%02x", scancodeInt + 0x80)) + scancode = append(scancode, fmt.Sprintf("%02x", scancodeInt+0x80)) log.Printf("Sending char '%c', code '%v', shift %v", r, scancode, keyShift) } diff --git a/builder/virtualbox/step_wait_for_ssh.go b/builder/virtualbox/step_wait_for_ssh.go new file mode 100644 index 000000000..e9069da16 --- /dev/null +++ b/builder/virtualbox/step_wait_for_ssh.go @@ -0,0 +1,132 @@ +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" + "net" + "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 + conn net.Conn +} + +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 + } + + 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.conn != nil { + s.conn.Close() + s.conn = 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) + + ui.Say("Waiting for SSH to become available...") + var comm packer.Communicator + var nc net.Conn + for { + if nc != nil { + nc.Close() + } + + 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 := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", sshHostPort)) + if err != nil { + log.Printf("TCP connection to SSH ip/port failed: %s", err) + continue + } + + // Then we attempt to connect via SSH + sshConfig := &gossh.ClientConfig{ + User: config.SSHUser, + Auth: []gossh.ClientAuth{ + gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)), + }, + } + + comm, err = ssh.New(nc, sshConfig) + if err != nil { + log.Printf("SSH connection fail: %s", err) + nc.Close() + continue + } + + ui.Say("Connected via SSH!") + break + } + + // Store the connection so we can close it later + s.conn = nc + return comm, nil +}