diff --git a/builder/parallels/common/driver.go b/builder/parallels/common/driver.go index 898070ae0..21870f394 100644 --- a/builder/parallels/common/driver.go +++ b/builder/parallels/common/driver.go @@ -15,9 +15,15 @@ import ( // versions out of the builder steps, so sometimes the methods are // extremely specific. type Driver interface { + // Compact a virtual disk image. + CompactDisk(string) error + // Adds new CD/DVD drive to the VM and returns name of this device DeviceAddCdRom(string, string) (string, error) + // Get path to the first virtual disk image + DiskPath(string) (string, error) + // Import a VM Import(string, string, string, bool) error diff --git a/builder/parallels/common/driver_9.go b/builder/parallels/common/driver_9.go index 5e0c41aa2..c105e47f5 100644 --- a/builder/parallels/common/driver_9.go +++ b/builder/parallels/common/driver_9.go @@ -102,6 +102,33 @@ func getAppPath(bundleId string) (string, error) { return pathOutput, nil } +func (d *Parallels9Driver) CompactDisk(diskPath string) error { + prlDiskToolPath, err := exec.LookPath("prl_disk_tool") + if err != nil { + return err + } + + // Analyze the disk content and remove unused blocks + command := []string{ + "compact", + "--hdd", diskPath, + } + if err := exec.Command(prlDiskToolPath, command...).Run(); err != nil { + return err + } + + // Remove null blocks + command = []string{ + "compact", "--buildmap", + "--hdd", diskPath, + } + if err := exec.Command(prlDiskToolPath, command...).Run(); err != nil { + return err + } + + return nil +} + func (d *Parallels9Driver) DeviceAddCdRom(name string, image string) (string, error) { command := []string{ "set", name, @@ -125,6 +152,23 @@ func (d *Parallels9Driver) DeviceAddCdRom(name string, image string) (string, er return device_name, nil } +func (d *Parallels9Driver) DiskPath(name string) (string, error) { + out, err := exec.Command(d.PrlctlPath, "list", "-i", name).Output() + if err != nil { + return "", err + } + + hddRe := regexp.MustCompile("hdd0.* image='(.*)' type=*") + matches := hddRe.FindStringSubmatch(string(out)) + if matches == nil { + return "", fmt.Errorf( + "Could not determine hdd image path in the output:\n%s", string(out)) + } + + hdd_path := matches[1] + return hdd_path, nil +} + func (d *Parallels9Driver) IsRunning(name string) (bool, error) { var stdout bytes.Buffer diff --git a/builder/parallels/common/driver_mock.go b/builder/parallels/common/driver_mock.go index 5629a6db9..fcd6b4b88 100644 --- a/builder/parallels/common/driver_mock.go +++ b/builder/parallels/common/driver_mock.go @@ -5,12 +5,21 @@ import "sync" type DriverMock struct { sync.Mutex + CompactDiskCalled bool + CompactDiskPath string + CompactDiskErr error + DeviceAddCdRomCalled bool DeviceAddCdRomName string DeviceAddCdRomImage string DeviceAddCdRomResult string DeviceAddCdRomErr error + DiskPathCalled bool + DiskPathName string + DiskPathResult string + DiskPathErr error + ImportCalled bool ImportName string ImportSrcPath string @@ -54,6 +63,12 @@ type DriverMock struct { IpAddressError error } +func (d *DriverMock) CompactDisk(path string) error { + d.CompactDiskCalled = true + d.CompactDiskPath = path + return d.CompactDiskErr +} + func (d *DriverMock) DeviceAddCdRom(name string, image string) (string, error) { d.DeviceAddCdRomCalled = true d.DeviceAddCdRomName = name @@ -61,6 +76,12 @@ func (d *DriverMock) DeviceAddCdRom(name string, image string) (string, error) { return d.DeviceAddCdRomResult, d.DeviceAddCdRomErr } +func (d *DriverMock) DiskPath(name string) (string, error) { + d.DiskPathCalled = true + d.DiskPathName = name + return d.DiskPathResult, d.DiskPathErr +} + func (d *DriverMock) Import(name, srcPath, dstPath string, reassignMac bool) error { d.ImportCalled = true d.ImportName = name diff --git a/builder/parallels/common/step_compact_disk.go b/builder/parallels/common/step_compact_disk.go new file mode 100644 index 000000000..0ebc7a134 --- /dev/null +++ b/builder/parallels/common/step_compact_disk.go @@ -0,0 +1,51 @@ +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +// This step removes all empty blocks from expanding Parallels virtual disks +// and reduces the result disk size +// +// Uses: +// driver Driver +// vmName string +// ui packer.Ui +// +// Produces: +// +type StepCompactDisk struct { + Skip bool +} + +func (s *StepCompactDisk) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + vmName := state.Get("vmName").(string) + ui := state.Get("ui").(packer.Ui) + + if s.Skip { + ui.Say("Skipping disk compaction step...") + return multistep.ActionContinue + } + + ui.Say("Compacting the disk image") + diskPath, err := driver.DiskPath(vmName) + if err != nil { + err := fmt.Errorf("Error detecting virtual disk path: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + if err := driver.CompactDisk(diskPath); err != nil { + state.Put("error", fmt.Errorf("Error compacting disk: %s", err)) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (*StepCompactDisk) Cleanup(multistep.StateBag) {} diff --git a/builder/parallels/common/step_compact_disk_test.go b/builder/parallels/common/step_compact_disk_test.go new file mode 100644 index 000000000..ace932a2d --- /dev/null +++ b/builder/parallels/common/step_compact_disk_test.go @@ -0,0 +1,73 @@ +package common + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/mitchellh/multistep" +) + +func TestStepCompactDisk_impl(t *testing.T) { + var _ multistep.Step = new(StepCompactDisk) +} + +func TestStepCompactDisk(t *testing.T) { + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + tf.Close() + defer os.Remove(tf.Name()) + + state := testState(t) + step := new(StepCompactDisk) + + state.Put("vmName", "foo") + + driver := state.Get("driver").(*DriverMock) + + // Mock results + driver.DiskPathResult = tf.Name() + + // Test the run + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + if _, ok := state.GetOk("error"); ok { + t.Fatal("should NOT have error") + } + + // Test the driver + if !driver.CompactDiskCalled { + t.Fatal("should've called") + } + + path, _ := driver.DiskPath("foo") + if path != tf.Name() { + t.Fatal("should call with right path") + } +} + +func TestStepCompactDisk_skip(t *testing.T) { + state := testState(t) + step := new(StepCompactDisk) + step.Skip = true + + state.Put("vmName", "foo") + + driver := state.Get("driver").(*DriverMock) + + // Test the run + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + if _, ok := state.GetOk("error"); ok { + t.Fatal("should NOT have error") + } + + // Test the driver + if driver.CompactDiskCalled { + t.Fatal("should not have called") + } +} diff --git a/builder/parallels/iso/builder.go b/builder/parallels/iso/builder.go index cba02dd19..2d1d96ba3 100644 --- a/builder/parallels/iso/builder.go +++ b/builder/parallels/iso/builder.go @@ -45,6 +45,7 @@ type Config struct { ISOChecksum string `mapstructure:"iso_checksum"` ISOChecksumType string `mapstructure:"iso_checksum_type"` ISOUrls []string `mapstructure:"iso_urls"` + SkipCompaction bool `mapstructure:"skip_compaction"` VMName string `mapstructure:"vm_name"` RawSingleISOUrl string `mapstructure:"iso_url"` @@ -271,6 +272,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe Commands: b.config.PrlctlPost, Ctx: b.config.ctx, }, + ¶llelscommon.StepCompactDisk{ + Skip: b.config.SkipCompaction, + }, } // Setup the state bag diff --git a/website/source/docs/builders/parallels-iso.html.markdown b/website/source/docs/builders/parallels-iso.html.markdown index 76278ec2b..4200adb73 100644 --- a/website/source/docs/builders/parallels-iso.html.markdown +++ b/website/source/docs/builders/parallels-iso.html.markdown @@ -196,6 +196,11 @@ builder. doesn't shut down in this time, it is an error. By default, the timeout is "5m", or five minutes. +- `skip_compaction` (boolean) - Virtual disk image is compacted at the end of + the build process using `prl_disk_tool` utility. In certain rare cases, this + might corrupt the resulting disk image. If you find this to be the case, + you can disable compaction using this configuration value. + - `vm_name` (string) - This is the name of the PVM directory for the new virtual machine, without the file extension. By default this is "packer-BUILDNAME", where "BUILDNAME" is the name of the build. diff --git a/website/source/docs/builders/parallels-pvm.html.markdown b/website/source/docs/builders/parallels-pvm.html.markdown index ce13f2c19..2c81ecd44 100644 --- a/website/source/docs/builders/parallels-pvm.html.markdown +++ b/website/source/docs/builders/parallels-pvm.html.markdown @@ -142,6 +142,11 @@ builder. doesn't shut down in this time, it is an error. By default, the timeout is "5m", or five minutes. +- `skip_compaction` (boolean) - Virtual disk image is compacted at the end of + the build process using `prl_disk_tool` utility. In certain rare cases, this + might corrupt the resulting disk image. If you find this to be the case, + you can disable compaction using this configuration value. + - `vm_name` (string) - This is the name of the virtual machine when it is imported as well as the name of the PVM directory when the virtual machine is exported. By default this is "packer-BUILDNAME", where "BUILDNAME" is the