diff --git a/builder/virtualbox/common/driver.go b/builder/virtualbox/common/driver.go index c0085fe2d..bfe8069ac 100644 --- a/builder/virtualbox/common/driver.go +++ b/builder/virtualbox/common/driver.go @@ -42,6 +42,7 @@ type Driver interface { SuppressMessages() error // VBoxManage executes the given VBoxManage command + // and returns the stdout channel as string VBoxManage(...string) error // Verify checks to make sure that this driver should function @@ -51,6 +52,25 @@ type Driver interface { // Version reads the version of VirtualBox that is installed. Version() (string, error) + + // LoadSnapshots Loads all defined snapshots for a vm. + // if no snapshots are defined nil will be returned + LoadSnapshots(string) (*VBoxSnapshot, error) + + // CreateSnapshot Creates a snapshot for a vm with a given name + CreateSnapshot(string, string) error + + // HasSnapshots tests if a vm has snapshots + HasSnapshots(string) (bool, error) + + // GetCurrentSnapshot Returns the current snapshot for a vm + GetCurrentSnapshot(string) (*VBoxSnapshot, error) + + // SetSnapshot sets the for a vm + SetSnapshot(string, *VBoxSnapshot) error + + // DeleteSnapshot deletes the specified snapshot from a vm + DeleteSnapshot(string, *VBoxSnapshot) error } func NewDriver() (Driver, error) { diff --git a/builder/virtualbox/common/driver_4_2.go b/builder/virtualbox/common/driver_4_2.go index fc065a678..1cc3e4ff9 100644 --- a/builder/virtualbox/common/driver_4_2.go +++ b/builder/virtualbox/common/driver_4_2.go @@ -177,6 +177,11 @@ func (d *VBox42Driver) SuppressMessages() error { } func (d *VBox42Driver) VBoxManage(args ...string) error { + _, err := d.VBoxManageWithOutput(args...) + return err +} + +func (d *VBox42Driver) VBoxManageWithOutput(args ...string) (string, error) { var stdout, stderr bytes.Buffer log.Printf("Executing VBoxManage: %#v", args) @@ -204,7 +209,7 @@ func (d *VBox42Driver) VBoxManage(args ...string) error { log.Printf("stdout: %s", stdoutString) log.Printf("stderr: %s", stderrString) - return err + return stdoutString, err } func (d *VBox42Driver) Verify() error { @@ -239,3 +244,84 @@ func (d *VBox42Driver) Version() (string, error) { log.Printf("VirtualBox version: %s", matches[0][1]) return matches[0][1], nil } + +// LoadSnapshots load the snapshots for a VM instance +func (d *VBox42Driver) LoadSnapshots(vmName string) (*VBoxSnapshot, error) { + if vmName == "" { + panic("Argument empty exception: vmName") + } + log.Printf("Executing LoadSnapshots: VM: %s", vmName) + + stdoutString, err := d.VBoxManageWithOutput("snapshot", vmName, "list", "--machinereadable") + if nil != err { + return nil, err + } + + var rootNode *VBoxSnapshot + if stdoutString != "This machine does not have any snapshots" { + rootNode, err = ParseSnapshotData(stdoutString) + if nil != err { + return nil, err + } + } + + return rootNode, nil +} + +func (d *VBox42Driver) CreateSnapshot(vmname string, snapshotName string) error { + if vmname == "" { + panic("Argument empty exception: vmname") + } + log.Printf("Executing CreateSnapshot: VM: %s, SnapshotName %s", vmname, snapshotName) + + return d.VBoxManage("snapshot", vmname, "take", snapshotName) +} + +func (d *VBox42Driver) HasSnapshots(vmname string) (bool, error) { + if vmname == "" { + panic("Argument empty exception: vmname") + } + log.Printf("Executing HasSnapshots: VM: %s", vmname) + + sn, err := d.LoadSnapshots(vmname) + if nil != err { + return false, err + } + return nil != sn, nil +} + +func (d *VBox42Driver) GetCurrentSnapshot(vmname string) (*VBoxSnapshot, error) { + if vmname == "" { + panic("Argument empty exception: vmname") + } + log.Printf("Executing GetCurrentSnapshot: VM: %s", vmname) + + sn, err := d.LoadSnapshots(vmname) + if nil != err { + return nil, err + } + return sn.GetCurrentSnapshot(), nil +} + +func (d *VBox42Driver) SetSnapshot(vmname string, sn *VBoxSnapshot) error { + if vmname == "" { + panic("Argument empty exception: vmname") + } + if nil == sn { + panic("Argument null exception: sn") + } + log.Printf("Executing SetSnapshot: VM: %s, SnapshotName %s", vmname, sn.UUID) + + return d.VBoxManage("snapshot", vmname, "restore", sn.UUID) +} + +func (d *VBox42Driver) DeleteSnapshot(vmname string, sn *VBoxSnapshot) error { + if vmname == "" { + panic("Argument empty exception: vmname") + } + if nil == sn { + panic("Argument null exception: sn") + } + log.Printf("Executing DeleteSnapshot: VM: %s, SnapshotName %s", vmname, sn.UUID) + return d.VBoxManage("snapshot", vmname, "delete", sn.UUID) +} diff --git a/builder/virtualbox/common/driver_mock.go b/builder/virtualbox/common/driver_mock.go index d69a06650..68d57fa64 100644 --- a/builder/virtualbox/common/driver_mock.go +++ b/builder/virtualbox/common/driver_mock.go @@ -45,6 +45,17 @@ type DriverMock struct { VersionCalled bool VersionResult string VersionErr error + + LoadSnapshotsCalled []string + LoadSnapshotsResult *VBoxSnapshot + CreateSnapshotCalled []string + CreateSnapshotError error + HasSnapshotsCalled []string + HasSnapshotsResult bool + GetCurrentSnapshotCalled []string + GetCurrentSnapshotResult *VBoxSnapshot + SetSnapshotCalled []*VBoxSnapshot + DeleteSnapshotCalled []*VBoxSnapshot } func (d *DriverMock) CreateSATAController(vm string, controller string, portcount int) error { @@ -114,3 +125,65 @@ func (d *DriverMock) Version() (string, error) { d.VersionCalled = true return d.VersionResult, d.VersionErr } + +func (d *DriverMock) LoadSnapshots(vmName string) (*VBoxSnapshot, error) { + if vmName == "" { + panic("Argument empty exception: vmName") + } + + d.LoadSnapshotsCalled = append(d.LoadSnapshotsCalled, vmName) + return d.LoadSnapshotsResult, nil +} + +func (d *DriverMock) CreateSnapshot(vmName string, snapshotName string) error { + if vmName == "" { + panic("Argument empty exception: vmName") + } + if snapshotName == "" { + panic("Argument empty exception: snapshotName") + } + + d.CreateSnapshotCalled = append(d.CreateSnapshotCalled, snapshotName) + return d.CreateSnapshotError +} + +func (d *DriverMock) HasSnapshots(vmName string) (bool, error) { + if vmName == "" { + panic("Argument empty exception: vmName") + } + + d.HasSnapshotsCalled = append(d.HasSnapshotsCalled, vmName) + return d.HasSnapshotsResult, nil +} + +func (d *DriverMock) GetCurrentSnapshot(vmName string) (*VBoxSnapshot, error) { + if vmName == "" { + panic("Argument empty exception: vmName") + } + + d.GetCurrentSnapshotCalled = append(d.GetCurrentSnapshotCalled, vmName) + return d.GetCurrentSnapshotResult, nil +} + +func (d *DriverMock) SetSnapshot(vmName string, snapshot *VBoxSnapshot) error { + if vmName == "" { + panic("Argument empty exception: vmName") + } + if snapshot == nil { + panic("Argument empty exception: snapshot") + } + + d.SetSnapshotCalled = append(d.SetSnapshotCalled, snapshot) + return nil +} + +func (d *DriverMock) DeleteSnapshot(vmName string, snapshot *VBoxSnapshot) error { + if vmName == "" { + panic("Argument empty exception: vmName") + } + if snapshot == nil { + panic("Argument empty exception: snapshot") + } + d.DeleteSnapshotCalled = append(d.DeleteSnapshotCalled, snapshot) + return nil +} diff --git a/builder/virtualbox/common/snapshot.go b/builder/virtualbox/common/snapshot.go new file mode 100644 index 000000000..e68540182 --- /dev/null +++ b/builder/virtualbox/common/snapshot.go @@ -0,0 +1,199 @@ +package common + +import ( + "bufio" + "log" + "regexp" + "strings" + + "github.com/golang-collections/collections/stack" +) + +// VBoxSnapshot stores the hierarchy of snapshots for a VM instance +type VBoxSnapshot struct { + Name string + UUID string + IsCurrent bool + Parent *VBoxSnapshot // nil if topmost (root) snapshot + Children []*VBoxSnapshot +} + +// ParseSnapshotData parses the machinereadable representation of a virtualbox snapshot tree +func ParseSnapshotData(snapshotData string) (*VBoxSnapshot, error) { + scanner := bufio.NewScanner(strings.NewReader(snapshotData)) + SnapshotNamePartsRe := regexp.MustCompile("Snapshot(?PName|UUID)(?P(-[1-9]+)*)=\"(?P[^\"]*)\"") + var currentIndicator string + parentStack := stack.New() + var node *VBoxSnapshot + var rootNode *VBoxSnapshot + + for scanner.Scan() { + txt := scanner.Text() + idx := strings.Index(txt, "=") + if idx > 0 { + if strings.HasPrefix(txt, "Current") { + node.IsCurrent = true + } else { + matches := SnapshotNamePartsRe.FindStringSubmatch(txt) + if matches[1] == "Name" { + if nil == rootNode { + node = new(VBoxSnapshot) + rootNode = node + currentIndicator = matches[2] + } else { + pathLenCur := strings.Count(currentIndicator, "-") + pathLen := strings.Count(matches[2], "-") + if pathLen > pathLenCur { + currentIndicator = matches[2] + parentStack.Push(node) + } else if pathLen < pathLenCur { + currentIndicator = matches[2] + for i := 0; i < pathLenCur-pathLen; i++ { + parentStack.Pop() + } + } + node = new(VBoxSnapshot) + parent := parentStack.Peek().(*VBoxSnapshot) + if nil != parent { + node.Parent = parent + parent.Children = append(parent.Children, node) + } + } + node.Name = matches[4] + } else if matches[1] == "UUID" { + node.UUID = matches[4] + } + } + } else { + log.Printf("Invalid key,value pair [%s]", txt) + } + } + return rootNode, nil +} + +// IsChildOf verifies if the current snaphot is a child of the passed as argument +func (sn *VBoxSnapshot) IsChildOf(candidate *VBoxSnapshot) bool { + if nil == candidate { + panic("Missing parameter value: candidate") + } + node := sn + for nil != node { + if candidate.UUID == node.UUID { + break + } + node = node.Parent + } + return nil != node +} + +// the walker uses a channel to return nodes from a snapshot tree in breadth approach +func walk(sn *VBoxSnapshot, ch chan *VBoxSnapshot) { + if nil == sn { + return + } + if 0 < len(sn.Children) { + for _, child := range sn.Children { + walk(child, ch) + } + } + ch <- sn +} + +func walker(sn *VBoxSnapshot) <-chan *VBoxSnapshot { + if nil == sn { + panic("Argument null exception: sn") + } + + ch := make(chan *VBoxSnapshot) + go func() { + walk(sn, ch) + close(ch) + }() + return ch +} + +// GetRoot returns the top-most (root) snapshot for a given snapshot +func (sn *VBoxSnapshot) GetRoot() *VBoxSnapshot { + if nil == sn { + panic("Argument null exception: sn") + } + + node := sn + for nil != node.Parent { + node = node.Parent + } + return node +} + +// GetSnapshots returns an array of all snapshots defined +func (sn *VBoxSnapshot) GetSnapshots() []*VBoxSnapshot { + var result []*VBoxSnapshot + root := sn.GetRoot() + ch := walker(root) + for { + node, ok := <-ch + if !ok { + break + } + result = append(result, node) + } + return result +} + +// GetSnapshotsByName find all snapshots with a given name +func (sn *VBoxSnapshot) GetSnapshotsByName(name string) []*VBoxSnapshot { + var result []*VBoxSnapshot + root := sn.GetRoot() + ch := walker(root) + for { + node, ok := <-ch + if !ok { + break + } + if strings.EqualFold(node.Name, name) { + result = append(result, node) + } + } + return result +} + +// GetSnapshotByUUID returns a snapshot by it's UUID +func (sn *VBoxSnapshot) GetSnapshotByUUID(uuid string) *VBoxSnapshot { + root := sn.GetRoot() + ch := walker(root) + for { + node, ok := <-ch + if !ok { + break + } + if strings.EqualFold(node.UUID, uuid) { + return node + } + } + return nil +} + +// GetCurrentSnapshot returns the currently attached snapshot +func (sn *VBoxSnapshot) GetCurrentSnapshot() *VBoxSnapshot { + root := sn.GetRoot() + ch := walker(root) + for { + node, ok := <-ch + if !ok { + break + } + if node.IsCurrent { + return node + } + } + return nil +} + +func (sn *VBoxSnapshot) GetChildWithName(name string) *VBoxSnapshot { + for _, child := range sn.Children { + if child.Name == name { + return child + } + } + return nil +} diff --git a/builder/virtualbox/common/snapshot_test.go b/builder/virtualbox/common/snapshot_test.go new file mode 100644 index 000000000..5b3ab78de --- /dev/null +++ b/builder/virtualbox/common/snapshot_test.go @@ -0,0 +1,136 @@ +package common + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func getTestData() string { + return `SnapshotName="Imported" +SnapshotUUID="7e5b4165-91ec-4091-a74c-a5709d584530" +SnapshotName-1="Snapshot 1" +SnapshotUUID-1="5fc461ec-da7a-40a8-a168-03134d7cdf5c" +SnapshotName-1-1="Snapshot 2" +SnapshotUUID-1-1="8e12833b-c6b5-4cbd-b42b-09eff8ffc173" +SnapshotName-1-1-1="Snapshot 3" +SnapshotUUID-1-1-1="eb342b39-b4bd-47b0-afd8-dcd1cc5c5929" +SnapshotName-1-1-2="Snapshot 4" +SnapshotUUID-1-1-2="17df1668-e79a-4ed6-a86b-713913699846" +SnapshotName-1-2="Snapshot-Export" +SnapshotUUID-1-2="c857d1b8-4fd6-4044-9d2c-c6e465b3cdd4" +CurrentSnapshotName="Snapshot-Export" +CurrentSnapshotUUID="c857d1b8-4fd6-4044-9d2c-c6e465b3cdd4" +CurrentSnapshotNode="SnapshotName-1-2" +SnapshotName-2="Snapshot 5" +SnapshotUUID-2="85646c6a-fb86-4112-b15e-cab090670778" +SnapshotName-2-1="Snapshot 2" +SnapshotUUID-2-1="7b093686-2981-4ada-8b0f-4c03ae23cd1a" +SnapshotName-3="Snapshot 7" +SnapshotUUID-3="0d977a1f-c9ef-412c-a08d-7c0707b3b18f" +SnapshotName-3-1="Snapshot 8" +SnapshotUUID-3-1="f4ed75b3-afc1-42d4-9e02-8df6f053d07e" +SnapshotName-3-2="Snapshot 9" +SnapshotUUID-3-2="a5903505-9261-4bd3-9972-bacd0064d667"` +} + +func TestSnapshot_ParseFullTree(t *testing.T) { + rootNode, err := ParseSnapshotData(getTestData()) + assert.NoError(t, err) + assert.NotNil(t, rootNode) + assert.Equal(t, rootNode.Name, "Imported") + assert.Equal(t, rootNode.UUID, "7e5b4165-91ec-4091-a74c-a5709d584530") + assert.Equal(t, 3, len(rootNode.Children)) + assert.Nil(t, rootNode.Parent) +} + +func TestSnapshot_FindCurrent(t *testing.T) { + rootNode, err := ParseSnapshotData(getTestData()) + assert.NoError(t, err) + assert.NotNil(t, rootNode) + + current := rootNode.GetCurrentSnapshot() + assert.NotNil(t, current) + assert.Equal(t, current.UUID, "c857d1b8-4fd6-4044-9d2c-c6e465b3cdd4") + assert.Equal(t, current.Name, "Snapshot-Export") + assert.NotNil(t, current.Parent) + assert.Equal(t, current.Parent.UUID, "5fc461ec-da7a-40a8-a168-03134d7cdf5c") + assert.Equal(t, current.Parent.Name, "Snapshot 1") +} + +func TestSnapshot_FindNodeByUUID(t *testing.T) { + rootNode, err := ParseSnapshotData(getTestData()) + assert.NoError(t, err) + assert.NotNil(t, rootNode) + + node := rootNode.GetSnapshotByUUID("7b093686-2981-4ada-8b0f-4c03ae23cd1a") + assert.NotNil(t, node) + assert.Equal(t, "Snapshot 2", node.Name) + assert.Equal(t, "7b093686-2981-4ada-8b0f-4c03ae23cd1a", node.UUID) + assert.Equal(t, 0, len(node.Children)) + assert.Nil(t, rootNode.Parent) + + otherNode := rootNode.GetSnapshotByUUID("f4ed75b3-afc1-42d4-9e02-8df6f053d07e") + assert.NotNil(t, otherNode) + assert.True(t, otherNode.IsChildOf(rootNode)) + assert.False(t, node.IsChildOf(otherNode)) + assert.False(t, otherNode.IsChildOf(node)) +} + +func TestSnapshot_FindNodesByName(t *testing.T) { + rootNode, err := ParseSnapshotData(getTestData()) + assert.NoError(t, err) + assert.NotNil(t, rootNode) + + nodes := rootNode.GetSnapshotsByName("Snapshot 2") + assert.NotNil(t, nodes) + assert.Equal(t, 2, len(nodes)) +} + +func TestSnapshot_IsChildOf(t *testing.T) { + rootNode, err := ParseSnapshotData(getTestData()) + assert.NoError(t, err) + assert.NotNil(t, rootNode) + + child := rootNode.GetSnapshotByUUID("c857d1b8-4fd6-4044-9d2c-c6e465b3cdd4") + assert.NotNil(t, child) + assert.True(t, child.IsChildOf(rootNode)) + assert.True(t, child.IsChildOf(child.Parent)) + assert.PanicsWithValue(t, "Missing parameter value: candidate", func() { child.IsChildOf(nil) }) +} + +func TestSnapshot_SingleSnapshot(t *testing.T) { + snapData := `SnapshotName="Imported" + SnapshotUUID="7e5b4165-91ec-4091-a74c-a5709d584530"` + + rootNode, err := ParseSnapshotData(snapData) + assert.NoError(t, err) + assert.NotNil(t, rootNode) + + assert.Equal(t, rootNode.Name, "Imported") + assert.Equal(t, rootNode.UUID, "7e5b4165-91ec-4091-a74c-a5709d584530") + assert.Equal(t, len(rootNode.Children), 0) + assert.Nil(t, rootNode.Parent) +} + +func TestSnapshot_EmptySnapshotData(t *testing.T) { + snapData := `` + + rootNode, err := ParseSnapshotData(snapData) + assert.NoError(t, err) + assert.Nil(t, rootNode) +} + +func TestSnapshot_EnsureParents(t *testing.T) { + rootNode, err := ParseSnapshotData(getTestData()) + assert.NoError(t, err) + assert.NotNil(t, rootNode) + + for _, snapshot := range rootNode.GetSnapshots() { + if snapshot == rootNode { + assert.Nil(t, snapshot.Parent) + } else { + assert.NotNil(t, snapshot.Parent) + } + } +} diff --git a/builder/virtualbox/common/step_forward_ssh.go b/builder/virtualbox/common/step_forward_ssh.go index 345b9c55f..ecb0947e0 100644 --- a/builder/virtualbox/common/step_forward_ssh.go +++ b/builder/virtualbox/common/step_forward_ssh.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "strings" "github.com/hashicorp/packer/common/net" "github.com/hashicorp/packer/helper/communicator" @@ -69,11 +70,29 @@ func (s *StepForwardSSH) Run(ctx context.Context, state multistep.StateBag) mult "--natpf1", fmt.Sprintf("packercomm,tcp,127.0.0.1,%d,,%d", sshHostPort, guestPort), } + retried := false + retry: if err := driver.VBoxManage(command...); err != nil { - err := fmt.Errorf("Error creating port forwarding rule: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt + if !strings.Contains(err.Error(), "A NAT rule of this name already exists") || retried { + err := fmt.Errorf("Error creating port forwarding rule: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } else { + log.Printf("A packer NAT rule already exists. Trying to delete ...") + delcommand := []string{ + "modifyvm", vmName, + "--natpf1", + "delete", "packercomm", + } + if err := driver.VBoxManage(delcommand...); err != nil { + err := fmt.Errorf("Error deleting packer NAT forwarding rule: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + goto retry + } } } diff --git a/builder/virtualbox/common/step_shutdown.go b/builder/virtualbox/common/step_shutdown.go index baad0dc82..f28e2deef 100644 --- a/builder/virtualbox/common/step_shutdown.go +++ b/builder/virtualbox/common/step_shutdown.go @@ -45,31 +45,6 @@ func (s *StepShutdown) Run(ctx context.Context, state multistep.StateBag) multis return multistep.ActionHalt } - // Wait for the machine to actually shut down - log.Printf("Waiting max %s for shutdown to complete", s.Timeout) - shutdownTimer := time.After(s.Timeout) - for { - running, _ := driver.IsRunning(vmName) - if !running { - - if s.Delay.Nanoseconds() > 0 { - log.Printf("Delay for %s after shutdown to allow locks to clear...", s.Delay) - time.Sleep(s.Delay) - } - - break - } - - select { - case <-shutdownTimer: - err := errors.New("Timeout while waiting for machine to shut down.") - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - default: - time.Sleep(500 * time.Millisecond) - } - } } else { ui.Say("Halting the virtual machine...") if err := driver.Stop(vmName); err != nil { @@ -80,6 +55,32 @@ func (s *StepShutdown) Run(ctx context.Context, state multistep.StateBag) multis } } + // Wait for the machine to actually shut down + log.Printf("Waiting max %s for shutdown to complete", s.Timeout) + shutdownTimer := time.After(s.Timeout) + for { + running, _ := driver.IsRunning(vmName) + if !running { + + if s.Delay.Nanoseconds() > 0 { + log.Printf("Delay for %s after shutdown to allow locks to clear...", s.Delay) + time.Sleep(s.Delay) + } + + break + } + + select { + case <-shutdownTimer: + err := errors.New("Timeout while waiting for machine to shut down.") + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + default: + time.Sleep(500 * time.Millisecond) + } + } + log.Println("VM shut down.") return multistep.ActionContinue } diff --git a/builder/virtualbox/vm/builder.go b/builder/virtualbox/vm/builder.go new file mode 100644 index 000000000..0b5d3bcf0 --- /dev/null +++ b/builder/virtualbox/vm/builder.go @@ -0,0 +1,178 @@ +package vm + +import ( + "context" + "errors" + "fmt" + + vboxcommon "github.com/hashicorp/packer/builder/virtualbox/common" + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/helper/communicator" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +// Builder implements packer.Builder and builds the actual VirtualBox +// images. +type Builder struct { + config *Config + runner multistep.Runner +} + +// Prepare processes the build configuration parameters. +func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { + c, warnings, errs := NewConfig(raws...) + if errs != nil { + return warnings, errs + } + b.config = c + + return warnings, nil +} + +// Run executes a Packer build and returns a packer.Artifact representing +// a VirtualBox appliance. +func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { + // Create the driver that we'll use to communicate with VirtualBox + driver, err := vboxcommon.NewDriver() + if err != nil { + return nil, fmt.Errorf("Failed creating VirtualBox driver: %s", err) + } + + // Set up the state. + state := new(multistep.BasicStateBag) + state.Put("config", b.config) + state.Put("debug", b.config.PackerDebug) + state.Put("driver", driver) + state.Put("hook", hook) + state.Put("ui", ui) + + // Build the steps. + steps := []multistep.Step{ + new(vboxcommon.StepSuppressMessages), + &common.StepCreateFloppy{ + Files: b.config.FloppyConfig.FloppyFiles, + Directories: b.config.FloppyConfig.FloppyDirectories, + }, + &StepSetSnapshot{ + Name: b.config.VMName, + AttachSnapshot: b.config.AttachSnapshot, + KeepRegistered: b.config.KeepRegistered, + }, + &common.StepHTTPServer{ + HTTPDir: b.config.HTTPDir, + HTTPPortMin: b.config.HTTPPortMin, + HTTPPortMax: b.config.HTTPPortMax, + }, + &vboxcommon.StepDownloadGuestAdditions{ + GuestAdditionsMode: b.config.GuestAdditionsMode, + GuestAdditionsURL: b.config.GuestAdditionsURL, + GuestAdditionsSHA256: b.config.GuestAdditionsSHA256, + Ctx: b.config.ctx, + }, + &StepImport{ + Name: b.config.VMName, + }, + &vboxcommon.StepAttachGuestAdditions{ + GuestAdditionsMode: b.config.GuestAdditionsMode, + }, + &vboxcommon.StepConfigureVRDP{ + VRDPBindAddress: b.config.VRDPBindAddress, + VRDPPortMin: b.config.VRDPPortMin, + VRDPPortMax: b.config.VRDPPortMax, + }, + new(vboxcommon.StepAttachFloppy), + &vboxcommon.StepForwardSSH{ + CommConfig: &b.config.SSHConfig.Comm, + HostPortMin: b.config.SSHHostPortMin, + HostPortMax: b.config.SSHHostPortMax, + SkipNatMapping: b.config.SSHSkipNatMapping, + }, + &vboxcommon.StepVBoxManage{ + Commands: b.config.VBoxManage, + Ctx: b.config.ctx, + }, + &vboxcommon.StepRun{ + Headless: b.config.Headless, + }, + &vboxcommon.StepTypeBootCommand{ + BootWait: b.config.BootWait, + BootCommand: b.config.FlatBootCommand(), + VMName: b.config.VMName, + Ctx: b.config.ctx, + GroupInterval: b.config.BootConfig.BootGroupInterval, + Comm: &b.config.Comm, + }, + &communicator.StepConnect{ + Config: &b.config.SSHConfig.Comm, + Host: vboxcommon.CommHost(b.config.SSHConfig.Comm.SSHHost), + SSHConfig: b.config.SSHConfig.Comm.SSHConfigFunc(), + SSHPort: vboxcommon.SSHPort, + WinRMPort: vboxcommon.SSHPort, + }, + &vboxcommon.StepUploadVersion{ + Path: *b.config.VBoxVersionFile, + }, + &vboxcommon.StepUploadGuestAdditions{ + GuestAdditionsMode: b.config.GuestAdditionsMode, + GuestAdditionsPath: b.config.GuestAdditionsPath, + Ctx: b.config.ctx, + }, + new(common.StepProvision), + &common.StepCleanupTempKeys{ + Comm: &b.config.SSHConfig.Comm, + }, + &vboxcommon.StepShutdown{ + Command: b.config.ShutdownCommand, + Timeout: b.config.ShutdownTimeout, + Delay: b.config.PostShutdownDelay, + }, + &vboxcommon.StepVBoxManage{ + Commands: b.config.VBoxManagePost, + Ctx: b.config.ctx, + }, + &StepCreateSnapshot{ + Name: b.config.VMName, + TargetSnapshot: b.config.TargetSnapshot, + }, + &vboxcommon.StepExport{ + Format: b.config.Format, + OutputDir: b.config.OutputDir, + ExportOpts: b.config.ExportOpts.ExportOpts, + SkipNatMapping: b.config.SSHSkipNatMapping, + SkipExport: b.config.SkipExport, + }, + } + + if !b.config.SkipExport { + steps = append(steps, nil) + copy(steps[1:], steps) + steps[0] = &common.StepOutputDir{ + Force: b.config.PackerForce, + Path: b.config.OutputDir, + } + } + // Run the steps. + b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state) + b.runner.Run(ctx, state) + + // Report any errors. + if rawErr, ok := state.GetOk("error"); ok { + return nil, rawErr.(error) + } + + // If we were interrupted or cancelled, then just exit. + if _, ok := state.GetOk(multistep.StateCancelled); ok { + return nil, errors.New("Build was cancelled.") + } + + if _, ok := state.GetOk(multistep.StateHalted); ok { + return nil, errors.New("Build was halted.") + } + + if b.config.SkipExport { + return nil, nil + } else { + return vboxcommon.NewArtifact(b.config.OutputDir) + } +} diff --git a/builder/virtualbox/vm/config.go b/builder/virtualbox/vm/config.go new file mode 100644 index 000000000..a427b5766 --- /dev/null +++ b/builder/virtualbox/vm/config.go @@ -0,0 +1,209 @@ +package vm + +import ( + "fmt" + "log" + "strings" + + vboxcommon "github.com/hashicorp/packer/builder/virtualbox/common" + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/common/bootcommand" + "github.com/hashicorp/packer/helper/config" + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/template/interpolate" +) + +// Config is the configuration structure for the builder. +type Config struct { + common.PackerConfig `mapstructure:",squash"` + common.HTTPConfig `mapstructure:",squash"` + common.FloppyConfig `mapstructure:",squash"` + bootcommand.BootConfig `mapstructure:",squash"` + vboxcommon.ExportConfig `mapstructure:",squash"` + vboxcommon.ExportOpts `mapstructure:",squash"` + vboxcommon.OutputConfig `mapstructure:",squash"` + vboxcommon.RunConfig `mapstructure:",squash"` + vboxcommon.SSHConfig `mapstructure:",squash"` + vboxcommon.ShutdownConfig `mapstructure:",squash"` + vboxcommon.VBoxManageConfig `mapstructure:",squash"` + vboxcommon.VBoxManagePostConfig `mapstructure:",squash"` + vboxcommon.VBoxVersionConfig `mapstructure:",squash"` + + GuestAdditionsMode string `mapstructure:"guest_additions_mode"` + GuestAdditionsPath string `mapstructure:"guest_additions_path"` + GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"` + GuestAdditionsURL string `mapstructure:"guest_additions_url"` + VMName string `mapstructure:"vm_name"` + AttachSnapshot string `mapstructure:"attach_snapshot"` + TargetSnapshot string `mapstructure:"target_snapshot"` + DeleteTargetSnapshot bool `mapstructure:"force_delete_snapshot"` + KeepRegistered bool `mapstructure:"keep_registered"` + SkipExport bool `mapstructure:"skip_export"` + + ctx interpolate.Context +} + +func NewConfig(raws ...interface{}) (*Config, []string, error) { + c := new(Config) + err := config.Decode(c, &config.DecodeOpts{ + Interpolate: true, + InterpolateContext: &c.ctx, + InterpolateFilter: &interpolate.RenderFilter{ + Exclude: []string{ + "boot_command", + "guest_additions_path", + "guest_additions_url", + "vboxmanage", + "vboxmanage_post", + }, + }, + }, raws...) + if err != nil { + return nil, nil, err + } + + // Defaults + if c.GuestAdditionsMode == "" { + c.GuestAdditionsMode = "upload" + } + + if c.GuestAdditionsPath == "" { + c.GuestAdditionsPath = "VBoxGuestAdditions.iso" + } + + if c.RawPostShutdownDelay == "" { + c.RawPostShutdownDelay = "2s" + } + + // Prepare the errors + var errs *packer.MultiError + errs = packer.MultiErrorAppend(errs, c.ExportConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.ExportOpts.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.FloppyConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.OutputConfig.Prepare(&c.ctx, &c.PackerConfig)...) + errs = packer.MultiErrorAppend(errs, c.RunConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.ShutdownConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.SSHConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.VBoxManageConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.VBoxManagePostConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.VBoxVersionConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.BootConfig.Prepare(&c.ctx)...) + + log.Printf("PostShutdownDelay: %f", c.PostShutdownDelay.Seconds()) + + if c.VMName == "" { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("vm_name is required")) + } + + if c.TargetSnapshot == "" { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("target_snapshot is required")) + } + + validMode := false + validModes := []string{ + vboxcommon.GuestAdditionsModeDisable, + vboxcommon.GuestAdditionsModeAttach, + vboxcommon.GuestAdditionsModeUpload, + } + + for _, mode := range validModes { + if c.GuestAdditionsMode == mode { + validMode = true + break + } + } + + if !validMode { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("guest_additions_mode is invalid. Must be one of: %v", validModes)) + } + + if c.GuestAdditionsSHA256 != "" { + c.GuestAdditionsSHA256 = strings.ToLower(c.GuestAdditionsSHA256) + } + + // Warnings + var warnings []string + if c.ShutdownCommand == "" { + warnings = append(warnings, + "A shutdown_command was not specified. Without a shutdown command, Packer\n"+ + "will forcibly halt the virtual machine, which may result in data loss.") + } + driver, err := vboxcommon.NewDriver() + if err != nil { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed creating VirtualBox driver: %s", err)) + } else { + if c.AttachSnapshot != "" && c.TargetSnapshot != "" && c.AttachSnapshot == c.TargetSnapshot { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Attach snapshot %s and target snapshot %s cannot be the same", c.AttachSnapshot, c.TargetSnapshot)) + } + snapshotTree, err := driver.LoadSnapshots(c.VMName) + log.Printf("") + if err != nil { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed to load snapshots for VM %s: %s", c.VMName, err)) + } else { + log.Printf("Snapshots loaded from VM %s", c.VMName) + + var attachSnapshot *vboxcommon.VBoxSnapshot + if nil != snapshotTree { + attachSnapshot = snapshotTree.GetCurrentSnapshot() + log.Printf("VM %s is currently attached to snapshot: %s/%s", c.VMName, attachSnapshot.Name, attachSnapshot.UUID) + } + if c.AttachSnapshot != "" { + log.Printf("Checking configuration attach_snapshot [%s]", c.AttachSnapshot) + if nil == snapshotTree { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("No snapshots defined on VM %s. Unable to attach to %s", c.VMName, c.AttachSnapshot)) + } else { + snapshots := snapshotTree.GetSnapshotsByName(c.AttachSnapshot) + if 0 >= len(snapshots) { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Snapshot %s does not exist on VM %s", c.AttachSnapshot, c.VMName)) + } else if 1 < len(snapshots) { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Multiple Snapshots with name %s exist on VM %s", c.AttachSnapshot, c.VMName)) + } else { + attachSnapshot = snapshots[0] + } + } + } + if c.TargetSnapshot != "" { + log.Printf("Checking configuration target_snapshot [%s]", c.TargetSnapshot) + if nil == snapshotTree { + log.Printf("Currently no snapshots defined in VM %s", c.VMName) + } else { + if c.TargetSnapshot == attachSnapshot.Name { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Target snapshot %s cannot be the same as the snapshot to which the builder shall attach: %s", c.TargetSnapshot, attachSnapshot.Name)) + } else { + snapshots := snapshotTree.GetSnapshotsByName(c.TargetSnapshot) + if 0 < len(snapshots) { + if nil == attachSnapshot { + panic("Internal error. Expecting a handle to a VBoxSnapshot") + } + isChild := false + for _, snapshot := range snapshots { + log.Printf("Checking if target snaphot %s/%s is child of %s/%s", snapshot.Name, snapshot.UUID, attachSnapshot.Name, attachSnapshot.UUID) + isChild = nil != snapshot.Parent && snapshot.Parent.UUID == attachSnapshot.UUID + } + if !isChild { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Target snapshot %s already exists and is not a direct child of %s", c.TargetSnapshot, attachSnapshot.Name)) + } else if !c.DeleteTargetSnapshot { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Target snapshot %s already exists as direct child of %s for VM %s. Use force_delete_snapshot = true to overwrite snapshot", + c.TargetSnapshot, + attachSnapshot.Name, + c.VMName)) + } + } else { + log.Printf("No snapshot with name %s defined in VM %s", c.TargetSnapshot, c.VMName) + } + } + } + } + } + } + // Check for any errors. + if errs != nil && len(errs.Errors) > 0 { + return nil, warnings, errs + } + + return c, warnings, nil +} diff --git a/builder/virtualbox/vm/step_create_snapshot.go b/builder/virtualbox/vm/step_create_snapshot.go new file mode 100644 index 000000000..95c676069 --- /dev/null +++ b/builder/virtualbox/vm/step_create_snapshot.go @@ -0,0 +1,69 @@ +package vm + +import ( + "context" + "fmt" + "log" + + vboxcommon "github.com/hashicorp/packer/builder/virtualbox/common" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +type StepCreateSnapshot struct { + Name string + TargetSnapshot string +} + +func (s *StepCreateSnapshot) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(vboxcommon.Driver) + ui := state.Get("ui").(packer.Ui) + if s.TargetSnapshot != "" { + running, err := driver.IsRunning(s.Name) + if err != nil { + err = fmt.Errorf("Failed to test if VM %s is still running: %s", s.Name, err) + } else if running { + err = fmt.Errorf("VM %s is still running. Unable to create snapshot %s", s.Name, s.TargetSnapshot) + } + if err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say(fmt.Sprintf("Creating snapshot %s on virtual machine %s", s.TargetSnapshot, s.Name)) + snapshotTree, err := driver.LoadSnapshots(s.Name) + if err != nil { + err = fmt.Errorf("Failed to load snapshots for VM %s: %s", s.Name, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + currentSnapshot := snapshotTree.GetCurrentSnapshot() + targetSnapshot := currentSnapshot.GetChildWithName(s.TargetSnapshot) + if nil != targetSnapshot { + log.Printf("Deleting existing target snapshot %s", s.TargetSnapshot) + err = driver.DeleteSnapshot(s.Name, targetSnapshot) + if nil != err { + err = fmt.Errorf("Unable to delete snapshot %s from VM %s: %s", s.TargetSnapshot, s.Name, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + err = driver.CreateSnapshot(s.Name, s.TargetSnapshot) + if err != nil { + err := fmt.Errorf("Error creating snaphot VM: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } else { + ui.Say("No target snapshot defined...") + } + + return multistep.ActionContinue +} + +func (s *StepCreateSnapshot) Cleanup(state multistep.StateBag) {} diff --git a/builder/virtualbox/vm/step_import.go b/builder/virtualbox/vm/step_import.go new file mode 100644 index 000000000..3e0a11fe4 --- /dev/null +++ b/builder/virtualbox/vm/step_import.go @@ -0,0 +1,20 @@ +package vm + +import ( + "context" + + "github.com/hashicorp/packer/helper/multistep" +) + +// This step imports an OVF VM into VirtualBox. +type StepImport struct { + Name string +} + +func (s *StepImport) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + state.Put("vmName", s.Name) + return multistep.ActionContinue +} + +func (s *StepImport) Cleanup(state multistep.StateBag) { +} diff --git a/builder/virtualbox/vm/step_set_snapshot.go b/builder/virtualbox/vm/step_set_snapshot.go new file mode 100644 index 000000000..515d9b2a0 --- /dev/null +++ b/builder/virtualbox/vm/step_set_snapshot.go @@ -0,0 +1,90 @@ +package vm + +import ( + "context" + "fmt" + + vboxcommon "github.com/hashicorp/packer/builder/virtualbox/common" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +type StepSetSnapshot struct { + Name string + AttachSnapshot string + KeepRegistered bool + revertToSnapshot string +} + +func (s *StepSetSnapshot) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(vboxcommon.Driver) + ui := state.Get("ui").(packer.Ui) + snapshotTree, err := driver.LoadSnapshots(s.Name) + if err != nil { + err := fmt.Errorf("Error loading snapshots for VM: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + if s.AttachSnapshot != "" { + if nil == snapshotTree { + err := fmt.Errorf("Unable to attach snapshot on VM %s when no snapshots exist", s.Name) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + currentSnapshot := snapshotTree.GetCurrentSnapshot() + s.revertToSnapshot = currentSnapshot.UUID + ui.Say(fmt.Sprintf("Attaching snapshot %s on virtual machine %s", s.AttachSnapshot, s.Name)) + candidateSnapshots := snapshotTree.GetSnapshotsByName(s.AttachSnapshot) + if 0 >= len(candidateSnapshots) { + err := fmt.Errorf("Snapshot %s not found on VM %s", s.AttachSnapshot, s.Name) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } else if 1 > len(candidateSnapshots) { + err := fmt.Errorf("More than one Snapshot %s found on VM %s", s.AttachSnapshot, s.Name) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } else { + err = driver.SetSnapshot(s.Name, candidateSnapshots[0]) + if err != nil { + err := fmt.Errorf("Unable to set snapshot for VM: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + } + return multistep.ActionContinue +} + +func (s *StepSetSnapshot) Cleanup(state multistep.StateBag) { + driver := state.Get("driver").(vboxcommon.Driver) + if s.revertToSnapshot != "" { + ui := state.Get("ui").(packer.Ui) + if s.KeepRegistered { + ui.Say("Keeping virtual machine state (keep_registered = true)") + return + } else { + ui.Say(fmt.Sprintf("Reverting to snapshot %s on virtual machine %s", s.revertToSnapshot, s.Name)) + snapshotTree, err := driver.LoadSnapshots(s.Name) + revertTo := snapshotTree.GetSnapshotByUUID(s.revertToSnapshot) + if nil == revertTo { + err := fmt.Errorf("Snapshot with UUID %s not found for VM %s", s.revertToSnapshot, s.Name) + state.Put("error", err) + ui.Error(err.Error()) + return + } + err = driver.SetSnapshot(s.Name, revertTo) + if err != nil { + err := fmt.Errorf("Unable to set snapshot for VM: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return + } + } + } +} diff --git a/command/build.go b/command/build.go index 9de68e83d..7f582d38a 100644 --- a/command/build.go +++ b/command/build.go @@ -241,9 +241,11 @@ func (c *BuildCommand) RunContext(buildCtx context.Context, args []string) int { errors.Unlock() } else { ui.Say(fmt.Sprintf("Build '%s' finished.", name)) - artifacts.Lock() - artifacts.m[name] = runArtifacts - artifacts.Unlock() + if nil != runArtifacts { + artifacts.Lock() + artifacts.m[name] = runArtifacts + artifacts.Unlock() + } } }() diff --git a/command/plugin.go b/command/plugin.go index 9ebd2a046..aff138dd7 100644 --- a/command/plugin.go +++ b/command/plugin.go @@ -54,6 +54,7 @@ import ( vagrantbuilder "github.com/hashicorp/packer/builder/vagrant" virtualboxisobuilder "github.com/hashicorp/packer/builder/virtualbox/iso" virtualboxovfbuilder "github.com/hashicorp/packer/builder/virtualbox/ovf" + virtualboxvmbuilder "github.com/hashicorp/packer/builder/virtualbox/vm" vmwareisobuilder "github.com/hashicorp/packer/builder/vmware/iso" vmwarevmxbuilder "github.com/hashicorp/packer/builder/vmware/vmx" yandexbuilder "github.com/hashicorp/packer/builder/yandex" @@ -141,6 +142,7 @@ var Builders = map[string]packer.Builder{ "vagrant": new(vagrantbuilder.Builder), "virtualbox-iso": new(virtualboxisobuilder.Builder), "virtualbox-ovf": new(virtualboxovfbuilder.Builder), + "virtualbox-vm": new(virtualboxvmbuilder.Builder), "vmware-iso": new(vmwareisobuilder.Builder), "vmware-vmx": new(vmwarevmxbuilder.Builder), "yandex": new(yandexbuilder.Builder), diff --git a/go.mod b/go.mod index dc8470b1a..03af8883d 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( github.com/go-ini/ini v1.25.4 github.com/gofrs/flock v0.7.1 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect + github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 github.com/google/go-cmp v0.2.0 github.com/google/shlex v0.0.0-20150127133951-6f45313302b9 github.com/google/uuid v1.0.0 diff --git a/go.sum b/go.sum index 454fa4da4..525fbd81c 100644 --- a/go.sum +++ b/go.sum @@ -111,7 +111,9 @@ github.com/gofrs/flock v0.7.1 h1:DP+LD/t0njgoPBvT5MJLeliUIVQR03hiKR6vezdwHlc= github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4= +github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= diff --git a/vendor/github.com/golang-collections/collections/LICENSE b/vendor/github.com/golang-collections/collections/LICENSE new file mode 100644 index 000000000..75a26aeb3 --- /dev/null +++ b/vendor/github.com/golang-collections/collections/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2012 Caleb Doxsey + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/golang-collections/collections/stack/stack.go b/vendor/github.com/golang-collections/collections/stack/stack.go new file mode 100644 index 000000000..04063dfab --- /dev/null +++ b/vendor/github.com/golang-collections/collections/stack/stack.go @@ -0,0 +1,44 @@ +package stack + +type ( + Stack struct { + top *node + length int + } + node struct { + value interface{} + prev *node + } +) +// Create a new stack +func New() *Stack { + return &Stack{nil,0} +} +// Return the number of items in the stack +func (this *Stack) Len() int { + return this.length +} +// View the top item on the stack +func (this *Stack) Peek() interface{} { + if this.length == 0 { + return nil + } + return this.top.value +} +// Pop the top item of the stack and return it +func (this *Stack) Pop() interface{} { + if this.length == 0 { + return nil + } + + n := this.top + this.top = n.prev + this.length-- + return n.value +} +// Push a value onto the top of the stack +func (this *Stack) Push(value interface{}) { + n := &node{value,this.top} + this.top = n + this.length++ +} \ No newline at end of file diff --git a/vendor/modules.txt b/vendor/modules.txt index fa5e22f2d..1b68e6045 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -177,6 +177,8 @@ github.com/go-ini/ini github.com/gofrs/flock # github.com/gofrs/uuid v3.2.0+incompatible github.com/gofrs/uuid +# github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 +github.com/golang-collections/collections/stack # github.com/golang/protobuf v1.3.1 github.com/golang/protobuf/proto github.com/golang/protobuf/ptypes/timestamp diff --git a/website/source/docs/builders/virtualbox-vm.html.md.erb b/website/source/docs/builders/virtualbox-vm.html.md.erb new file mode 100644 index 000000000..155b0c054 --- /dev/null +++ b/website/source/docs/builders/virtualbox-vm.html.md.erb @@ -0,0 +1,395 @@ +--- +modeline: | + vim: set ft=pandoc: +description: | + The VirtualBox Packer builder is able to create VirtualBox virtual machines snapshots + and export them in the OVF format, starting from an ISO image. +layout: docs +page_title: 'VirtualBox Snapshot - Builders' +sidebar_current: 'docs-builders-virtualbox-vm' +--- + +# VirtualBox Builder (from an existing VM) + +Type: `virtualbox-vm` + +The VirtualBox Packer builder is able to create +[VirtualBox](https://www.virtualbox.org/) virtual machines snapshots and +(optionally) export them in the OVF format, starting from an **existing** +virtual machine. + +The builder builds a virtual machine snapshot by using an existing virtual +machine, booting it, provisioning software within the OS, then shutting it down. +The result of the VirtualBox builder is a new snapshot persisting all changes +from the applied provisioners. + +## Basic Example + +Here is a basic example. which serves to show the basic configuration: + +``` json +{ + "type" : "virtualbox-vm", + "communicator" : "winrm", + "headless" : "{{user `headless`}}", + "winrm_username" : "vagrant", + "winrm_password" : "vagrant", + "winrm_timeout" : "2h", + "shutdown_command" : "shutdown /s /t 10 /f /d p:4:1 /c \"Packer Shutdown\"", + "guest_additions_mode" : "disable", + "output_directory" : "./builds-vm", + "vm_name" : "target-vm", + "attach_snapshot" : "Snapshot", + "target_snapshot" : "Target-Snapshot", + "force_delete_snapshot" : "true", + "keep_registered" : "false", + "skip_export" : "false" +} +``` + +It is important to add a `shutdown_command`. By default Packer halts the virtual +machine and the file system may not be sync'd. Thus, changes made in a +provisioner might not be saved. + +## Configuration Reference + +There are many configuration options available for the VirtualBox builder. They +are organized below into two categories: required and optional. Within each +category, the available options are alphabetized and described. + +In addition to the options listed here, a +[communicator](/docs/templates/communicator.html) can be configured for this +builder. + +### Required: + +- `vm_name` (string) - This is the name of the virtual machine to which the + builder shall attach. + +### Optional: + +- `attach_snapshot` (string) - Default to `null/empty`. The name of an + **existing** snapshot to which the builder shall attach the VM before + starting it. If no snapshot is specified the builder will simply start the + VM from it's current state i.e. snapshot. + +- `boot_command` (array of strings) - This is an array of commands to type + when the virtual machine is first booted. The goal of these commands should + be to type just enough to initialize the operating system installer. Special + keys can be typed as well, and are covered in the section below on the + boot command. If this is not specified, it is assumed the installer will + start itself. + +- `boot_wait` (string) - The time to wait after booting the initial virtual + machine before typing the `boot_command`. The value of this should be + a duration. Examples are `5s` and `1m30s` which will cause Packer to wait + five seconds and one minute 30 seconds, respectively. If this isn't + specified, the default is `10s` or 10 seconds. + +- `export_opts` (array of strings) - Additional options to pass to the + [VBoxManage + export](https://www.virtualbox.org/manual/ch09.html#vboxmanage-export). This + can be useful for passing product information to include in the resulting + appliance file. Packer JSON configuration file example: + + ``` json + { + "type": "virtualbox-vm", + "export_opts": + [ + "--manifest", + "--vsys", "0", + "--description", "{{user `vm_description`}}", + "--version", "{{user `vm_version`}}" + ], + "format": "ova", + } + ``` + + A VirtualBox [VM + description](https://www.virtualbox.org/manual/ch09.html#vboxmanage-export-ovf) + may contain arbitrary strings; the GUI interprets HTML formatting. However, + the JSON format does not allow arbitrary newlines within a value. Add a + multi-line description by preparing the string in the shell before the + packer call like this (shell `>` continuation character snipped for easier + copy & paste): + + ``` {.shell} + + vm_description='some + multiline + description' + + vm_version='0.2.0' + + packer build \ + -var "vm_description=${vm_description}" \ + -var "vm_version=${vm_version}" \ + "packer_conf.json" + ``` + +- `floppy_dirs` (array of strings) - A list of directories to place onto + the floppy disk recursively. This is similar to the `floppy_files` option + except that the directory structure is preserved. This is useful for when + your floppy disk includes drivers or if you just want to organize it's + contents as a hierarchy. Wildcard characters (\*, ?, and \[\]) are allowed. + +- `floppy_files` (array of strings) - A list of files to place onto a floppy + disk that is attached when the VM is booted. This is most useful for + unattended Windows installs, which look for an `Autounattend.xml` file on + removable media. By default, no floppy will be attached. All files listed in + this setting get placed into the root directory of the floppy and the floppy + is attached as the first floppy device. Currently, no support exists for + creating sub-directories on the floppy. Wildcard characters (\*, ?, + and \[\]) are allowed. Directory names are also allowed, which will add all + the files found in the directory to the floppy. + +- `force_delete_snapshot` (boolean) - Defaults to `false`. If set to `true`, + overwrite an existing `target_snapshot`. Otherwise the builder will yield an + error if the specified target snapshot already exists. + +- `format` (string) - Either `ovf` or `ova`, this specifies the output format + of the exported virtual machine. This defaults to `ovf`. + +- `guest_additions_interface` (string) - The interface type to use to mount + guest additions when `guest_additions_mode` is set to `attach`. Will + default to the value set in `iso_interface`, if `iso_interface` is set. + Will default to "ide", if `iso_interface` is not set. Options are "ide" and + "sata". + +- `guest_additions_mode` (string) - The method by which guest additions are + made available to the guest for installation. Valid options are `upload`, + `attach`, or `disable`. If the mode is `attach` the guest additions ISO will + be attached as a CD device to the virtual machine. If the mode is `upload` + the guest additions ISO will be uploaded to the path specified by + `guest_additions_path`. The default value is `upload`. If `disable` is used, + guest additions won't be downloaded, either. + +- `guest_additions_path` (string) - The path on the guest virtual machine + where the VirtualBox guest additions ISO will be uploaded. By default this + is `VBoxGuestAdditions.iso` which should upload into the login directory of + the user. This is a [configuration + template](/docs/templates/engine.html) where the `Version` + variable is replaced with the VirtualBox version. + +- `guest_additions_sha256` (string) - The SHA256 checksum of the guest + additions ISO that will be uploaded to the guest VM. By default the + checksums will be downloaded from the VirtualBox website, so this only needs + to be set if you want to be explicit about the checksum. + +- `guest_additions_url` (string) - The URL to the guest additions ISO + to upload. This can also be a file URL if the ISO is at a local path. By + default, the VirtualBox builder will attempt to find the guest additions ISO + on the local file system. If it is not available locally, the builder will + download the proper guest additions ISO from the internet. + +- `guest_os_type` (string) - The guest OS type being installed. By default + this is `other`, but you can get *dramatic* performance improvements by + setting this to the proper value. To view all available values for this run + `VBoxManage list ostypes`. Setting the correct value hints to VirtualBox how + to optimize the virtual hardware to work best with that operating system. + +- `headless` (boolean) - Packer defaults to building VirtualBox virtual + machines by launching a GUI that shows the console of the machine + being built. When this value is set to `true`, the machine will start + without a console. + +- `http_directory` (string) - Path to a directory to serve using an + HTTP server. The files in this directory will be available over HTTP that + will be requestable from the virtual machine. This is useful for hosting + kickstart files and so on. By default this is an empty string, which means + no HTTP server will be started. The address and port of the HTTP server will + be available as variables in `boot_command`. This is covered in more detail + below. + +- `http_port_min` and `http_port_max` (number) - These are the minimum and + maximum port to use for the HTTP server started to serve the + `http_directory`. Because Packer often runs in parallel, Packer will choose + a randomly available port in this range to run the HTTP server. If you want + to force the HTTP server to be on one port, make this minimum and maximum + port the same. By default the values are `8000` and `9000`, respectively. + +- `keep_registered` (boolean) - Set this to `true` if you would like to keep + the VM attached to the snapshot specified by `attach_snapshot`. Otherwise + the builder will reset the VM to the snapshot to which the VM was attached + before the builder started. Defaults to `false`. + +- `output_directory` (string) - This is the path to the directory where the + resulting virtual machine will be created. This may be relative or absolute. + If relative, the path is relative to the working directory when `packer` + is executed. This directory must not exist or be empty prior to running + the builder. By default this is `output-BUILDNAME` where "BUILDNAME" is the + name of the build. + +- `post_shutdown_delay` (string) - The amount of time to wait after shutting + down the virtual machine. Defaults to `2s`. **Hint:** Don't specify a value + smaller than `2s` because otherwise the creation of a target snapshot might + corrupt the VM because not all locks has been released by VirtualBox. + +- `shutdown_command` (string) - The command to use to gracefully shut down the + machine once all the provisioning is done. By default this is an empty + string, which tells Packer to just forcefully shut down the machine unless a + shutdown command takes place inside script so this may safely be omitted. If + one or more scripts require a reboot it is suggested to leave this blank + since reboots may fail and specify the final shutdown command in your + last script. + +- `shutdown_timeout` (string) - The amount of time to wait after executing the + `shutdown_command` for the virtual machine to actually shut down. If it + doesn't shut down in this time, it is an error. By default, the timeout is + `5m` or five minutes. + +- `skip_export` (boolean) - Defaults to `false`. When enabled, Packer will + not export the VM. Useful if the builder should be applied again on the created + target snapshot. + +- `ssh_host_port_min` and `ssh_host_port_max` (number) - The minimum and + maximum port to use for the SSH port on the host machine which is forwarded + to the SSH port on the guest machine. Because Packer often runs in parallel, + Packer will choose a randomly available port in this range to use as the + host port. By default this is `2222` to `4444`. + +- `ssh_skip_nat_mapping` (boolean) - Defaults to `false`. When enabled, Packer + does not setup forwarded port mapping for SSH requests and uses `ssh_port` + on the host to communicate to the virtual machine. + +- `target_snapshot` (string) - Default to `null/empty`. The name of the + snapshot which shall be created after all provisioners has been run by the + builder. If no target snapshot is specified and `keep_registered` is set to + `false` the builder will revert to the snapshot to which the VM was attached + before the builder has been executed, which will revert all changes applied + by the provisioners. This is handy if only an export shall be created and no + further snapshot is required. + +- `vboxmanage` (array of array of strings) - Custom `VBoxManage` commands to + execute in order to further customize the virtual machine being created. The + value of this is an array of commands to execute. The commands are executed + in the order defined in the template. For each command, the command is + defined itself as an array of strings, where each string represents a single + argument on the command-line to `VBoxManage` (but excluding + `VBoxManage` itself). Each arg is treated as a [configuration + template](/docs/templates/engine.html), where the `Name` + variable is replaced with the VM name. More details on how to use + `VBoxManage` are below. + +- `vboxmanage_post` (array of array of strings) - Identical to `vboxmanage`, + except that it is run after the virtual machine is shutdown, and before the + virtual machine is exported. + +- `virtualbox_version_file` (string) - The path within the virtual machine to + upload a file that contains the VirtualBox version that was used to create + the machine. This information can be useful for provisioning. By default + this is `.vbox_version`, which will generally be uploaded into the home + directory. Set to an empty string to skip uploading this file, which can be + useful when using the `none` communicator. + +- `vrdp_bind_address` (string / IP address) - The IP address that should be + binded to for VRDP. By default packer will use `127.0.0.1` for this. If you + wish to bind to all interfaces use `0.0.0.0`. + +- `vrdp_port_min` and `vrdp_port_max` (number) - The minimum and maximum port + to use for VRDP access to the virtual machine. Packer uses a randomly chosen + port in this range that appears available. By default this is `5900` to + `6000`. The minimum and maximum ports are inclusive. + +## Boot Command + +The `boot_command` configuration is very important: it specifies the keys to +type when the virtual machine is first booted in order to start the OS +installer. This command is typed after `boot_wait`, which gives the virtual +machine some time to actually load the ISO. + +As documented above, the `boot_command` is an array of strings. The strings are +all typed in sequence. It is an array only to improve readability within the +template. + +The boot command is sent to the VM through the `VBoxManage` utility in as few +invocations as possible. We send each character in groups of 25, with a default +delay of 100ms between groups. The delay alleviates issues with latency and CPU +contention. If you notice missing keys, you can tune this delay by specifying +"boot_keygroup_interval" in your Packer template, for example: + +``` +{ + "builders": [ + { + "type": "virtualbox", + "boot_keygroup_interval": "500ms" + ... + } + ] +} +``` + +<%= partial "partials/builders/boot-command" %> + +<%= partial "partials/builders/virtualbox-ssh-key-pair" %> + +Example boot command. This is actually a working boot command used to start an +Ubuntu 12.04 installer: + +``` text +[ + "", + "/install/vmlinuz noapic ", + "preseed/url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/preseed.cfg ", + "debian-installer=en_US auto locale=en_US kbd-chooser/method=us ", + "hostname={{ .Name }} ", + "fb=false debconf/frontend=noninteractive ", + "keyboard-configuration/modelcode=SKIP keyboard-configuration/layout=USA ", + "keyboard-configuration/variant=USA console-setup/ask_detect=false ", + "initrd=/install/initrd.gz -- " +] +``` + +Please note that for the Virtuabox builder, the IP address of the HTTP server +Packer launches for you to access files like the preseed file in the example +above (`{{ .HTTPIP }}`) is hardcoded to 10.0.2.2. If you change the network +of your VM you must guarantee that you can still access this HTTP server. + +For more examples of various boot commands, see the sample projects from our +[community templates page](/community-tools.html#templates). + +## Guest Additions + +Packer will automatically download the proper guest additions for the version of +VirtualBox that is running and upload those guest additions into the virtual +machine so that provisioners can easily install them. + +Packer downloads the guest additions from the official VirtualBox website, and +verifies the file with the official checksums released by VirtualBox. + +After the virtual machine is up and the operating system is installed, Packer +uploads the guest additions into the virtual machine. The path where they are +uploaded is controllable by `guest_additions_path`, and defaults to +"VBoxGuestAdditions.iso". Without an absolute path, it is uploaded to the home +directory of the SSH user. + +## VBoxManage Commands + +In order to perform extra customization of the virtual machine, a template can +define extra calls to `VBoxManage` to perform. +[VBoxManage](https://www.virtualbox.org/manual/ch09.html) is the command-line +interface to VirtualBox where you can completely control VirtualBox. It can be +used to do things such as set RAM, CPUs, etc. + +Extra VBoxManage commands are defined in the template in the `vboxmanage` +section. An example is shown below that sets the VRAM within the virtual machine: + +``` json +{ + "vboxmanage": [ + ["modifyvm", "{{.Name}}", "--vram", "64"] + ] +} +``` + +The value of `vboxmanage` is an array of commands to execute. These commands are +executed in the order defined. So in the above example, the memory will be set +followed by the CPUs. + +Each command itself is an array of strings, where each string is an argument to +`VBoxManage`. Each argument is treated as a [configuration +template](/docs/templates/engine.html). The only available +variable is `Name` which is replaced with the unique name of the VM, which is +required for many VBoxManage calls. diff --git a/website/source/docs/builders/virtualbox.html.md b/website/source/docs/builders/virtualbox.html.md index 4aafd82d5..1de794329 100644 --- a/website/source/docs/builders/virtualbox.html.md +++ b/website/source/docs/builders/virtualbox.html.md @@ -28,3 +28,10 @@ supports the following VirtualBox builders: VirtualBox VM export you want to use as the source. As an additional benefit, you can feed the artifact of this builder back into itself to iterate on a machine. + +- [virtualbox-vm](/docs/builders/virtualbox-vm.html) - This builder uses an + existing VM to run defined provisioners on top of that VM, and optionally + creates a snapshot to save the changes applied from the provisioners. In + addition the builder is able to export that machine to create an image. The + builder is able to attach to a defined snapshot as a starting point, which + could be defined statically or dynamically via a variable. diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index acf919440..b36336994 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -209,6 +209,9 @@ > OVF + > + VM + >