diff --git a/builder/parallels/common/driver.go b/builder/parallels/common/driver.go index 898070ae09c..21870f394e9 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 5e0c41aa2f2..c105e47f5e5 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 5629a6db98d..fcd6b4b880c 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 00000000000..0ebc7a1345e --- /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 00000000000..ace932a2dfd --- /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 cba02dd199e..2d1d96ba355 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 76278ec2bcf..4200adb73c4 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 ce13f2c199d..2c81ecd442f 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