From 48a3892ce65a894f0275c9118a985855f76765ac Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 12 Jun 2013 18:02:42 -0700 Subject: [PATCH] builder/virtualbox: graceful shutdown --- builder/virtualbox/builder.go | 13 ++++++ builder/virtualbox/builder_test.go | 19 ++++++++ builder/virtualbox/driver.go | 32 +++++++++++++ builder/virtualbox/step_shutdown.go | 72 +++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+) create mode 100644 builder/virtualbox/step_shutdown.go diff --git a/builder/virtualbox/builder.go b/builder/virtualbox/builder.go index b73cf6695..b06466c57 100644 --- a/builder/virtualbox/builder.go +++ b/builder/virtualbox/builder.go @@ -32,6 +32,8 @@ type config struct { ISOMD5 string `mapstructure:"iso_md5"` ISOUrl string `mapstructure:"iso_url"` OutputDir string `mapstructure:"output_directory"` + ShutdownCommand string `mapstructure:"shutdown_command"` + ShutdownTimeout time.Duration `` SSHHostPortMin uint `mapstructure:"ssh_host_port_min"` SSHHostPortMax uint `mapstructure:"ssh_host_port_max"` SSHPassword string `mapstructure:"ssh_password"` @@ -41,6 +43,7 @@ type config struct { VMName string `mapstructure:"vm_name"` RawBootWait string `mapstructure:"boot_wait"` + RawShutdownTimeout string `mapstructure:"shutdown_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 { 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(stepWaitForSSH), new(stepProvision), + new(stepShutdown), } // Setup the state bag diff --git a/builder/virtualbox/builder_test.go b/builder/virtualbox/builder_test.go index a0558ae35..6a741d0e1 100644 --- a/builder/virtualbox/builder_test.go +++ b/builder/virtualbox/builder_test.go @@ -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) { var b Builder config := testConfig() diff --git a/builder/virtualbox/driver.go b/builder/virtualbox/driver.go index 73c903a42..1723c1fe6 100644 --- a/builder/virtualbox/driver.go +++ b/builder/virtualbox/driver.go @@ -12,6 +12,12 @@ import ( // A driver is able to talk to VirtualBox and perform certain // operations with it. 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 // suppress any annoying popups from VirtualBox. SuppressMessages() error @@ -30,6 +36,32 @@ type VBox42Driver struct { 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 { extraData := map[string]string{ "GUI/RegistrationData": "triesLeft=0", diff --git a/builder/virtualbox/step_shutdown.go b/builder/virtualbox/step_shutdown.go new file mode 100644 index 000000000..edc725b8d --- /dev/null +++ b/builder/virtualbox/step_shutdown.go @@ -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: +// +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{}) {}