diff --git a/builder/parallels/common/driver.go b/builder/parallels/common/driver.go index ab9693461..0a276a6a1 100644 --- a/builder/parallels/common/driver.go +++ b/builder/parallels/common/driver.go @@ -27,6 +27,9 @@ type Driver interface { // Prlctl executes the given Prlctl command Prlctl(...string) error + // Get the path to the Parallels Tools ISO for the given flavor. + ToolsIsoPath(string) (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. diff --git a/builder/parallels/common/driver_9.go b/builder/parallels/common/driver_9.go index 54e93a4bf..3bc608520 100644 --- a/builder/parallels/common/driver_9.go +++ b/builder/parallels/common/driver_9.go @@ -6,6 +6,7 @@ import ( "log" "os" "os/exec" + "path/filepath" "regexp" "strings" "time" @@ -73,16 +74,21 @@ func getConfigValueFromXpath(path, xpath string) (string, error) { // Finds an application bundle by identifier (for "darwin" platform only) func getAppPath(bundleId string) (string, error) { + var stdout bytes.Buffer + cmd := exec.Command("mdfind", "kMDItemCFBundleIdentifier ==", bundleId) - out, err := cmd.Output() - if err != nil { + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { return "", err } - if string(out) == "" { + + pathOutput := strings.TrimSpace(stdout.String()) + if pathOutput == "" { return "", fmt.Errorf( "Could not detect Parallels Desktop! Make sure it is properly installed.") } - return string(out), nil + + return pathOutput, nil } func (d *Parallels9Driver) IsRunning(name string) (bool, error) { @@ -251,3 +257,14 @@ func (d *Parallels9Driver) IpAddress(mac string) (string, error) { log.Printf("Found IP lease: %s for MAC address %s\n", ip, mac) return ip, nil } + +func (d *Parallels9Driver) ToolsIsoPath(k string) (string, error) { + appPath, err := getAppPath("com.parallels.desktop.console") + if err != nil { + return "", err + } + + toolsPath := filepath.Join(appPath, "Contents", "Resources", "Tools", "prl-tools-"+k+".iso") + log.Printf("Parallels Tools path: '%s'", toolsPath) + return toolsPath, nil +} diff --git a/builder/parallels/common/driver_mock.go b/builder/parallels/common/driver_mock.go index e54c5cd5a..acb3f9533 100644 --- a/builder/parallels/common/driver_mock.go +++ b/builder/parallels/common/driver_mock.go @@ -31,6 +31,11 @@ type DriverMock struct { SendKeyScanCodesCalls [][]string SendKeyScanCodesErrs []error + ToolsIsoPathCalled bool + ToolsIsoPathFlavor string + ToolsIsoPathResult string + ToolsIsoPathErr error + MacName string MacReturn string MacError error @@ -98,3 +103,9 @@ func (d *DriverMock) IpAddress(mac string) (string, error) { d.IpAddressMac = mac return d.IpAddressReturn, d.IpAddressError } + +func (d *DriverMock) ToolsIsoPath(flavor string) (string, error) { + d.ToolsIsoPathCalled = true + d.ToolsIsoPathFlavor = flavor + return d.ToolsIsoPathResult, d.ToolsIsoPathErr +} diff --git a/builder/parallels/common/step_attach_parallels_tools.go b/builder/parallels/common/step_attach_parallels_tools.go index 0d626c844..b80ddd87e 100644 --- a/builder/parallels/common/step_attach_parallels_tools.go +++ b/builder/parallels/common/step_attach_parallels_tools.go @@ -7,19 +7,19 @@ import ( "log" ) -// This step attaches the Parallels Tools as a inserted CD onto +// This step attaches the Parallels Tools as an inserted CD onto // the virtual machine. // // Uses: // driver Driver -// toolsPath string +// parallels_tools_path string // ui packer.Ui // vmName string // // Produces: +// attachedToolsIso boolean type StepAttachParallelsTools struct { - ParallelsToolsHostPath string - ParallelsToolsMode string + ParallelsToolsMode string } func (s *StepAttachParallelsTools) Run(state multistep.StateBag) multistep.StepAction { @@ -33,12 +33,15 @@ func (s *StepAttachParallelsTools) Run(state multistep.StateBag) multistep.StepA return multistep.ActionContinue } + // Get the Paralells Tools path on the host machine + parallelsToolsPath := state.Get("parallels_tools_path").(string) + // Attach the guest additions to the computer ui.Say("Attaching Parallels Tools ISO onto IDE controller...") command := []string{ "set", vmName, "--device-add", "cdrom", - "--image", s.ParallelsToolsHostPath, + "--image", parallelsToolsPath, } if err := driver.Prlctl(command...); err != nil { err := fmt.Errorf("Error attaching Parallels Tools: %s", err) @@ -59,6 +62,7 @@ func (s *StepAttachParallelsTools) Cleanup(state multistep.StateBag) { } driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) vmName := state.Get("vmName").(string) log.Println("Detaching Parallels Tools ISO...") @@ -71,5 +75,8 @@ func (s *StepAttachParallelsTools) Cleanup(state multistep.StateBag) { "set", vmName, "--device-del", cdDevice, } - driver.Prlctl(command...) + + if err := driver.Prlctl(command...); err != nil { + ui.Error(fmt.Sprintf("Error detaching Parallels Tools ISO: %s", err)) + } } diff --git a/builder/parallels/common/step_prepare_parallels_tools.go b/builder/parallels/common/step_prepare_parallels_tools.go new file mode 100644 index 000000000..752268fd7 --- /dev/null +++ b/builder/parallels/common/step_prepare_parallels_tools.go @@ -0,0 +1,47 @@ +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "os" +) + +// This step prepares parameters related to Parallels Tools. +// +// Uses: +// driver Driver +// +// Produces: +// parallels_tools_path string +type StepPrepareParallelsTools struct { + ParallelsToolsFlavor string + ParallelsToolsMode string +} + +func (s *StepPrepareParallelsTools) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + + if s.ParallelsToolsMode == ParallelsToolsModeDisable { + return multistep.ActionContinue + } + + path, err := driver.ToolsIsoPath(s.ParallelsToolsFlavor) + + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + if _, err := os.Stat(path); err != nil { + state.Put("error", fmt.Errorf( + "Couldn't find Parallels Tools for the '%s' flavor! Please, check the\n"+ + "value of 'parallels_tools_flavor'. Valid flavors are: 'win', 'lin',\n"+ + "'mac', 'os2' and 'other'", s.ParallelsToolsFlavor)) + return multistep.ActionHalt + } + + state.Put("parallels_tools_path", path) + return multistep.ActionContinue +} + +func (s *StepPrepareParallelsTools) Cleanup(multistep.StateBag) {} diff --git a/builder/parallels/common/step_prepare_parallels_tools_test.go b/builder/parallels/common/step_prepare_parallels_tools_test.go new file mode 100644 index 000000000..931ecb972 --- /dev/null +++ b/builder/parallels/common/step_prepare_parallels_tools_test.go @@ -0,0 +1,115 @@ +package common + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/mitchellh/multistep" +) + +func TestStepPrepareParallelsTools_impl(t *testing.T) { + var _ multistep.Step = new(StepPrepareParallelsTools) +} + +func TestStepPrepareParallelsTools(t *testing.T) { + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + tf.Close() + defer os.Remove(tf.Name()) + + state := testState(t) + step := &StepPrepareParallelsTools{ + ParallelsToolsMode: "", + ParallelsToolsFlavor: "foo", + } + + driver := state.Get("driver").(*DriverMock) + + // Mock results + driver.ToolsIsoPathResult = tf.Name() + + // Test the run + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + if _, ok := state.GetOk("error"); ok { + t.Fatal("should NOT have error") + } + + // Test the driver + if !driver.ToolsIsoPathCalled { + t.Fatal("tools iso path should be called") + } + if driver.ToolsIsoPathFlavor != "foo" { + t.Fatalf("bad: %#v", driver.ToolsIsoPathFlavor) + } + + // Test the resulting state + path, ok := state.GetOk("parallels_tools_path") + if !ok { + t.Fatal("should have parallels_tools_path") + } + if path != tf.Name() { + t.Fatalf("bad: %#v", path) + } +} + +func TestStepPrepareParallelsTools_disabled(t *testing.T) { + state := testState(t) + step := &StepPrepareParallelsTools{ + ParallelsToolsFlavor: "foo", + ParallelsToolsMode: ParallelsToolsModeDisable, + } + + driver := state.Get("driver").(*DriverMock) + + // Test the run + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + if _, ok := state.GetOk("error"); ok { + t.Fatal("should NOT have error") + } + + // Test the driver + if driver.ToolsIsoPathCalled { + t.Fatal("tools iso path should NOT be called") + } +} + +func TestStepPrepareParallelsTools_nonExist(t *testing.T) { + state := testState(t) + step := &StepPrepareParallelsTools{ + ParallelsToolsFlavor: "foo", + ParallelsToolsMode: "", + } + + driver := state.Get("driver").(*DriverMock) + + // Mock results + driver.ToolsIsoPathResult = "foo" + + // 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") + } + + // Test the driver + if !driver.ToolsIsoPathCalled { + t.Fatal("tools iso path should be called") + } + if driver.ToolsIsoPathFlavor != "foo" { + t.Fatalf("bad: %#v", driver.ToolsIsoPathFlavor) + } + + // Test the resulting state + if _, ok := state.GetOk("parallels_tools_path"); ok { + t.Fatal("should NOT have parallels_tools_path") + } +} diff --git a/builder/parallels/common/step_upload_parallels_tools.go b/builder/parallels/common/step_upload_parallels_tools.go index 374a8163e..9c421a22b 100644 --- a/builder/parallels/common/step_upload_parallels_tools.go +++ b/builder/parallels/common/step_upload_parallels_tools.go @@ -8,13 +8,21 @@ import ( "os" ) +// This step uploads the Parallels Tools ISO to the virtual machine. +// +// Uses: +// communicator packer.Communicator +// parallels_tools_path string +// ui packer.Ui +// +// Produces: type toolsPathTemplate struct { - Version string + Flavor string } // This step uploads the guest additions ISO to the VM. type StepUploadParallelsTools struct { - ParallelsToolsHostPath string + ParallelsToolsFlavor string ParallelsToolsGuestPath string ParallelsToolsMode string Tpl *packer.ConfigTemplate @@ -22,7 +30,6 @@ type StepUploadParallelsTools struct { 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. @@ -31,20 +38,18 @@ func (s *StepUploadParallelsTools) Run(state multistep.StateBag) multistep.StepA 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 - } + // Get the Paralells Tools path on the host machine + parallelsToolsPath := state.Get("parallels_tools_path").(string) - f, err := os.Open(s.ParallelsToolsHostPath) + f, err := os.Open(parallelsToolsPath) if err != nil { state.Put("error", fmt.Errorf("Error opening Parallels Tools ISO: %s", err)) return multistep.ActionHalt } + defer f.Close() tplData := &toolsPathTemplate{ - Version: version, + Flavor: s.ParallelsToolsFlavor, } s.ParallelsToolsGuestPath, err = s.Tpl.Process(s.ParallelsToolsGuestPath, tplData) @@ -55,9 +60,12 @@ func (s *StepUploadParallelsTools) Run(state multistep.StateBag) multistep.StepA return multistep.ActionHalt } - ui.Say("Uploading Parallels Tools ISO...") + ui.Say(fmt.Sprintf("Uploading Parallels Tools for '%s' to path: '%s'", + s.ParallelsToolsFlavor, s.ParallelsToolsGuestPath)) if err := comm.Upload(s.ParallelsToolsGuestPath, f); err != nil { - state.Put("error", fmt.Errorf("Error uploading Parallels Tools: %s", err)) + err := fmt.Errorf("Error uploading Parallels Tools: %s", err) + state.Put("error", err) + ui.Error(err.Error()) return multistep.ActionHalt } diff --git a/builder/parallels/common/tools_config.go b/builder/parallels/common/tools_config.go new file mode 100644 index 000000000..19993fb77 --- /dev/null +++ b/builder/parallels/common/tools_config.go @@ -0,0 +1,78 @@ +package common + +import ( + "errors" + "fmt" + "github.com/mitchellh/packer/packer" + "text/template" +) + +// 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" +) + +type ToolsConfig struct { + ParallelsToolsFlavor string `mapstructure:"parallels_tools_flavor"` + ParallelsToolsGuestPath string `mapstructure:"parallels_tools_guest_path"` + ParallelsToolsMode string `mapstructure:"parallels_tools_mode"` +} + +func (c *ToolsConfig) Prepare(t *packer.ConfigTemplate) []error { + if c.ParallelsToolsMode == "" { + c.ParallelsToolsMode = ParallelsToolsModeUpload + } + + if c.ParallelsToolsGuestPath == "" { + c.ParallelsToolsGuestPath = "prl-tools-{{.Flavor}}.iso" + } + + templates := map[string]*string{ + "parallels_tools_flavor": &c.ParallelsToolsFlavor, + "parallels_tools_mode": &c.ParallelsToolsMode, + } + + var err error + errs := make([]error, 0) + for n, ptr := range templates { + *ptr, err = t.Process(*ptr, nil) + if err != nil { + errs = append(errs, fmt.Errorf("Error processing %s: %s", n, err)) + } + } + + if _, err := template.New("path").Parse(c.ParallelsToolsGuestPath); err != nil { + errs = append(errs, fmt.Errorf("parallels_tools_guest_path invalid: %s", err)) + } + + validMode := false + validModes := []string{ + ParallelsToolsModeDisable, + ParallelsToolsModeAttach, + ParallelsToolsModeUpload, + } + + for _, mode := range validModes { + if c.ParallelsToolsMode == mode { + validMode = true + break + } + } + + if !validMode { + errs = append(errs, + fmt.Errorf("parallels_tools_mode is invalid. Must be one of: %v", + validModes)) + } + + if c.ParallelsToolsFlavor == "" { + if c.ParallelsToolsMode != ParallelsToolsModeDisable { + errs = append(errs, errors.New("parallels_tools_flavor must be specified.")) + } + } + + return errs +} diff --git a/builder/parallels/common/tools_config_test.go b/builder/parallels/common/tools_config_test.go new file mode 100644 index 000000000..5f5a8a35d --- /dev/null +++ b/builder/parallels/common/tools_config_test.go @@ -0,0 +1,123 @@ +package common + +import ( + "testing" +) + +func testToolsConfig() *ToolsConfig { + return &ToolsConfig{ + ParallelsToolsFlavor: "foo", + ParallelsToolsGuestPath: "foo", + ParallelsToolsMode: "attach", + } +} + +func TestToolsConfigPrepare(t *testing.T) { + c := testToolsConfig() + errs := c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("bad err: %#v", errs) + } +} + +func TestToolsConfigPrepare_ParallelsToolsMode(t *testing.T) { + var c *ToolsConfig + var errs []error + + // Test default mode + c = testToolsConfig() + c.ParallelsToolsMode = "" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("should not have error: %#v", errs) + } + if c.ParallelsToolsMode != ParallelsToolsModeUpload { + t.Errorf("bad parallels tools mode: %s", c.ParallelsToolsMode) + } + + // Test another mode + c = testToolsConfig() + c.ParallelsToolsMode = "attach" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("should not have error: %#v", errs) + } + if c.ParallelsToolsMode != ParallelsToolsModeAttach { + t.Fatalf("bad mode: %s", c.ParallelsToolsMode) + } + + // Test invalid mode + c = testToolsConfig() + c.ParallelsToolsMode = "invalid_mode" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) == 0 { + t.Fatal("should have error") + } +} + +func TestToolsConfigPrepare_ParallelsToolsGuestPath(t *testing.T) { + var c *ToolsConfig + var errs []error + + // Test default path + c = testToolsConfig() + c.ParallelsToolsGuestPath = "" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("should not have error: %#v", errs) + } + if c.ParallelsToolsGuestPath == "" { + t.Fatal("should not be empty") + } + + // Test with a bad value + c = testToolsConfig() + c.ParallelsToolsGuestPath = "{{{nope}" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) == 0 { + t.Fatal("should have error") + } + + // Test with a good one + c = testToolsConfig() + c.ParallelsToolsGuestPath = "foo" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("should not have error: %s", errs) + } + + if c.ParallelsToolsGuestPath != "foo" { + t.Fatalf("bad guest path: %s", c.ParallelsToolsGuestPath) + } +} + +func TestToolsConfigPrepare_ParallelsToolsFlavor(t *testing.T) { + var c *ToolsConfig + var errs []error + + // Test with a default value + c = testToolsConfig() + c.ParallelsToolsFlavor = "" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) == 0 { + t.Fatal("should have error") + } + + // Test with an bad value + c = testToolsConfig() + c.ParallelsToolsMode = "attach" + c.ParallelsToolsFlavor = "" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) == 0 { + t.Fatal("should have error") + } + + // Test with a good one + c = testToolsConfig() + c.ParallelsToolsMode = "disable" + c.ParallelsToolsFlavor = "" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("should not have error: %s", errs) + } +} diff --git a/builder/parallels/common/tools_modes.go b/builder/parallels/common/tools_modes.go deleted file mode 100644 index 3523fbca7..000000000 --- a/builder/parallels/common/tools_modes.go +++ /dev/null @@ -1,9 +0,0 @@ -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 index 0fa38d5fb..b177bb27d 100644 --- a/builder/parallels/iso/builder.go +++ b/builder/parallels/iso/builder.go @@ -22,32 +22,31 @@ type config struct { common.PackerConfig `mapstructure:",squash"` parallelscommon.FloppyConfig `mapstructure:",squash"` parallelscommon.OutputConfig `mapstructure:",squash"` + parallelscommon.PrlctlConfig `mapstructure:",squash"` + parallelscommon.PrlctlVersionConfig `mapstructure:",squash"` parallelscommon.RunConfig `mapstructure:",squash"` parallelscommon.ShutdownConfig `mapstructure:",squash"` parallelscommon.SSHConfig `mapstructure:",squash"` - parallelscommon.PrlctlConfig `mapstructure:",squash"` - parallelscommon.PrlctlVersionConfig `mapstructure:",squash"` + parallelscommon.ToolsConfig `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"` - GuestOSDistribution string `mapstructure:"guest_os_distribution"` - HardDriveInterface string `mapstructure:"hard_drive_interface"` - HostInterfaces []string `mapstructure:"host_interfaces"` - 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"` + BootCommand []string `mapstructure:"boot_command"` + DiskSize uint `mapstructure:"disk_size"` + GuestOSType string `mapstructure:"guest_os_type"` + HardDriveInterface string `mapstructure:"hard_drive_interface"` + HostInterfaces []string `mapstructure:"host_interfaces"` + 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"` // Deprecated parameters - GuestOSType string `mapstructure:"guest_os_type"` + GuestOSDistribution string `mapstructure:"guest_os_distribution"` + ParallelsToolsHostPath string `mapstructure:"parallels_tools_host_path"` tpl *packer.ConfigTemplate } @@ -71,34 +70,33 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { 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)...) + 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.ToolsConfig.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.GuestOSDistribution == "" { - b.config.GuestOSDistribution = "other" + if b.config.GuestOSType == "" { + b.config.GuestOSType = "other" + } + + if b.config.GuestOSDistribution != "" { + // Compatibility with older templates: + // Use value of 'guest_os_distribution' if it is defined. + b.config.GuestOSType = b.config.GuestOSDistribution + warnings = append(warnings, + "A 'guest_os_distribution' has been completely replaced with 'guest_os_type'\n"+ + "It is recommended to remove it and assign the previous value to 'guest_os_type'.\n"+ + "Run it to see all available values: `prlctl create x -d list` ") } if b.config.HTTPPortMin == 0 { @@ -120,16 +118,13 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { // 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_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, + "guest_os_type": &b.config.GuestOSType, + "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 { @@ -150,17 +145,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { } } - 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, @@ -217,25 +201,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { } } - 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, @@ -249,6 +214,12 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { "will forcibly halt the virtual machine, which may result in data loss.") } + if b.config.ParallelsToolsHostPath != "" { + warnings = append(warnings, + "A 'parallels_tools_host_path' has been deprecated and not in use anymore\n"+ + "You can remove it from your Packer template.") + } + if errs != nil && len(errs.Errors) > 0 { return warnings, errs } @@ -264,6 +235,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe } steps := []multistep.Step{ + ¶llelscommon.StepPrepareParallelsTools{ + ParallelsToolsFlavor: b.config.ParallelsToolsFlavor, + ParallelsToolsMode: b.config.ParallelsToolsMode, + }, &common.StepDownload{ Checksum: b.config.ISOChecksum, ChecksumType: b.config.ISOChecksumType, @@ -283,8 +258,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe new(stepCreateDisk), new(stepAttachISO), ¶llelscommon.StepAttachParallelsTools{ - ParallelsToolsHostPath: b.config.ParallelsToolsHostPath, - ParallelsToolsMode: b.config.ParallelsToolsMode, + ParallelsToolsMode: b.config.ParallelsToolsMode, }, new(parallelscommon.StepAttachFloppy), ¶llelscommon.StepPrlctl{ @@ -310,8 +284,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe Path: b.config.PrlctlVersionFile, }, ¶llelscommon.StepUploadParallelsTools{ + ParallelsToolsFlavor: b.config.ParallelsToolsFlavor, ParallelsToolsGuestPath: b.config.ParallelsToolsGuestPath, - ParallelsToolsHostPath: b.config.ParallelsToolsHostPath, ParallelsToolsMode: b.config.ParallelsToolsMode, Tpl: b.config.tpl, }, diff --git a/builder/parallels/iso/builder_test.go b/builder/parallels/iso/builder_test.go index f20f544d6..9c7f56f6b 100644 --- a/builder/parallels/iso/builder_test.go +++ b/builder/parallels/iso/builder_test.go @@ -1,7 +1,6 @@ package iso import ( - "github.com/mitchellh/packer/builder/parallels/common" "github.com/mitchellh/packer/packer" "reflect" "testing" @@ -9,11 +8,12 @@ import ( 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", + "iso_checksum": "foo", + "iso_checksum_type": "md5", + "iso_url": "http://www.google.com/", + "shutdown_command": "yes", + "ssh_username": "foo", + "parallels_tools_flavor": "lin", packer.BuildNameConfigKey: "foo", } @@ -38,12 +38,8 @@ func TestBuilderPrepare_Defaults(t *testing.T) { 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.GuestOSDistribution != "other" { - t.Errorf("bad guest OS distribution: %s", b.config.GuestOSDistribution) + if b.config.GuestOSType != "other" { + t.Errorf("bad guest OS type: %s", b.config.GuestOSType) } if b.config.VMName != "packer-foo" { @@ -83,104 +79,22 @@ func TestBuilderPrepare_DiskSize(t *testing.T) { } } -func TestBuilderPrepare_ParallelsToolsMode(t *testing.T) { +func TestBuilderPrepare_GuestOSType(t *testing.T) { var b Builder config := testConfig() + delete(config, "guest_os_distribution") - // test default mode - delete(config, "parallels_tools_mode") + // Test deprecated parameter + config["guest_os_distribution"] = "bolgenos" 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 len(warns) == 0 { + t.Fatalf("should have warning") } 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) + if b.config.GuestOSType != "bolgenos" { + t.Fatalf("bad: %s", b.config.GuestOSType) } } @@ -434,3 +348,19 @@ func TestBuilderPrepare_ISOUrl(t *testing.T) { t.Fatalf("bad: %#v", b.config.ISOUrls) } } + +func TestBuilderPrepare_ParallelsToolsHostPath(t *testing.T) { + var b Builder + config := testConfig() + delete(config, "parallels_tools_host_path") + + // Test that it is deprecated + config["parallels_tools_host_path"] = "/path/to/iso" + warns, err := b.Prepare(config) + if len(warns) == 0 { + t.Fatalf("should have warning") + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} diff --git a/builder/parallels/iso/step_create_vm.go b/builder/parallels/iso/step_create_vm.go index 1b2468cfe..9da4b678c 100644 --- a/builder/parallels/iso/step_create_vm.go +++ b/builder/parallels/iso/step_create_vm.go @@ -28,7 +28,7 @@ func (s *stepCreateVM) Run(state multistep.StateBag) multistep.StepAction { commands := make([][]string, 8) commands[0] = []string{ "create", name, - "--distribution", config.GuestOSDistribution, + "--distribution", config.GuestOSType, "--dst", path, "--vmtype", "vm", } diff --git a/builder/parallels/pvm/builder.go b/builder/parallels/pvm/builder.go index aabdc8d30..83ac73b12 100644 --- a/builder/parallels/pvm/builder.go +++ b/builder/parallels/pvm/builder.go @@ -47,6 +47,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe // Build the steps. steps := []multistep.Step{ + ¶llelscommon.StepPrepareParallelsTools{ + ParallelsToolsMode: b.config.ParallelsToolsMode, + ParallelsToolsFlavor: b.config.ParallelsToolsFlavor, + }, ¶llelscommon.StepOutputDir{ Force: b.config.PackerForce, Path: b.config.OutputDir, @@ -59,8 +63,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe SourcePath: b.config.SourcePath, }, ¶llelscommon.StepAttachParallelsTools{ - ParallelsToolsHostPath: b.config.ParallelsToolsHostPath, - ParallelsToolsMode: b.config.ParallelsToolsMode, + ParallelsToolsMode: b.config.ParallelsToolsMode, }, new(parallelscommon.StepAttachFloppy), ¶llelscommon.StepPrlctl{ @@ -86,8 +89,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe Path: b.config.PrlctlVersionFile, }, ¶llelscommon.StepUploadParallelsTools{ + ParallelsToolsFlavor: b.config.ParallelsToolsFlavor, ParallelsToolsGuestPath: b.config.ParallelsToolsGuestPath, - ParallelsToolsHostPath: b.config.ParallelsToolsHostPath, ParallelsToolsMode: b.config.ParallelsToolsMode, Tpl: b.config.tpl, }, diff --git a/builder/parallels/pvm/config.go b/builder/parallels/pvm/config.go index 84bc7fbdd..a3c20bb5d 100644 --- a/builder/parallels/pvm/config.go +++ b/builder/parallels/pvm/config.go @@ -13,18 +13,16 @@ type Config struct { common.PackerConfig `mapstructure:",squash"` parallelscommon.FloppyConfig `mapstructure:",squash"` parallelscommon.OutputConfig `mapstructure:",squash"` + parallelscommon.PrlctlConfig `mapstructure:",squash"` + parallelscommon.PrlctlVersionConfig `mapstructure:",squash"` parallelscommon.RunConfig `mapstructure:",squash"` parallelscommon.SSHConfig `mapstructure:",squash"` parallelscommon.ShutdownConfig `mapstructure:",squash"` - parallelscommon.PrlctlConfig `mapstructure:",squash"` - parallelscommon.PrlctlVersionConfig `mapstructure:",squash"` + parallelscommon.ToolsConfig `mapstructure:",squash"` - BootCommand []string `mapstructure:"boot_command"` - 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"` + BootCommand []string `mapstructure:"boot_command"` + SourcePath string `mapstructure:"source_path"` + VMName string `mapstructure:"vm_name"` tpl *packer.ConfigTemplate } @@ -42,19 +40,6 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { } 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) } @@ -63,18 +48,16 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { 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.PrlctlConfig.Prepare(c.tpl)...) + errs = packer.MultiErrorAppend(errs, c.PrlctlVersionConfig.Prepare(c.tpl)...) 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)...) + errs = packer.MultiErrorAppend(errs, c.ToolsConfig.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, + "source_path": &c.SourcePath, + "vm_name": &c.VMName, } for n, ptr := range templates { @@ -93,25 +76,6 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { } } - 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 { diff --git a/builder/parallels/pvm/config_test.go b/builder/parallels/pvm/config_test.go index 1fb15849b..c4270c366 100644 --- a/builder/parallels/pvm/config_test.go +++ b/builder/parallels/pvm/config_test.go @@ -8,8 +8,9 @@ import ( func testConfig(t *testing.T) map[string]interface{} { return map[string]interface{}{ - "ssh_username": "foo", - "shutdown_command": "foo", + "ssh_username": "foo", + "shutdown_command": "foo", + "parallels_tools_flavor": "lin", } } diff --git a/website/source/docs/builders/parallels-iso.html.markdown b/website/source/docs/builders/parallels-iso.html.markdown index a79c96813..9f8cb4400 100644 --- a/website/source/docs/builders/parallels-iso.html.markdown +++ b/website/source/docs/builders/parallels-iso.html.markdown @@ -26,10 +26,11 @@ Ubuntu to self-install. Still, the example serves to show the basic configuratio
{ "type": "parallels-iso", - "guest_os_type": "Ubuntu_64", + "guest_os_type": "ubuntu", "iso_url": "http://releases.ubuntu.com/12.04/ubuntu-12.04.3-server-amd64.iso", "iso_checksum": "2cbe868812a871242cdcdd8f2fd6feb9", "iso_checksum_type": "md5", + "parallels_tools_flavor": "lin" "ssh_username": "packer", "ssh_password": "packer", "ssh_wait_timeout": "30s", @@ -68,6 +69,10 @@ each category, the available options are alphabetized and described. * `ssh_username` (string) - The username to use to SSH into the machine once the OS is installed. +* `parallels_tools_flavor` (string) - The flavor of the Parallels Tools ISO to + install into the VM. Valid values are "win", "lin", "mac", "os2" and "other". + This can be ommited only if `parallels_tools_mode` is "disable". + ### Optional: * `boot_command` (array of strings) - This is an array of commands to type @@ -96,17 +101,10 @@ each category, the available options are alphabetized and described. characters (*, ?, and []) are allowed. Directory names are also allowed, which will add all the files found in the directory to the floppy. -* `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. - * `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 + run `prlctl create x --distribution list`. Setting the correct value hints to Parallels Desktop how to optimize the virtual hardware to work best with that operating system. @@ -148,18 +146,14 @@ each category, the available options are alphabetized and described. By default this is "output-BUILDNAME" where "BUILDNAME" is the name of the build. -* `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_guest_path` (string) - The path in the VM to upload Parallels + Tools. This only takes effect if `parallels_tools_mode` is not "disable". + This is a [configuration template](/docs/templates/configuration-templates.html) + that has a single valid variable: `Flavor`, which will be the value of + `parallels_tools_flavor`. By default the upload path is set to + `prl-tools-{{.Flavor}}.iso`. -* `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" - -* `parallels_tools_mode` (string) - The method by which Parallels tools are +* `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". @@ -260,20 +254,12 @@ an Ubuntu 12.04 installer: ]-## 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. +[prlctl](http://download.parallels.com/desktop/v9/ga/docs/en_US/Parallels%20Command%20Line%20Reference%20Guide.pdf) +is the command-line interface to Parallels Desktop. It can be used to configure +the virtual machine, 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 diff --git a/website/source/docs/builders/parallels-pvm.html.markdown b/website/source/docs/builders/parallels-pvm.html.markdown index 74a81e9d3..8b0624291 100644 --- a/website/source/docs/builders/parallels-pvm.html.markdown +++ b/website/source/docs/builders/parallels-pvm.html.markdown @@ -25,6 +25,7 @@ the settings here.
{ "type": "parallels-pvm", + "parallels_tools_flavor": "lin" "source_path": "source.pvm", "ssh_username": "packer", "ssh_password": "packer", @@ -51,6 +52,10 @@ each category, the available options are alphabetized and described. * `ssh_username` (string) - The username to use to SSH into the machine once the OS is installed. +* `parallels_tools_flavor` (string) - The flavor of the Parallels Tools ISO to + install into the VM. Valid values are "win", "lin", "mac", "os2" and "other". + This can be ommited only if `parallels_tools_mode` is "disable". + ### Optional: * `boot_command` (array of strings) - This is an array of commands to type @@ -80,18 +85,14 @@ each category, the available options are alphabetized and described. By default this is "output-BUILDNAME" where "BUILDNAME" is the name of the build. -* `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_guest_path` (string) - The path in the VM to upload Parallels + Tools. This only takes effect if `parallels_tools_mode` is not "disable". + This is a [configuration template](/docs/templates/configuration-templates.html) + that has a single valid variable: `Flavor`, which will be the value of + `parallels_tools_flavor`. By default the upload path is set to + `prl-tools-{{.Flavor}}.iso`. -* `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" - -* `parallels_tools_mode` (string) - The method by which Parallels tools are +* `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". @@ -179,9 +180,9 @@ The available variables are: ## 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. +[prlctl](http://download.parallels.com/desktop/v9/ga/docs/en_US/Parallels%20Command%20Line%20Reference%20Guide.pdf) +is the command-line interface to Parallels Desktop. It can be used to configure +the virtual machine, 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