Added support for Parallels Desktop for Mac [GH-223]

Added builder plugins: builder-parallels-iso and builder-parallels-pvm.
This commit is contained in:
Rickard von Essen 2014-04-06 19:21:22 +02:00
parent 2010bf184f
commit cdaa9d5a8e
58 changed files with 4108 additions and 0 deletions

View File

@ -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)
}

View File

@ -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()))
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -0,0 +1,9 @@
package common
import (
"testing"
)
func TestParallels9Driver_impl(t *testing.T) {
var _ Driver = new(Parallels9Driver)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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")
}
}

View File

@ -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
}

View File

@ -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")
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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-----
`

View File

@ -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) {}

View File

@ -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")
}
}

View File

@ -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) {}

View File

@ -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)
}
}
}

View File

@ -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")
}
}

View File

@ -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) {}

View File

@ -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) {
}

View File

@ -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)
}
}

View File

@ -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))
}
}
}

View File

@ -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:
// <nothing>
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) {}

View File

@ -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")
}
}

View File

@ -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
}

View File

@ -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) {}

View File

@ -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) {}

View File

@ -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")
}
}

View File

@ -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"
)

View File

@ -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,
},
&parallelscommon.StepOutputDir{
Force: b.config.PackerForce,
Path: b.config.OutputDir,
},
&common.StepCreateFloppy{
Files: b.config.FloppyFiles,
},
new(stepHTTPServer),
new(stepCreateVM),
new(stepCreateDisk),
new(stepAttachISO),
&parallelscommon.StepAttachParallelsTools{
ParallelsToolsHostPath: b.config.ParallelsToolsHostPath,
ParallelsToolsMode: b.config.ParallelsToolsMode,
},
new(parallelscommon.StepAttachFloppy),
&parallelscommon.StepPrlctl{
Commands: b.config.Prlctl,
Tpl: b.config.tpl,
},
&parallelscommon.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,
},
&parallelscommon.StepUploadVersion{
Path: b.config.PrlctlVersionFile,
},
&parallelscommon.StepUploadParallelsTools{
ParallelsToolsGuestPath: b.config.ParallelsToolsGuestPath,
ParallelsToolsHostPath: b.config.ParallelsToolsHostPath,
ParallelsToolsMode: b.config.ParallelsToolsMode,
Tpl: b.config.tpl,
},
new(common.StepProvision),
&parallelscommon.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()
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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...")
}

View File

@ -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")
}
}

View File

@ -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) {}

View File

@ -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) {}

View File

@ -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))
}
}

View File

@ -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()
}
}

View File

@ -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:
// <nothing>
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["<bs>"] = []string{"0e", "8e"}
special["<del>"] = []string{"53", "d3"}
special["<enter>"] = []string{"1c", "9c"}
special["<esc>"] = []string{"01", "81"}
special["<f1>"] = []string{"3b", "bb"}
special["<f2>"] = []string{"3c", "bc"}
special["<f3>"] = []string{"3d", "bd"}
special["<f4>"] = []string{"3e", "be"}
special["<f5>"] = []string{"3f", "bf"}
special["<f6>"] = []string{"40", "c0"}
special["<f7>"] = []string{"41", "c1"}
special["<f8>"] = []string{"42", "c2"}
special["<f9>"] = []string{"43", "c3"}
special["<f10>"] = []string{"44", "c4"}
special["<return>"] = []string{"1c", "9c"}
special["<tab>"] = []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, "<wait>") {
log.Printf("Special code <wait> found, will sleep 1 second at this point.")
scancode = []string{"wait"}
message = message[len("<wait>"):]
}
if strings.HasPrefix(message, "<wait5>") {
log.Printf("Special code <wait5> found, will sleep 5 seconds at this point.")
scancode = []string{"wait5"}
message = message[len("<wait5>"):]
}
if strings.HasPrefix(message, "<wait10>") {
log.Printf("Special code <wait10> found, will sleep 10 seconds at this point.")
scancode = []string{"wait10"}
message = message[len("<wait10>"):]
}
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
}

View File

@ -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{
&parallelscommon.StepOutputDir{
Force: b.config.PackerForce,
Path: b.config.OutputDir,
},
&common.StepCreateFloppy{
Files: b.config.FloppyFiles,
},
&StepImport{
Name: b.config.VMName,
SourcePath: b.config.SourcePath,
},
&parallelscommon.StepAttachParallelsTools{
ParallelsToolsHostPath: b.config.ParallelsToolsHostPath,
ParallelsToolsMode: b.config.ParallelsToolsMode,
},
new(parallelscommon.StepAttachFloppy),
&parallelscommon.StepPrlctl{
Commands: b.config.Prlctl,
Tpl: b.config.tpl,
},
&parallelscommon.StepRun{
BootWait: b.config.BootWait,
Headless: b.config.Headless,
},
&common.StepConnectSSH{
SSHAddress: parallelscommon.SSHAddress,
SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig),
SSHWaitTimeout: b.config.SSHWaitTimeout,
},
&parallelscommon.StepUploadVersion{
Path: b.config.PrlctlVersionFile,
},
&parallelscommon.StepUploadParallelsTools{
ParallelsToolsGuestPath: b.config.ParallelsToolsGuestPath,
ParallelsToolsHostPath: b.config.ParallelsToolsHostPath,
ParallelsToolsMode: b.config.ParallelsToolsMode,
Tpl: b.config.tpl,
},
new(common.StepProvision),
&parallelscommon.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()
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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))
}
}

View File

@ -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
}

View File

@ -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"
},

View File

@ -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()
}

View File

@ -0,0 +1 @@
package main

View File

@ -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()
}

View File

@ -0,0 +1 @@
package main