From d3202497ae2c5dca416bd7188d0c486a82a286a6 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Sun, 10 Mar 2019 15:38:38 +0100 Subject: [PATCH] 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() + } } }()