From cdaa9d5a8e166be0dda6b7dd04f4fabf4c41d76d Mon Sep 17 00:00:00 2001 From: Rickard von Essen Date: Sun, 6 Apr 2014 19:21:22 +0200 Subject: [PATCH 1/4] Added support for Parallels Desktop for Mac [GH-223] Added builder plugins: builder-parallels-iso and builder-parallels-pvm. --- builder/parallels/common/artifact.go | 71 +++ builder/parallels/common/artifact_test.go | 43 ++ builder/parallels/common/config_test.go | 15 + builder/parallels/common/driver.go | 63 +++ builder/parallels/common/driver_9.go | 241 ++++++++++ builder/parallels/common/driver_9_test.go | 9 + builder/parallels/common/driver_mock.go | 100 ++++ builder/parallels/common/floppy_config.go | 31 ++ .../parallels/common/floppy_config_test.go | 18 + builder/parallels/common/output_config.go | 40 ++ .../parallels/common/output_config_test.go | 65 +++ builder/parallels/common/prlctl_config.go | 28 ++ .../parallels/common/prlctl_config_test.go | 37 ++ .../parallels/common/prlctl_version_config.go | 31 ++ .../common/prlctl_version_config_test.go | 33 ++ builder/parallels/common/run_config.go | 41 ++ builder/parallels/common/run_config_test.go | 37 ++ builder/parallels/common/shutdown_config.go | 42 ++ .../parallels/common/shutdown_config_test.go | 45 ++ builder/parallels/common/ssh.go | 71 +++ builder/parallels/common/ssh_config.go | 65 +++ builder/parallels/common/ssh_config_test.go | 155 +++++++ .../parallels/common/step_attach_floppy.go | 55 +++ .../common/step_attach_floppy_test.go | 71 +++ .../common/step_attach_parallels_tools.go | 56 +++ builder/parallels/common/step_output_dir.go | 73 +++ .../parallels/common/step_output_dir_test.go | 96 ++++ builder/parallels/common/step_prlctl.go | 68 +++ .../parallels/common/step_remove_devices.go | 63 +++ .../common/step_remove_devices_test.go | 90 ++++ builder/parallels/common/step_run.go | 80 ++++ builder/parallels/common/step_shutdown.go | 78 ++++ .../parallels/common/step_shutdown_test.go | 105 +++++ builder/parallels/common/step_test.go | 18 + .../common/step_upload_parallels_tools.go | 67 +++ .../parallels/common/step_upload_version.go | 44 ++ .../common/step_upload_version_test.go | 61 +++ builder/parallels/common/tools_modes.go | 9 + builder/parallels/iso/builder.go | 360 +++++++++++++++ builder/parallels/iso/builder_test.go | 436 ++++++++++++++++++ builder/parallels/iso/host_ip.go | 7 + builder/parallels/iso/host_ip_ifconfig.go | 55 +++ .../parallels/iso/host_ip_ifconfig_test.go | 11 + builder/parallels/iso/step_attach_iso.go | 45 ++ builder/parallels/iso/step_create_disk.go | 40 ++ builder/parallels/iso/step_create_vm.go | 79 ++++ builder/parallels/iso/step_http_server.go | 75 +++ .../parallels/iso/step_type_boot_command.go | 239 ++++++++++ builder/parallels/pvm/builder.go | 129 ++++++ builder/parallels/pvm/config.go | 131 ++++++ builder/parallels/pvm/config_test.go | 85 ++++ builder/parallels/pvm/step_import.go | 48 ++ builder/parallels/pvm/step_test.go | 19 + config.go | 2 + plugin/builder-parallels-iso/main.go | 15 + plugin/builder-parallels-iso/main_test.go | 1 + plugin/builder-parallels-pvm/main.go | 15 + plugin/builder-parallels-pvm/main_test.go | 1 + 58 files changed, 4108 insertions(+) create mode 100644 builder/parallels/common/artifact.go create mode 100644 builder/parallels/common/artifact_test.go create mode 100644 builder/parallels/common/config_test.go create mode 100644 builder/parallels/common/driver.go create mode 100644 builder/parallels/common/driver_9.go create mode 100644 builder/parallels/common/driver_9_test.go create mode 100644 builder/parallels/common/driver_mock.go create mode 100644 builder/parallels/common/floppy_config.go create mode 100644 builder/parallels/common/floppy_config_test.go create mode 100644 builder/parallels/common/output_config.go create mode 100644 builder/parallels/common/output_config_test.go create mode 100644 builder/parallels/common/prlctl_config.go create mode 100644 builder/parallels/common/prlctl_config_test.go create mode 100644 builder/parallels/common/prlctl_version_config.go create mode 100644 builder/parallels/common/prlctl_version_config_test.go create mode 100644 builder/parallels/common/run_config.go create mode 100644 builder/parallels/common/run_config_test.go create mode 100644 builder/parallels/common/shutdown_config.go create mode 100644 builder/parallels/common/shutdown_config_test.go create mode 100644 builder/parallels/common/ssh.go create mode 100644 builder/parallels/common/ssh_config.go create mode 100644 builder/parallels/common/ssh_config_test.go create mode 100644 builder/parallels/common/step_attach_floppy.go create mode 100644 builder/parallels/common/step_attach_floppy_test.go create mode 100644 builder/parallels/common/step_attach_parallels_tools.go create mode 100644 builder/parallels/common/step_output_dir.go create mode 100644 builder/parallels/common/step_output_dir_test.go create mode 100644 builder/parallels/common/step_prlctl.go create mode 100644 builder/parallels/common/step_remove_devices.go create mode 100644 builder/parallels/common/step_remove_devices_test.go create mode 100644 builder/parallels/common/step_run.go create mode 100644 builder/parallels/common/step_shutdown.go create mode 100644 builder/parallels/common/step_shutdown_test.go create mode 100644 builder/parallels/common/step_test.go create mode 100644 builder/parallels/common/step_upload_parallels_tools.go create mode 100644 builder/parallels/common/step_upload_version.go create mode 100644 builder/parallels/common/step_upload_version_test.go create mode 100644 builder/parallels/common/tools_modes.go create mode 100644 builder/parallels/iso/builder.go create mode 100644 builder/parallels/iso/builder_test.go create mode 100644 builder/parallels/iso/host_ip.go create mode 100644 builder/parallels/iso/host_ip_ifconfig.go create mode 100644 builder/parallels/iso/host_ip_ifconfig_test.go create mode 100644 builder/parallels/iso/step_attach_iso.go create mode 100644 builder/parallels/iso/step_create_disk.go create mode 100644 builder/parallels/iso/step_create_vm.go create mode 100644 builder/parallels/iso/step_http_server.go create mode 100644 builder/parallels/iso/step_type_boot_command.go create mode 100644 builder/parallels/pvm/builder.go create mode 100644 builder/parallels/pvm/config.go create mode 100644 builder/parallels/pvm/config_test.go create mode 100644 builder/parallels/pvm/step_import.go create mode 100644 builder/parallels/pvm/step_test.go create mode 100644 plugin/builder-parallels-iso/main.go create mode 100644 plugin/builder-parallels-iso/main_test.go create mode 100644 plugin/builder-parallels-pvm/main.go create mode 100644 plugin/builder-parallels-pvm/main_test.go diff --git a/builder/parallels/common/artifact.go b/builder/parallels/common/artifact.go new file mode 100644 index 000000000..308b99f72 --- /dev/null +++ b/builder/parallels/common/artifact.go @@ -0,0 +1,71 @@ +package common + +import ( + "fmt" + "github.com/mitchellh/packer/packer" + "os" + "path/filepath" + "regexp" +) + +// This is the common builder ID to all of these artifacts. +const BuilderId = "rickard-von-essen.parallels" + +// These are the extensions of files and directories that are unnecessary for the function +// of a Parallels virtual machine. +var unnecessaryFiles = []string{"\\.log$", "\\.backup$", "\\.Backup$", "\\.app"} + +// Artifact is the result of running the parallels builder, namely a set +// of files associated with the resulting machine. +type artifact struct { + dir string + f []string +} + +// NewArtifact returns a Parallels artifact containing the files +// in the given directory. +func NewArtifact(dir string) (packer.Artifact, error) { + files := make([]string, 0, 5) + visit := func(path string, info os.FileInfo, err error) error { + for _, unnecessaryFile := range unnecessaryFiles { + if unnecessary, _ := regexp.MatchString(unnecessaryFile, path); unnecessary { + return os.RemoveAll(path) + } + } + + if !info.IsDir() { + files = append(files, path) + } + + return err + } + + if err := filepath.Walk(dir, visit); err != nil { + return nil, err + } + + return &artifact{ + dir: dir, + f: files, + }, nil +} + +func (*artifact) BuilderId() string { + return BuilderId +} + +func (a *artifact) Files() []string { + return a.f +} + +func (*artifact) Id() string { + return "VM" +} + +func (a *artifact) String() string { + return fmt.Sprintf("VM files in directory: %s", a.dir) +} + +func (a *artifact) Destroy() error { + return os.RemoveAll(a.dir) +} diff --git a/builder/parallels/common/artifact_test.go b/builder/parallels/common/artifact_test.go new file mode 100644 index 000000000..f9ddc5dbf --- /dev/null +++ b/builder/parallels/common/artifact_test.go @@ -0,0 +1,43 @@ +package common + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/mitchellh/packer/packer" +) + +func TestArtifact_impl(t *testing.T) { + var _ packer.Artifact = new(artifact) +} + +func TestNewArtifact(t *testing.T) { + td, err := ioutil.TempDir("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(td) + + err = ioutil.WriteFile(filepath.Join(td, "a"), []byte("foo"), 0644) + if err != nil { + t.Fatalf("err: %s", err) + } + + if err := os.Mkdir(filepath.Join(td, "b"), 0755); err != nil { + t.Fatalf("err: %s", err) + } + + a, err := NewArtifact(td) + if err != nil { + t.Fatalf("err: %s", err) + } + + if a.BuilderId() != BuilderId { + t.Fatalf("bad: %#v", a.BuilderId()) + } + if len(a.Files()) != 1 { + t.Fatalf("should length 1: %d", len(a.Files())) + } +} diff --git a/builder/parallels/common/config_test.go b/builder/parallels/common/config_test.go new file mode 100644 index 000000000..a84c51bc1 --- /dev/null +++ b/builder/parallels/common/config_test.go @@ -0,0 +1,15 @@ +package common + +import ( + "github.com/mitchellh/packer/packer" + "testing" +) + +func testConfigTemplate(t *testing.T) *packer.ConfigTemplate { + result, err := packer.NewConfigTemplate() + if err != nil { + t.Fatalf("err: %s", err) + } + + return result +} diff --git a/builder/parallels/common/driver.go b/builder/parallels/common/driver.go new file mode 100644 index 000000000..c28f0f9f4 --- /dev/null +++ b/builder/parallels/common/driver.go @@ -0,0 +1,63 @@ +package common + +import ( + "log" + "os/exec" +) + +// A driver is able to talk to Parallels and perform certain +// operations with it. Some of the operations on here may seem overly +// specific, but they were built specifically in mind to handle features +// of the Parallels builder for Packer, and to abstract differences in +// versions out of the builder steps, so sometimes the methods are +// extremely specific. +type Driver interface { + // Import a VM + Import(string, string, string) error + + // Checks if the VM with the given name is running. + IsRunning(string) (bool, error) + + // Stop stops a running machine, forcefully. + Stop(string) error + + // Prlctl executes the given Prlctl command + Prlctl(...string) error + + // Verify checks to make sure that this driver should function + // properly. If there is any indication the driver can't function, + // this will return an error. + Verify() error + + // Version reads the version of Parallels that is installed. + Version() (string, error) + + // Send scancodes to the vm using the prltype tool. + SendKeyScanCodes(string, ...string) error + + // Finds the MAC address of the NIC nic0 + Mac(string) (string, error) + + // Finds the IP address of a VM connected that uses DHCP by its MAC address + IpAddress(string) (string, error) +} + +func NewDriver() (Driver, error) { + var prlctlPath string + + if prlctlPath == "" { + var err error + prlctlPath, err = exec.LookPath("prlctl") + if err != nil { + return nil, err + } + } + + log.Printf("prlctl path: %s", prlctlPath) + driver := &Parallels9Driver{prlctlPath} + if err := driver.Verify(); err != nil { + return nil, err + } + + return driver, nil +} diff --git a/builder/parallels/common/driver_9.go b/builder/parallels/common/driver_9.go new file mode 100644 index 000000000..e29c81bc6 --- /dev/null +++ b/builder/parallels/common/driver_9.go @@ -0,0 +1,241 @@ +package common + +import ( + "bytes" + "fmt" + "github.com/going/toolkit/xmlpath" + "log" + "os" + "os/exec" + "regexp" + "strings" + "time" +) + +type Parallels9Driver struct { + // This is the path to the "prlctl" application. + PrlctlPath string +} + +func (d *Parallels9Driver) Import(name, srcPath, dstDir string) error { + + err := d.Prlctl("register", srcPath, "--preserve-uuid") + if err != nil { + return err + } + + srcId, err := getVmId(srcPath) + if err != nil { + return err + } + + srcMac, err := getFirtsMacAddress(srcPath) + if err != nil { + return err + } + + err = d.Prlctl("clone", srcId, "--name", name, "--dst", dstDir) + if err != nil { + return err + } + + err = d.Prlctl("unregister", srcId) + if err != nil { + return err + } + + err = d.Prlctl("set", name, "--device-set", "net0", "--mac", srcMac) + return nil +} + +func getVmId(path string) (string, error) { + return getConfigValueFromXpath(path, "/ParallelsVirtualMachine/Identification/VmUuid") +} + +func getFirtsMacAddress(path string) (string, error) { + return getConfigValueFromXpath(path, "/ParallelsVirtualMachine/Hardware/NetworkAdapter[@id='0']/MAC") +} + +func getConfigValueFromXpath(path, xpath string) (string, error) { + file, err := os.Open(path + "/config.pvs") + if err != nil { + return "", err + } + xpathComp := xmlpath.MustCompile(xpath) + root, err := xmlpath.Parse(file) + if err != nil { + return "", err + } + value, _ := xpathComp.String(root) + return value, nil +} + +func (d *Parallels9Driver) IsRunning(name string) (bool, error) { + var stdout bytes.Buffer + + cmd := exec.Command(d.PrlctlPath, "list", name, "--no-header", "--output", "status") + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + return false, err + } + + for _, line := range strings.Split(stdout.String(), "\n") { + if line == "running" { + return true, nil + } + + if line == "suspended" { + return true, nil + } + if line == "paused" { + return true, nil + } + } + + return false, nil +} + +func (d *Parallels9Driver) Stop(name string) error { + if err := d.Prlctl("stop", name); err != nil { + return err + } + + // We sleep here for a little bit to let the session "unlock" + time.Sleep(2 * time.Second) + + return nil +} + +func (d *Parallels9Driver) Prlctl(args ...string) error { + var stdout, stderr bytes.Buffer + + log.Printf("Executing prlctl: %#v", args) + cmd := exec.Command(d.PrlctlPath, args...) + 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 { + err = fmt.Errorf("prlctl error: %s", stderrString) + } + + log.Printf("stdout: %s", stdoutString) + log.Printf("stderr: %s", stderrString) + + return err +} + +func (d *Parallels9Driver) Verify() error { + version, _ := d.Version() + if !strings.HasPrefix(version, "9.") { + return fmt.Errorf("The packer-parallels builder plugin only supports Parallels Desktop v. 9. You have: %s!\n", version) + } + return nil +} + +func (d *Parallels9Driver) Version() (string, error) { + var stdout bytes.Buffer + + cmd := exec.Command(d.PrlctlPath, "--version") + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + return "", err + } + + versionOutput := strings.TrimSpace(stdout.String()) + re := regexp.MustCompile("prlctl version ([0-9\\.]+)") + verMatch := re.FindAllStringSubmatch(versionOutput, 1) + + if len(verMatch) != 1 { + return "", fmt.Errorf("prlctl version not found!\n") + } + + version := verMatch[0][1] + log.Printf("prlctl version: %s\n", version) + return version, nil +} + +func (d *Parallels9Driver) SendKeyScanCodes(vmName string, codes ...string) error { + var stdout, stderr bytes.Buffer + + args := prepend(vmName, codes) + cmd := exec.Command("prltype", args...) + 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 { + err = fmt.Errorf("prltype error: %s", stderrString) + } + + log.Printf("stdout: %s", stdoutString) + log.Printf("stderr: %s", stderrString) + + return err +} + +func prepend(head string, tail []string) []string { + tmp := make([]string, len(tail)+1) + for i := 0; i < len(tail); i++ { + tmp[i+1] = tail[i] + } + tmp[0] = head + return tmp +} + +func (d *Parallels9Driver) Mac(vmName string) (string, error) { + var stdout bytes.Buffer + + cmd := exec.Command(d.PrlctlPath, "list", "-i", vmName) + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + log.Printf("MAC address for NIC: nic0 on Virtual Machine: %s not found!\n", vmName) + return "", err + } + + stdoutString := strings.TrimSpace(stdout.String()) + re := regexp.MustCompile("net0.* mac=([0-9A-F]{12}) card=.*") + macMatch := re.FindAllStringSubmatch(stdoutString, 1) + + if len(macMatch) != 1 { + return "", fmt.Errorf("MAC address for NIC: nic0 on Virtual Machine: %s not found!\n", vmName) + } + + mac := macMatch[0][1] + log.Printf("Found MAC address for NIC: net0 - %s\n", mac) + return mac, nil +} + +// Finds the IP address of a VM connected that uses DHCP by its MAC address +func (d *Parallels9Driver) IpAddress(mac string) (string, error) { + var stdout bytes.Buffer + dhcp_lease_file := "/Library/Preferences/Parallels/parallels_dhcp_leases" + + if len(mac) != 12 { + return "", fmt.Errorf("Not a valid MAC address: %s. It should be exactly 12 digits.", mac) + } + + cmd := exec.Command("grep", "-i", mac, dhcp_lease_file) + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + return "", err + } + + stdoutString := strings.TrimSpace(stdout.String()) + re := regexp.MustCompile("(.*)=.*") + ipMatch := re.FindAllStringSubmatch(stdoutString, 1) + + if len(ipMatch) != 1 { + return "", fmt.Errorf("IP lease not found for MAC address %s in: %s\n", mac, dhcp_lease_file) + } + + ip := ipMatch[0][1] + log.Printf("Found IP lease: %s for MAC address %s\n", ip, mac) + return ip, nil +} diff --git a/builder/parallels/common/driver_9_test.go b/builder/parallels/common/driver_9_test.go new file mode 100644 index 000000000..a7c61b9d2 --- /dev/null +++ b/builder/parallels/common/driver_9_test.go @@ -0,0 +1,9 @@ +package common + +import ( + "testing" +) + +func TestParallels9Driver_impl(t *testing.T) { + var _ Driver = new(Parallels9Driver) +} diff --git a/builder/parallels/common/driver_mock.go b/builder/parallels/common/driver_mock.go new file mode 100644 index 000000000..e54c5cd5a --- /dev/null +++ b/builder/parallels/common/driver_mock.go @@ -0,0 +1,100 @@ +package common + +import "sync" + +type DriverMock struct { + sync.Mutex + + ImportCalled bool + ImportName string + ImportSrcPath string + ImportDstPath string + ImportErr error + + IsRunningName string + IsRunningReturn bool + IsRunningErr error + + StopName string + StopErr error + + PrlctlCalls [][]string + PrlctlErrs []error + + VerifyCalled bool + VerifyErr error + + VersionCalled bool + VersionResult string + VersionErr error + + SendKeyScanCodesCalls [][]string + SendKeyScanCodesErrs []error + + MacName string + MacReturn string + MacError error + + IpAddressMac string + IpAddressReturn string + IpAddressError error +} + +func (d *DriverMock) Import(name, srcPath, dstPath string) error { + d.ImportCalled = true + d.ImportName = name + d.ImportSrcPath = srcPath + d.ImportDstPath = dstPath + return d.ImportErr +} + +func (d *DriverMock) IsRunning(name string) (bool, error) { + d.Lock() + defer d.Unlock() + + d.IsRunningName = name + return d.IsRunningReturn, d.IsRunningErr +} + +func (d *DriverMock) Stop(name string) error { + d.StopName = name + return d.StopErr +} + +func (d *DriverMock) Prlctl(args ...string) error { + d.PrlctlCalls = append(d.PrlctlCalls, args) + + if len(d.PrlctlErrs) >= len(d.PrlctlCalls) { + return d.PrlctlErrs[len(d.PrlctlCalls)-1] + } + return nil +} + +func (d *DriverMock) Verify() error { + d.VerifyCalled = true + return d.VerifyErr +} + +func (d *DriverMock) Version() (string, error) { + d.VersionCalled = true + return d.VersionResult, d.VersionErr +} + +func (d *DriverMock) SendKeyScanCodes(name string, scancodes ...string) error { + d.SendKeyScanCodesCalls = append(d.SendKeyScanCodesCalls, scancodes) + + if len(d.SendKeyScanCodesErrs) >= len(d.SendKeyScanCodesCalls) { + return d.SendKeyScanCodesErrs[len(d.SendKeyScanCodesCalls)-1] + } + return nil +} + +func (d *DriverMock) Mac(name string) (string, error) { + d.MacName = name + return d.MacReturn, d.MacError +} + +func (d *DriverMock) IpAddress(mac string) (string, error) { + d.IpAddressMac = mac + return d.IpAddressReturn, d.IpAddressError +} diff --git a/builder/parallels/common/floppy_config.go b/builder/parallels/common/floppy_config.go new file mode 100644 index 000000000..35cd7aca4 --- /dev/null +++ b/builder/parallels/common/floppy_config.go @@ -0,0 +1,31 @@ +package common + +import ( + "fmt" + + "github.com/mitchellh/packer/packer" +) + +// FloppyConfig is configuration related to created floppy disks and attaching +// them to a VirtualBox machine. +type FloppyConfig struct { + FloppyFiles []string `mapstructure:"floppy_files"` +} + +func (c *FloppyConfig) Prepare(t *packer.ConfigTemplate) []error { + if c.FloppyFiles == nil { + c.FloppyFiles = make([]string, 0) + } + + errs := make([]error, 0) + for i, file := range c.FloppyFiles { + var err error + c.FloppyFiles[i], err = t.Process(file, nil) + if err != nil { + errs = append(errs, fmt.Errorf( + "Error processing floppy_files[%d]: %s", i, err)) + } + } + + return errs +} diff --git a/builder/parallels/common/floppy_config_test.go b/builder/parallels/common/floppy_config_test.go new file mode 100644 index 000000000..3e4fc55db --- /dev/null +++ b/builder/parallels/common/floppy_config_test.go @@ -0,0 +1,18 @@ +package common + +import ( + "testing" +) + +func TestFloppyConfigPrepare(t *testing.T) { + c := new(FloppyConfig) + + errs := c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("err: %#v", errs) + } + + if len(c.FloppyFiles) > 0 { + t.Fatal("should not have floppy files") + } +} diff --git a/builder/parallels/common/output_config.go b/builder/parallels/common/output_config.go new file mode 100644 index 000000000..19be1ba00 --- /dev/null +++ b/builder/parallels/common/output_config.go @@ -0,0 +1,40 @@ +package common + +import ( + "fmt" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" + "os" +) + +type OutputConfig struct { + OutputDir string `mapstructure:"output_directory"` +} + +func (c *OutputConfig) Prepare(t *packer.ConfigTemplate, pc *common.PackerConfig) []error { + if c.OutputDir == "" { + c.OutputDir = fmt.Sprintf("output-%s", pc.PackerBuildName) + } + + templates := map[string]*string{ + "output_directory": &c.OutputDir, + } + + errs := make([]error, 0) + for n, ptr := range templates { + var err error + *ptr, err = t.Process(*ptr, nil) + if err != nil { + errs = append(errs, fmt.Errorf("Error processing %s: %s", n, err)) + } + } + + if !pc.PackerForce { + if _, err := os.Stat(c.OutputDir); err == nil { + errs = append(errs, fmt.Errorf( + "Output directory '%s' already exists. It must not exist.", c.OutputDir)) + } + } + + return errs +} diff --git a/builder/parallels/common/output_config_test.go b/builder/parallels/common/output_config_test.go new file mode 100644 index 000000000..7fa039a16 --- /dev/null +++ b/builder/parallels/common/output_config_test.go @@ -0,0 +1,65 @@ +package common + +import ( + "github.com/mitchellh/packer/common" + "io/ioutil" + "os" + "testing" +) + +func TestOutputConfigPrepare(t *testing.T) { + c := new(OutputConfig) + if c.OutputDir != "" { + t.Fatalf("what: %s", c.OutputDir) + } + + pc := &common.PackerConfig{PackerBuildName: "foo"} + errs := c.Prepare(testConfigTemplate(t), pc) + if len(errs) > 0 { + t.Fatalf("err: %#v", errs) + } + + if c.OutputDir == "" { + t.Fatal("should have output dir") + } +} + +func TestOutputConfigPrepare_exists(t *testing.T) { + td, err := ioutil.TempDir("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(td) + + c := new(OutputConfig) + c.OutputDir = td + + pc := &common.PackerConfig{ + PackerBuildName: "foo", + PackerForce: false, + } + errs := c.Prepare(testConfigTemplate(t), pc) + if len(errs) == 0 { + t.Fatal("should have errors") + } +} + +func TestOutputConfigPrepare_forceExists(t *testing.T) { + td, err := ioutil.TempDir("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(td) + + c := new(OutputConfig) + c.OutputDir = td + + pc := &common.PackerConfig{ + PackerBuildName: "foo", + PackerForce: true, + } + errs := c.Prepare(testConfigTemplate(t), pc) + if len(errs) > 0 { + t.Fatal("should not have errors") + } +} diff --git a/builder/parallels/common/prlctl_config.go b/builder/parallels/common/prlctl_config.go new file mode 100644 index 000000000..eff0618b3 --- /dev/null +++ b/builder/parallels/common/prlctl_config.go @@ -0,0 +1,28 @@ +package common + +import ( + "fmt" + "github.com/mitchellh/packer/packer" +) + +type PrlctlConfig struct { + Prlctl [][]string `mapstructure:"prlctl"` +} + +func (c *PrlctlConfig) Prepare(t *packer.ConfigTemplate) []error { + if c.Prlctl == nil { + c.Prlctl = make([][]string, 0) + } + + errs := make([]error, 0) + for i, args := range c.Prlctl { + for j, arg := range args { + if err := t.Validate(arg); err != nil { + errs = append(errs, + fmt.Errorf("Error processing prlctl[%d][%d]: %s", i, j, err)) + } + } + } + + return errs +} diff --git a/builder/parallels/common/prlctl_config_test.go b/builder/parallels/common/prlctl_config_test.go new file mode 100644 index 000000000..cafb3d23c --- /dev/null +++ b/builder/parallels/common/prlctl_config_test.go @@ -0,0 +1,37 @@ +package common + +import ( + "reflect" + "testing" +) + +func TestPrlctlConfigPrepare_Prlctl(t *testing.T) { + // Test with empty + c := new(PrlctlConfig) + errs := c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("err: %#v", errs) + } + + if !reflect.DeepEqual(c.Prlctl, [][]string{}) { + t.Fatalf("bad: %#v", c.Prlctl) + } + + // Test with a good one + c = new(PrlctlConfig) + c.Prlctl = [][]string{ + {"foo", "bar", "baz"}, + } + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("err: %#v", errs) + } + + expected := [][]string{ + []string{"foo", "bar", "baz"}, + } + + if !reflect.DeepEqual(c.Prlctl, expected) { + t.Fatalf("bad: %#v", c.Prlctl) + } +} diff --git a/builder/parallels/common/prlctl_version_config.go b/builder/parallels/common/prlctl_version_config.go new file mode 100644 index 000000000..2103d9a59 --- /dev/null +++ b/builder/parallels/common/prlctl_version_config.go @@ -0,0 +1,31 @@ +package common + +import ( + "fmt" + "github.com/mitchellh/packer/packer" +) + +type PrlctlVersionConfig struct { + PrlctlVersionFile string `mapstructure:"prlctl_version_file"` +} + +func (c *PrlctlVersionConfig) Prepare(t *packer.ConfigTemplate) []error { + if c.PrlctlVersionFile == "" { + c.PrlctlVersionFile = ".prlctl_version" + } + + templates := map[string]*string{ + "prlctl_version_file": &c.PrlctlVersionFile, + } + + errs := make([]error, 0) + for n, ptr := range templates { + var err error + *ptr, err = t.Process(*ptr, nil) + if err != nil { + errs = append(errs, fmt.Errorf("Error processing %s: %s", n, err)) + } + } + + return errs +} diff --git a/builder/parallels/common/prlctl_version_config_test.go b/builder/parallels/common/prlctl_version_config_test.go new file mode 100644 index 000000000..daf934a16 --- /dev/null +++ b/builder/parallels/common/prlctl_version_config_test.go @@ -0,0 +1,33 @@ +package common + +import ( + "testing" +) + +func TestPrlctlVersionConfigPrepare_BootWait(t *testing.T) { + var c *PrlctlVersionConfig + var errs []error + + // Test empty + c = new(PrlctlVersionConfig) + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("should not have error: %s", errs) + } + + if c.PrlctlVersionFile != ".prlctl_version" { + t.Fatalf("bad value: %s", c.PrlctlVersionFile) + } + + // Test with a good one + c = new(PrlctlVersionConfig) + c.PrlctlVersionFile = "foo" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("should not have error: %s", errs) + } + + if c.PrlctlVersionFile != "foo" { + t.Fatalf("bad value: %s", c.PrlctlVersionFile) + } +} diff --git a/builder/parallels/common/run_config.go b/builder/parallels/common/run_config.go new file mode 100644 index 000000000..755d0f1c1 --- /dev/null +++ b/builder/parallels/common/run_config.go @@ -0,0 +1,41 @@ +package common + +import ( + "fmt" + "github.com/mitchellh/packer/packer" + "time" +) + +type RunConfig struct { + Headless bool `mapstructure:"headless"` + RawBootWait string `mapstructure:"boot_wait"` + + BootWait time.Duration `` +} + +func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error { + if c.RawBootWait == "" { + c.RawBootWait = "10s" + } + + templates := map[string]*string{ + "boot_wait": &c.RawBootWait, + } + + errs := make([]error, 0) + for n, ptr := range templates { + var err error + *ptr, err = t.Process(*ptr, nil) + if err != nil { + errs = append(errs, fmt.Errorf("Error processing %s: %s", n, err)) + } + } + + var err error + c.BootWait, err = time.ParseDuration(c.RawBootWait) + if err != nil { + errs = append(errs, fmt.Errorf("Failed parsing boot_wait: %s", err)) + } + + return errs +} diff --git a/builder/parallels/common/run_config_test.go b/builder/parallels/common/run_config_test.go new file mode 100644 index 000000000..8068fe625 --- /dev/null +++ b/builder/parallels/common/run_config_test.go @@ -0,0 +1,37 @@ +package common + +import ( + "testing" +) + +func TestRunConfigPrepare_BootWait(t *testing.T) { + var c *RunConfig + var errs []error + + // Test a default boot_wait + c = new(RunConfig) + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("should not have error: %s", errs) + } + + if c.RawBootWait != "10s" { + t.Fatalf("bad value: %s", c.RawBootWait) + } + + // Test with a bad boot_wait + c = new(RunConfig) + c.RawBootWait = "this is not good" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) == 0 { + t.Fatalf("bad: %#v", errs) + } + + // Test with a good one + c = new(RunConfig) + c.RawBootWait = "5s" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("should not have error: %s", errs) + } +} diff --git a/builder/parallels/common/shutdown_config.go b/builder/parallels/common/shutdown_config.go new file mode 100644 index 000000000..05e5fdfeb --- /dev/null +++ b/builder/parallels/common/shutdown_config.go @@ -0,0 +1,42 @@ +package common + +import ( + "fmt" + "github.com/mitchellh/packer/packer" + "time" +) + +type ShutdownConfig struct { + ShutdownCommand string `mapstructure:"shutdown_command"` + RawShutdownTimeout string `mapstructure:"shutdown_timeout"` + + ShutdownTimeout time.Duration `` +} + +func (c *ShutdownConfig) Prepare(t *packer.ConfigTemplate) []error { + if c.RawShutdownTimeout == "" { + c.RawShutdownTimeout = "5m" + } + + templates := map[string]*string{ + "shutdown_command": &c.ShutdownCommand, + "shutdown_timeout": &c.RawShutdownTimeout, + } + + errs := make([]error, 0) + for n, ptr := range templates { + var err error + *ptr, err = t.Process(*ptr, nil) + if err != nil { + errs = append(errs, fmt.Errorf("Error processing %s: %s", n, err)) + } + } + + var err error + c.ShutdownTimeout, err = time.ParseDuration(c.RawShutdownTimeout) + if err != nil { + errs = append(errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err)) + } + + return errs +} diff --git a/builder/parallels/common/shutdown_config_test.go b/builder/parallels/common/shutdown_config_test.go new file mode 100644 index 000000000..5da613a19 --- /dev/null +++ b/builder/parallels/common/shutdown_config_test.go @@ -0,0 +1,45 @@ +package common + +import ( + "testing" + "time" +) + +func testShutdownConfig() *ShutdownConfig { + return &ShutdownConfig{} +} + +func TestShutdownConfigPrepare_ShutdownCommand(t *testing.T) { + var c *ShutdownConfig + var errs []error + + c = testShutdownConfig() + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("err: %#v", errs) + } +} + +func TestShutdownConfigPrepare_ShutdownTimeout(t *testing.T) { + var c *ShutdownConfig + var errs []error + + // Test with a bad value + c = testShutdownConfig() + c.RawShutdownTimeout = "this is not good" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) == 0 { + t.Fatalf("should have error") + } + + // Test with a good one + c = testShutdownConfig() + c.RawShutdownTimeout = "5s" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("err: %#v", errs) + } + if c.ShutdownTimeout != 5*time.Second { + t.Fatalf("bad: %s", c.ShutdownTimeout) + } +} diff --git a/builder/parallels/common/ssh.go b/builder/parallels/common/ssh.go new file mode 100644 index 000000000..04aed86a2 --- /dev/null +++ b/builder/parallels/common/ssh.go @@ -0,0 +1,71 @@ +package common + +import ( + "code.google.com/p/go.crypto/ssh" + "fmt" + "github.com/mitchellh/multistep" + packerssh "github.com/mitchellh/packer/communicator/ssh" + "io/ioutil" + "os" +) + +func SSHAddress(state multistep.StateBag) (string, error) { + vmName := state.Get("vmName").(string) + driver := state.Get("driver").(Driver) + + mac, err := driver.Mac(vmName) + if err != nil { + return "", err + } + + ip, err := driver.IpAddress(mac) + if err != nil { + return "", err + } + + return fmt.Sprintf("%s:22", ip), nil +} + +func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*ssh.ClientConfig, error) { + return func(state multistep.StateBag) (*ssh.ClientConfig, error) { + auth := []ssh.AuthMethod{ + ssh.Password(config.SSHPassword), + ssh.KeyboardInteractive( + packerssh.PasswordKeyboardInteractive(config.SSHPassword)), + } + + if config.SSHKeyPath != "" { + signer, err := sshKeyToSigner(config.SSHKeyPath) + if err != nil { + return nil, err + } + + auth = append(auth, ssh.PublicKeys(signer)) + } + + return &ssh.ClientConfig{ + User: config.SSHUser, + Auth: auth, + }, nil + } +} + +func sshKeyToSigner(path string) (ssh.Signer, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + keyBytes, err := ioutil.ReadAll(f) + if err != nil { + return nil, err + } + + signer, err := ssh.ParsePrivateKey(keyBytes) + if err != nil { + return nil, fmt.Errorf("Error setting up SSH config: %s", err) + } + + return signer, nil +} diff --git a/builder/parallels/common/ssh_config.go b/builder/parallels/common/ssh_config.go new file mode 100644 index 000000000..f28fb3029 --- /dev/null +++ b/builder/parallels/common/ssh_config.go @@ -0,0 +1,65 @@ +package common + +import ( + "errors" + "fmt" + "github.com/mitchellh/packer/packer" + "os" + "time" +) + +type SSHConfig struct { + SSHKeyPath string `mapstructure:"ssh_key_path"` + SSHPassword string `mapstructure:"ssh_password"` + SSHPort uint `mapstructure:"ssh_port"` + SSHUser string `mapstructure:"ssh_username"` + RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"` + + SSHWaitTimeout time.Duration +} + +func (c *SSHConfig) Prepare(t *packer.ConfigTemplate) []error { + if c.SSHPort == 0 { + c.SSHPort = 22 + } + + if c.RawSSHWaitTimeout == "" { + c.RawSSHWaitTimeout = "20m" + } + + templates := map[string]*string{ + "ssh_key_path": &c.SSHKeyPath, + "ssh_password": &c.SSHPassword, + "ssh_username": &c.SSHUser, + "ssh_wait_timeout": &c.RawSSHWaitTimeout, + } + + errs := make([]error, 0) + for n, ptr := range templates { + var err error + *ptr, err = t.Process(*ptr, nil) + if err != nil { + errs = append(errs, fmt.Errorf("Error processing %s: %s", n, err)) + } + } + + if c.SSHKeyPath != "" { + if _, err := os.Stat(c.SSHKeyPath); err != nil { + errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err)) + } else if _, err := sshKeyToSigner(c.SSHKeyPath); err != nil { + errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err)) + } + } + + if c.SSHUser == "" { + errs = append(errs, errors.New("An ssh_username must be specified.")) + } + + var err error + c.SSHWaitTimeout, err = time.ParseDuration(c.RawSSHWaitTimeout) + if err != nil { + errs = append(errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err)) + } + + return errs +} diff --git a/builder/parallels/common/ssh_config_test.go b/builder/parallels/common/ssh_config_test.go new file mode 100644 index 000000000..a6c9e8ef5 --- /dev/null +++ b/builder/parallels/common/ssh_config_test.go @@ -0,0 +1,155 @@ +package common + +import ( + "io/ioutil" + "os" + "testing" +) + +func testSSHConfig() *SSHConfig { + return &SSHConfig{ + SSHUser: "foo", + } +} + +func TestSSHConfigPrepare(t *testing.T) { + c := testSSHConfig() + errs := c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("err: %#v", errs) + } + + if c.SSHPort != 22 { + t.Errorf("bad ssh port: %d", c.SSHPort) + } +} + +func TestSSHConfigPrepare_SSHKeyPath(t *testing.T) { + var c *SSHConfig + var errs []error + + c = testSSHConfig() + c.SSHKeyPath = "" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("should not have error: %#v", errs) + } + + c = testSSHConfig() + c.SSHKeyPath = "/i/dont/exist" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) == 0 { + t.Fatal("should have error") + } + + // Test bad contents + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(tf.Name()) + defer tf.Close() + + if _, err := tf.Write([]byte("HELLO!")); err != nil { + t.Fatalf("err: %s", err) + } + + c = testSSHConfig() + c.SSHKeyPath = tf.Name() + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) == 0 { + t.Fatal("should have error") + } + + // Test good contents + tf.Seek(0, 0) + tf.Truncate(0) + tf.Write([]byte(testPem)) + c = testSSHConfig() + c.SSHKeyPath = tf.Name() + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("should not have error: %#v", errs) + } +} + +func TestSSHConfigPrepare_SSHUser(t *testing.T) { + var c *SSHConfig + var errs []error + + c = testSSHConfig() + c.SSHUser = "" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) == 0 { + t.Fatalf("should have error") + } + + c = testSSHConfig() + c.SSHUser = "exists" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("should not have error: %#v", errs) + } +} + +func TestSSHConfigPrepare_SSHWaitTimeout(t *testing.T) { + var c *SSHConfig + var errs []error + + // Defaults + c = testSSHConfig() + c.RawSSHWaitTimeout = "" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("should not have error: %#v", errs) + } + if c.RawSSHWaitTimeout != "20m" { + t.Fatalf("bad value: %s", c.RawSSHWaitTimeout) + } + + // Test with a bad value + c = testSSHConfig() + c.RawSSHWaitTimeout = "this is not good" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) == 0 { + t.Fatal("should have error") + } + + // Test with a good one + c = testSSHConfig() + c.RawSSHWaitTimeout = "5s" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("should not have error: %#v", errs) + } +} + +const testPem = ` +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAxd4iamvrwRJvtNDGQSIbNvvIQN8imXTRWlRY62EvKov60vqu +hh+rDzFYAIIzlmrJopvOe0clqmi3mIP9dtkjPFrYflq52a2CF5q+BdwsJXuRHbJW +LmStZUwW1khSz93DhvhmK50nIaczW63u4EO/jJb3xj+wxR1Nkk9bxi3DDsYFt8SN +AzYx9kjlEYQ/+sI4/ATfmdV9h78SVotjScupd9KFzzi76gWq9gwyCBLRynTUWlyD +2UOfJRkOvhN6/jKzvYfVVwjPSfA9IMuooHdScmC4F6KBKJl/zf/zETM0XyzIDNmH +uOPbCiljq2WoRM+rY6ET84EO0kVXbfx8uxUsqQIDAQABAoIBAQCkPj9TF0IagbM3 +5BSs/CKbAWS4dH/D4bPlxx4IRCNirc8GUg+MRb04Xz0tLuajdQDqeWpr6iLZ0RKV +BvreLF+TOdV7DNQ4XE4gSdJyCtCaTHeort/aordL3l0WgfI7mVk0L/yfN1PEG4YG +E9q1TYcyrB3/8d5JwIkjabxERLglCcP+geOEJp+QijbvFIaZR/n2irlKW4gSy6ko +9B0fgUnhkHysSg49ChHQBPQ+o5BbpuLrPDFMiTPTPhdfsvGGcyCGeqfBA56oHcSF +K02Fg8OM+Bd1lb48LAN9nWWY4WbwV+9bkN3Ym8hO4c3a/Dxf2N7LtAQqWZzFjvM3 +/AaDvAgBAoGBAPLD+Xn1IYQPMB2XXCXfOuJewRY7RzoVWvMffJPDfm16O7wOiW5+ +2FmvxUDayk4PZy6wQMzGeGKnhcMMZTyaq2g/QtGfrvy7q1Lw2fB1VFlVblvqhoJa +nMJojjC4zgjBkXMHsRLeTmgUKyGs+fdFbfI6uejBnnf+eMVUMIdJ+6I9AoGBANCn +kWO9640dttyXURxNJ3lBr2H3dJOkmD6XS+u+LWqCSKQe691Y/fZ/ZL0Oc4Mhy7I6 +hsy3kDQ5k2V0fkaNODQIFJvUqXw2pMewUk8hHc9403f4fe9cPrL12rQ8WlQw4yoC +v2B61vNczCCUDtGxlAaw8jzSRaSI5s6ax3K7enbdAoGBAJB1WYDfA2CoAQO6y9Sl +b07A/7kQ8SN5DbPaqrDrBdJziBQxukoMJQXJeGFNUFD/DXFU5Fp2R7C86vXT7HIR +v6m66zH+CYzOx/YE6EsUJms6UP9VIVF0Rg/RU7teXQwM01ZV32LQ8mswhTH20o/3 +uqMHmxUMEhZpUMhrfq0isyApAoGAe1UxGTXfj9AqkIVYylPIq2HqGww7+jFmVEj1 +9Wi6S6Sq72ffnzzFEPkIQL/UA4TsdHMnzsYKFPSbbXLIWUeMGyVTmTDA5c0e5XIR +lPhMOKCAzv8w4VUzMnEkTzkFY5JqFCD/ojW57KvDdNZPVB+VEcdxyAW6aKELXMAc +eHLc1nkCgYEApm/motCTPN32nINZ+Vvywbv64ZD+gtpeMNP3CLrbe1X9O+H52AXa +1jCoOldWR8i2bs2NVPcKZgdo6fFULqE4dBX7Te/uYEIuuZhYLNzRO1IKU/YaqsXG +3bfQ8hKYcSnTfE0gPtLDnqCIxTocaGLSHeG3TH9fTw+dA8FvWpUztI4= +-----END RSA PRIVATE KEY----- +` diff --git a/builder/parallels/common/step_attach_floppy.go b/builder/parallels/common/step_attach_floppy.go new file mode 100644 index 000000000..83814f17c --- /dev/null +++ b/builder/parallels/common/step_attach_floppy.go @@ -0,0 +1,55 @@ +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" +) + +// This step attaches a floppy to the virtual machine. +// +// Uses: +// driver Driver +// ui packer.Ui +// vmName string +// +// Produces: +type StepAttachFloppy struct { + floppyPath string +} + +func (s *StepAttachFloppy) Run(state multistep.StateBag) multistep.StepAction { + // Determine if we even have a floppy disk to attach + var floppyPath string + if floppyPathRaw, ok := state.GetOk("floppy_path"); ok { + floppyPath = floppyPathRaw.(string) + } else { + log.Println("No floppy disk, not attaching.") + return multistep.ActionContinue + } + + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + + ui.Say("Attaching floppy disk...") + + // Create the floppy disk controller + command := []string{ + "set", vmName, + "--device-add", "fdd", + "--image", floppyPath, + } + if err := driver.Prlctl(command...); err != nil { + state.Put("error", fmt.Errorf("Error adding floppy: %s", err)) + return multistep.ActionHalt + } + + // Track the path so that we can unregister it from Parallels later + s.floppyPath = floppyPath + + return multistep.ActionContinue +} + +func (s *StepAttachFloppy) Cleanup(state multistep.StateBag) {} diff --git a/builder/parallels/common/step_attach_floppy_test.go b/builder/parallels/common/step_attach_floppy_test.go new file mode 100644 index 000000000..2a4f8bf11 --- /dev/null +++ b/builder/parallels/common/step_attach_floppy_test.go @@ -0,0 +1,71 @@ +package common + +import ( + "github.com/mitchellh/multistep" + "io/ioutil" + "os" + "testing" +) + +func TestStepAttachFloppy_impl(t *testing.T) { + var _ multistep.Step = new(StepAttachFloppy) +} + +func TestStepAttachFloppy(t *testing.T) { + state := testState(t) + step := new(StepAttachFloppy) + + // Create a temporary file for our floppy file + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + tf.Close() + defer os.Remove(tf.Name()) + + state.Put("floppy_path", tf.Name()) + 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") + } + + if len(driver.PrlctlCalls) != 1 { + t.Fatal("not enough calls to prlctl") + } + + if driver.PrlctlCalls[0][0] != "set" { + t.Fatal("bad call") + } + if driver.PrlctlCalls[0][2] != "--device-add" { + t.Fatal("bad call") + } + if driver.PrlctlCalls[0][3] != "fdd" { + t.Fatal("bad call") + } +} + +func TestStepAttachFloppy_noFloppy(t *testing.T) { + state := testState(t) + step := new(StepAttachFloppy) + + 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") + } + + if len(driver.PrlctlCalls) > 0 { + t.Fatal("should not call vboxmanage") + } +} diff --git a/builder/parallels/common/step_attach_parallels_tools.go b/builder/parallels/common/step_attach_parallels_tools.go new file mode 100644 index 000000000..250f2e50c --- /dev/null +++ b/builder/parallels/common/step_attach_parallels_tools.go @@ -0,0 +1,56 @@ +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" +) + +// This step attaches the Parallels Tools as a inserted CD onto +// the virtual machine. +// +// Uses: +// driver Driver +// toolsPath string +// ui packer.Ui +// vmName string +// +// Produces: +type StepAttachParallelsTools struct { + ParallelsToolsHostPath string + ParallelsToolsMode string +} + +func (s *StepAttachParallelsTools) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + + // If we're not attaching the guest additions then just return + if s.ParallelsToolsMode != ParallelsToolsModeAttach { + log.Println("Not attaching parallels tools since we're uploading.") + return multistep.ActionContinue + } + + // Attach the guest additions to the computer + log.Println("Attaching Parallels Tools ISO onto IDE controller...") + command := []string{ + "set", vmName, + "--device-add", "cdrom", + "--image", s.ParallelsToolsHostPath, + } + if err := driver.Prlctl(command...); err != nil { + err := fmt.Errorf("Error attaching Parallels Tools: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Set some state so we know to remove + state.Put("attachedToolsIso", true) + + return multistep.ActionContinue +} + +func (s *StepAttachParallelsTools) Cleanup(state multistep.StateBag) {} diff --git a/builder/parallels/common/step_output_dir.go b/builder/parallels/common/step_output_dir.go new file mode 100644 index 000000000..0c7c04d15 --- /dev/null +++ b/builder/parallels/common/step_output_dir.go @@ -0,0 +1,73 @@ +package common + +import ( + "fmt" + "log" + "os" + "path/filepath" + "time" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +// StepOutputDir sets up the output directory by creating it if it does +// not exist, deleting it if it does exist and we're forcing, and cleaning +// it up when we're done with it. +type StepOutputDir struct { + Force bool + Path string + success bool +} + +func (s *StepOutputDir) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + + if _, err := os.Stat(s.Path); err == nil && s.Force { + ui.Say("Deleting previous output directory...") + os.RemoveAll(s.Path) + } + + // Create the directory + if err := os.MkdirAll(s.Path, 0755); err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + // Make sure we can write in the directory + f, err := os.Create(filepath.Join(s.Path, "_packer_perm_check")) + if err != nil { + err = fmt.Errorf("Couldn't write to output directory: %s", err) + state.Put("error", err) + return multistep.ActionHalt + } + f.Close() + os.Remove(f.Name()) + + s.success = true + return multistep.ActionContinue +} + +func (s *StepOutputDir) Cleanup(state multistep.StateBag) { + _, cancelled := state.GetOk(multistep.StateCancelled) + _, halted := state.GetOk(multistep.StateHalted) + + if !s.success { + return + } + + if cancelled || halted { + ui := state.Get("ui").(packer.Ui) + + ui.Say("Deleting output directory...") + for i := 0; i < 5; i++ { + err := os.RemoveAll(s.Path) + if err == nil { + break + } + + log.Printf("Error removing output dir: %s", err) + time.Sleep(2 * time.Second) + } + } +} diff --git a/builder/parallels/common/step_output_dir_test.go b/builder/parallels/common/step_output_dir_test.go new file mode 100644 index 000000000..be485c278 --- /dev/null +++ b/builder/parallels/common/step_output_dir_test.go @@ -0,0 +1,96 @@ +package common + +import ( + "github.com/mitchellh/multistep" + "io/ioutil" + "os" + "testing" +) + +func testStepOutputDir(t *testing.T) *StepOutputDir { + td, err := ioutil.TempDir("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.RemoveAll(td); err != nil { + t.Fatalf("err: %s", err) + } + + return &StepOutputDir{Force: false, Path: td} +} + +func TestStepOutputDir_impl(t *testing.T) { + var _ multistep.Step = new(StepOutputDir) +} + +func TestStepOutputDir(t *testing.T) { + state := testState(t) + step := testStepOutputDir(t) + + // 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") + } + if _, err := os.Stat(step.Path); err != nil { + t.Fatalf("err: %s", err) + } + + // Test the cleanup + step.Cleanup(state) + if _, err := os.Stat(step.Path); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestStepOutputDir_cancelled(t *testing.T) { + state := testState(t) + step := testStepOutputDir(t) + + // 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") + } + if _, err := os.Stat(step.Path); err != nil { + t.Fatalf("err: %s", err) + } + + // Mark + state.Put(multistep.StateCancelled, true) + + // Test the cleanup + step.Cleanup(state) + if _, err := os.Stat(step.Path); err == nil { + t.Fatal("should not exist") + } +} + +func TestStepOutputDir_halted(t *testing.T) { + state := testState(t) + step := testStepOutputDir(t) + + // 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") + } + if _, err := os.Stat(step.Path); err != nil { + t.Fatalf("err: %s", err) + } + + // Mark + state.Put(multistep.StateHalted, true) + + // Test the cleanup + step.Cleanup(state) + if _, err := os.Stat(step.Path); err == nil { + t.Fatal("should not exist") + } +} diff --git a/builder/parallels/common/step_prlctl.go b/builder/parallels/common/step_prlctl.go new file mode 100644 index 000000000..9bd7c4b73 --- /dev/null +++ b/builder/parallels/common/step_prlctl.go @@ -0,0 +1,68 @@ +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "strings" +) + +type commandTemplate struct { + Name string +} + +// This step executes additional prlctl commands as specified by the +// template. +// +// Uses: +// driver Driver +// ui packer.Ui +// vmName string +// +// Produces: +type StepPrlctl struct { + Commands [][]string + Tpl *packer.ConfigTemplate +} + +func (s *StepPrlctl) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + + if len(s.Commands) > 0 { + ui.Say("Executing custom prlctl commands...") + } + + tplData := &commandTemplate{ + Name: vmName, + } + + for _, originalCommand := range s.Commands { + command := make([]string, len(originalCommand)) + copy(command, originalCommand) + + for i, arg := range command { + var err error + command[i], err = s.Tpl.Process(arg, tplData) + if err != nil { + err := fmt.Errorf("Error preparing prlctl command: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + ui.Message(fmt.Sprintf("Executing: prlctl %s", strings.Join(command, " "))) + if err := driver.Prlctl(command...); err != nil { + err := fmt.Errorf("Error executing command: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + return multistep.ActionContinue +} + +func (s *StepPrlctl) Cleanup(state multistep.StateBag) {} diff --git a/builder/parallels/common/step_remove_devices.go b/builder/parallels/common/step_remove_devices.go new file mode 100644 index 000000000..4d1b29611 --- /dev/null +++ b/builder/parallels/common/step_remove_devices.go @@ -0,0 +1,63 @@ +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +// This step removes any devices (floppy disks, ISOs, etc.) from the +// machine that we may have added. +// +// Uses: +// driver Driver +// ui packer.Ui +// vmName string +// +// Produces: +type StepRemoveDevices struct{} + +func (s *StepRemoveDevices) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + + // Remove the attached floppy disk, if it exists + if _, ok := state.GetOk("floppy_path"); ok { + ui.Message("Removing floppy drive...") + command := []string{"set", vmName, "--device-del", "fdd0"} + if err := driver.Prlctl(command...); err != nil { + err := fmt.Errorf("Error removing floppy: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + if _, ok := state.GetOk("attachedIso"); ok { + command := []string{"set", vmName, "--device-del", "cdrom0"} + + if err := driver.Prlctl(command...); err != nil { + err := fmt.Errorf("Error detaching ISO: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + if _, ok := state.GetOk("attachedToolsIso"); ok { + command := []string{"set", vmName, "--device-del", "cdrom1"} + + if err := driver.Prlctl(command...); err != nil { + err := fmt.Errorf("Error detaching ISO: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + return multistep.ActionContinue +} + +func (s *StepRemoveDevices) Cleanup(state multistep.StateBag) { +} diff --git a/builder/parallels/common/step_remove_devices_test.go b/builder/parallels/common/step_remove_devices_test.go new file mode 100644 index 000000000..eb8638097 --- /dev/null +++ b/builder/parallels/common/step_remove_devices_test.go @@ -0,0 +1,90 @@ +package common + +import ( + "github.com/mitchellh/multistep" + "testing" +) + +func TestStepRemoveDevices_impl(t *testing.T) { + var _ multistep.Step = new(StepRemoveDevices) +} + +func TestStepRemoveDevices(t *testing.T) { + state := testState(t) + step := new(StepRemoveDevices) + + 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 that ISO was removed + if len(driver.PrlctlCalls) != 0 { + t.Fatalf("bad: %#v", driver.PrlctlCalls) + } +} + +func TestStepRemoveDevices_attachedIso(t *testing.T) { + state := testState(t) + step := new(StepRemoveDevices) + + state.Put("attachedIso", 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 that ISO was removed + if len(driver.PrlctlCalls) != 1 { + t.Fatalf("bad: %#v", driver.PrlctlCalls) + } + if driver.PrlctlCalls[0][2] != "--device-del" { + t.Fatalf("bad: %#v", driver.PrlctlCalls) + } + if driver.PrlctlCalls[0][3] != "cdrom0" { + t.Fatalf("bad: %#v", driver.PrlctlCalls) + } +} + +func TestStepRemoveDevices_floppyPath(t *testing.T) { + state := testState(t) + step := new(StepRemoveDevices) + + state.Put("floppy_path", "foo") + 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 that both were removed + if len(driver.PrlctlCalls) != 1 { + t.Fatalf("bad: %#v", driver.PrlctlCalls) + } + if driver.PrlctlCalls[0][2] != "--device-del" { + t.Fatalf("bad: %#v", driver.PrlctlCalls) + } + if driver.PrlctlCalls[0][3] != "fdd0" { + t.Fatalf("bad: %#v", driver.PrlctlCalls) + } +} diff --git a/builder/parallels/common/step_run.go b/builder/parallels/common/step_run.go new file mode 100644 index 000000000..e9c3ab27d --- /dev/null +++ b/builder/parallels/common/step_run.go @@ -0,0 +1,80 @@ +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "time" +) + +// This step starts the virtual machine. +// +// Uses: +// driver Driver +// ui packer.Ui +// vmName string +// +// Produces: +type StepRun struct { + BootWait time.Duration + Headless bool + + vmName string +} + +func (s *StepRun) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + + ui.Say("Starting the virtual machine...") + //guiArgument := "gui" + if s.Headless == true { + ui.Message("WARNING: The VM will be started in headless mode, as configured.\n" + + "In headless mode, errors during the boot sequence or OS setup\n" + + "won't be easily visible. Use at your own discretion.") + //guiArgument = "headless" + } + command := []string{"start", vmName} + if err := driver.Prlctl(command...); err != nil { + err := fmt.Errorf("Error starting VM: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + s.vmName = vmName + + if int64(s.BootWait) > 0 { + ui.Say(fmt.Sprintf("Waiting %s for boot...", s.BootWait)) + wait := time.After(s.BootWait) + WAITLOOP: + for { + select { + case <-wait: + break WAITLOOP + case <-time.After(1 * time.Second): + if _, ok := state.GetOk(multistep.StateCancelled); ok { + return multistep.ActionHalt + } + } + } + } + + return multistep.ActionContinue +} + +func (s *StepRun) Cleanup(state multistep.StateBag) { + if s.vmName == "" { + return + } + + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + if running, _ := driver.IsRunning(s.vmName); running { + if err := driver.Prlctl("stop", s.vmName); err != nil { + ui.Error(fmt.Sprintf("Error stopping VM: %s", err)) + } + } +} diff --git a/builder/parallels/common/step_shutdown.go b/builder/parallels/common/step_shutdown.go new file mode 100644 index 000000000..0f33692ce --- /dev/null +++ b/builder/parallels/common/step_shutdown.go @@ -0,0 +1,78 @@ +package common + +import ( + "errors" + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" + "time" +) + +// This step shuts down the machine. It first attempts to do so gracefully, +// but ultimately forcefully shuts it down if that fails. +// +// Uses: +// communicator packer.Communicator +// driver Driver +// ui packer.Ui +// vmName string +// +// Produces: +// +type StepShutdown struct { + Command string + Timeout time.Duration +} + +func (s *StepShutdown) Run(state multistep.StateBag) multistep.StepAction { + comm := state.Get("communicator").(packer.Communicator) + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + + if s.Command != "" { + ui.Say("Gracefully halting virtual machine...") + log.Printf("Executing shutdown command: %s", s.Command) + cmd := &packer.RemoteCmd{Command: s.Command} + if err := cmd.StartWithUi(comm, ui); err != nil { + err := fmt.Errorf("Failed to send shutdown command: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + 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 { + 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 { + err := fmt.Errorf("Error stopping VM: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + log.Println("VM shut down.") + return multistep.ActionContinue +} + +func (s *StepShutdown) Cleanup(state multistep.StateBag) {} diff --git a/builder/parallels/common/step_shutdown_test.go b/builder/parallels/common/step_shutdown_test.go new file mode 100644 index 000000000..215eefd30 --- /dev/null +++ b/builder/parallels/common/step_shutdown_test.go @@ -0,0 +1,105 @@ +package common + +import ( + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "testing" + "time" +) + +func TestStepShutdown_impl(t *testing.T) { + var _ multistep.Step = new(StepShutdown) +} + +func TestStepShutdown_noShutdownCommand(t *testing.T) { + state := testState(t) + step := new(StepShutdown) + + comm := new(packer.MockCommunicator) + state.Put("communicator", comm) + 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 that Stop was just called + if driver.StopName != "foo" { + t.Fatal("should call stop") + } + if comm.StartCalled { + t.Fatal("comm start should not be called") + } +} + +func TestStepShutdown_shutdownCommand(t *testing.T) { + state := testState(t) + step := new(StepShutdown) + step.Command = "poweroff" + step.Timeout = 1 * time.Second + + comm := new(packer.MockCommunicator) + state.Put("communicator", comm) + state.Put("vmName", "foo") + + driver := state.Get("driver").(*DriverMock) + driver.IsRunningReturn = true + + go func() { + time.Sleep(10 * time.Millisecond) + driver.Lock() + defer driver.Unlock() + driver.IsRunningReturn = false + }() + + // 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 that Stop was just called + if driver.StopName != "" { + t.Fatal("should not call stop") + } + if comm.StartCmd.Command != step.Command { + t.Fatal("comm start should be called") + } +} + +func TestStepShutdown_shutdownTimeout(t *testing.T) { + state := testState(t) + step := new(StepShutdown) + step.Command = "poweroff" + step.Timeout = 1 * time.Second + + comm := new(packer.MockCommunicator) + state.Put("communicator", comm) + state.Put("vmName", "foo") + + driver := state.Get("driver").(*DriverMock) + driver.IsRunningReturn = true + + go func() { + time.Sleep(2 * time.Second) + driver.Lock() + defer driver.Unlock() + driver.IsRunningReturn = false + }() + + // Test the run + if action := step.Run(state); action != multistep.ActionHalt { + t.Fatalf("bad action: %#v", action) + } + if _, ok := state.GetOk("error"); !ok { + t.Fatal("should have error") + } +} diff --git a/builder/parallels/common/step_test.go b/builder/parallels/common/step_test.go new file mode 100644 index 000000000..9bf6d5d67 --- /dev/null +++ b/builder/parallels/common/step_test.go @@ -0,0 +1,18 @@ +package common + +import ( + "bytes" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "testing" +) + +func testState(t *testing.T) multistep.StateBag { + state := new(multistep.BasicStateBag) + state.Put("driver", new(DriverMock)) + state.Put("ui", &packer.BasicUi{ + Reader: new(bytes.Buffer), + Writer: new(bytes.Buffer), + }) + return state +} diff --git a/builder/parallels/common/step_upload_parallels_tools.go b/builder/parallels/common/step_upload_parallels_tools.go new file mode 100644 index 000000000..374a8163e --- /dev/null +++ b/builder/parallels/common/step_upload_parallels_tools.go @@ -0,0 +1,67 @@ +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" + "os" +) + +type toolsPathTemplate struct { + Version string +} + +// This step uploads the guest additions ISO to the VM. +type StepUploadParallelsTools struct { + ParallelsToolsHostPath string + ParallelsToolsGuestPath string + ParallelsToolsMode string + Tpl *packer.ConfigTemplate +} + +func (s *StepUploadParallelsTools) Run(state multistep.StateBag) multistep.StepAction { + comm := state.Get("communicator").(packer.Communicator) + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + // If we're attaching then don't do this, since we attached. + if s.ParallelsToolsMode != ParallelsToolsModeUpload { + log.Println("Not uploading Parallels Tools since mode is not upload") + return multistep.ActionContinue + } + + version, err := driver.Version() + if err != nil { + state.Put("error", fmt.Errorf("Error reading version for Parallels Tools upload: %s", err)) + return multistep.ActionHalt + } + + f, err := os.Open(s.ParallelsToolsHostPath) + if err != nil { + state.Put("error", fmt.Errorf("Error opening Parallels Tools ISO: %s", err)) + return multistep.ActionHalt + } + + tplData := &toolsPathTemplate{ + Version: version, + } + + s.ParallelsToolsGuestPath, err = s.Tpl.Process(s.ParallelsToolsGuestPath, tplData) + if err != nil { + err := fmt.Errorf("Error preparing Parallels Tools path: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say("Uploading Parallels Tools ISO...") + if err := comm.Upload(s.ParallelsToolsGuestPath, f); err != nil { + state.Put("error", fmt.Errorf("Error uploading Parallels Tools: %s", err)) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepUploadParallelsTools) Cleanup(state multistep.StateBag) {} diff --git a/builder/parallels/common/step_upload_version.go b/builder/parallels/common/step_upload_version.go new file mode 100644 index 000000000..f5e03c4f5 --- /dev/null +++ b/builder/parallels/common/step_upload_version.go @@ -0,0 +1,44 @@ +package common + +import ( + "bytes" + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" +) + +// This step uploads a file containing the Parallels version, which +// can be useful for various provisioning reasons. +type StepUploadVersion struct { + Path string +} + +func (s *StepUploadVersion) Run(state multistep.StateBag) multistep.StepAction { + comm := state.Get("communicator").(packer.Communicator) + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + if s.Path == "" { + log.Println("ParallelsVersionFile is empty. Not uploading.") + return multistep.ActionContinue + } + + version, err := driver.Version() + if err != nil { + state.Put("error", fmt.Errorf("Error reading version for metadata upload: %s", err)) + return multistep.ActionHalt + } + + ui.Say(fmt.Sprintf("Uploading Parallels version info (%s)", version)) + var data bytes.Buffer + data.WriteString(version) + if err := comm.Upload(s.Path, &data); err != nil { + state.Put("error", fmt.Errorf("Error uploading Parallels version: %s", err)) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepUploadVersion) Cleanup(state multistep.StateBag) {} diff --git a/builder/parallels/common/step_upload_version_test.go b/builder/parallels/common/step_upload_version_test.go new file mode 100644 index 000000000..234c4b5df --- /dev/null +++ b/builder/parallels/common/step_upload_version_test.go @@ -0,0 +1,61 @@ +package common + +import ( + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "testing" +) + +func TestStepUploadVersion_impl(t *testing.T) { + var _ multistep.Step = new(StepUploadVersion) +} + +func TestStepUploadVersion(t *testing.T) { + state := testState(t) + step := new(StepUploadVersion) + step.Path = "foopath" + + comm := new(packer.MockCommunicator) + state.Put("communicator", comm) + + driver := state.Get("driver").(*DriverMock) + driver.VersionResult = "foo" + + // 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") + } + + // Verify + if comm.UploadPath != "foopath" { + t.Fatalf("bad: %#v", comm.UploadPath) + } + if comm.UploadData != "foo" { + t.Fatalf("upload data bad: %#v", comm.UploadData) + } +} + +func TestStepUploadVersion_noPath(t *testing.T) { + state := testState(t) + step := new(StepUploadVersion) + step.Path = "" + + comm := new(packer.MockCommunicator) + state.Put("communicator", comm) + + // 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") + } + + // Verify + if comm.UploadCalled { + t.Fatal("bad") + } +} diff --git a/builder/parallels/common/tools_modes.go b/builder/parallels/common/tools_modes.go new file mode 100644 index 000000000..3523fbca7 --- /dev/null +++ b/builder/parallels/common/tools_modes.go @@ -0,0 +1,9 @@ +package common + +// These are the different valid mode values for "parallels_tools_mode" which +// determine how guest additions are delivered to the guest. +const ( + ParallelsToolsModeDisable string = "disable" + ParallelsToolsModeAttach = "attach" + ParallelsToolsModeUpload = "upload" +) diff --git a/builder/parallels/iso/builder.go b/builder/parallels/iso/builder.go new file mode 100644 index 000000000..12cb33d0a --- /dev/null +++ b/builder/parallels/iso/builder.go @@ -0,0 +1,360 @@ +package iso + +import ( + "errors" + "fmt" + "github.com/mitchellh/multistep" + parallelscommon "github.com/mitchellh/packer/builder/parallels/common" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" + "log" + "strings" +) + +const BuilderId = "rickard-von-essen.parallels" + +type Builder struct { + config config + runner multistep.Runner +} + +type config struct { + common.PackerConfig `mapstructure:",squash"` + parallelscommon.FloppyConfig `mapstructure:",squash"` + parallelscommon.OutputConfig `mapstructure:",squash"` + parallelscommon.RunConfig `mapstructure:",squash"` + parallelscommon.ShutdownConfig `mapstructure:",squash"` + parallelscommon.SSHConfig `mapstructure:",squash"` + parallelscommon.PrlctlConfig `mapstructure:",squash"` + parallelscommon.PrlctlVersionConfig `mapstructure:",squash"` + + BootCommand []string `mapstructure:"boot_command"` + DiskSize uint `mapstructure:"disk_size"` + ParallelsToolsMode string `mapstructure:"parallels_tools_mode"` + ParallelsToolsGuestPath string `mapstructure:"parallels_tools_guest_path"` + ParallelsToolsHostPath string `mapstructure:"parallels_tools_host_path"` + GuestOSType string `mapstructure:"guest_os_type"` + GuestOSDistribution string `mapstructure:"guest_os_distribution"` + HardDriveInterface string `mapstructure:"hard_drive_interface"` + HTTPDir string `mapstructure:"http_directory"` + HTTPPortMin uint `mapstructure:"http_port_min"` + HTTPPortMax uint `mapstructure:"http_port_max"` + ISOChecksum string `mapstructure:"iso_checksum"` + ISOChecksumType string `mapstructure:"iso_checksum_type"` + ISOUrls []string `mapstructure:"iso_urls"` + VMName string `mapstructure:"vm_name"` + + RawSingleISOUrl string `mapstructure:"iso_url"` + + tpl *packer.ConfigTemplate +} + +func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { + + md, err := common.DecodeConfig(&b.config, raws...) + if err != nil { + return nil, err + } + + b.config.tpl, err = packer.NewConfigTemplate() + if err != nil { + return nil, err + } + b.config.tpl.UserVars = b.config.PackerUserVars + + // Accumulate any errors and warnings + errs := common.CheckUnusedConfig(md) + errs = packer.MultiErrorAppend(errs, b.config.FloppyConfig.Prepare(b.config.tpl)...) + errs = packer.MultiErrorAppend( + errs, b.config.OutputConfig.Prepare(b.config.tpl, &b.config.PackerConfig)...) + errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(b.config.tpl)...) + errs = packer.MultiErrorAppend(errs, b.config.ShutdownConfig.Prepare(b.config.tpl)...) + errs = packer.MultiErrorAppend(errs, b.config.SSHConfig.Prepare(b.config.tpl)...) + errs = packer.MultiErrorAppend(errs, b.config.PrlctlConfig.Prepare(b.config.tpl)...) + errs = packer.MultiErrorAppend(errs, b.config.PrlctlVersionConfig.Prepare(b.config.tpl)...) + warnings := make([]string, 0) + + if b.config.DiskSize == 0 { + b.config.DiskSize = 40000 + } + + if b.config.ParallelsToolsMode == "" { + b.config.ParallelsToolsMode = "upload" + } + + if b.config.ParallelsToolsGuestPath == "" { + b.config.ParallelsToolsGuestPath = "prl-tools.iso" + } + + if b.config.ParallelsToolsHostPath == "" { + b.config.ParallelsToolsHostPath = "/Applications/Parallels Desktop.app/Contents/Resources/Tools/prl-tools-other.iso" + } + + if b.config.HardDriveInterface == "" { + b.config.HardDriveInterface = "sata" + } + + if b.config.GuestOSType == "" { + b.config.GuestOSType = "other" + } + + if b.config.GuestOSDistribution == "" { + b.config.GuestOSDistribution = "other" + } + + if b.config.HTTPPortMin == 0 { + b.config.HTTPPortMin = 8000 + } + + if b.config.HTTPPortMax == 0 { + b.config.HTTPPortMax = 9000 + } + + if b.config.VMName == "" { + b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName) + } + + // Errors + templates := map[string]*string{ + "parallels_tools_mode": &b.config.ParallelsToolsMode, + "parallels_tools_host_path": &b.config.ParallelsToolsHostPath, + "parallels_tools_guest_path": &b.config.ParallelsToolsGuestPath, + "guest_os_type": &b.config.GuestOSType, + "guest_os_distribution": &b.config.GuestOSDistribution, + "hard_drive_interface": &b.config.HardDriveInterface, + "http_directory": &b.config.HTTPDir, + "iso_checksum": &b.config.ISOChecksum, + "iso_checksum_type": &b.config.ISOChecksumType, + "iso_url": &b.config.RawSingleISOUrl, + "vm_name": &b.config.VMName, + } + + for n, ptr := range templates { + var err error + *ptr, err = b.config.tpl.Process(*ptr, nil) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Error processing %s: %s", n, err)) + } + } + + for i, url := range b.config.ISOUrls { + var err error + b.config.ISOUrls[i], err = b.config.tpl.Process(url, nil) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Error processing iso_urls[%d]: %s", i, err)) + } + } + + validates := map[string]*string{ + "parallels_tools_guest_path": &b.config.ParallelsToolsGuestPath, + } + + for n, ptr := range validates { + if err := b.config.tpl.Validate(*ptr); err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Error parsing %s: %s", n, err)) + } + } + + for i, command := range b.config.BootCommand { + if err := b.config.tpl.Validate(command); err != nil { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("Error processing boot_command[%d]: %s", i, err)) + } + } + + if b.config.HardDriveInterface != "ide" && b.config.HardDriveInterface != "sata" && b.config.HardDriveInterface != "scsi" { + errs = packer.MultiErrorAppend( + errs, errors.New("hard_drive_interface can only be ide, sata, or scsi")) + } + + if b.config.HTTPPortMin > b.config.HTTPPortMax { + errs = packer.MultiErrorAppend( + errs, errors.New("http_port_min must be less than http_port_max")) + } + + if b.config.ISOChecksumType == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("The iso_checksum_type must be specified.")) + } else { + b.config.ISOChecksumType = strings.ToLower(b.config.ISOChecksumType) + if b.config.ISOChecksumType != "none" { + if b.config.ISOChecksum == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("Due to large file sizes, an iso_checksum is required")) + } else { + b.config.ISOChecksum = strings.ToLower(b.config.ISOChecksum) + } + + if h := common.HashForType(b.config.ISOChecksumType); h == nil { + errs = packer.MultiErrorAppend( + errs, + fmt.Errorf("Unsupported checksum type: %s", b.config.ISOChecksumType)) + } + } + } + + if b.config.RawSingleISOUrl == "" && len(b.config.ISOUrls) == 0 { + errs = packer.MultiErrorAppend( + errs, errors.New("One of iso_url or iso_urls must be specified.")) + } else if b.config.RawSingleISOUrl != "" && len(b.config.ISOUrls) > 0 { + errs = packer.MultiErrorAppend( + errs, errors.New("Only one of iso_url or iso_urls may be specified.")) + } else if b.config.RawSingleISOUrl != "" { + b.config.ISOUrls = []string{b.config.RawSingleISOUrl} + } + + for i, url := range b.config.ISOUrls { + b.config.ISOUrls[i], err = common.DownloadableURL(url) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Failed to parse iso_url %d: %s", i+1, err)) + } + } + + validMode := false + validModes := []string{ + parallelscommon.ParallelsToolsModeDisable, + parallelscommon.ParallelsToolsModeAttach, + parallelscommon.ParallelsToolsModeUpload, + } + + for _, mode := range validModes { + if b.config.ParallelsToolsMode == mode { + validMode = true + break + } + } + + if !validMode { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("parallels_tools_mode is invalid. Must be one of: %v", validModes)) + } + + // Warnings + if b.config.ISOChecksumType == "none" { + warnings = append(warnings, + "A checksum type of 'none' was specified. Since ISO files are so big,\n"+ + "a checksum is highly recommended.") + } + + if b.config.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.") + } + + if errs != nil && len(errs.Errors) > 0 { + return warnings, errs + } + + return warnings, nil +} + +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 Parallels + driver, err := parallelscommon.NewDriver() + if err != nil { + return nil, fmt.Errorf("Failed creating Parallels driver: %s", err) + } + + steps := []multistep.Step{ + &common.StepDownload{ + Checksum: b.config.ISOChecksum, + ChecksumType: b.config.ISOChecksumType, + Description: "ISO", + ResultKey: "iso_path", + Url: b.config.ISOUrls, + }, + ¶llelscommon.StepOutputDir{ + Force: b.config.PackerForce, + Path: b.config.OutputDir, + }, + &common.StepCreateFloppy{ + Files: b.config.FloppyFiles, + }, + new(stepHTTPServer), + new(stepCreateVM), + new(stepCreateDisk), + new(stepAttachISO), + ¶llelscommon.StepAttachParallelsTools{ + ParallelsToolsHostPath: b.config.ParallelsToolsHostPath, + ParallelsToolsMode: b.config.ParallelsToolsMode, + }, + new(parallelscommon.StepAttachFloppy), + ¶llelscommon.StepPrlctl{ + Commands: b.config.Prlctl, + Tpl: b.config.tpl, + }, + ¶llelscommon.StepRun{ + BootWait: b.config.BootWait, + Headless: b.config.Headless, // TODO: migth work on Enterprise Ed. + }, + new(stepTypeBootCommand), + &common.StepConnectSSH{ + SSHAddress: parallelscommon.SSHAddress, + SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig), + SSHWaitTimeout: b.config.SSHWaitTimeout, + }, + ¶llelscommon.StepUploadVersion{ + Path: b.config.PrlctlVersionFile, + }, + ¶llelscommon.StepUploadParallelsTools{ + ParallelsToolsGuestPath: b.config.ParallelsToolsGuestPath, + ParallelsToolsHostPath: b.config.ParallelsToolsHostPath, + ParallelsToolsMode: b.config.ParallelsToolsMode, + Tpl: b.config.tpl, + }, + new(common.StepProvision), + ¶llelscommon.StepShutdown{ + Command: b.config.ShutdownCommand, + Timeout: b.config.ShutdownTimeout, + }, + new(parallelscommon.StepRemoveDevices), + } + + // Setup the state bag + state := new(multistep.BasicStateBag) + state.Put("cache", cache) + state.Put("config", &b.config) + state.Put("driver", driver) + state.Put("hook", hook) + state.Put("ui", ui) + + // Run + if b.config.PackerDebug { + b.runner = &multistep.DebugRunner{ + Steps: steps, + PauseFn: common.MultistepDebugFn(ui), + } + } else { + b.runner = &multistep.BasicRunner{Steps: steps} + } + + b.runner.Run(state) + + // If there was an error, return that + 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 parallelscommon.NewArtifact(b.config.OutputDir) +} + +func (b *Builder) Cancel() { + if b.runner != nil { + log.Println("Cancelling the step runner...") + b.runner.Cancel() + } +} diff --git a/builder/parallels/iso/builder_test.go b/builder/parallels/iso/builder_test.go new file mode 100644 index 000000000..ba9cecdd8 --- /dev/null +++ b/builder/parallels/iso/builder_test.go @@ -0,0 +1,436 @@ +package iso + +import ( + "github.com/mitchellh/packer/builder/parallels/common" + "github.com/mitchellh/packer/packer" + "reflect" + "testing" +) + +func testConfig() map[string]interface{} { + return map[string]interface{}{ + "iso_checksum": "foo", + "iso_checksum_type": "md5", + "iso_url": "http://www.google.com/", + "shutdown_command": "yes", + "ssh_username": "foo", + + packer.BuildNameConfigKey: "foo", + } +} + +func TestBuilder_ImplementsBuilder(t *testing.T) { + var raw interface{} + raw = &Builder{} + if _, ok := raw.(packer.Builder); !ok { + t.Error("Builder must implement builder.") + } +} + +func TestBuilderPrepare_Defaults(t *testing.T) { + var b Builder + config := testConfig() + warns, err := b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.ParallelsToolsMode != common.ParallelsToolsModeUpload { + t.Errorf("bad parallels tools mode: %s", b.config.ParallelsToolsMode) + } + + if b.config.GuestOSType != "other" { + t.Errorf("bad guest OS type: %s", b.config.GuestOSType) + } + + if b.config.VMName != "packer-foo" { + t.Errorf("bad vm name: %s", b.config.VMName) + } +} + +func TestBuilderPrepare_DiskSize(t *testing.T) { + var b Builder + config := testConfig() + + delete(config, "disk_size") + warns, err := b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("bad err: %s", err) + } + + if b.config.DiskSize != 40000 { + t.Fatalf("bad size: %d", b.config.DiskSize) + } + + config["disk_size"] = 60000 + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.DiskSize != 60000 { + t.Fatalf("bad size: %s", b.config.DiskSize) + } +} + +func TestBuilderPrepare_ParallelsToolsMode(t *testing.T) { + var b Builder + config := testConfig() + + // test default mode + delete(config, "parallels_tools_mode") + warns, err := b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("bad err: %s", err) + } + + // Test another mode + config["parallels_tools_mode"] = "attach" + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.ParallelsToolsMode != common.ParallelsToolsModeAttach { + t.Fatalf("bad: %s", b.config.ParallelsToolsMode) + } + + // Test bad mode + config["parllels_tools_mode"] = "teleport" + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should error") + } +} + +func TestBuilderPrepare_ParallelsToolsGuestPath(t *testing.T) { + var b Builder + config := testConfig() + + delete(config, "parallesl_tools_guest_path") + warns, err := b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("bad err: %s", err) + } + + if b.config.ParallelsToolsGuestPath != "prl-tools.iso" { + t.Fatalf("bad: %s", b.config.ParallelsToolsGuestPath) + } + + config["parallels_tools_guest_path"] = "foo" + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.ParallelsToolsGuestPath != "foo" { + t.Fatalf("bad size: %s", b.config.ParallelsToolsGuestPath) + } +} + +func TestBuilderPrepare_ParallelsToolsHostPath(t *testing.T) { + var b Builder + config := testConfig() + + config["parallels_tools_host_path"] = "" + warns, err := b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("err: %s", err) + } + + if b.config.ParallelsToolsHostPath != "/Applications/Parallels Desktop.app/Contents/Resources/Tools/prl-tools-other.iso" { + t.Fatalf("bad: %s", b.config.ParallelsToolsHostPath) + } + + config["parallels_tools_host_path"] = "./prl-tools-lin.iso" + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Errorf("should not have error: %s", err) + } +} + +func TestBuilderPrepare_HardDriveInterface(t *testing.T) { + var b Builder + config := testConfig() + + // Test a default boot_wait + delete(config, "hard_drive_interface") + warns, err := b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("err: %s", err) + } + + if b.config.HardDriveInterface != "sata" { + t.Fatalf("bad: %s", b.config.HardDriveInterface) + } + + // Test with a bad + config["hard_drive_interface"] = "fake" + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Test with a good + config["hard_drive_interface"] = "scsi" + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestBuilderPrepare_HTTPPort(t *testing.T) { + var b Builder + config := testConfig() + + // Bad + config["http_port_min"] = 1000 + config["http_port_max"] = 500 + warns, err := b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Bad + config["http_port_min"] = -500 + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Good + config["http_port_min"] = 500 + config["http_port_max"] = 1000 + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestBuilderPrepare_InvalidKey(t *testing.T) { + var b Builder + config := testConfig() + + // Add a random key + config["i_should_not_be_valid"] = true + warns, err := b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } +} + +func TestBuilderPrepare_ISOChecksum(t *testing.T) { + var b Builder + config := testConfig() + + // Test bad + config["iso_checksum"] = "" + warns, err := b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Test good + config["iso_checksum"] = "FOo" + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.ISOChecksum != "foo" { + t.Fatalf("should've lowercased: %s", b.config.ISOChecksum) + } +} + +func TestBuilderPrepare_ISOChecksumType(t *testing.T) { + var b Builder + config := testConfig() + + // Test bad + config["iso_checksum_type"] = "" + warns, err := b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Test good + config["iso_checksum_type"] = "mD5" + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.ISOChecksumType != "md5" { + t.Fatalf("should've lowercased: %s", b.config.ISOChecksumType) + } + + // Test unknown + config["iso_checksum_type"] = "fake" + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Test none + config["iso_checksum_type"] = "none" + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) == 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.ISOChecksumType != "none" { + t.Fatalf("should've lowercased: %s", b.config.ISOChecksumType) + } +} + +func TestBuilderPrepare_ISOUrl(t *testing.T) { + var b Builder + config := testConfig() + delete(config, "iso_url") + delete(config, "iso_urls") + + // Test both epty + config["iso_url"] = "" + b = Builder{} + warns, err := b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Test iso_url set + config["iso_url"] = "http://www.packer.io" + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Errorf("should not have error: %s", err) + } + + expected := []string{"http://www.packer.io"} + if !reflect.DeepEqual(b.config.ISOUrls, expected) { + t.Fatalf("bad: %#v", b.config.ISOUrls) + } + + // Test both set + config["iso_url"] = "http://www.packer.io" + config["iso_urls"] = []string{"http://www.packer.io"} + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Test just iso_urls set + delete(config, "iso_url") + config["iso_urls"] = []string{ + "http://www.packer.io", + "http://www.hashicorp.com", + } + + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Errorf("should not have error: %s", err) + } + + expected = []string{ + "http://www.packer.io", + "http://www.hashicorp.com", + } + if !reflect.DeepEqual(b.config.ISOUrls, expected) { + t.Fatalf("bad: %#v", b.config.ISOUrls) + } +} diff --git a/builder/parallels/iso/host_ip.go b/builder/parallels/iso/host_ip.go new file mode 100644 index 000000000..98a6d26df --- /dev/null +++ b/builder/parallels/iso/host_ip.go @@ -0,0 +1,7 @@ +package iso + +// Interface to help find the host IP that is available from within +// the VMware virtual machines. +type HostIPFinder interface { + HostIP() (string, error) +} diff --git a/builder/parallels/iso/host_ip_ifconfig.go b/builder/parallels/iso/host_ip_ifconfig.go new file mode 100644 index 000000000..bb756cec3 --- /dev/null +++ b/builder/parallels/iso/host_ip_ifconfig.go @@ -0,0 +1,55 @@ +package iso + +import ( + "bytes" + "errors" + "os" + "os/exec" + "regexp" +) + +// IfconfigIPFinder finds the host IP based on the output of `ifconfig`. +type IfconfigIPFinder struct { + Devices []string +} + +func (f *IfconfigIPFinder) HostIP() (string, error) { + var ifconfigPath string + + // On some systems, ifconfig is in /sbin which is generally not + // on the PATH for a standard user, so we just check that first. + if _, err := os.Stat("/sbin/ifconfig"); err == nil { + ifconfigPath = "/sbin/ifconfig" + } + + if ifconfigPath == "" { + var err error + ifconfigPath, err = exec.LookPath("ifconfig") + if err != nil { + return "", err + } + } + + for _, device := range f.Devices { + stdout := new(bytes.Buffer) + + cmd := exec.Command(ifconfigPath, device) + cmd.Env = append(cmd.Env, os.Environ()...) + + // Force LANG=C so that the output is what we expect it to be + // despite the locale. + cmd.Env = append(cmd.Env, "LANG=C") + + cmd.Stdout = stdout + cmd.Stderr = new(bytes.Buffer) + + if err := cmd.Run(); err == nil { + re := regexp.MustCompile(`inet\s+(?:addr:)?(.+?)\s`) + matches := re.FindStringSubmatch(stdout.String()) + if matches != nil { + return matches[1], nil + } + } + } + return "", errors.New("IP not found in ifconfig output...") +} diff --git a/builder/parallels/iso/host_ip_ifconfig_test.go b/builder/parallels/iso/host_ip_ifconfig_test.go new file mode 100644 index 000000000..51ccc272c --- /dev/null +++ b/builder/parallels/iso/host_ip_ifconfig_test.go @@ -0,0 +1,11 @@ +package iso + +import "testing" + +func TestIfconfigIPFinder_Impl(t *testing.T) { + var raw interface{} + raw = &IfconfigIPFinder{} + if _, ok := raw.(HostIPFinder); !ok { + t.Fatalf("IfconfigIPFinder is not a host IP finder") + } +} diff --git a/builder/parallels/iso/step_attach_iso.go b/builder/parallels/iso/step_attach_iso.go new file mode 100644 index 000000000..c18143409 --- /dev/null +++ b/builder/parallels/iso/step_attach_iso.go @@ -0,0 +1,45 @@ +package iso + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + parallelscommon "github.com/mitchellh/packer/builder/parallels/common" +) + +// This step attaches the ISO to the virtual machine. +// +// Uses: +// +// Produces: +type stepAttachISO struct { + diskPath string +} + +func (s *stepAttachISO) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(parallelscommon.Driver) + isoPath := state.Get("iso_path").(string) + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + + // Attach the disk to the controller + command := []string{ + "set", vmName, + "--device-set", "cdrom0", + "--image", isoPath, + "--enable", "--connect", + } + if err := driver.Prlctl(command...); err != nil { + err := fmt.Errorf("Error attaching ISO: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Set some state so we know to remove + state.Put("attachedIso", true) + + return multistep.ActionContinue +} + +func (s *stepAttachISO) Cleanup(state multistep.StateBag) {} diff --git a/builder/parallels/iso/step_create_disk.go b/builder/parallels/iso/step_create_disk.go new file mode 100644 index 000000000..7571abce3 --- /dev/null +++ b/builder/parallels/iso/step_create_disk.go @@ -0,0 +1,40 @@ +package iso + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + parallelscommon "github.com/mitchellh/packer/builder/parallels/common" + "strconv" +) + +// This step creates the virtual disk that will be used as the +// hard drive for the virtual machine. +type stepCreateDisk struct{} + +func (s *stepCreateDisk) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*config) + driver := state.Get("driver").(parallelscommon.Driver) + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + + command := []string{ + "set", vmName, + "--device-set", "hdd0", + "--size", strconv.FormatUint(uint64(config.DiskSize), 10), + "--iface", config.HardDriveInterface, + } + + ui.Say("Creating hard drive...") + err := driver.Prlctl(command...) + if err != nil { + err := fmt.Errorf("Error creating hard drive: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *stepCreateDisk) Cleanup(state multistep.StateBag) {} diff --git a/builder/parallels/iso/step_create_vm.go b/builder/parallels/iso/step_create_vm.go new file mode 100644 index 000000000..3b8824302 --- /dev/null +++ b/builder/parallels/iso/step_create_vm.go @@ -0,0 +1,79 @@ +package iso + +import ( + "fmt" + "github.com/mitchellh/multistep" + parallelscommon "github.com/mitchellh/packer/builder/parallels/common" + "github.com/mitchellh/packer/packer" + "path/filepath" +) + +// This step creates the actual virtual machine. +// +// Produces: +// vmName string - The name of the VM +type stepCreateVM struct { + vmName string +} + +func (s *stepCreateVM) Run(state multistep.StateBag) multistep.StepAction { + + config := state.Get("config").(*config) + driver := state.Get("driver").(parallelscommon.Driver) + ui := state.Get("ui").(packer.Ui) + + name := config.VMName + path := filepath.Join(".", config.OutputDir) + + commands := make([][]string, 9) + commands[0] = []string{ + "create", name, + "--ostype", config.GuestOSType, + "--distribution", config.GuestOSDistribution, + "--dst", path, + "--vmtype", "vm", + } + commands[1] = []string{"set", name, "--cpus", "1"} + commands[2] = []string{"set", name, "--memsize", "512"} + commands[3] = []string{"set", name, "--startup-view", "same"} + commands[4] = []string{"set", name, "--on-shutdown", "close"} + commands[5] = []string{"set", name, "--on-window-close", "keep-running"} + commands[6] = []string{"set", name, "--auto-share-camera", "off"} + commands[7] = []string{"set", name, "--device-del", "sound0"} + commands[8] = []string{"set", name, "--smart-guard", "off"} + + ui.Say("Creating virtual machine...") + for _, command := range commands { + err := driver.Prlctl(command...) + ui.Say(fmt.Sprintf("Executing: prlctl %s", command)) + if err != nil { + err := fmt.Errorf("Error creating VM: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Set the VM name property on the first command + if s.vmName == "" { + s.vmName = name + } + } + + // Set the final name in the state bag so others can use it + state.Put("vmName", s.vmName) + return multistep.ActionContinue +} + +func (s *stepCreateVM) Cleanup(state multistep.StateBag) { + if s.vmName == "" { + return + } + + driver := state.Get("driver").(parallelscommon.Driver) + ui := state.Get("ui").(packer.Ui) + + ui.Say("Unregistering virtual machine...") + if err := driver.Prlctl("unregister", s.vmName); err != nil { + ui.Error(fmt.Sprintf("Error unregistering virtual machine: %s", err)) + } +} diff --git a/builder/parallels/iso/step_http_server.go b/builder/parallels/iso/step_http_server.go new file mode 100644 index 000000000..24da8dd9d --- /dev/null +++ b/builder/parallels/iso/step_http_server.go @@ -0,0 +1,75 @@ +package iso + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" + "math/rand" + "net" + "net/http" +) + +// This step creates and runs the HTTP server that is serving the files +// specified by the 'http_files` configuration parameter in the template. +// +// Uses: +// config *config +// ui packer.Ui +// +// Produces: +// http_port int - The port the HTTP server started on. +type stepHTTPServer struct { + l net.Listener +} + +func (s *stepHTTPServer) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*config) + ui := state.Get("ui").(packer.Ui) + + var httpPort uint = 0 + if config.HTTPDir == "" { + state.Put("http_port", httpPort) + return multistep.ActionContinue + } + + // Find an available TCP port for our HTTP server + var httpAddr string + portRange := int(config.HTTPPortMax - config.HTTPPortMin) + for { + var err error + var offset uint = 0 + + if portRange > 0 { + // Intn will panic if portRange == 0, so we do a check. + offset = uint(rand.Intn(portRange)) + } + + httpPort = offset + config.HTTPPortMin + httpAddr = fmt.Sprintf(":%d", httpPort) + log.Printf("Trying port: %d", httpPort) + s.l, err = net.Listen("tcp", httpAddr) + if err == nil { + break + } + } + + ui.Say(fmt.Sprintf("Starting HTTP server on port %d", httpPort)) + + // Start the HTTP server and run it in the background + fileServer := http.FileServer(http.Dir(config.HTTPDir)) + server := &http.Server{Addr: httpAddr, Handler: fileServer} + go server.Serve(s.l) + + // Save the address into the state so it can be accessed in the future + state.Put("http_port", httpPort) + + return multistep.ActionContinue +} + +func (s *stepHTTPServer) Cleanup(multistep.StateBag) { + if s.l != nil { + // Close the listener so that the HTTP server stops + s.l.Close() + } +} diff --git a/builder/parallels/iso/step_type_boot_command.go b/builder/parallels/iso/step_type_boot_command.go new file mode 100644 index 000000000..342fa91fb --- /dev/null +++ b/builder/parallels/iso/step_type_boot_command.go @@ -0,0 +1,239 @@ +package iso + +import ( + "fmt" + "github.com/mitchellh/multistep" + parallelscommon "github.com/mitchellh/packer/builder/parallels/common" + "github.com/mitchellh/packer/packer" + "log" + "strings" + "time" + "unicode" + "unicode/utf8" +) + +const KeyLeftShift uint32 = 0xFFE1 + +type bootCommandTemplateData struct { + HTTPIP string + HTTPPort uint + Name string +} + +// This step "types" the boot command into the VM via prltype, built on the +// Parallels Virtualization SDK - C API. +// +// Uses: +// config *config +// driver Driver +// http_port int +// ui packer.Ui +// vmName string +// +// Produces: +// +type stepTypeBootCommand struct{} + +func (s *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*config) + httpPort := state.Get("http_port").(uint) + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + driver := state.Get("driver").(parallelscommon.Driver) + + // Determine the host IP + ipFinder := &IfconfigIPFinder{Devices: []string{"en0", "en1", "en2", "en3", "en4", "en5", "en6", "en7", "en8", "en9"}} + + hostIp, err := ipFinder.HostIP() + if err != nil { + err := fmt.Errorf("Error detecting host IP: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say(fmt.Sprintf("Host IP for the Parallels machine: %s", hostIp)) + + tplData := &bootCommandTemplateData{ + hostIp, + httpPort, + config.VMName, + } + + ui.Say("Typing the boot command...") + for _, command := range config.BootCommand { + command, err := config.tpl.Process(command, tplData) + if err != nil { + err := fmt.Errorf("Error preparing boot command: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + codes := []string{} + for _, code := range scancodes(command) { + if code == "wait" { + if err := driver.SendKeyScanCodes(vmName, codes...); err != nil { + err := fmt.Errorf("Error sending boot command: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + codes = []string{} + time.Sleep(1 * time.Second) + continue + } + + if code == "wait5" { + if err := driver.SendKeyScanCodes(vmName, codes...); err != nil { + err := fmt.Errorf("Error sending boot command: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + codes = []string{} + time.Sleep(5 * time.Second) + continue + } + + if code == "wait10" { + if err := driver.SendKeyScanCodes(vmName, codes...); err != nil { + err := fmt.Errorf("Error sending boot command: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + codes = []string{} + time.Sleep(10 * time.Second) + continue + } + + // Since typing is sometimes so slow, we check for an interrupt + // in between each character. + if _, ok := state.GetOk(multistep.StateCancelled); ok { + return multistep.ActionHalt + } + codes = append(codes, code) + } + log.Printf("Sending scancodes: %#v", codes) + if err := driver.SendKeyScanCodes(vmName, codes...); err != nil { + err := fmt.Errorf("Error sending boot command: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + return multistep.ActionContinue +} + +func (*stepTypeBootCommand) Cleanup(multistep.StateBag) {} + +func scancodes(message string) []string { + // Scancodes reference: http://www.win.tue.nl/~aeb/linux/kbd/scancodes-1.html + // + // Scancodes represent raw keyboard output and are fed to the VM by the + // Parallels Virtualization SDK - C API, PrlDevKeyboard_SendKeyEvent + // + // Scancodes are recorded here in pairs. The first entry represents + // the key press and the second entry represents the key release and is + // derived from the first by the addition of 0x80. + special := make(map[string][]string) + special[""] = []string{"0e", "8e"} + special[""] = []string{"53", "d3"} + special[""] = []string{"1c", "9c"} + special[""] = []string{"01", "81"} + special[""] = []string{"3b", "bb"} + special[""] = []string{"3c", "bc"} + special[""] = []string{"3d", "bd"} + special[""] = []string{"3e", "be"} + special[""] = []string{"3f", "bf"} + special[""] = []string{"40", "c0"} + special[""] = []string{"41", "c1"} + special[""] = []string{"42", "c2"} + special[""] = []string{"43", "c3"} + special[""] = []string{"44", "c4"} + special[""] = []string{"1c", "9c"} + special[""] = []string{"0f", "8f"} + + shiftedChars := "!@#$%^&*()_+{}:\"~|<>?" + + scancodeIndex := make(map[string]uint) + scancodeIndex["1234567890-="] = 0x02 + scancodeIndex["!@#$%^&*()_+"] = 0x02 + scancodeIndex["qwertyuiop[]"] = 0x10 + scancodeIndex["QWERTYUIOP{}"] = 0x10 + scancodeIndex["asdfghjkl;ยด`"] = 0x1e + scancodeIndex[`ASDFGHJKL:"~`] = 0x1e + scancodeIndex["\\zxcvbnm,./"] = 0x2b + scancodeIndex["|ZXCVBNM<>?"] = 0x2b + scancodeIndex[" "] = 0x39 + + scancodeMap := make(map[rune]uint) + for chars, start := range scancodeIndex { + var i uint = 0 + for len(chars) > 0 { + r, size := utf8.DecodeRuneInString(chars) + chars = chars[size:] + scancodeMap[r] = start + i + i += 1 + } + } + + result := make([]string, 0, len(message)*2) + for len(message) > 0 { + var scancode []string + + if strings.HasPrefix(message, "") { + log.Printf("Special code found, will sleep 1 second at this point.") + scancode = []string{"wait"} + message = message[len(""):] + } + + if strings.HasPrefix(message, "") { + log.Printf("Special code found, will sleep 5 seconds at this point.") + scancode = []string{"wait5"} + message = message[len(""):] + } + + if strings.HasPrefix(message, "") { + log.Printf("Special code found, will sleep 10 seconds at this point.") + scancode = []string{"wait10"} + message = message[len(""):] + } + + if scancode == nil { + for specialCode, specialValue := range special { + if strings.HasPrefix(message, specialCode) { + log.Printf("Special code '%s' found, replacing with: %s", specialCode, specialValue) + scancode = specialValue + message = message[len(specialCode):] + break + } + } + } + + if scancode == nil { + r, size := utf8.DecodeRuneInString(message) + message = message[size:] + scancodeInt := scancodeMap[r] + keyShift := unicode.IsUpper(r) || strings.ContainsRune(shiftedChars, r) + + scancode = make([]string, 0, 4) + if keyShift { + scancode = append(scancode, "2a") + } + + scancode = append(scancode, fmt.Sprintf("%02x", scancodeInt)) + scancode = append(scancode, fmt.Sprintf("%02x", scancodeInt+0x80)) + + if keyShift { + scancode = append(scancode, "aa") + } + } + + result = append(result, scancode...) + } + + return result +} diff --git a/builder/parallels/pvm/builder.go b/builder/parallels/pvm/builder.go new file mode 100644 index 000000000..50eedaa3b --- /dev/null +++ b/builder/parallels/pvm/builder.go @@ -0,0 +1,129 @@ +package pvm + +import ( + "errors" + "fmt" + "github.com/mitchellh/multistep" + parallelscommon "github.com/mitchellh/packer/builder/parallels/common" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" + "log" +) + +// Builder implements packer.Builder and builds the actual Parallels +// 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 Parallels 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 Parallels + driver, err := parallelscommon.NewDriver() + if err != nil { + return nil, fmt.Errorf("Failed creating Paralles driver: %s", err) + } + + // Set up the state. + state := new(multistep.BasicStateBag) + state.Put("config", b.config) + state.Put("driver", driver) + state.Put("hook", hook) + state.Put("ui", ui) + + // Build the steps. + steps := []multistep.Step{ + ¶llelscommon.StepOutputDir{ + Force: b.config.PackerForce, + Path: b.config.OutputDir, + }, + &common.StepCreateFloppy{ + Files: b.config.FloppyFiles, + }, + &StepImport{ + Name: b.config.VMName, + SourcePath: b.config.SourcePath, + }, + ¶llelscommon.StepAttachParallelsTools{ + ParallelsToolsHostPath: b.config.ParallelsToolsHostPath, + ParallelsToolsMode: b.config.ParallelsToolsMode, + }, + new(parallelscommon.StepAttachFloppy), + ¶llelscommon.StepPrlctl{ + Commands: b.config.Prlctl, + Tpl: b.config.tpl, + }, + ¶llelscommon.StepRun{ + BootWait: b.config.BootWait, + Headless: b.config.Headless, + }, + &common.StepConnectSSH{ + SSHAddress: parallelscommon.SSHAddress, + SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig), + SSHWaitTimeout: b.config.SSHWaitTimeout, + }, + ¶llelscommon.StepUploadVersion{ + Path: b.config.PrlctlVersionFile, + }, + ¶llelscommon.StepUploadParallelsTools{ + ParallelsToolsGuestPath: b.config.ParallelsToolsGuestPath, + ParallelsToolsHostPath: b.config.ParallelsToolsHostPath, + ParallelsToolsMode: b.config.ParallelsToolsMode, + Tpl: b.config.tpl, + }, + new(common.StepProvision), + ¶llelscommon.StepShutdown{ + Command: b.config.ShutdownCommand, + Timeout: b.config.ShutdownTimeout, + }, + new(parallelscommon.StepRemoveDevices), + } + + // Run the steps. + if b.config.PackerDebug { + b.runner = &multistep.DebugRunner{ + Steps: steps, + PauseFn: common.MultistepDebugFn(ui), + } + } else { + b.runner = &multistep.BasicRunner{Steps: steps} + } + 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 parallelscommon.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/parallels/pvm/config.go b/builder/parallels/pvm/config.go new file mode 100644 index 000000000..4eed5db10 --- /dev/null +++ b/builder/parallels/pvm/config.go @@ -0,0 +1,131 @@ +package pvm + +import ( + "fmt" + parallelscommon "github.com/mitchellh/packer/builder/parallels/common" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" + "os" +) + +// Config is the configuration structure for the builder. +type Config struct { + common.PackerConfig `mapstructure:",squash"` + parallelscommon.FloppyConfig `mapstructure:",squash"` + parallelscommon.OutputConfig `mapstructure:",squash"` + parallelscommon.RunConfig `mapstructure:",squash"` + parallelscommon.SSHConfig `mapstructure:",squash"` + parallelscommon.ShutdownConfig `mapstructure:",squash"` + parallelscommon.PrlctlConfig `mapstructure:",squash"` + parallelscommon.PrlctlVersionConfig `mapstructure:",squash"` + + ParallelsToolsMode string `mapstructure:"parallels_tools_mode"` + ParallelsToolsGuestPath string `mapstructure:"parallels_tools_guest_path"` + ParallelsToolsHostPath string `mapstructure:"parallels_tools_host_path"` + + SourcePath string `mapstructure:"source_path"` + VMName string `mapstructure:"vm_name"` + + tpl *packer.ConfigTemplate +} + +func NewConfig(raws ...interface{}) (*Config, []string, error) { + c := new(Config) + md, err := common.DecodeConfig(c, raws...) + if err != nil { + return nil, nil, err + } + + c.tpl, err = packer.NewConfigTemplate() + if err != nil { + return nil, nil, err + } + c.tpl.UserVars = c.PackerUserVars + + // Defaults + if c.ParallelsToolsMode == "" { + c.ParallelsToolsMode = "disable" + } + + if c.ParallelsToolsGuestPath == "" { + c.ParallelsToolsGuestPath = "prl-tools.iso" + } + + if c.ParallelsToolsHostPath == "" { + c.ParallelsToolsHostPath = "/Applications/Parallels Desktop.app/Contents/Resources/Tools/prl-tools-other.iso" + } + + if c.VMName == "" { + c.VMName = fmt.Sprintf("packer-%s-{{timestamp}}", c.PackerBuildName) + } + + // Prepare the errors + errs := common.CheckUnusedConfig(md) + errs = packer.MultiErrorAppend(errs, c.FloppyConfig.Prepare(c.tpl)...) + errs = packer.MultiErrorAppend(errs, c.OutputConfig.Prepare(c.tpl, &c.PackerConfig)...) + errs = packer.MultiErrorAppend(errs, c.RunConfig.Prepare(c.tpl)...) + errs = packer.MultiErrorAppend(errs, c.ShutdownConfig.Prepare(c.tpl)...) + errs = packer.MultiErrorAppend(errs, c.SSHConfig.Prepare(c.tpl)...) + errs = packer.MultiErrorAppend(errs, c.PrlctlConfig.Prepare(c.tpl)...) + errs = packer.MultiErrorAppend(errs, c.PrlctlVersionConfig.Prepare(c.tpl)...) + + templates := map[string]*string{ + "parallels_tools_mode": &c.ParallelsToolsMode, + "parallels_tools_host_paht": &c.ParallelsToolsHostPath, + "parallels_tools_guest_path": &c.ParallelsToolsGuestPath, + "source_path": &c.SourcePath, + "vm_name": &c.VMName, + } + + for n, ptr := range templates { + var err error + *ptr, err = c.tpl.Process(*ptr, nil) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Error processing %s: %s", n, err)) + } + } + + validMode := false + validModes := []string{ + parallelscommon.ParallelsToolsModeDisable, + parallelscommon.ParallelsToolsModeAttach, + parallelscommon.ParallelsToolsModeUpload, + } + + for _, mode := range validModes { + if c.ParallelsToolsMode == mode { + validMode = true + break + } + } + + if !validMode { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("parallels_tools_mode is invalid. Must be one of: %v", validModes)) + } + + if c.SourcePath == "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_path is required")) + } else { + if _, err := os.Stat(c.SourcePath); err != nil { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("source_path is invalid: %s", err)) + } + } + + // 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.") + } + + // Check for any errors. + if errs != nil && len(errs.Errors) > 0 { + return nil, warnings, errs + } + + return c, warnings, nil +} diff --git a/builder/parallels/pvm/config_test.go b/builder/parallels/pvm/config_test.go new file mode 100644 index 000000000..1fb15849b --- /dev/null +++ b/builder/parallels/pvm/config_test.go @@ -0,0 +1,85 @@ +package pvm + +import ( + "io/ioutil" + "os" + "testing" +) + +func testConfig(t *testing.T) map[string]interface{} { + return map[string]interface{}{ + "ssh_username": "foo", + "shutdown_command": "foo", + } +} + +func getTempFile(t *testing.T) *os.File { + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + tf.Close() + + // don't forget to cleanup the file downstream: + // defer os.Remove(tf.Name()) + + return tf +} + +func testConfigErr(t *testing.T, warns []string, err error) { + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should error") + } +} + +func testConfigOk(t *testing.T, warns []string, err error) { + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("bad: %s", err) + } +} + +func TestNewConfig_sourcePath(t *testing.T) { + // Bad + c := testConfig(t) + delete(c, "source_path") + _, warns, errs := NewConfig(c) + testConfigErr(t, warns, errs) + + // Bad + c = testConfig(t) + c["source_path"] = "/i/dont/exist" + _, warns, errs = NewConfig(c) + testConfigErr(t, warns, errs) + + // Good + tf := getTempFile(t) + defer os.Remove(tf.Name()) + + c = testConfig(t) + c["source_path"] = tf.Name() + _, warns, errs = NewConfig(c) + testConfigOk(t, warns, errs) +} + +func TestNewConfig_shutdown_timeout(t *testing.T) { + c := testConfig(t) + tf := getTempFile(t) + defer os.Remove(tf.Name()) + + // Expect this to fail + c["source_path"] = tf.Name() + c["shutdown_timeout"] = "NaN" + _, warns, errs := NewConfig(c) + testConfigErr(t, warns, errs) + + // Passes when given a valid time duration + c["shutdown_timeout"] = "10s" + _, warns, errs = NewConfig(c) + testConfigOk(t, warns, errs) +} diff --git a/builder/parallels/pvm/step_import.go b/builder/parallels/pvm/step_import.go new file mode 100644 index 000000000..71ceac6a7 --- /dev/null +++ b/builder/parallels/pvm/step_import.go @@ -0,0 +1,48 @@ +package pvm + +import ( + "fmt" + "github.com/mitchellh/multistep" + parallelscommon "github.com/mitchellh/packer/builder/parallels/common" + "github.com/mitchellh/packer/packer" +) + +// This step imports an PVM VM into Parallels. +type StepImport struct { + Name string + SourcePath string + vmName string +} + +func (s *StepImport) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(parallelscommon.Driver) + ui := state.Get("ui").(packer.Ui) + config := state.Get("config").(*Config) + + ui.Say(fmt.Sprintf("Importing VM: %s", s.SourcePath)) + if err := driver.Import(s.Name, s.SourcePath, config.OutputDir); err != nil { + err := fmt.Errorf("Error importing VM: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + s.vmName = s.Name + state.Put("vmName", s.Name) + return multistep.ActionContinue +} + +func (s *StepImport) Cleanup(state multistep.StateBag) { + + if s.vmName == "" { + return + } + + driver := state.Get("driver").(parallelscommon.Driver) + ui := state.Get("ui").(packer.Ui) + + ui.Say("Unregistering virtual machine...") + if err := driver.Prlctl("unregister", s.vmName); err != nil { + ui.Error(fmt.Sprintf("Error unregistering virtual machine: %s", err)) + } +} diff --git a/builder/parallels/pvm/step_test.go b/builder/parallels/pvm/step_test.go new file mode 100644 index 000000000..9a5fd6e38 --- /dev/null +++ b/builder/parallels/pvm/step_test.go @@ -0,0 +1,19 @@ +package pvm + +import ( + "bytes" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + parallelscommon "github.com/mitchellh/packer/builder/parallels/common" + "testing" +) + +func testState(t *testing.T) multistep.StateBag { + state := new(multistep.BasicStateBag) + state.Put("driver", new(parallelscommon.DriverMock)) + state.Put("ui", &packer.BasicUi{ + Reader: new(bytes.Buffer), + Writer: new(bytes.Buffer), + }) + return state +} diff --git a/config.go b/config.go index 1a58a0862..9f1485aa8 100644 --- a/config.go +++ b/config.go @@ -31,6 +31,8 @@ const defaultConfig = ` "virtualbox-ovf": "packer-builder-virtualbox-ovf", "vmware-iso": "packer-builder-vmware-iso", "vmware-vmx": "packer-builder-vmware-vmx", + "parallels-iso": "packer-builder-parallels-iso", + "parallels-pvm": "packer-builder-parallels-pvm", "null": "packer-builder-null" }, diff --git a/plugin/builder-parallels-iso/main.go b/plugin/builder-parallels-iso/main.go new file mode 100644 index 000000000..9d0f85e43 --- /dev/null +++ b/plugin/builder-parallels-iso/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/mitchellh/packer/builder/parallels/iso" + "github.com/mitchellh/packer/packer/plugin" +) + +func main() { + server, err := plugin.Server() + if err != nil { + panic(err) + } + server.RegisterBuilder(new(iso.Builder)) + server.Serve() +} diff --git a/plugin/builder-parallels-iso/main_test.go b/plugin/builder-parallels-iso/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/plugin/builder-parallels-iso/main_test.go @@ -0,0 +1 @@ +package main diff --git a/plugin/builder-parallels-pvm/main.go b/plugin/builder-parallels-pvm/main.go new file mode 100644 index 000000000..36aafbd62 --- /dev/null +++ b/plugin/builder-parallels-pvm/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/mitchellh/packer/builder/parallels/pvm" + "github.com/mitchellh/packer/packer/plugin" +) + +func main() { + server, err := plugin.Server() + if err != nil { + panic(err) + } + server.RegisterBuilder(new(pvm.Builder)) + server.Serve() +} diff --git a/plugin/builder-parallels-pvm/main_test.go b/plugin/builder-parallels-pvm/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/plugin/builder-parallels-pvm/main_test.go @@ -0,0 +1 @@ +package main From c5e9fbcb504564d2391f30be52a3b4325c3513aa Mon Sep 17 00:00:00 2001 From: Rickard von Essen Date: Thu, 10 Apr 2014 21:20:17 +0200 Subject: [PATCH 2/4] Added support for Parallels Desktop for Mac [GH-223] Added documentation for Parallels builder. --- .../docs/builders/parallels-iso.html.markdown | 290 ++++++++++++++++++ .../docs/builders/parallels-pvm.html.markdown | 159 ++++++++++ .../docs/builders/parallels.html.markdown | 39 +++ website/source/layouts/docs.erb | 1 + 4 files changed, 489 insertions(+) create mode 100644 website/source/docs/builders/parallels-iso.html.markdown create mode 100644 website/source/docs/builders/parallels-pvm.html.markdown create mode 100644 website/source/docs/builders/parallels.html.markdown diff --git a/website/source/docs/builders/parallels-iso.html.markdown b/website/source/docs/builders/parallels-iso.html.markdown new file mode 100644 index 000000000..e27152731 --- /dev/null +++ b/website/source/docs/builders/parallels-iso.html.markdown @@ -0,0 +1,290 @@ +--- +layout: "docs" +page_title: "Parallels Builder (from an ISO)" +--- + +# Parallels Builder (from an ISO) + +Type: `parallels-iso` + +The Parallels builder is able to create +[Parallels Desktop for Mac](http://www.parallels.com/products/desktop/) virtual +machines and export them in the PVM format, starting from an +ISO image. + +The builder builds a virtual machine by creating a new virtual machine +from scratch, booting it, installing an OS, provisioning software within +the OS, then shutting it down. The result of the Parallels builder is a directory +containing all the files necessary to run the virtual machine portably. + +## Basic Example + +Here is a basic example. This example is not functional. It will start the +OS installer but then fail because we don't provide the preseed file for +Ubuntu to self-install. Still, the example serves to show the basic configuration: + +
+{
+  "type": "parallels-iso",
+  "guest_os_type": "Ubuntu_64",
+  "iso_url": "http://releases.ubuntu.com/12.04/ubuntu-12.04.3-server-amd64.iso",
+  "iso_checksum": "2cbe868812a871242cdcdd8f2fd6feb9",
+  "iso_checksum_type": "md5",
+  "ssh_username": "packer",
+  "ssh_password": "packer",
+  "ssh_wait_timeout": "30s",
+  "shutdown_command": "echo 'packer' | sudo -S shutdown -P now"
+}
+
+ +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 Parallels builder. +They are organized below into two categories: required and optional. Within +each category, the available options are alphabetized and described. + +Required: + +* `iso_checksum` (string) - The checksum for the OS ISO file. Because ISO + files are so large, this is required and Packer will verify it prior + to booting a virtual machine with the ISO attached. The type of the + checksum is specified with `iso_checksum_type`, documented below. + +* `iso_checksum_type` (string) - The type of the checksum specified in + `iso_checksum`. Valid values are "none", "md5", "sha1", "sha256", or + "sha512" currently. While "none" will skip checksumming, this is not + recommended since ISO files are generally large and corruption does happen + from time to time. + +* `iso_url` (string) - A URL to the ISO containing the installation image. + This URL can be either an HTTP URL or a file URL (or path to a file). + If this is an HTTP URL, Packer will download it and cache it between + runs. + +* `ssh_username` (string) - The username to use to SSH into the machine + once the OS is installed. + +Optional: + +* `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 10 seconds. + +* `disk_size` (int) - The size, in megabytes, of the hard disk to create + for the VM. By default, this is 40000 (about 40 GB). + +* `floppy_files` (array of strings) - A list of files to put onto a floppy + disk that is attached when the VM is booted for the first time. 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. The files listed in this configuration will all be put + into the root directory of the floppy disk; sub-directories are not supported. + +* `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 `prlctl create x --ostype list`. Setting the correct value hints to + Parallels Desktop how to optimize the virtual hardware to work best with + that operating system. + +* `guest_os_distribution` (string) - The guest OS distribution 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 `prlctl create x --distribution list`. + Setting the correct value hints to Parallels how to optimize the virtual + hardware to work best with that operating system. + +* `hard_drive_interface` (string) - The type of controller that the + hard drives are attached to, defaults to "sata". Valid options are + "sata", "ide", and "scsi". + +* `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 "", 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` (int) - 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. + +* `iso_urls` (array of strings) - Multiple URLs for the ISO to download. + Packer will try these in order. If anything goes wrong attempting to download + or while downloading a single URL, it will move on to the next. All URLs + must point to the same file (same checksum). By default this is empty + and `iso_url` is used. Only one of `iso_url` or `iso_urls` can be specified. + +* `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. + +* `prlctl` (array of array of strings) - Custom `prlctl` 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 `prlctl` (but excluding `prlctl` itself). Each arg is treated + as a [configuration template](/docs/templates/configuration-templates.html), + where the `Name` variable is replaced with the VM name. More details on how + to use `prlctl` are below. + +* `parallels_tools_mode` (string) - The method by which Parallels tools are + made available to the guest for installation. Valid options are "upload", + "attach", or "disable". The functions of each of these should be + self-explanatory. The default value is "upload". + +* `parallels_tools_guest_path` (string) - The path on the guest virtual machine + where the Parallels tools ISO will be uploaded. By default this is + "prl-tools.iso" which should upload into the login directory of the user. + This is a configuration template where the `Version` variable is replaced + with the prlctl version. + +* `parallels_tools_host_path` (string) - The path to the Parallels Tools ISO to + upload. By default the Parallels builder will use the "other" OS tools ISO from + the Parallels installation: + "/Applications/Parallels Desktop.app/Contents/Resources/Tools/prl-tools-other.iso" + +* `prlctl_version_file` (string) - The path within the virtual machine to upload + a file that contains the `prlctl` version that was used to create the machine. + This information can be useful for provisioning. By default this is + ".prlctl_version", which will generally upload it into the home directory. + +* `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. + +* `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. + +* `ssh_key_path` (string) - Path to a private key to use for authenticating + with SSH. By default this is not set (key-based auth won't be used). + The associated public key is expected to already be configured on the + VM being prepared by some other process (kickstart, etc.). + +* `ssh_password` (string) - The password for `ssh_username` to use to + authenticate with SSH. By default this is the empty string. + +* `ssh_port` (int) - The port that SSH will be listening on in the guest + virtual machine. By default this is 22. + +* `ssh_wait_timeout` (string) - The duration to wait for SSH to become + available. By default this is "20m", or 20 minutes. Note that this should + be quite long since the timer begins as soon as the virtual machine is booted. + +* `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. + +## 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 "typed" character for character using the `prltype` (part +of prl-utils, see [Parallels Builder](/docs/builders/parallels.html)) +command connected to the machine, simulating a human actually typing the +keyboard. There are a set of special keys available. If these are in your +boot command, they will be replaced by the proper key: + +* `` and `` - Simulates an actual "enter" or "return" keypress. + +* `` - Simulates pressing the escape key. + +* `` - Simulates pressing the tab key. + +* `` `` `` - Adds a 1, 5 or 10 second pause before sending + any additional keys. This is useful if you have to generally wait for the UI + to update before typing more. + +In addition to the special keys, each command to type is treated as a +[configuration template](/docs/templates/configuration-templates.html). +The available variables are: + +* `HTTPIP` and `HTTPPort` - The IP and port, respectively of an HTTP server + that is started serving the directory specified by the `http_directory` + configuration parameter. If `http_directory` isn't specified, these will + be blank! + +Example boot command. This is actually a working boot command used to start +an Ubuntu 12.04 installer: + +
+[
+  "<esc><esc><enter><wait>",
+  "/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 -- <enter>"
+]
+
+ +## Parallels Tools +After the virtual machine is up and the operating system is installed, Packer +uploads the Parallels Tools into the virtual machine. The path where they are +uploaded is controllable by `parallels_tools_path`, and defaults to +"prl-tools.iso". Without an absolute path, it is uploaded to the home directory +of the SSH user. Parallels Tools ISO's can be found in: +"/Applications/Parallels Desktop.app/Contents/Resources/Tools/" + +## prlctl Commands +In order to perform extra customization of the virtual machine, a template can +define extra calls to `prlctl` to perform. +[prlctl](http://download.parallels.com/desktop/v4/wl/docs/en/Parallels_Command_Line_Reference_Guide/) +is the command-line interface to Parallels. It can be used to do things such as +set RAM, CPUs, etc. + +Extra `prlctl` commands are defined in the template in the `prlctl` section. +An example is shown below that sets the memory and number of CPUs within the +virtual machine: + +
+{
+  "prlctl": [
+    ["set", "{{.Name}}", "--memsize", "1024"],
+    ["set", "{{.Name}}", "--cpus", "2"]
+  ]
+}
+
+ +The value of `prlctl` 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 +`prlctl`. Each argument is treated as a +[configuration template](/docs/templates/configuration-templates.html). The only +available variable is `Name` which is replaced with the unique name of the VM, +which is required for many `prlctl` calls. diff --git a/website/source/docs/builders/parallels-pvm.html.markdown b/website/source/docs/builders/parallels-pvm.html.markdown new file mode 100644 index 000000000..0e32ab975 --- /dev/null +++ b/website/source/docs/builders/parallels-pvm.html.markdown @@ -0,0 +1,159 @@ +--- +layout: "docs" +page_title: "Parallels Builder (from a PVM)" +--- + +# Parallels Builder (from a PVM) + +Type: `parallels-pvm` + +This Parallels builder is able to create +[Parallels Desktop for Mac](http://www.parallels.com/products/desktop/) +virtual machines and export them in the PVM format, starting from an +existing PVM (exported virtual machine image). + +The builder builds a virtual machine by importing an existing PVM +file. It then boots this image, runs provisioners on this new VM, and +exports that VM to create the image. The imported machine is deleted prior +to finishing the build. + +## Basic Example + +Here is a basic example. This example is functional if you have an PVM matching +the settings here. + +
+{
+  "type": "parallels-pvm",
+  "source_path": "source.pvm",
+  "ssh_username": "packer",
+  "ssh_password": "packer",
+  "ssh_wait_timeout": "30s",
+  "shutdown_command": "echo 'packer' | sudo -S shutdown -P now"
+}
+
+ +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 Parallels builder. +They are organized below into two categories: required and optional. Within +each category, the available options are alphabetized and described. + +Required: + +* `source_path` (string) - The path to a PVM directory that acts as + the source of this build. + +* `ssh_username` (string) - The username to use to SSH into the machine + once the OS is installed. + +Optional: + +* `floppy_files` (array of strings) - A list of files to put onto a floppy + disk that is attached when the VM is booted for the first time. 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. The files listed in this configuration will all be put + into the root directory of the floppy disk; sub-directories are not supported. + +* `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. + +* `prlctl` (array of array of strings) - Custom `prlctl` 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 `prlctl` (but excluding `prlctl` itself). Each arg is treated + as a [configuration template](/docs/templates/configuration-templates.html), + where the `Name` variable is replaced with the VM name. More details on how + to use `prlctl` are below. + +* `parallels_tools_mode` (string) - The method by which Parallels tools are + made available to the guest for installation. Valid options are "upload", + "attach", or "disable". The functions of each of these should be + self-explanatory. The default value is "upload". + +* `parallels_tools_guest_path` (string) - The path on the guest virtual machine + where the Parallels tools ISO will be uploaded. By default this is + "prl-tools.iso" which should upload into the login directory of the user. + This is a configuration template where the `Version` variable is replaced + with the prlctl version. + +* `parallels_tools_host_path` (string) - The path to the Parallels Tools ISO to + upload. By default the Parallels builder will use the "other" OS tools ISO from + the Parallels installation: + "/Applications/Parallels Desktop.app/Contents/Resources/Tools/prl-tools-other.iso" + +* `prlctl_version_file` (string) - The path within the virtual machine to upload + a file that contains the `prlctl` version that was used to create the machine. + This information can be useful for provisioning. By default this is + ".prlctl_version", which will generally upload it into the home directory. + +* `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. + +* `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. + +* `ssh_key_path` (string) - Path to a private key to use for authenticating + with SSH. By default this is not set (key-based auth won't be used). + The associated public key is expected to already be configured on the + VM being prepared by some other process (kickstart, etc.). + +* `ssh_password` (string) - The password for `ssh_username` to use to + authenticate with SSH. By default this is the empty string. + +* `ssh_port` (int) - The port that SSH will be listening on in the guest + virtual machine. By default this is 22. + +* `ssh_wait_timeout` (string) - The duration to wait for SSH to become + available. By default this is "20m", or 20 minutes. Note that this should + be quite long since the timer begins as soon as the virtual machine is booted. + + +* `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 name of the build. + +## prlctl Commands +In order to perform extra customization of the virtual machine, a template can +define extra calls to `prlctl` to perform. +[prlctl](http://download.parallels.com/desktop/v4/wl/docs/en/Parallels_Command_Line_Reference_Guide/) +is the command-line interface to Parallels. It can be used to do things such as +set RAM, CPUs, etc. + +Extra `prlctl` commands are defined in the template in the `prlctl` section. +An example is shown below that sets the memory and number of CPUs within the +virtual machine: + +
+{
+  "prlctl": [
+    ["set", "{{.Name}}", "--memsize", "1024"],
+    ["set", "{{.Name}}", "--cpus", "2"]
+  ]
+}
+
+ +The value of `prlctl` 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 +`prlctl`. Each argument is treated as a +[configuration template](/docs/templates/configuration-templates.html). The only +available variable is `Name` which is replaced with the unique name of the VM, +which is required for many `prlctl` calls. diff --git a/website/source/docs/builders/parallels.html.markdown b/website/source/docs/builders/parallels.html.markdown new file mode 100644 index 000000000..89a7840b4 --- /dev/null +++ b/website/source/docs/builders/parallels.html.markdown @@ -0,0 +1,39 @@ +--- +layout: "docs" +page_title: "Parallels Builder" +--- + +# Parallels Builder + +The Parallels builder is able to create [Parallels Desktop for Mac](http://www.parallels.com/products/desktop/) virtual machines and export them in the PVM format. + +Packer actually comes with multiple builders able to create Parallels +machines, depending on the strategy you want to use to build the image. +Packer supports the following Parallels builders: + +* [parallels-iso](/docs/builders/parallels-iso.html) - Starts from + an ISO file, creates a brand new Parallels VM, installs an OS, + provisions software within the OS, then exports that machine to create + an image. This is best for people who want to start from scratch. + +* [parallels-pvm](/docs/builders/parallels-pvm.html) - This builder + imports an existing PVM file, runs provisioners on top of that VM, + and exports that machine to create an image. This is best if you have + an existing Parallels 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. + + +## Requirements + +In addition to [Parallels Desktop for Mac](http://www.parallels.com/products/desktop/) this requires: + +- [Parallels Virtualization SDK 9 for Mac](http://download.parallels.com//desktop/v9/pde.hf1/ParallelsVirtualizationSDK-9.0.24172.951362.dmg) +- [prl-utils](https://github.com/rickard-von-essen/prl-utils/) + +The SDK can be installed by downloading and following the instructions in the dmg. The easiest way to install _prl-utils_ is using [Homebrew](http://brew.sh/) + + ``` + brew tap rickard-von-essen/homebrew-formulae + brew install --HEAD prl-utils + ``` diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 3c0bae143..5fe4f6eaf 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -38,6 +38,7 @@
  • QEMU
  • VirtualBox
  • VMware
  • +
  • Parallels
  • Custom
  • From 75e26ee955f516ae3be6fecd9f533870f7ad9796 Mon Sep 17 00:00:00 2001 From: YungSang Date: Wed, 9 Apr 2014 07:22:48 +0200 Subject: [PATCH 3/4] Added support for Parallels Desktop for Mac [GH-233] in the vagrant post-processor. The code originate from https://github.com/YungSang/packer-parallels --- post-processor/vagrant/parallels.go | 98 +++++++++++++++++++ post-processor/vagrant/parallels_test.go | 9 ++ post-processor/vagrant/post-processor.go | 14 ++- .../post-processors/vagrant.html.markdown | 1 + 4 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 post-processor/vagrant/parallels.go create mode 100644 post-processor/vagrant/parallels_test.go diff --git a/post-processor/vagrant/parallels.go b/post-processor/vagrant/parallels.go new file mode 100644 index 000000000..0e501c3ce --- /dev/null +++ b/post-processor/vagrant/parallels.go @@ -0,0 +1,98 @@ +package vagrant + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/going/toolkit/xmlpath" + "github.com/mitchellh/packer/packer" +) + +// These are the extensions of files that are unnecessary for the function +// of a Parallels virtual machine. +var UnnecessaryFileExtensions = []string{".log", ".backup", ".Backup"} + +type ParallelsProvider struct{} + +func (p *ParallelsProvider) KeepInputArtifact() bool { + return false +} + +func (p *ParallelsProvider) Process(ui packer.Ui, artifact packer.Artifact, dir string) (vagrantfile string, metadata map[string]interface{}, err error) { + // Create the metadata + metadata = map[string]interface{}{"provider": "parallels"} + var configPath string + + // Copy all of the original contents into the temporary directory + for _, path := range artifact.Files() { + // If the file isn't critical to the function of the + // virtual machine, we get rid of it. + // It's done by the builder, but we need one more time + // because unregistering a vm creates config.pvs.backup again. + unnecessary := false + ext := filepath.Ext(path) + for _, unnecessaryExt := range UnnecessaryFileExtensions { + if unnecessaryExt == ext { + unnecessary = true + break + } + } + if unnecessary { + continue + } + + tmpPath := filepath.ToSlash(path) + pathRe := regexp.MustCompile(`^(.+?)([^/]+\.pvm/.+?)$`) + matches := pathRe.FindStringSubmatch(tmpPath) + var pvmPath string + if matches != nil { + pvmPath = filepath.FromSlash(matches[2]) + } else { + continue // Just copy a pvm + } + dstPath := filepath.Join(dir, pvmPath) + + ui.Message(fmt.Sprintf("Copying: %s", path)) + if err = CopyContents(dstPath, path); err != nil { + return + } + if strings.HasSuffix(dstPath, "/config.pvs") { + configPath = dstPath + } + } + + // Create the Vagrantfile from the template + var baseMacAddress string + baseMacAddress, err = findBaseMacAddress(configPath) + if err != nil { + ui.Message(fmt.Sprintf("Problem determining Vagarant Box MAC address: %s", err)) + } + + vagrantfile = fmt.Sprintf(parallelsVagrantfile, baseMacAddress) + + return +} + +func findBaseMacAddress(path string) (string, error) { + xpath := "/ParallelsVirtualMachine/Hardware/NetworkAdapter[@id='0']/MAC" + file, err := os.Open(path) + if err != nil { + return "", err + } + xpathComp := xmlpath.MustCompile(xpath) + root, err := xmlpath.Parse(file) + if err != nil { + return "", err + } + value, _ := xpathComp.String(root) + return value, nil +} + +var parallelsVagrantfile = ` +Vagrant.configure("2") do |config| + config.vm.base_mac = "%s" +end +` diff --git a/post-processor/vagrant/parallels_test.go b/post-processor/vagrant/parallels_test.go new file mode 100644 index 000000000..093680f82 --- /dev/null +++ b/post-processor/vagrant/parallels_test.go @@ -0,0 +1,9 @@ +package vagrant + +import ( + "testing" +) + +func TestParallelsProvider_impl(t *testing.T) { + var _ Provider = new(ParallelsProvider) +} diff --git a/post-processor/vagrant/post-processor.go b/post-processor/vagrant/post-processor.go index e0076febf..1e094d68a 100644 --- a/post-processor/vagrant/post-processor.go +++ b/post-processor/vagrant/post-processor.go @@ -16,11 +16,12 @@ import ( ) var builtins = map[string]string{ - "mitchellh.amazonebs": "aws", - "mitchellh.amazon.instance": "aws", - "mitchellh.virtualbox": "virtualbox", - "mitchellh.vmware": "vmware", - "pearkes.digitalocean": "digitalocean", + "mitchellh.amazonebs": "aws", + "mitchellh.amazon.instance": "aws", + "mitchellh.virtualbox": "virtualbox", + "mitchellh.vmware": "vmware", + "pearkes.digitalocean": "digitalocean", + "rickard-von-essen.parallels": "parallels", } type Config struct { @@ -63,6 +64,7 @@ func (p *PostProcessor) Configure(raws ...interface{}) error { } func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) { + name, ok := builtins[artifact.BuilderId()] if !ok { return nil, false, fmt.Errorf( @@ -216,6 +218,8 @@ func providerForName(name string) Provider { return new(VBoxProvider) case "vmware": return new(VMwareProvider) + case "parallels": + return new(ParallelsProvider) default: return nil } diff --git a/website/source/docs/post-processors/vagrant.html.markdown b/website/source/docs/post-processors/vagrant.html.markdown index 204f235d0..72501d5ed 100644 --- a/website/source/docs/post-processors/vagrant.html.markdown +++ b/website/source/docs/post-processors/vagrant.html.markdown @@ -30,6 +30,7 @@ providers. * DigitalOcean * VirtualBox * VMware +* Parallels
    Support for additional providers is planned. If the From d08ee4adfd4942525b7dfd915636513deab5a8b3 Mon Sep 17 00:00:00 2001 From: Rickard von Essen Date: Wed, 30 Apr 2014 08:03:52 +0200 Subject: [PATCH 4/4] Added support for Parallels Desktop for Mac [GH-233] in the vagrant post-processor. Fixes https://github.com/rickard-von-essen/packer-parallels/issues/3 --- post-processor/vagrant/parallels.go | 42 +++---------------- post-processor/vagrant/util.go | 8 ++++ .../post-processors/vagrant.html.markdown | 2 +- 3 files changed, 14 insertions(+), 38 deletions(-) diff --git a/post-processor/vagrant/parallels.go b/post-processor/vagrant/parallels.go index 0e501c3ce..f5c291490 100644 --- a/post-processor/vagrant/parallels.go +++ b/post-processor/vagrant/parallels.go @@ -2,18 +2,15 @@ package vagrant import ( "fmt" - "os" "path/filepath" "regexp" - "strings" - "github.com/going/toolkit/xmlpath" "github.com/mitchellh/packer/packer" ) -// These are the extensions of files that are unnecessary for the function +// These are the extensions of files and directories that are unnecessary for the function // of a Parallels virtual machine. -var UnnecessaryFileExtensions = []string{".log", ".backup", ".Backup"} +var UnnecessaryFilesPatterns = []string{"\\.log$", "\\.backup$", "\\.Backup$", "\\.app/"} type ParallelsProvider struct{} @@ -24,18 +21,14 @@ func (p *ParallelsProvider) KeepInputArtifact() bool { func (p *ParallelsProvider) Process(ui packer.Ui, artifact packer.Artifact, dir string) (vagrantfile string, metadata map[string]interface{}, err error) { // Create the metadata metadata = map[string]interface{}{"provider": "parallels"} - var configPath string // Copy all of the original contents into the temporary directory for _, path := range artifact.Files() { // If the file isn't critical to the function of the // virtual machine, we get rid of it. - // It's done by the builder, but we need one more time - // because unregistering a vm creates config.pvs.backup again. unnecessary := false - ext := filepath.Ext(path) - for _, unnecessaryExt := range UnnecessaryFileExtensions { - if unnecessaryExt == ext { + for _, unnecessaryPat := range UnnecessaryFilesPatterns { + if matched, _ := regexp.MatchString(unnecessaryPat, path); matched { unnecessary = true break } @@ -59,40 +52,15 @@ func (p *ParallelsProvider) Process(ui packer.Ui, artifact packer.Artifact, dir if err = CopyContents(dstPath, path); err != nil { return } - if strings.HasSuffix(dstPath, "/config.pvs") { - configPath = dstPath - } } // Create the Vagrantfile from the template - var baseMacAddress string - baseMacAddress, err = findBaseMacAddress(configPath) - if err != nil { - ui.Message(fmt.Sprintf("Problem determining Vagarant Box MAC address: %s", err)) - } - - vagrantfile = fmt.Sprintf(parallelsVagrantfile, baseMacAddress) + vagrantfile = fmt.Sprintf(parallelsVagrantfile) return } -func findBaseMacAddress(path string) (string, error) { - xpath := "/ParallelsVirtualMachine/Hardware/NetworkAdapter[@id='0']/MAC" - file, err := os.Open(path) - if err != nil { - return "", err - } - xpathComp := xmlpath.MustCompile(xpath) - root, err := xmlpath.Parse(file) - if err != nil { - return "", err - } - value, _ := xpathComp.String(root) - return value, nil -} - var parallelsVagrantfile = ` Vagrant.configure("2") do |config| - config.vm.base_mac = "%s" end ` diff --git a/post-processor/vagrant/util.go b/post-processor/vagrant/util.go index 3cce14a82..db695289a 100644 --- a/post-processor/vagrant/util.go +++ b/post-processor/vagrant/util.go @@ -21,6 +21,14 @@ func CopyContents(dst, src string) error { } defer srcF.Close() + dstDir, _ := filepath.Split(dst) + if dstDir != "" { + err := os.MkdirAll(dstDir, os.ModePerm) + if err != nil { + return err + } + } + dstF, err := os.Create(dst) if err != nil { return err diff --git a/website/source/docs/post-processors/vagrant.html.markdown b/website/source/docs/post-processors/vagrant.html.markdown index 72501d5ed..ed7f7708b 100644 --- a/website/source/docs/post-processors/vagrant.html.markdown +++ b/website/source/docs/post-processors/vagrant.html.markdown @@ -97,7 +97,7 @@ In the example above, the compression level will be set to 1 except for VMware, where it will be set to 0. The available provider names are: `aws`, `digitalocean`, `virtualbox`, -and `vmware`. +`vmware`, and `parallels`. ## Input Artifacts