builder/virtualbox: graceful shutdown

This commit is contained in:
Mitchell Hashimoto 2013-06-12 18:02:42 -07:00
parent 52391bb1f3
commit 48a3892ce6
4 changed files with 136 additions and 0 deletions

View File

@ -32,6 +32,8 @@ type config struct {
ISOMD5 string `mapstructure:"iso_md5"` ISOMD5 string `mapstructure:"iso_md5"`
ISOUrl string `mapstructure:"iso_url"` ISOUrl string `mapstructure:"iso_url"`
OutputDir string `mapstructure:"output_directory"` OutputDir string `mapstructure:"output_directory"`
ShutdownCommand string `mapstructure:"shutdown_command"`
ShutdownTimeout time.Duration ``
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"` SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"` SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
SSHPassword string `mapstructure:"ssh_password"` SSHPassword string `mapstructure:"ssh_password"`
@ -41,6 +43,7 @@ type config struct {
VMName string `mapstructure:"vm_name"` VMName string `mapstructure:"vm_name"`
RawBootWait string `mapstructure:"boot_wait"` RawBootWait string `mapstructure:"boot_wait"`
RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"` RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
} }
@ -140,6 +143,15 @@ func (b *Builder) Prepare(raw interface{}) error {
} }
} }
if b.config.RawShutdownTimeout == "" {
b.config.RawShutdownTimeout = "5m"
}
b.config.ShutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
}
if b.config.SSHHostPortMin > b.config.SSHHostPortMax { if b.config.SSHHostPortMin > b.config.SSHHostPortMax {
errs = append(errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max")) errs = append(errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
} }
@ -183,6 +195,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
new(stepTypeBootCommand), new(stepTypeBootCommand),
new(stepWaitForSSH), new(stepWaitForSSH),
new(stepProvision), new(stepProvision),
new(stepShutdown),
} }
// Setup the state bag // Setup the state bag

View File

@ -171,6 +171,25 @@ func TestBuilderPrepare_ISOUrl(t *testing.T) {
} }
} }
func TestBuilderPrepare_ShutdownTimeout(t *testing.T) {
var b Builder
config := testConfig()
// Test with a bad value
config["shutdown_timeout"] = "this is not good"
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test with a good one
config["shutdown_timeout"] = "5s"
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_SSHHostPort(t *testing.T) { func TestBuilderPrepare_SSHHostPort(t *testing.T) {
var b Builder var b Builder
config := testConfig() config := testConfig()

View File

@ -12,6 +12,12 @@ import (
// A driver is able to talk to VirtualBox and perform certain // A driver is able to talk to VirtualBox and perform certain
// operations with it. // operations with it.
type Driver interface { type Driver interface {
// Checks if the VM with the given name is running.
IsRunning(string) (bool, error)
// Stop stops a running machine, forcefully.
Stop(string) error
// SuppressMessages should do what needs to be done in order to // SuppressMessages should do what needs to be done in order to
// suppress any annoying popups from VirtualBox. // suppress any annoying popups from VirtualBox.
SuppressMessages() error SuppressMessages() error
@ -30,6 +36,32 @@ type VBox42Driver struct {
VBoxManagePath string VBoxManagePath string
} }
func (d *VBox42Driver) IsRunning(name string) (bool, error) {
var stdout bytes.Buffer
cmd := exec.Command(d.VBoxManagePath, "showvminfo", name, "--machinereadable")
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
return false, err
}
for _, line := range strings.Split(stdout.String(), "\n") {
if line == `VMState="running"` {
return true, nil
}
}
return false, nil
}
func (d *VBox42Driver) Stop(name string) error {
if err := d.VBoxManage("controlvm", name, "poweroff"); err != nil {
return err
}
return nil
}
func (d *VBox42Driver) SuppressMessages() error { func (d *VBox42Driver) SuppressMessages() error {
extraData := map[string]string{ extraData := map[string]string{
"GUI/RegistrationData": "triesLeft=0", "GUI/RegistrationData": "triesLeft=0",

View File

@ -0,0 +1,72 @@
package virtualbox
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"time"
)
// This step shuts down the machine. It first attempts to do so gracefully,
// but ultimately forcefully shuts it down if that fails.
//
// Uses:
// communicator packer.Communicator
// config *config
// driver Driver
// ui packer.Ui
// vmName string
//
// Produces:
// <nothing>
type stepShutdown struct{}
func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
comm := state["communicator"].(packer.Communicator)
config := state["config"].(*config)
driver := state["driver"].(Driver)
ui := state["ui"].(packer.Ui)
vmName := state["vmName"].(string)
if config.ShutdownCommand != "" {
ui.Say("Gracefully halting virtual machine...")
log.Printf("Executing shutdown command: %s", config.ShutdownCommand)
cmd := &packer.RemoteCmd{Command: config.ShutdownCommand}
if err := comm.Start(cmd); err != nil {
ui.Error(fmt.Sprintf("Failed to send shutdown command: %s", err))
return multistep.ActionHalt
}
// Wait for the command to run
cmd.Wait()
// Wait for the machine to actually shut down
log.Printf("Waiting max %s for shutdown to complete", config.ShutdownTimeout)
shutdownTimer := time.After(config.ShutdownTimeout)
for {
running, _ := driver.IsRunning(vmName)
if !running {
break
}
select {
case <-shutdownTimer:
ui.Error("Timeout while waiting for machine to shut down.")
return multistep.ActionHalt
default:
time.Sleep(1 * time.Second)
}
}
} else {
if err := driver.Stop(vmName); err != nil {
ui.Error(fmt.Sprintf("Error stopping VM: %s", err))
return multistep.ActionHalt
}
}
log.Println("VM shut down.")
return multistep.ActionContinue
}
func (s *stepShutdown) Cleanup(state map[string]interface{}) {}