builder/virtualbox: graceful shutdown
This commit is contained in:
parent
52391bb1f3
commit
48a3892ce6
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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{}) {}
|
Loading…
Reference in New Issue