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),