Commit 5c0d8ecd authored by Mitchell Hashimoto's avatar Mitchell Hashimoto

builder/virtualbox: Wait for SSH to become available

parent e7cc3505
......@@ -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"`
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
......
......@@ -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)
}
}
......@@ -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)
}
......
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
}
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