From 2a531f8ad631e5316cdbbb7cb39fce8d368b7ab8 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Sat, 16 Feb 2019 14:32:22 +0100 Subject: [PATCH 01/23] Merged changes from local branch --- builder/virtualbox/common/driver.go | 18 ++ builder/virtualbox/common/driver_4_2.go | 92 +++++++++ builder/virtualbox/common/step_forward_ssh.go | 11 +- builder/virtualbox/vm/builder.go | 177 ++++++++++++++++++ builder/virtualbox/vm/config.go | 158 ++++++++++++++++ builder/virtualbox/vm/step_create_snapshot.go | 53 ++++++ builder/virtualbox/vm/step_import.go | 20 ++ builder/virtualbox/vm/step_set_snapshot.go | 68 +++++++ command/plugin.go | 2 + 9 files changed, 595 insertions(+), 4 deletions(-) create mode 100644 builder/virtualbox/vm/builder.go create mode 100644 builder/virtualbox/vm/config.go create mode 100644 builder/virtualbox/vm/step_create_snapshot.go create mode 100644 builder/virtualbox/vm/step_import.go create mode 100644 builder/virtualbox/vm/step_set_snapshot.go diff --git a/builder/virtualbox/common/driver.go b/builder/virtualbox/common/driver.go index c0085fe2d..29dec0e97 100644 --- a/builder/virtualbox/common/driver.go +++ b/builder/virtualbox/common/driver.go @@ -51,6 +51,24 @@ type Driver interface { // Version reads the version of VirtualBox that is installed. Version() (string, error) + + // + CreateSnapshot(string, string) error + + // + HasSnapshots(string) (bool, error) + + // + GetCurrentSnapshot(string) (string, error) + + // + SetSnapshot(string, string) error + + // + DeleteSnapshot(string, string) error + + // + SnapshotExists(string, string) (bool, 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..caca1a408 100644 --- a/builder/virtualbox/common/driver_4_2.go +++ b/builder/virtualbox/common/driver_4_2.go @@ -239,3 +239,95 @@ func (d *VBox42Driver) Version() (string, error) { log.Printf("VirtualBox version: %s", matches[0][1]) return matches[0][1], nil } + +func (d *VBox42Driver) CreateSnapshot(vmname string, snapshotName string) error { + return d.VBoxManage("snapshot", vmname, "take", snapshotName) +} + +func (d *VBox42Driver) HasSnapshots(vmname string) (bool, error) { + var stdout, stderr bytes.Buffer + var hasSnapshots = false + + cmd := exec.Command(d.VBoxManagePath, "snapshot", vmname, "list", "--machinereadable") + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + + stdoutString := strings.TrimSpace(stdout.String()) + stderrString := strings.TrimSpace(stderr.String()) + + if _, ok := err.(*exec.ExitError); ok { + if stdoutString != "This machine does not have any snapshots" { + err = fmt.Errorf("VBoxManage error: %s", stderrString) + } + } else { + hasSnapshots = true + } + + return hasSnapshots, err +} + +func (d *VBox42Driver) GetCurrentSnapshot(vmname string) (string, error) { + var stdout, stderr bytes.Buffer + + cmd := exec.Command(d.VBoxManagePath, "snapshot", vmname, "list", "--machinereadable") + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + + stdoutString := strings.TrimSpace(stdout.String()) + stderrString := strings.TrimSpace(stderr.String()) + + if _, ok := err.(*exec.ExitError); ok { + if stdoutString == "This machine does not have any snapshots" { + return "", nil + } else { + return "", (fmt.Errorf("VBoxManage error: %s", stderrString)) + } + } + + CurrentSnapshotNameRe := regexp.MustCompile("CurrentSnapshotName=\"(?P[^\"]*)\"") + + for _, line := range strings.Split(stdout.String(), "\n") { + result := CurrentSnapshotNameRe.FindStringSubmatch(line) + if len(result) > 1 { + return result[1], nil + } + } + + return "", (fmt.Errorf("VBoxManage unable to find current snapshot name")) +} + +func (d *VBox42Driver) SetSnapshot(vmname string, snapshotName string) error { + var err error + if snapshotName == "" { + err = d.VBoxManage("snapshot", vmname, "restorecurrent") + } else { + err = d.VBoxManage("snapshot", vmname, "restore", snapshotName) + } + return err +} + +func (d *VBox42Driver) DeleteSnapshot(vmname string, snapshotName string) error { + return d.VBoxManage("snapshot", vmname, "delete", snapshotName) +} + +func (d *VBox42Driver) SnapshotExists(vmname string, snapshotName string) (bool, error) { + var stdout bytes.Buffer + + cmd := exec.Command(d.VBoxManagePath, "snapshot", vmname, "list", "--machinereadable") + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + return false, err + } + + SnapshotNameRe := regexp.MustCompile(fmt.Sprintf("SnapshotName[^=]*=[^\"]*\"%s\"", snapshotName)) + + for _, line := range strings.Split(stdout.String(), "\n") { + if SnapshotNameRe.MatchString(line) { + return true, nil + } + } + + return false, nil +} diff --git a/builder/virtualbox/common/step_forward_ssh.go b/builder/virtualbox/common/step_forward_ssh.go index 345b9c55f..07a7be5c4 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" @@ -70,10 +71,12 @@ func (s *StepForwardSSH) Run(ctx context.Context, state multistep.StateBag) mult fmt.Sprintf("packercomm,tcp,127.0.0.1,%d,,%d", sshHostPort, guestPort), } 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") { + err := fmt.Errorf("Error creating port forwarding rule: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } } } diff --git a/builder/virtualbox/vm/builder.go b/builder/virtualbox/vm/builder.go new file mode 100644 index 000000000..1ced9148e --- /dev/null +++ b/builder/virtualbox/vm/builder.go @@ -0,0 +1,177 @@ +package vm + +import ( + "errors" + "fmt" + "log" + + 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(ui packer.Ui, hook packer.Hook, cache packer.Cache) (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("cache", cache) + state.Put("hook", hook) + state.Put("ui", ui) + + // Build the steps. + steps := []multistep.Step{ + &common.StepOutputDir{ + Force: b.config.PackerForce, + Path: b.config.OutputDir, + }, + new(vboxcommon.StepSuppressMessages), + &common.StepCreateFloppy{ + Files: b.config.FloppyConfig.FloppyFiles, + Directories: b.config.FloppyConfig.FloppyDirectories, + }, + &StepSetSnapshot{ + Name: b.config.VMName, + AttachSnapshot: b.config.AttachSnapshot, + }, + &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, + }, + &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, + }, + } + + // Run the steps. + b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state) + b.runner.Run(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.") + } + + return vboxcommon.NewArtifact(b.config.OutputDir) +} + +// Cancel. +func (b *Builder) Cancel() { + if b.runner != nil { + log.Println("Cancelling the step runner...") + b.runner.Cancel() + } +} diff --git a/builder/virtualbox/vm/config.go b/builder/virtualbox/vm/config.go new file mode 100644 index 000000000..9786cb0f3 --- /dev/null +++ b/builder/virtualbox/vm/config.go @@ -0,0 +1,158 @@ +package vm + +import ( + "fmt" + "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"` + 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" + } + + // 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)...) + + 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 != "" { + snapshotExists, err := driver.SnapshotExists(c.VMName, c.AttachSnapshot) + if err != nil { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed to check for snapshot: %s", err)) + } else { + if !snapshotExists { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Snapshot does not exist: %s", c.AttachSnapshot)) + } + } + } + if c.TargetSnapshot != "" { + snapshotExists, err := driver.SnapshotExists(c.VMName, c.TargetSnapshot) + if err != nil { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed to check for snapshot: %s", err)) + } else { + if snapshotExists { + warnings = append(warnings, fmt.Sprintf("Target snapshot already exists: %s.", c.TargetSnapshot)) + } + } + } + } + // 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..0348a8578 --- /dev/null +++ b/builder/virtualbox/vm/step_create_snapshot.go @@ -0,0 +1,53 @@ +package vm + +import ( + "context" + "fmt" + "time" + + 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 != "" { + time.Sleep(10 * time.Second) // Wait after the Vm has been shutdown, otherwise creating the snapshot might make the VM unstartable + ui.Say(fmt.Sprintf("Creating snapshot %s on virtual machine %s", s.TargetSnapshot, s.Name)) + 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) { + /* + driver := state.Get("driver").(vboxcommon.Driver) + if s.TargetSnapshot != "" { + ui := state.Get("ui").(packer.Ui) + ui.Say(fmt.Sprintf("Deleting snapshot %s on virtual machine %s", s.TargetSnapshot, s.Name)) + err := driver.DeleteSnapshot(s.Name, s.TargetSnapshot) + if err != nil { + err := fmt.Errorf("Error cleaning up created snaphot VM: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return + } + } + */ +} 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..c4b957f19 --- /dev/null +++ b/builder/virtualbox/vm/step_set_snapshot.go @@ -0,0 +1,68 @@ +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 + revertToSnapshot string +} + +func (s *StepSetSnapshot) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(vboxcommon.Driver) + if s.AttachSnapshot != "" { + ui := state.Get("ui").(packer.Ui) + hasSnapshots, err := driver.HasSnapshots(s.Name) + if err != nil { + err := fmt.Errorf("Error checking for snapshots VM: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + if !hasSnapshots { + 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, err := driver.GetCurrentSnapshot(s.Name) + if err != nil { + err := fmt.Errorf("Unable to get current snapshot for VM: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + ui.Say(fmt.Sprintf("Attaching snapshot %s on virtual machine %s", s.AttachSnapshot, s.Name)) + err = driver.SetSnapshot(s.Name, s.AttachSnapshot) + 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 + } + s.revertToSnapshot = currentSnapshot + } + 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) + ui.Say(fmt.Sprintf("Reverting to snapshot %s on virtual machine %s", s.revertToSnapshot, s.Name)) + err := driver.SetSnapshot(s.Name, s.revertToSnapshot) + 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/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), From d3202497ae2c5dca416bd7188d0c486a82a286a6 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Sun, 10 Mar 2019 15:38:38 +0100 Subject: [PATCH 02/23] First working version of virtualbox/vm builder --- builder/virtualbox/common/driver.go | 3 + builder/virtualbox/common/driver_4_2.go | 84 ++++++++++++++++++- builder/virtualbox/common/step_forward_ssh.go | 18 +++- builder/virtualbox/vm/builder.go | 20 +++-- builder/virtualbox/vm/config.go | 31 ++++++- builder/virtualbox/vm/step_create_snapshot.go | 19 ++++- builder/virtualbox/vm/step_set_snapshot.go | 51 ++++++----- command/build.go | 8 +- 8 files changed, 195 insertions(+), 39 deletions(-) diff --git a/builder/virtualbox/common/driver.go b/builder/virtualbox/common/driver.go index 29dec0e97..2c1c69b38 100644 --- a/builder/virtualbox/common/driver.go +++ b/builder/virtualbox/common/driver.go @@ -69,6 +69,9 @@ type Driver interface { // SnapshotExists(string, string) (bool, error) + + // + GetParentSnapshot(string, string) (string, error) } func NewDriver() (Driver, error) { diff --git a/builder/virtualbox/common/driver_4_2.go b/builder/virtualbox/common/driver_4_2.go index caca1a408..fe7a6964a 100644 --- a/builder/virtualbox/common/driver_4_2.go +++ b/builder/virtualbox/common/driver_4_2.go @@ -12,7 +12,8 @@ import ( "time" versionUtil "github.com/hashicorp/go-version" - "github.com/hashicorp/packer/common/retry" + + packer "github.com/hashicorp/packer/common" ) type VBox42Driver struct { @@ -241,10 +242,14 @@ func (d *VBox42Driver) Version() (string, error) { } func (d *VBox42Driver) CreateSnapshot(vmname string, snapshotName string) error { + 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) { + log.Printf("Executing HasSnapshots: VM: %s", vmname) + var stdout, stderr bytes.Buffer var hasSnapshots = false @@ -268,6 +273,8 @@ func (d *VBox42Driver) HasSnapshots(vmname string) (bool, error) { } func (d *VBox42Driver) GetCurrentSnapshot(vmname string) (string, error) { + log.Printf("Executing GetCurrentSnapshot: VM: %s", vmname) + var stdout, stderr bytes.Buffer cmd := exec.Command(d.VBoxManagePath, "snapshot", vmname, "list", "--machinereadable") @@ -299,6 +306,8 @@ func (d *VBox42Driver) GetCurrentSnapshot(vmname string) (string, error) { } func (d *VBox42Driver) SetSnapshot(vmname string, snapshotName string) error { + log.Printf("Executing SetSnapshot: VM: %s, SnapshotName %s", vmname, snapshotName) + var err error if snapshotName == "" { err = d.VBoxManage("snapshot", vmname, "restorecurrent") @@ -313,12 +322,18 @@ func (d *VBox42Driver) DeleteSnapshot(vmname string, snapshotName string) error } func (d *VBox42Driver) SnapshotExists(vmname string, snapshotName string) (bool, error) { - var stdout bytes.Buffer + log.Printf("Executing SnapshotExists: VM %s, SnapshotName %s", vmname, snapshotName) + + var stdout, stderr bytes.Buffer cmd := exec.Command(d.VBoxManagePath, "snapshot", vmname, "list", "--machinereadable") cmd.Stdout = &stdout - if err := cmd.Run(); err != nil { - return false, err + cmd.Stderr = &stderr + err := cmd.Run() + + if _, ok := err.(*exec.ExitError); ok { + stderrString := strings.TrimSpace(stderr.String()) + return false, (fmt.Errorf("VBoxManage error: %s", stderrString)) } SnapshotNameRe := regexp.MustCompile(fmt.Sprintf("SnapshotName[^=]*=[^\"]*\"%s\"", snapshotName)) @@ -331,3 +346,64 @@ func (d *VBox42Driver) SnapshotExists(vmname string, snapshotName string) (bool, return false, nil } + +func (d *VBox42Driver) GetParentSnapshot(vmname string, snapshotName string) (string, error) { + log.Printf("Executing GetParentSnapshot: VM %s, SnapshotName %s", vmname, snapshotName) + + var stdout, stderr bytes.Buffer + + cmd := exec.Command(d.VBoxManagePath, "snapshot", vmname, "list", "--machinereadable") + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + + if _, ok := err.(*exec.ExitError); ok { + stderrString := strings.TrimSpace(stderr.String()) + return "", (fmt.Errorf("VBoxManage error: %s", stderrString)) + } + + SnapshotNameRe := regexp.MustCompile(fmt.Sprintf("SnapshotName[^=]*=[^\"]*\"%s\"", snapshotName)) + + var snapshot string + for _, line := range strings.Split(stdout.String(), "\n") { + if SnapshotNameRe.MatchString(line) { + snapshot = line + break + } + } + + if snapshot == "" { + return "", (fmt.Errorf("Snapshot %s does not exist", snapshotName)) + } + + SnapshotNamePartsRe := regexp.MustCompile("SnapshotName(?P(-[1-9]+)*)") + matches := SnapshotNamePartsRe.FindStringSubmatch(snapshot) + log.Printf("************ Snapshot %s name parts", snapshot) + log.Printf("Matches %#v\n", matches) + log.Printf("Node %s\n", matches[0]) + log.Printf("Path %s\n", matches[1]) + log.Printf("Leaf %s\n", matches[2]) + leaf := matches[2] + node := matches[0] + if node == "" { + return "", (fmt.Errorf("Unsupported format for snapshot %s", snapshot)) + } + if leaf != "" && node != "" { + SnapshotNodeRe := regexp.MustCompile("^(?PSnapshotName[^=]*)=[^\"]*\"(?P[^\"]+)\"") + parentNode := node[:len(node)-len(leaf)] + log.Printf("Parent node %s\n", parentNode) + var parentName string + for _, line := range strings.Split(stdout.String(), "\n") { + if matches := SnapshotNodeRe.FindStringSubmatch(line); len(matches) > 1 && parentNode == matches[1] { + parentName = matches[2] + log.Printf("Parent Snapshot name %s\n", parentName) + break + } + } + if parentName == "" { + return "", (fmt.Errorf("Internal error: Unable to find name for snapshot node %s", parentNode)) + } + return parentName, nil + } + return "", nil +} diff --git a/builder/virtualbox/common/step_forward_ssh.go b/builder/virtualbox/common/step_forward_ssh.go index 07a7be5c4..ecb0947e0 100644 --- a/builder/virtualbox/common/step_forward_ssh.go +++ b/builder/virtualbox/common/step_forward_ssh.go @@ -70,12 +70,28 @@ 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 { - if !strings.Contains(err.Error(), "A NAT rule of this name already exists") { + 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/vm/builder.go b/builder/virtualbox/vm/builder.go index 1ced9148e..59f1b2ce3 100644 --- a/builder/virtualbox/vm/builder.go +++ b/builder/virtualbox/vm/builder.go @@ -50,10 +50,6 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe // Build the steps. steps := []multistep.Step{ - &common.StepOutputDir{ - Force: b.config.PackerForce, - Path: b.config.OutputDir, - }, new(vboxcommon.StepSuppressMessages), &common.StepCreateFloppy{ Files: b.config.FloppyConfig.FloppyFiles, @@ -62,6 +58,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &StepSetSnapshot{ Name: b.config.VMName, AttachSnapshot: b.config.AttachSnapshot, + KeepRegistered: b.config.KeepRegistered, }, &common.StepHTTPServer{ HTTPDir: b.config.HTTPDir, @@ -105,6 +102,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe VMName: b.config.VMName, Ctx: b.config.ctx, GroupInterval: b.config.BootConfig.BootGroupInterval, + Comm: &b.config.Comm, }, &communicator.StepConnect{ Config: &b.config.SSHConfig.Comm, @@ -147,6 +145,14 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe }, } + 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(state) @@ -165,7 +171,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe return nil, errors.New("Build was halted.") } - return vboxcommon.NewArtifact(b.config.OutputDir) + if b.config.SkipExport { + return nil, nil + } else { + return vboxcommon.NewArtifact(b.config.OutputDir) + } } // Cancel. diff --git a/builder/virtualbox/vm/config.go b/builder/virtualbox/vm/config.go index 9786cb0f3..7ae1567cf 100644 --- a/builder/virtualbox/vm/config.go +++ b/builder/virtualbox/vm/config.go @@ -131,10 +131,10 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { if c.AttachSnapshot != "" { snapshotExists, err := driver.SnapshotExists(c.VMName, c.AttachSnapshot) if err != nil { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed to check for snapshot: %s", err)) + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed to check for snapshot: %s with VM %s ; Error: %s", c.AttachSnapshot, c.VMName, err)) } else { if !snapshotExists { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("Snapshot does not exist: %s", c.AttachSnapshot)) + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Snapshot %s does not exist on with VM %s", c.AttachSnapshot, c.VMName)) } } } @@ -144,10 +144,35 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed to check for snapshot: %s", err)) } else { if snapshotExists { - warnings = append(warnings, fmt.Sprintf("Target snapshot already exists: %s.", c.TargetSnapshot)) + parent, err := driver.GetParentSnapshot(c.VMName, c.TargetSnapshot) + if err != nil { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed to get parent for snapshot %s: %s", c.TargetSnapshot, err)) + return nil, warnings, errs + } else { + var selfSnapshotName string + if "" != c.AttachSnapshot { + selfSnapshotName = c.AttachSnapshot + } else { + currentSnapshot, err := driver.GetCurrentSnapshot(c.VMName) + if err != nil { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed to get current snapshot for VM %s: %s", c.VMName, err)) + return nil, warnings, errs + } + selfSnapshotName = currentSnapshot + } + selfSnapshotParent, err := driver.GetParentSnapshot(c.VMName, selfSnapshotName) + if err != nil { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed to get parent for snapshot %s: %s", selfSnapshotName, err)) + } else if parent != selfSnapshotName { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Target snapshot %s already exists and is not a direct child of %s", c.TargetSnapshot, selfSnapshotParent)) + } + } } } } + 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)) + } } // Check for any errors. if errs != nil && len(errs.Errors) > 0 { diff --git a/builder/virtualbox/vm/step_create_snapshot.go b/builder/virtualbox/vm/step_create_snapshot.go index 0348a8578..902e684b0 100644 --- a/builder/virtualbox/vm/step_create_snapshot.go +++ b/builder/virtualbox/vm/step_create_snapshot.go @@ -3,6 +3,7 @@ package vm import ( "context" "fmt" + "log" "time" vboxcommon "github.com/hashicorp/packer/builder/virtualbox/common" @@ -21,7 +22,23 @@ func (s *StepCreateSnapshot) Run(_ context.Context, state multistep.StateBag) mu if s.TargetSnapshot != "" { time.Sleep(10 * time.Second) // Wait after the Vm has been shutdown, otherwise creating the snapshot might make the VM unstartable ui.Say(fmt.Sprintf("Creating snapshot %s on virtual machine %s", s.TargetSnapshot, s.Name)) - err := driver.CreateSnapshot(s.Name, s.TargetSnapshot) + snapshotExists, err := driver.SnapshotExists(s.Name, s.TargetSnapshot) + if err != nil { + err = fmt.Errorf("Failed to check for snapshot: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } else if snapshotExists { + log.Printf("Deleting existing target snapshot %s", s.TargetSnapshot) + err = driver.DeleteSnapshot(s.Name, s.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) diff --git a/builder/virtualbox/vm/step_set_snapshot.go b/builder/virtualbox/vm/step_set_snapshot.go index c4b957f19..9d4eb9c8f 100644 --- a/builder/virtualbox/vm/step_set_snapshot.go +++ b/builder/virtualbox/vm/step_set_snapshot.go @@ -12,26 +12,20 @@ import ( 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) - if s.AttachSnapshot != "" { - ui := state.Get("ui").(packer.Ui) - hasSnapshots, err := driver.HasSnapshots(s.Name) - if err != nil { - err := fmt.Errorf("Error checking for snapshots VM: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - if !hasSnapshots { - 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 - } + ui := state.Get("ui").(packer.Ui) + hasSnapshots, err := driver.HasSnapshots(s.Name) + if err != nil { + err := fmt.Errorf("Error checking for snapshots VM: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } else if hasSnapshots { currentSnapshot, err := driver.GetCurrentSnapshot(s.Name) if err != nil { err := fmt.Errorf("Unable to get current snapshot for VM: %s", err) @@ -39,6 +33,15 @@ func (s *StepSetSnapshot) Run(_ context.Context, state multistep.StateBag) multi ui.Error(err.Error()) return multistep.ActionHalt } + s.revertToSnapshot = currentSnapshot + } + if s.AttachSnapshot != "" { + if !hasSnapshots { + 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 + } ui.Say(fmt.Sprintf("Attaching snapshot %s on virtual machine %s", s.AttachSnapshot, s.Name)) err = driver.SetSnapshot(s.Name, s.AttachSnapshot) if err != nil { @@ -47,7 +50,6 @@ func (s *StepSetSnapshot) Run(_ context.Context, state multistep.StateBag) multi ui.Error(err.Error()) return multistep.ActionHalt } - s.revertToSnapshot = currentSnapshot } return multistep.ActionContinue } @@ -56,13 +58,18 @@ func (s *StepSetSnapshot) Cleanup(state multistep.StateBag) { driver := state.Get("driver").(vboxcommon.Driver) if s.revertToSnapshot != "" { ui := state.Get("ui").(packer.Ui) - ui.Say(fmt.Sprintf("Reverting to snapshot %s on virtual machine %s", s.revertToSnapshot, s.Name)) - err := driver.SetSnapshot(s.Name, s.revertToSnapshot) - if err != nil { - err := fmt.Errorf("Unable to set snapshot for VM: %s", err) - state.Put("error", err) - ui.Error(err.Error()) + 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)) + err := driver.SetSnapshot(s.Name, s.revertToSnapshot) + 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() + } } }() From a6074894f100b291a860b8d45dd54408e60ea0ed Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Sat, 16 Mar 2019 14:59:36 +0100 Subject: [PATCH 03/23] Defined struct and first function for enhanced snapshot handling --- builder/virtualbox/common/driver_4_2.go | 5 +++++ builder/virtualbox/common/snapshot.go | 15 +++++++++++++++ builder/virtualbox/common/snapshot_test.go | 1 + 3 files changed, 21 insertions(+) create mode 100644 builder/virtualbox/common/snapshot.go create mode 100644 builder/virtualbox/common/snapshot_test.go diff --git a/builder/virtualbox/common/driver_4_2.go b/builder/virtualbox/common/driver_4_2.go index fe7a6964a..0a2e5dbe5 100644 --- a/builder/virtualbox/common/driver_4_2.go +++ b/builder/virtualbox/common/driver_4_2.go @@ -241,6 +241,11 @@ func (d *VBox42Driver) Version() (string, error) { return matches[0][1], nil } +// LoadSnapshots load the snapshots for a VM instance +func (d *VBox42Driver) LoadSnapshots(vmName string) (*VBoxSnapshot, error) { + return nil, nil +} + func (d *VBox42Driver) CreateSnapshot(vmname string, snapshotName string) error { log.Printf("Executing CreateSnapshot: VM: %s, SnapshotName %s", vmname, snapshotName) diff --git a/builder/virtualbox/common/snapshot.go b/builder/virtualbox/common/snapshot.go new file mode 100644 index 000000000..c12804ed2 --- /dev/null +++ b/builder/virtualbox/common/snapshot.go @@ -0,0 +1,15 @@ +package common + +// 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 +} + +// IsChildOf verifies if the current snaphot is a child of the passed as argument +func (sn *VBoxSnapshot) IsChildOf(candidate *VBoxSnapshot) bool { + return false +} diff --git a/builder/virtualbox/common/snapshot_test.go b/builder/virtualbox/common/snapshot_test.go new file mode 100644 index 000000000..805d0c79a --- /dev/null +++ b/builder/virtualbox/common/snapshot_test.go @@ -0,0 +1 @@ +package common From 092e32fe9e2fa308d450372dfeebf2dea553e9e3 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Sun, 17 Mar 2019 20:50:13 +0100 Subject: [PATCH 04/23] First version of reworked snapshot implementation --- builder/virtualbox/common/driver.go | 27 ++- builder/virtualbox/common/driver_4_2.go | 173 ++++++++++++------ builder/virtualbox/common/snapshot.go | 116 +++++++++++- builder/virtualbox/vm/builder.go | 3 +- builder/virtualbox/vm/config.go | 64 +++---- builder/virtualbox/vm/step_create_snapshot.go | 12 +- builder/virtualbox/vm/step_set_snapshot.go | 47 +++-- 7 files changed, 305 insertions(+), 137 deletions(-) diff --git a/builder/virtualbox/common/driver.go b/builder/virtualbox/common/driver.go index 2c1c69b38..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 @@ -52,26 +53,24 @@ 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(string) (string, error) + // GetCurrentSnapshot Returns the current snapshot for a vm + GetCurrentSnapshot(string) (*VBoxSnapshot, error) - // - SetSnapshot(string, string) error + // SetSnapshot sets the for a vm + SetSnapshot(string, *VBoxSnapshot) error - // - DeleteSnapshot(string, string) error - - // - SnapshotExists(string, string) (bool, error) - - // - GetParentSnapshot(string, string) (string, 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 0a2e5dbe5..0e749f324 100644 --- a/builder/virtualbox/common/driver_4_2.go +++ b/builder/virtualbox/common/driver_4_2.go @@ -1,6 +1,7 @@ package common import ( + "bufio" "bytes" "context" "fmt" @@ -11,8 +12,8 @@ import ( "strings" "time" + "github.com/golang-collections/collections/stack" versionUtil "github.com/hashicorp/go-version" - packer "github.com/hashicorp/packer/common" ) @@ -178,6 +179,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) @@ -205,7 +211,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 { @@ -243,89 +249,133 @@ func (d *VBox42Driver) Version() (string, error) { // LoadSnapshots load the snapshots for a VM instance func (d *VBox42Driver) LoadSnapshots(vmName string) (*VBoxSnapshot, error) { - return nil, nil + 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" { + scanner := bufio.NewScanner(strings.NewReader(stdoutString)) + SnapshotNamePartsRe := regexp.MustCompile("Snapshot(?PName|UUID)(?P(-[1-9]+)*)=\"(?P[^\"]*)\"") + var currentIndicator string + parentStack := stack.New() + var node *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) + log.Printf("************ Snapshot %s name parts", txt) + log.Printf("Matches %#v\n", matches) + log.Printf("Node %s\n", matches[0]) + log.Printf("Type %s\n", matches[1]) + log.Printf("Path %s\n", matches[2]) + log.Printf("Leaf %s\n", matches[3]) + log.Printf("Value %s\n", matches[4]) + 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 { + for i := 0; i < pathLenCur-1; i++ { + parentStack.Pop() + } + } + node = new(VBoxSnapshot) + parent := parentStack.Peek().(*VBoxSnapshot) + if nil != 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 } 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) - var stdout, stderr bytes.Buffer - var hasSnapshots = false - - cmd := exec.Command(d.VBoxManagePath, "snapshot", vmname, "list", "--machinereadable") - cmd.Stdout = &stdout - cmd.Stderr = &stderr - err := cmd.Run() - - stdoutString := strings.TrimSpace(stdout.String()) - stderrString := strings.TrimSpace(stderr.String()) - - if _, ok := err.(*exec.ExitError); ok { - if stdoutString != "This machine does not have any snapshots" { - err = fmt.Errorf("VBoxManage error: %s", stderrString) - } - } else { - hasSnapshots = true + sn, err := d.LoadSnapshots(vmname) + if nil != err { + return false, err } - - return hasSnapshots, err + return nil != sn, nil } -func (d *VBox42Driver) GetCurrentSnapshot(vmname string) (string, error) { +func (d *VBox42Driver) GetCurrentSnapshot(vmname string) (*VBoxSnapshot, error) { + if vmname == "" { + panic("Argument empty exception: vmname") + } log.Printf("Executing GetCurrentSnapshot: VM: %s", vmname) - var stdout, stderr bytes.Buffer - - cmd := exec.Command(d.VBoxManagePath, "snapshot", vmname, "list", "--machinereadable") - cmd.Stdout = &stdout - cmd.Stderr = &stderr - err := cmd.Run() - - stdoutString := strings.TrimSpace(stdout.String()) - stderrString := strings.TrimSpace(stderr.String()) - - if _, ok := err.(*exec.ExitError); ok { - if stdoutString == "This machine does not have any snapshots" { - return "", nil - } else { - return "", (fmt.Errorf("VBoxManage error: %s", stderrString)) - } + sn, err := d.LoadSnapshots(vmname) + if nil != err { + return nil, err } - - CurrentSnapshotNameRe := regexp.MustCompile("CurrentSnapshotName=\"(?P[^\"]*)\"") - - for _, line := range strings.Split(stdout.String(), "\n") { - result := CurrentSnapshotNameRe.FindStringSubmatch(line) - if len(result) > 1 { - return result[1], nil - } - } - - return "", (fmt.Errorf("VBoxManage unable to find current snapshot name")) + return sn.GetCurrentSnapshot(), nil } -func (d *VBox42Driver) SetSnapshot(vmname string, snapshotName string) error { - log.Printf("Executing SetSnapshot: VM: %s, SnapshotName %s", vmname, snapshotName) - - var err error - if snapshotName == "" { - err = d.VBoxManage("snapshot", vmname, "restorecurrent") - } else { - err = d.VBoxManage("snapshot", vmname, "restore", snapshotName) +func (d *VBox42Driver) SetSnapshot(vmname string, sn *VBoxSnapshot) error { + if vmname == "" { + panic("Argument empty exception: vmname") } - return err + 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, snapshotName string) error { - return d.VBoxManage("snapshot", vmname, "delete", snapshotName) +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) } +/* func (d *VBox42Driver) SnapshotExists(vmname string, snapshotName string) (bool, error) { log.Printf("Executing SnapshotExists: VM %s, SnapshotName %s", vmname, snapshotName) @@ -412,3 +462,4 @@ func (d *VBox42Driver) GetParentSnapshot(vmname string, snapshotName string) (st } return "", nil } +*/ diff --git a/builder/virtualbox/common/snapshot.go b/builder/virtualbox/common/snapshot.go index c12804ed2..cd5449f78 100644 --- a/builder/virtualbox/common/snapshot.go +++ b/builder/virtualbox/common/snapshot.go @@ -1,15 +1,127 @@ package common +import ( + "strings" +) + // 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 + Children []*VBoxSnapshot } // IsChildOf verifies if the current snaphot is a child of the passed as argument func (sn *VBoxSnapshot) IsChildOf(candidate *VBoxSnapshot) bool { - return false + 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) + } + } else { + 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 +} + +// 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 { + panic("Internal channel error while traversing the snapshot tree") + } + 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 { + panic("Internal channel error while traversing the snapshot tree") + } + 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 { + panic("Internal channel error while traversing the snapshot tree") + } + 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/vm/builder.go b/builder/virtualbox/vm/builder.go index 59f1b2ce3..2b9c86268 100644 --- a/builder/virtualbox/vm/builder.go +++ b/builder/virtualbox/vm/builder.go @@ -32,7 +32,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { // Run executes a Packer build and returns a packer.Artifact representing // a VirtualBox appliance. -func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { +func (b *Builder) Run(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 { @@ -44,7 +44,6 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe state.Put("config", b.config) state.Put("debug", b.config.PackerDebug) state.Put("driver", driver) - state.Put("cache", cache) state.Put("hook", hook) state.Put("ui", ui) diff --git a/builder/virtualbox/vm/config.go b/builder/virtualbox/vm/config.go index 7ae1567cf..48741f282 100644 --- a/builder/virtualbox/vm/config.go +++ b/builder/virtualbox/vm/config.go @@ -2,6 +2,7 @@ package vm import ( "fmt" + "log" "strings" vboxcommon "github.com/hashicorp/packer/builder/virtualbox/common" @@ -128,51 +129,38 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { if err != nil { errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed creating VirtualBox driver: %s", err)) } else { - if c.AttachSnapshot != "" { - snapshotExists, err := driver.SnapshotExists(c.VMName, c.AttachSnapshot) - if err != nil { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed to check for snapshot: %s with VM %s ; Error: %s", c.AttachSnapshot, c.VMName, err)) - } else { - if !snapshotExists { + snapshotTree, err := driver.LoadSnapshots(c.VMName) + 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)) + } + attachSnapshot := snapshotTree.GetCurrentSnapshot() + if c.AttachSnapshot != "" { + snapshots := snapshotTree.GetSnapshotsByName(c.AttachSnapshot) + if 0 >= len(snapshots) { errs = packer.MultiErrorAppend(errs, fmt.Errorf("Snapshot %s does not exist on with VM %s", c.AttachSnapshot, c.VMName)) + } else if 1 < len(snapshots) { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Multiple Snapshots %s exist on with VM %s", c.AttachSnapshot, c.VMName)) + } else { + attachSnapshot = snapshots[0] } } - } - if c.TargetSnapshot != "" { - snapshotExists, err := driver.SnapshotExists(c.VMName, c.TargetSnapshot) - if err != nil { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed to check for snapshot: %s", err)) - } else { - if snapshotExists { - parent, err := driver.GetParentSnapshot(c.VMName, c.TargetSnapshot) - if err != nil { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed to get parent for snapshot %s: %s", c.TargetSnapshot, err)) - return nil, warnings, errs - } else { - var selfSnapshotName string - if "" != c.AttachSnapshot { - selfSnapshotName = c.AttachSnapshot - } else { - currentSnapshot, err := driver.GetCurrentSnapshot(c.VMName) - if err != nil { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed to get current snapshot for VM %s: %s", c.VMName, err)) - return nil, warnings, errs - } - selfSnapshotName = currentSnapshot - } - selfSnapshotParent, err := driver.GetParentSnapshot(c.VMName, selfSnapshotName) - if err != nil { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed to get parent for snapshot %s: %s", selfSnapshotName, err)) - } else if parent != selfSnapshotName { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("Target snapshot %s already exists and is not a direct child of %s", c.TargetSnapshot, selfSnapshotParent)) - } + if c.TargetSnapshot != "" { + snapshots := snapshotTree.GetSnapshotsByName(c.TargetSnapshot) + if 0 >= len(snapshots) { + isChild := false + for _, snapshot := range snapshots { + log.Printf("Checking if target snaphot %v is child of %s") + 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)) } } } } - 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)) - } } // Check for any errors. if errs != nil && len(errs.Errors) > 0 { diff --git a/builder/virtualbox/vm/step_create_snapshot.go b/builder/virtualbox/vm/step_create_snapshot.go index 902e684b0..1ff4e13bd 100644 --- a/builder/virtualbox/vm/step_create_snapshot.go +++ b/builder/virtualbox/vm/step_create_snapshot.go @@ -22,15 +22,19 @@ func (s *StepCreateSnapshot) Run(_ context.Context, state multistep.StateBag) mu if s.TargetSnapshot != "" { time.Sleep(10 * time.Second) // Wait after the Vm has been shutdown, otherwise creating the snapshot might make the VM unstartable ui.Say(fmt.Sprintf("Creating snapshot %s on virtual machine %s", s.TargetSnapshot, s.Name)) - snapshotExists, err := driver.SnapshotExists(s.Name, s.TargetSnapshot) + snapshotTree, err := driver.LoadSnapshots(s.Name) if err != nil { - err = fmt.Errorf("Failed to check for snapshot: %s", err) + 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 - } else if snapshotExists { + } + + 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, 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) diff --git a/builder/virtualbox/vm/step_set_snapshot.go b/builder/virtualbox/vm/step_set_snapshot.go index 9d4eb9c8f..0129ee196 100644 --- a/builder/virtualbox/vm/step_set_snapshot.go +++ b/builder/virtualbox/vm/step_set_snapshot.go @@ -19,36 +19,43 @@ type StepSetSnapshot struct { func (s *StepSetSnapshot) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { driver := state.Get("driver").(vboxcommon.Driver) ui := state.Get("ui").(packer.Ui) - hasSnapshots, err := driver.HasSnapshots(s.Name) + snapshotTree, err := driver.LoadSnapshots(s.Name) if err != nil { - err := fmt.Errorf("Error checking for snapshots VM: %s", err) + err := fmt.Errorf("Error loading snapshots for VM: %s", err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt - } else if hasSnapshots { - currentSnapshot, err := driver.GetCurrentSnapshot(s.Name) - if err != nil { - err := fmt.Errorf("Unable to get current snapshot for VM: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - s.revertToSnapshot = currentSnapshot } + if s.AttachSnapshot != "" { - if !hasSnapshots { + 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)) - err = driver.SetSnapshot(s.Name, s.AttachSnapshot) - if err != nil { - err := fmt.Errorf("Unable to set snapshot for VM: %s", err) + 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 @@ -63,7 +70,15 @@ func (s *StepSetSnapshot) Cleanup(state multistep.StateBag) { return } else { ui.Say(fmt.Sprintf("Reverting to snapshot %s on virtual machine %s", s.revertToSnapshot, s.Name)) - err := driver.SetSnapshot(s.Name, s.revertToSnapshot) + 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) From 45e2095ae389ce19df6153bf41cac6e5ecd39c6f Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Sat, 23 Mar 2019 22:01:40 +0100 Subject: [PATCH 05/23] Moved parsing of tree string representation to new function in snapshot.go --- builder/virtualbox/common/driver_4_2.go | 55 ++----------------------- 1 file changed, 3 insertions(+), 52 deletions(-) diff --git a/builder/virtualbox/common/driver_4_2.go b/builder/virtualbox/common/driver_4_2.go index 0e749f324..b8dc33211 100644 --- a/builder/virtualbox/common/driver_4_2.go +++ b/builder/virtualbox/common/driver_4_2.go @@ -1,7 +1,6 @@ package common import ( - "bufio" "bytes" "context" "fmt" @@ -12,7 +11,6 @@ import ( "strings" "time" - "github.com/golang-collections/collections/stack" versionUtil "github.com/hashicorp/go-version" packer "github.com/hashicorp/packer/common" ) @@ -261,56 +259,9 @@ func (d *VBox42Driver) LoadSnapshots(vmName string) (*VBoxSnapshot, error) { var rootNode *VBoxSnapshot if stdoutString != "This machine does not have any snapshots" { - scanner := bufio.NewScanner(strings.NewReader(stdoutString)) - SnapshotNamePartsRe := regexp.MustCompile("Snapshot(?PName|UUID)(?P(-[1-9]+)*)=\"(?P[^\"]*)\"") - var currentIndicator string - parentStack := stack.New() - var node *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) - log.Printf("************ Snapshot %s name parts", txt) - log.Printf("Matches %#v\n", matches) - log.Printf("Node %s\n", matches[0]) - log.Printf("Type %s\n", matches[1]) - log.Printf("Path %s\n", matches[2]) - log.Printf("Leaf %s\n", matches[3]) - log.Printf("Value %s\n", matches[4]) - 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 { - for i := 0; i < pathLenCur-1; i++ { - parentStack.Pop() - } - } - node = new(VBoxSnapshot) - parent := parentStack.Peek().(*VBoxSnapshot) - if nil != 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) - } + rootNode, err = ParseSnapshotData(stdoutString) + if nil != err { + return nil, err } } From f1f7c4e10c66573fe0c0686cc9eaa46f1ef47370 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Sat, 23 Mar 2019 22:02:36 +0100 Subject: [PATCH 06/23] * added function ParseSnapshotData to parse a string representation of a VBox snapshot tree * fixed bugs --- builder/virtualbox/common/snapshot.go | 74 +++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/builder/virtualbox/common/snapshot.go b/builder/virtualbox/common/snapshot.go index cd5449f78..cc7a3499d 100644 --- a/builder/virtualbox/common/snapshot.go +++ b/builder/virtualbox/common/snapshot.go @@ -1,7 +1,12 @@ package common import ( + "bufio" + "log" + "regexp" "strings" + + "github.com/golang-collections/collections/stack" ) // VBoxSnapshot stores the hierarchy of snapshots for a VM instance @@ -13,6 +18,66 @@ type VBoxSnapshot struct { 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) + log.Printf("************ Snapshot %s name parts", txt) + log.Printf("Matches %#v\n", matches) + log.Printf("Node %s\n", matches[0]) + log.Printf("Type %s\n", matches[1]) + log.Printf("Path %s\n", matches[2]) + log.Printf("Leaf %s\n", matches[3]) + log.Printf("Value %s\n", matches[4]) + 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-1; 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 { @@ -37,9 +102,8 @@ func walk(sn *VBoxSnapshot, ch chan *VBoxSnapshot) { for _, child := range sn.Children { walk(child, ch) } - } else { - ch <- sn } + ch <- sn } func walker(sn *VBoxSnapshot) <-chan *VBoxSnapshot { @@ -76,7 +140,7 @@ func (sn *VBoxSnapshot) GetSnapshotsByName(name string) []*VBoxSnapshot { for { node, ok := <-ch if !ok { - panic("Internal channel error while traversing the snapshot tree") + break } if strings.EqualFold(node.Name, name) { result = append(result, node) @@ -92,7 +156,7 @@ func (sn *VBoxSnapshot) GetSnapshotByUUID(uuid string) *VBoxSnapshot { for { node, ok := <-ch if !ok { - panic("Internal channel error while traversing the snapshot tree") + break } if strings.EqualFold(node.UUID, uuid) { return node @@ -108,7 +172,7 @@ func (sn *VBoxSnapshot) GetCurrentSnapshot() *VBoxSnapshot { for { node, ok := <-ch if !ok { - panic("Internal channel error while traversing the snapshot tree") + break } if node.IsCurrent { return node From ebd2780e5d9f65cbf81b91c6cf42f4df46e71e7f Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Sat, 23 Mar 2019 22:02:59 +0100 Subject: [PATCH 07/23] implemented first unit tests for snapshot.go --- builder/virtualbox/common/snapshot_test.go | 67 ++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/builder/virtualbox/common/snapshot_test.go b/builder/virtualbox/common/snapshot_test.go index 805d0c79a..31cb55272 100644 --- a/builder/virtualbox/common/snapshot_test.go +++ b/builder/virtualbox/common/snapshot_test.go @@ -1 +1,68 @@ 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-1-3="Snapshot-Export" +SnapshotUUID-1-1-3="c857d1b8-4fd6-4044-9d2c-c6e465b3cdd4" +CurrentSnapshotName="Snapshot-Export" +CurrentSnapshotUUID="c857d1b8-4fd6-4044-9d2c-c6e465b3cdd4" +CurrentSnapshotNode="SnapshotName-1-1-3" +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.NotEqual(t, rootNode, (*VBoxSnapshot)(nil)) + assert.Equal(t, rootNode.Name, "Imported") + assert.Equal(t, rootNode.UUID, "7e5b4165-91ec-4091-a74c-a5709d584530") + assert.Equal(t, len(rootNode.Children), 3) + assert.Equal(t, rootNode.Parent, (*VBoxSnapshot)(nil)) +} + +func TestSnapshot_FindNodeByUUID(t *testing.T) { + rootNode, err := ParseSnapshotData(getTestData()) + assert.NoError(t, err) + assert.NotEqual(t, rootNode, (*VBoxSnapshot)(nil)) + + node := rootNode.GetSnapshotByUUID("7b093686-2981-4ada-8b0f-4c03ae23cd1a") + assert.NotEqual(t, node, (*VBoxSnapshot)(nil)) + assert.Equal(t, node.Name, "Snapshot 6") + assert.Equal(t, node.UUID, "7b093686-2981-4ada-8b0f-4c03ae23cd1a") + assert.Equal(t, len(node.Children), 0) + assert.NotEqual(t, node.Parent, (*VBoxSnapshot)(nil)) +} + +func TestSnapshot_FindNodesByName(t *testing.T) { + rootNode, err := ParseSnapshotData(getTestData()) + assert.NoError(t, err) + assert.NotEqual(t, nil, rootNode) + + nodes := rootNode.GetSnapshotsByName("Snapshot 2") + assert.NotEqual(t, nil, nodes) + assert.Equal(t, 2, len(nodes)) +} From 2ef911f6f063742f73443b21a9718b87adf1dd10 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Sat, 30 Mar 2019 16:27:18 +0100 Subject: [PATCH 08/23] Fixed bugs in snapshot and builder code --- builder/virtualbox/common/snapshot.go | 7 --- builder/virtualbox/common/snapshot_test.go | 63 ++++++++++++++++++++-- builder/virtualbox/vm/config.go | 60 ++++++++++++++------- builder/virtualbox/vm/step_set_snapshot.go | 2 +- 4 files changed, 100 insertions(+), 32 deletions(-) diff --git a/builder/virtualbox/common/snapshot.go b/builder/virtualbox/common/snapshot.go index cc7a3499d..34a796092 100644 --- a/builder/virtualbox/common/snapshot.go +++ b/builder/virtualbox/common/snapshot.go @@ -35,13 +35,6 @@ func ParseSnapshotData(snapshotData string) (*VBoxSnapshot, error) { node.IsCurrent = true } else { matches := SnapshotNamePartsRe.FindStringSubmatch(txt) - log.Printf("************ Snapshot %s name parts", txt) - log.Printf("Matches %#v\n", matches) - log.Printf("Node %s\n", matches[0]) - log.Printf("Type %s\n", matches[1]) - log.Printf("Path %s\n", matches[2]) - log.Printf("Leaf %s\n", matches[3]) - log.Printf("Value %s\n", matches[4]) if matches[1] == "Name" { if nil == rootNode { node = new(VBoxSnapshot) diff --git a/builder/virtualbox/common/snapshot_test.go b/builder/virtualbox/common/snapshot_test.go index 31cb55272..7cfd8289b 100644 --- a/builder/virtualbox/common/snapshot_test.go +++ b/builder/virtualbox/common/snapshot_test.go @@ -41,7 +41,20 @@ func TestSnapshot_ParseFullTree(t *testing.T) { assert.Equal(t, rootNode.Name, "Imported") assert.Equal(t, rootNode.UUID, "7e5b4165-91ec-4091-a74c-a5709d584530") assert.Equal(t, len(rootNode.Children), 3) - assert.Equal(t, rootNode.Parent, (*VBoxSnapshot)(nil)) + assert.Equal(t, (*VBoxSnapshot)(nil), rootNode.Parent) +} + +func TestSnapshot_FindCurrent(t *testing.T) { + rootNode, err := ParseSnapshotData(getTestData()) + assert.NoError(t, err) + assert.NotEqual(t, rootNode, (*VBoxSnapshot)(nil)) + current := rootNode.GetCurrentSnapshot() + assert.NotEqual(t, current, (*VBoxSnapshot)(nil)) + assert.Equal(t, current.UUID, "c857d1b8-4fd6-4044-9d2c-c6e465b3cdd4") + assert.Equal(t, current.Name, "Snapshot-Export") + assert.NotEqual(t, current.Parent, (*VBoxSnapshot)(nil)) + assert.Equal(t, current.Parent.UUID, "8e12833b-c6b5-4cbd-b42b-09eff8ffc173") + assert.Equal(t, current.Parent.Name, "Snapshot 2") } func TestSnapshot_FindNodeByUUID(t *testing.T) { @@ -51,10 +64,16 @@ func TestSnapshot_FindNodeByUUID(t *testing.T) { node := rootNode.GetSnapshotByUUID("7b093686-2981-4ada-8b0f-4c03ae23cd1a") assert.NotEqual(t, node, (*VBoxSnapshot)(nil)) - assert.Equal(t, node.Name, "Snapshot 6") - assert.Equal(t, node.UUID, "7b093686-2981-4ada-8b0f-4c03ae23cd1a") - assert.Equal(t, len(node.Children), 0) - assert.NotEqual(t, node.Parent, (*VBoxSnapshot)(nil)) + 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.Equal(t, rootNode.Parent, (*VBoxSnapshot)(nil)) + + otherNode := rootNode.GetSnapshotByUUID("f4ed75b3-afc1-42d4-9e02-8df6f053d07e") + assert.NotEqual(t, otherNode, (*VBoxSnapshot)(nil)) + assert.True(t, otherNode.IsChildOf(rootNode)) + assert.False(t, node.IsChildOf(otherNode)) + assert.False(t, otherNode.IsChildOf(node)) } func TestSnapshot_FindNodesByName(t *testing.T) { @@ -66,3 +85,37 @@ func TestSnapshot_FindNodesByName(t *testing.T) { assert.NotEqual(t, nil, nodes) assert.Equal(t, 2, len(nodes)) } + +func TestSnapshot_IsChildOf(t *testing.T) { + rootNode, err := ParseSnapshotData(getTestData()) + assert.NoError(t, err) + assert.NotEqual(t, nil, rootNode) + + child := rootNode.GetSnapshotByUUID("c857d1b8-4fd6-4044-9d2c-c6e465b3cdd4") + assert.NotEqual(t, (*VBoxSnapshot)(nil), 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.NotEqual(t, (*VBoxSnapshot)(nil), 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.Equal(t, (*VBoxSnapshot)(nil), rootNode.Parent) +} + +func TestSnapshot_EmptySnapshotData(t *testing.T) { + snapData := `` + + rootNode, err := ParseSnapshotData(snapData) + assert.NoError(t, err) + assert.Equal(t, (*VBoxSnapshot)(nil), rootNode) +} diff --git a/builder/virtualbox/vm/config.go b/builder/virtualbox/vm/config.go index 48741f282..a43b7d33b 100644 --- a/builder/virtualbox/vm/config.go +++ b/builder/virtualbox/vm/config.go @@ -129,34 +129,56 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { 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 creating VirtualBox driver: %s", err)) + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed to load snapshots for VM %s: %s", c.VMName, 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)) + 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) } - attachSnapshot := snapshotTree.GetCurrentSnapshot() if c.AttachSnapshot != "" { - snapshots := snapshotTree.GetSnapshotsByName(c.AttachSnapshot) - if 0 >= len(snapshots) { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("Snapshot %s does not exist on with VM %s", c.AttachSnapshot, c.VMName)) - } else if 1 < len(snapshots) { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("Multiple Snapshots %s exist on with VM %s", c.AttachSnapshot, c.VMName)) + 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 { - attachSnapshot = snapshots[0] + 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 != "" { - snapshots := snapshotTree.GetSnapshotsByName(c.TargetSnapshot) - if 0 >= len(snapshots) { - isChild := false - for _, snapshot := range snapshots { - log.Printf("Checking if target snaphot %v is child of %s") - 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)) + log.Printf("Checking configuration target_snapshot [%s]", c.TargetSnapshot) + if nil == snapshotTree { + log.Printf("Currently no snapshots defined in VM %s", c.VMName) + } 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 { + log.Printf("No snapshot with name %s defined in VM %s", c.TargetSnapshot, c.VMName) } } } diff --git a/builder/virtualbox/vm/step_set_snapshot.go b/builder/virtualbox/vm/step_set_snapshot.go index 0129ee196..515d9b2a0 100644 --- a/builder/virtualbox/vm/step_set_snapshot.go +++ b/builder/virtualbox/vm/step_set_snapshot.go @@ -38,7 +38,7 @@ func (s *StepSetSnapshot) Run(_ context.Context, state multistep.StateBag) multi 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) { + 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()) From 7d3c84e5f9cc7447f2eef266f7ce9d525d2f16f4 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Sat, 11 May 2019 15:12:40 +0200 Subject: [PATCH 09/23] Changed VirtualBox VM builder according to current builder interface --- builder/virtualbox/common/driver_4_2.go | 2 +- builder/virtualbox/vm/builder.go | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/builder/virtualbox/common/driver_4_2.go b/builder/virtualbox/common/driver_4_2.go index b8dc33211..296ca5343 100644 --- a/builder/virtualbox/common/driver_4_2.go +++ b/builder/virtualbox/common/driver_4_2.go @@ -12,7 +12,7 @@ import ( "time" versionUtil "github.com/hashicorp/go-version" - packer "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/common/retry" ) type VBox42Driver struct { diff --git a/builder/virtualbox/vm/builder.go b/builder/virtualbox/vm/builder.go index 2b9c86268..0b5d3bcf0 100644 --- a/builder/virtualbox/vm/builder.go +++ b/builder/virtualbox/vm/builder.go @@ -1,9 +1,9 @@ package vm import ( + "context" "errors" "fmt" - "log" vboxcommon "github.com/hashicorp/packer/builder/virtualbox/common" "github.com/hashicorp/packer/common" @@ -32,7 +32,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { // Run executes a Packer build and returns a packer.Artifact representing // a VirtualBox appliance. -func (b *Builder) Run(ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { +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 { @@ -154,7 +154,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { } // Run the steps. b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state) - b.runner.Run(state) + b.runner.Run(ctx, state) // Report any errors. if rawErr, ok := state.GetOk("error"); ok { @@ -176,11 +176,3 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { return vboxcommon.NewArtifact(b.config.OutputDir) } } - -// Cancel. -func (b *Builder) Cancel() { - if b.runner != nil { - log.Println("Cancelling the step runner...") - b.runner.Cancel() - } -} From 4faed184c90122e05e70ea3755ead4f5b7009416 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Sun, 23 Jun 2019 15:32:29 +0200 Subject: [PATCH 10/23] Added github.com/golang-collections/collections to vendor directory --- .../golang-collections/collections/LICENSE | 20 +++++++++ .../collections/stack/stack.go | 44 +++++++++++++++++++ .../collections/stack/stack_test.go | 42 ++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 vendor/github.com/golang-collections/collections/LICENSE create mode 100644 vendor/github.com/golang-collections/collections/stack/stack.go create mode 100644 vendor/github.com/golang-collections/collections/stack/stack_test.go 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/github.com/golang-collections/collections/stack/stack_test.go b/vendor/github.com/golang-collections/collections/stack/stack_test.go new file mode 100644 index 000000000..52c909475 --- /dev/null +++ b/vendor/github.com/golang-collections/collections/stack/stack_test.go @@ -0,0 +1,42 @@ +package stack + +import ( + "testing" +) + +func Test(t *testing.T) { + s := New() + + if s.Len() != 0 { + t.Errorf("Length of an empty stack should be 0") + } + + s.Push(1) + + if s.Len() != 1 { + t.Errorf("Length should be 0") + } + + if s.Peek().(int) != 1 { + t.Errorf("Top item on the stack should be 1") + } + + if s.Pop().(int) != 1 { + t.Errorf("Top item should have been 1") + } + + if s.Len() != 0 { + t.Errorf("Stack should be empty") + } + + s.Push(1) + s.Push(2) + + if s.Len() != 2 { + t.Errorf("Length should be 2") + } + + if s.Peek().(int) != 2 { + t.Errorf("Top of the stack should be 2") + } +} \ No newline at end of file From a4d168ec4b47fed4453bb989c40256b305d658d3 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Sun, 23 Jun 2019 15:46:37 +0200 Subject: [PATCH 11/23] Corrected go.mod and modules.txt --- go.mod | 1 + go.sum | 4 +++- vendor/modules.txt | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) 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/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 From 5e00253450407ca2980144cd55806f7b46bd82b9 Mon Sep 17 00:00:00 2001 From: Thomas Meckel <14177833+tmeckel@users.noreply.github.com> Date: Tue, 25 Jun 2019 12:41:43 +0200 Subject: [PATCH 12/23] Delete stack_test.go --- .../collections/stack/stack_test.go | 42 ------------------- 1 file changed, 42 deletions(-) delete mode 100644 vendor/github.com/golang-collections/collections/stack/stack_test.go diff --git a/vendor/github.com/golang-collections/collections/stack/stack_test.go b/vendor/github.com/golang-collections/collections/stack/stack_test.go deleted file mode 100644 index 52c909475..000000000 --- a/vendor/github.com/golang-collections/collections/stack/stack_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package stack - -import ( - "testing" -) - -func Test(t *testing.T) { - s := New() - - if s.Len() != 0 { - t.Errorf("Length of an empty stack should be 0") - } - - s.Push(1) - - if s.Len() != 1 { - t.Errorf("Length should be 0") - } - - if s.Peek().(int) != 1 { - t.Errorf("Top item on the stack should be 1") - } - - if s.Pop().(int) != 1 { - t.Errorf("Top item should have been 1") - } - - if s.Len() != 0 { - t.Errorf("Stack should be empty") - } - - s.Push(1) - s.Push(2) - - if s.Len() != 2 { - t.Errorf("Length should be 2") - } - - if s.Peek().(int) != 2 { - t.Errorf("Top of the stack should be 2") - } -} \ No newline at end of file From fd5a2db58fc7ca5977f6aeb7982817b8c5438809 Mon Sep 17 00:00:00 2001 From: Thomas Meckel <14177833+tmeckel@users.noreply.github.com> Date: Wed, 26 Jun 2019 16:06:20 +0200 Subject: [PATCH 13/23] Removed commented (obsolete) code from builder\virtualbox\common\driver_4_2.go --- builder/virtualbox/common/driver_4_2.go | 89 ------------------------- 1 file changed, 89 deletions(-) diff --git a/builder/virtualbox/common/driver_4_2.go b/builder/virtualbox/common/driver_4_2.go index 296ca5343..1cc3e4ff9 100644 --- a/builder/virtualbox/common/driver_4_2.go +++ b/builder/virtualbox/common/driver_4_2.go @@ -325,92 +325,3 @@ func (d *VBox42Driver) DeleteSnapshot(vmname string, sn *VBoxSnapshot) error { log.Printf("Executing DeleteSnapshot: VM: %s, SnapshotName %s", vmname, sn.UUID) return d.VBoxManage("snapshot", vmname, "delete", sn.UUID) } - -/* -func (d *VBox42Driver) SnapshotExists(vmname string, snapshotName string) (bool, error) { - log.Printf("Executing SnapshotExists: VM %s, SnapshotName %s", vmname, snapshotName) - - var stdout, stderr bytes.Buffer - - cmd := exec.Command(d.VBoxManagePath, "snapshot", vmname, "list", "--machinereadable") - cmd.Stdout = &stdout - cmd.Stderr = &stderr - err := cmd.Run() - - if _, ok := err.(*exec.ExitError); ok { - stderrString := strings.TrimSpace(stderr.String()) - return false, (fmt.Errorf("VBoxManage error: %s", stderrString)) - } - - SnapshotNameRe := regexp.MustCompile(fmt.Sprintf("SnapshotName[^=]*=[^\"]*\"%s\"", snapshotName)) - - for _, line := range strings.Split(stdout.String(), "\n") { - if SnapshotNameRe.MatchString(line) { - return true, nil - } - } - - return false, nil -} - -func (d *VBox42Driver) GetParentSnapshot(vmname string, snapshotName string) (string, error) { - log.Printf("Executing GetParentSnapshot: VM %s, SnapshotName %s", vmname, snapshotName) - - var stdout, stderr bytes.Buffer - - cmd := exec.Command(d.VBoxManagePath, "snapshot", vmname, "list", "--machinereadable") - cmd.Stdout = &stdout - cmd.Stderr = &stderr - err := cmd.Run() - - if _, ok := err.(*exec.ExitError); ok { - stderrString := strings.TrimSpace(stderr.String()) - return "", (fmt.Errorf("VBoxManage error: %s", stderrString)) - } - - SnapshotNameRe := regexp.MustCompile(fmt.Sprintf("SnapshotName[^=]*=[^\"]*\"%s\"", snapshotName)) - - var snapshot string - for _, line := range strings.Split(stdout.String(), "\n") { - if SnapshotNameRe.MatchString(line) { - snapshot = line - break - } - } - - if snapshot == "" { - return "", (fmt.Errorf("Snapshot %s does not exist", snapshotName)) - } - - SnapshotNamePartsRe := regexp.MustCompile("SnapshotName(?P(-[1-9]+)*)") - matches := SnapshotNamePartsRe.FindStringSubmatch(snapshot) - log.Printf("************ Snapshot %s name parts", snapshot) - log.Printf("Matches %#v\n", matches) - log.Printf("Node %s\n", matches[0]) - log.Printf("Path %s\n", matches[1]) - log.Printf("Leaf %s\n", matches[2]) - leaf := matches[2] - node := matches[0] - if node == "" { - return "", (fmt.Errorf("Unsupported format for snapshot %s", snapshot)) - } - if leaf != "" && node != "" { - SnapshotNodeRe := regexp.MustCompile("^(?PSnapshotName[^=]*)=[^\"]*\"(?P[^\"]+)\"") - parentNode := node[:len(node)-len(leaf)] - log.Printf("Parent node %s\n", parentNode) - var parentName string - for _, line := range strings.Split(stdout.String(), "\n") { - if matches := SnapshotNodeRe.FindStringSubmatch(line); len(matches) > 1 && parentNode == matches[1] { - parentName = matches[2] - log.Printf("Parent Snapshot name %s\n", parentName) - break - } - } - if parentName == "" { - return "", (fmt.Errorf("Internal error: Unable to find name for snapshot node %s", parentNode)) - } - return parentName, nil - } - return "", nil -} -*/ From e9b5332e9920bdbc2dd3d4fbde496897bdbaca9a Mon Sep 17 00:00:00 2001 From: Thomas Meckel <14177833+tmeckel@users.noreply.github.com> Date: Wed, 26 Jun 2019 16:06:37 +0200 Subject: [PATCH 14/23] Added missing functions to builder\virtualbox\common\driver_mock.go --- builder/virtualbox/common/driver_mock.go | 73 ++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/builder/virtualbox/common/driver_mock.go b/builder/virtualbox/common/driver_mock.go index d69a06650..7414e8c87 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(string vmName) (*VBoxSnapshot, error) { + if vmName == "" { + panic("Argument empty exception: vmName") + } + + d.LoadSnapshotsCalled = append(d.LoadSnapshotsCalled, vmName) + return d.LoadSnapshotsResult, nil +} + +func (d *DriverMock) CreateSnapshot(string vmName, string snapshotName) 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(string vmName) (bool, error) { + if vmName == "" { + panic("Argument empty exception: vmName") + } + + d.HasSnapshotsCalled = append(d.HasSnapshotsCalled, vmName) + return d.HasSnapshotsResult, nil +} + +func (d *DriverMock) GetCurrentSnapshot(string vmName) (*VBoxSnapshot, error) { + if vmName == "" { + panic("Argument empty exception: vmName") + } + + d.GetCurrentSnapshotCalled = append(d.GetCurrentSnapshotCalled, vmName) + return d.GetCurrentSnapshotResult, nil +} + +func (d *DriverMock) SetSnapshot(string vmName, *VBoxSnapshot snapshot) 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(string vmName, *VBoxSnapshot snapshot) error { + if vmName == "" { + panic("Argument empty exception: vmName") + } + if snapshot == nil { + panic("Argument empty exception: snapshot") + } + d.DeleteSnapshotCalled = append(d.DeleteSnapshotCalled, snapshot) + return nil +} \ No newline at end of file From ab6a163ff2f6d85af441de1d0fa055353a985bd3 Mon Sep 17 00:00:00 2001 From: Thomas Meckel <14177833+tmeckel@users.noreply.github.com> Date: Wed, 26 Jun 2019 16:11:11 +0200 Subject: [PATCH 15/23] Added missing newline at end of builder\virtualbox\common\driver_mock.go --- builder/virtualbox/common/driver_mock.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builder/virtualbox/common/driver_mock.go b/builder/virtualbox/common/driver_mock.go index 7414e8c87..828b3b750 100644 --- a/builder/virtualbox/common/driver_mock.go +++ b/builder/virtualbox/common/driver_mock.go @@ -186,4 +186,4 @@ func (d *DriverMock) DeleteSnapshot(string vmName, *VBoxSnapshot snapshot) error } d.DeleteSnapshotCalled = append(d.DeleteSnapshotCalled, snapshot) return nil -} \ No newline at end of file +} From 51e5ae69db1dd9926f267d58233d971e2e238159 Mon Sep 17 00:00:00 2001 From: Thomas Meckel <14177833+tmeckel@users.noreply.github.com> Date: Wed, 26 Jun 2019 16:26:10 +0200 Subject: [PATCH 16/23] Corrected method signatures in builder\virtualbox\common\driver_mock.go --- builder/virtualbox/common/driver_mock.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/builder/virtualbox/common/driver_mock.go b/builder/virtualbox/common/driver_mock.go index 828b3b750..7ecc7130f 100644 --- a/builder/virtualbox/common/driver_mock.go +++ b/builder/virtualbox/common/driver_mock.go @@ -126,7 +126,7 @@ func (d *DriverMock) Version() (string, error) { return d.VersionResult, d.VersionErr } -func (d *DriverMock) LoadSnapshots(string vmName) (*VBoxSnapshot, error) { +func (d *DriverMock) LoadSnapshots(vmName string) (*VBoxSnapshot, error) { if vmName == "" { panic("Argument empty exception: vmName") } @@ -135,7 +135,7 @@ func (d *DriverMock) LoadSnapshots(string vmName) (*VBoxSnapshot, error) { return d.LoadSnapshotsResult, nil } -func (d *DriverMock) CreateSnapshot(string vmName, string snapshotName) error { +func (d *DriverMock) CreateSnapshot(vmName string, snapshotName string) error { if vmName == "" { panic("Argument empty exception: vmName") } @@ -147,7 +147,7 @@ func (d *DriverMock) CreateSnapshot(string vmName, string snapshotName) error { return d.CreateSnapshotError } -func (d *DriverMock) HasSnapshots(string vmName) (bool, error) { +func (d *DriverMock) HasSnapshots(vmName string) (bool, error) { if vmName == "" { panic("Argument empty exception: vmName") } @@ -156,7 +156,7 @@ func (d *DriverMock) HasSnapshots(string vmName) (bool, error) { return d.HasSnapshotsResult, nil } -func (d *DriverMock) GetCurrentSnapshot(string vmName) (*VBoxSnapshot, error) { +func (d *DriverMock) GetCurrentSnapshot(vmName string) (*VBoxSnapshot, error) { if vmName == "" { panic("Argument empty exception: vmName") } @@ -165,7 +165,7 @@ func (d *DriverMock) GetCurrentSnapshot(string vmName) (*VBoxSnapshot, error) { return d.GetCurrentSnapshotResult, nil } -func (d *DriverMock) SetSnapshot(string vmName, *VBoxSnapshot snapshot) error { +func (d *DriverMock) SetSnapshot(vmName string, snapshot *VBoxSnapshot) error { if vmName == "" { panic("Argument empty exception: vmName") } @@ -177,7 +177,7 @@ func (d *DriverMock) SetSnapshot(string vmName, *VBoxSnapshot snapshot) error { return nil } -func (d *DriverMock) DeleteSnapshot(string vmName, *VBoxSnapshot snapshot) error { +func (d *DriverMock) DeleteSnapshot(vmName string, snapshot *VBoxSnapshot) error { if vmName == "" { panic("Argument empty exception: vmName") } From 225a140055115350977f910dcd57e830f720bf8b Mon Sep 17 00:00:00 2001 From: Thomas Meckel <14177833+tmeckel@users.noreply.github.com> Date: Wed, 26 Jun 2019 17:22:13 +0200 Subject: [PATCH 17/23] Applied correct source format (got fmt) to builder\virtualbox\common\driver_mock.go --- builder/virtualbox/common/driver_mock.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/builder/virtualbox/common/driver_mock.go b/builder/virtualbox/common/driver_mock.go index 7ecc7130f..68d57fa64 100644 --- a/builder/virtualbox/common/driver_mock.go +++ b/builder/virtualbox/common/driver_mock.go @@ -46,16 +46,16 @@ type DriverMock struct { VersionResult string VersionErr error - LoadSnapshotsCalled []string - LoadSnapshotsResult *VBoxSnapshot - CreateSnapshotCalled []string - CreateSnapshotError error - HasSnapshotsCalled []string - HasSnapshotsResult bool + LoadSnapshotsCalled []string + LoadSnapshotsResult *VBoxSnapshot + CreateSnapshotCalled []string + CreateSnapshotError error + HasSnapshotsCalled []string + HasSnapshotsResult bool GetCurrentSnapshotCalled []string GetCurrentSnapshotResult *VBoxSnapshot - SetSnapshotCalled []*VBoxSnapshot - DeleteSnapshotCalled []*VBoxSnapshot + SetSnapshotCalled []*VBoxSnapshot + DeleteSnapshotCalled []*VBoxSnapshot } func (d *DriverMock) CreateSATAController(vm string, controller string, portcount int) error { From 6fd0b3e83e14da85ffbfe405908a322f819a5f2e Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Fri, 26 Jul 2019 11:28:13 +0200 Subject: [PATCH 18/23] Fixed a bug in ParseSnapshotData Added fuction GetSnapshots --- builder/virtualbox/common/snapshot.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/builder/virtualbox/common/snapshot.go b/builder/virtualbox/common/snapshot.go index 34a796092..e68540182 100644 --- a/builder/virtualbox/common/snapshot.go +++ b/builder/virtualbox/common/snapshot.go @@ -48,7 +48,7 @@ func ParseSnapshotData(snapshotData string) (*VBoxSnapshot, error) { parentStack.Push(node) } else if pathLen < pathLenCur { currentIndicator = matches[2] - for i := 0; i < pathLenCur-1; i++ { + for i := 0; i < pathLenCur-pathLen; i++ { parentStack.Pop() } } @@ -125,6 +125,21 @@ func (sn *VBoxSnapshot) GetRoot() *VBoxSnapshot { 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 From b8738766706e5032f4e0b921a0d3192226877950 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Fri, 26 Jul 2019 11:31:57 +0200 Subject: [PATCH 19/23] Added test TestSnapshot_EnsureParents No using asserts NotNil and Nil --- builder/virtualbox/common/snapshot_test.go | 59 ++++++++++++++-------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/builder/virtualbox/common/snapshot_test.go b/builder/virtualbox/common/snapshot_test.go index 7cfd8289b..5b3ab78de 100644 --- a/builder/virtualbox/common/snapshot_test.go +++ b/builder/virtualbox/common/snapshot_test.go @@ -17,11 +17,11 @@ 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-1-3="Snapshot-Export" -SnapshotUUID-1-1-3="c857d1b8-4fd6-4044-9d2c-c6e465b3cdd4" +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-1-3" +CurrentSnapshotNode="SnapshotName-1-2" SnapshotName-2="Snapshot 5" SnapshotUUID-2="85646c6a-fb86-4112-b15e-cab090670778" SnapshotName-2-1="Snapshot 2" @@ -37,40 +37,41 @@ SnapshotUUID-3-2="a5903505-9261-4bd3-9972-bacd0064d667"` func TestSnapshot_ParseFullTree(t *testing.T) { rootNode, err := ParseSnapshotData(getTestData()) assert.NoError(t, err) - assert.NotEqual(t, rootNode, (*VBoxSnapshot)(nil)) + 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), 3) - assert.Equal(t, (*VBoxSnapshot)(nil), rootNode.Parent) + 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.NotEqual(t, rootNode, (*VBoxSnapshot)(nil)) + assert.NotNil(t, rootNode) + current := rootNode.GetCurrentSnapshot() - assert.NotEqual(t, current, (*VBoxSnapshot)(nil)) + assert.NotNil(t, current) assert.Equal(t, current.UUID, "c857d1b8-4fd6-4044-9d2c-c6e465b3cdd4") assert.Equal(t, current.Name, "Snapshot-Export") - assert.NotEqual(t, current.Parent, (*VBoxSnapshot)(nil)) - assert.Equal(t, current.Parent.UUID, "8e12833b-c6b5-4cbd-b42b-09eff8ffc173") - assert.Equal(t, current.Parent.Name, "Snapshot 2") + 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.NotEqual(t, rootNode, (*VBoxSnapshot)(nil)) + assert.NotNil(t, rootNode) node := rootNode.GetSnapshotByUUID("7b093686-2981-4ada-8b0f-4c03ae23cd1a") - assert.NotEqual(t, node, (*VBoxSnapshot)(nil)) + 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.Equal(t, rootNode.Parent, (*VBoxSnapshot)(nil)) + assert.Nil(t, rootNode.Parent) otherNode := rootNode.GetSnapshotByUUID("f4ed75b3-afc1-42d4-9e02-8df6f053d07e") - assert.NotEqual(t, otherNode, (*VBoxSnapshot)(nil)) + assert.NotNil(t, otherNode) assert.True(t, otherNode.IsChildOf(rootNode)) assert.False(t, node.IsChildOf(otherNode)) assert.False(t, otherNode.IsChildOf(node)) @@ -79,20 +80,20 @@ func TestSnapshot_FindNodeByUUID(t *testing.T) { func TestSnapshot_FindNodesByName(t *testing.T) { rootNode, err := ParseSnapshotData(getTestData()) assert.NoError(t, err) - assert.NotEqual(t, nil, rootNode) + assert.NotNil(t, rootNode) nodes := rootNode.GetSnapshotsByName("Snapshot 2") - assert.NotEqual(t, nil, nodes) + 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.NotEqual(t, nil, rootNode) + assert.NotNil(t, rootNode) child := rootNode.GetSnapshotByUUID("c857d1b8-4fd6-4044-9d2c-c6e465b3cdd4") - assert.NotEqual(t, (*VBoxSnapshot)(nil), child) + 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) }) @@ -104,12 +105,12 @@ func TestSnapshot_SingleSnapshot(t *testing.T) { rootNode, err := ParseSnapshotData(snapData) assert.NoError(t, err) - assert.NotEqual(t, (*VBoxSnapshot)(nil), rootNode) + 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.Equal(t, (*VBoxSnapshot)(nil), rootNode.Parent) + assert.Nil(t, rootNode.Parent) } func TestSnapshot_EmptySnapshotData(t *testing.T) { @@ -117,5 +118,19 @@ func TestSnapshot_EmptySnapshotData(t *testing.T) { rootNode, err := ParseSnapshotData(snapData) assert.NoError(t, err) - assert.Equal(t, (*VBoxSnapshot)(nil), rootNode) + 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) + } + } } From d9b8623f648d4bfe9f66498abe54d52919889ac9 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Fri, 26 Jul 2019 11:33:39 +0200 Subject: [PATCH 20/23] StepShutdown now always waits for the shutdown of the virtual machine to complete, not only if a shutdown command is specified --- builder/virtualbox/common/step_shutdown.go | 51 +++++++++++----------- 1 file changed, 26 insertions(+), 25 deletions(-) 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 } From e7583de847f121776b2a0366e33aaec4232c35c1 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Fri, 26 Jul 2019 11:35:12 +0200 Subject: [PATCH 21/23] StepCreateSnapshot.Run() will stop and create an error if the virtual machine is still running Removed left over code from StepCreateSnapshot.Cleanup() --- builder/virtualbox/vm/step_create_snapshot.go | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/builder/virtualbox/vm/step_create_snapshot.go b/builder/virtualbox/vm/step_create_snapshot.go index 1ff4e13bd..95c676069 100644 --- a/builder/virtualbox/vm/step_create_snapshot.go +++ b/builder/virtualbox/vm/step_create_snapshot.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "time" vboxcommon "github.com/hashicorp/packer/builder/virtualbox/common" "github.com/hashicorp/packer/helper/multistep" @@ -20,7 +19,18 @@ func (s *StepCreateSnapshot) Run(_ context.Context, state multistep.StateBag) mu driver := state.Get("driver").(vboxcommon.Driver) ui := state.Get("ui").(packer.Ui) if s.TargetSnapshot != "" { - time.Sleep(10 * time.Second) // Wait after the Vm has been shutdown, otherwise creating the snapshot might make the VM unstartable + 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 { @@ -56,19 +66,4 @@ func (s *StepCreateSnapshot) Run(_ context.Context, state multistep.StateBag) mu return multistep.ActionContinue } -func (s *StepCreateSnapshot) Cleanup(state multistep.StateBag) { - /* - driver := state.Get("driver").(vboxcommon.Driver) - if s.TargetSnapshot != "" { - ui := state.Get("ui").(packer.Ui) - ui.Say(fmt.Sprintf("Deleting snapshot %s on virtual machine %s", s.TargetSnapshot, s.Name)) - err := driver.DeleteSnapshot(s.Name, s.TargetSnapshot) - if err != nil { - err := fmt.Errorf("Error cleaning up created snaphot VM: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return - } - } - */ -} +func (s *StepCreateSnapshot) Cleanup(state multistep.StateBag) {} From 822b72bd14f1500675036edf39f5e84249256041 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Fri, 26 Jul 2019 11:36:05 +0200 Subject: [PATCH 22/23] Added force_delete_snapshot configuration item and the corresponding checks --- builder/virtualbox/vm/config.go | 44 ++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/builder/virtualbox/vm/config.go b/builder/virtualbox/vm/config.go index a43b7d33b..a427b5766 100644 --- a/builder/virtualbox/vm/config.go +++ b/builder/virtualbox/vm/config.go @@ -36,6 +36,7 @@ type Config struct { 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"` @@ -70,6 +71,10 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { 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)...) @@ -85,6 +90,8 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { 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")) @@ -164,21 +171,30 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { if nil == snapshotTree { log.Printf("Currently no snapshots defined in VM %s", c.VMName) } 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)) - } + 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 { - log.Printf("No snapshot with name %s defined in VM %s", c.TargetSnapshot, c.VMName) + 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) + } } } } From a876bf3c4e280d743afba3e8fc90f611904b8a69 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Sat, 27 Jul 2019 16:08:13 +0200 Subject: [PATCH 23/23] Added documentation for virtualbox-vm builder --- .../docs/builders/virtualbox-vm.html.md.erb | 395 ++++++++++++++++++ .../source/docs/builders/virtualbox.html.md | 7 + website/source/layouts/docs.erb | 3 + 3 files changed, 405 insertions(+) create mode 100644 website/source/docs/builders/virtualbox-vm.html.md.erb 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 + >