github.com/emate/packer@v0.8.1-0.20150625195101-fe0fde195dc6/builder/parallels/common/driver_9.go (about)

     1  package common
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"log"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"regexp"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/going/toolkit/xmlpath"
    17  )
    18  
    19  type Parallels9Driver struct {
    20  	// This is the path to the "prlctl" application.
    21  	PrlctlPath string
    22  	// The path to the parallels_dhcp_leases file
    23  	dhcp_lease_file string
    24  }
    25  
    26  func (d *Parallels9Driver) Import(name, srcPath, dstDir string, reassignMac bool) error {
    27  
    28  	err := d.Prlctl("register", srcPath, "--preserve-uuid")
    29  	if err != nil {
    30  		return err
    31  	}
    32  
    33  	srcId, err := getVmId(srcPath)
    34  	if err != nil {
    35  		return err
    36  	}
    37  
    38  	srcMac := "auto"
    39  	if !reassignMac {
    40  		srcMac, err = getFirtsMacAddress(srcPath)
    41  		if err != nil {
    42  			return err
    43  		}
    44  	}
    45  
    46  	err = d.Prlctl("clone", srcId, "--name", name, "--dst", dstDir)
    47  	if err != nil {
    48  		return err
    49  	}
    50  
    51  	err = d.Prlctl("unregister", srcId)
    52  	if err != nil {
    53  		return err
    54  	}
    55  
    56  	err = d.Prlctl("set", name, "--device-set", "net0", "--mac", srcMac)
    57  	return nil
    58  }
    59  
    60  func getVmId(path string) (string, error) {
    61  	return getConfigValueFromXpath(path, "/ParallelsVirtualMachine/Identification/VmUuid")
    62  }
    63  
    64  func getFirtsMacAddress(path string) (string, error) {
    65  	return getConfigValueFromXpath(path, "/ParallelsVirtualMachine/Hardware/NetworkAdapter[@id='0']/MAC")
    66  }
    67  
    68  func getConfigValueFromXpath(path, xpath string) (string, error) {
    69  	file, err := os.Open(path + "/config.pvs")
    70  	if err != nil {
    71  		return "", err
    72  	}
    73  	xpathComp := xmlpath.MustCompile(xpath)
    74  	root, err := xmlpath.Parse(file)
    75  	if err != nil {
    76  		return "", err
    77  	}
    78  	value, _ := xpathComp.String(root)
    79  	return value, nil
    80  }
    81  
    82  // Finds an application bundle by identifier (for "darwin" platform only)
    83  func getAppPath(bundleId string) (string, error) {
    84  	var stdout bytes.Buffer
    85  
    86  	cmd := exec.Command("mdfind", "kMDItemCFBundleIdentifier ==", bundleId)
    87  	cmd.Stdout = &stdout
    88  	if err := cmd.Run(); err != nil {
    89  		return "", err
    90  	}
    91  
    92  	pathOutput := strings.TrimSpace(stdout.String())
    93  	if pathOutput == "" {
    94  		return "", fmt.Errorf(
    95  			"Could not detect Parallels Desktop! Make sure it is properly installed.")
    96  	}
    97  
    98  	return pathOutput, nil
    99  }
   100  
   101  func (d *Parallels9Driver) DeviceAddCdRom(name string, image string) (string, error) {
   102  	command := []string{
   103  		"set", name,
   104  		"--device-add", "cdrom",
   105  		"--image", image,
   106  	}
   107  
   108  	out, err := exec.Command(d.PrlctlPath, command...).Output()
   109  	if err != nil {
   110  		return "", err
   111  	}
   112  
   113  	deviceRe := regexp.MustCompile(`\s+(cdrom\d+)\s+`)
   114  	matches := deviceRe.FindStringSubmatch(string(out))
   115  	if matches == nil {
   116  		return "", fmt.Errorf(
   117  			"Could not determine cdrom device name in the output:\n%s", string(out))
   118  	}
   119  
   120  	device_name := matches[1]
   121  	return device_name, nil
   122  }
   123  
   124  func (d *Parallels9Driver) IsRunning(name string) (bool, error) {
   125  	var stdout bytes.Buffer
   126  
   127  	cmd := exec.Command(d.PrlctlPath, "list", name, "--no-header", "--output", "status")
   128  	cmd.Stdout = &stdout
   129  	if err := cmd.Run(); err != nil {
   130  		return false, err
   131  	}
   132  
   133  	log.Printf("Checking VM state: %s\n", strings.TrimSpace(stdout.String()))
   134  
   135  	for _, line := range strings.Split(stdout.String(), "\n") {
   136  		if line == "running" {
   137  			return true, nil
   138  		}
   139  
   140  		if line == "suspended" {
   141  			return true, nil
   142  		}
   143  		if line == "paused" {
   144  			return true, nil
   145  		}
   146  		if line == "stopping" {
   147  			return true, nil
   148  		}
   149  	}
   150  
   151  	return false, nil
   152  }
   153  
   154  func (d *Parallels9Driver) Stop(name string) error {
   155  	if err := d.Prlctl("stop", name); err != nil {
   156  		return err
   157  	}
   158  
   159  	// We sleep here for a little bit to let the session "unlock"
   160  	time.Sleep(2 * time.Second)
   161  
   162  	return nil
   163  }
   164  
   165  func (d *Parallels9Driver) Prlctl(args ...string) error {
   166  	var stdout, stderr bytes.Buffer
   167  
   168  	log.Printf("Executing prlctl: %#v", args)
   169  	cmd := exec.Command(d.PrlctlPath, args...)
   170  	cmd.Stdout = &stdout
   171  	cmd.Stderr = &stderr
   172  	err := cmd.Run()
   173  
   174  	stdoutString := strings.TrimSpace(stdout.String())
   175  	stderrString := strings.TrimSpace(stderr.String())
   176  
   177  	if _, ok := err.(*exec.ExitError); ok {
   178  		err = fmt.Errorf("prlctl error: %s", stderrString)
   179  	}
   180  
   181  	log.Printf("stdout: %s", stdoutString)
   182  	log.Printf("stderr: %s", stderrString)
   183  
   184  	return err
   185  }
   186  
   187  func (d *Parallels9Driver) Verify() error {
   188  	return nil
   189  }
   190  
   191  func (d *Parallels9Driver) Version() (string, error) {
   192  	out, err := exec.Command(d.PrlctlPath, "--version").Output()
   193  	if err != nil {
   194  		return "", err
   195  	}
   196  
   197  	versionRe := regexp.MustCompile(`prlctl version (\d+\.\d+.\d+)`)
   198  	matches := versionRe.FindStringSubmatch(string(out))
   199  	if matches == nil {
   200  		return "", fmt.Errorf(
   201  			"Could not find Parallels Desktop version in output:\n%s", string(out))
   202  	}
   203  
   204  	version := matches[1]
   205  	log.Printf("Parallels Desktop version: %s", version)
   206  	return version, nil
   207  }
   208  
   209  func (d *Parallels9Driver) SendKeyScanCodes(vmName string, codes ...string) error {
   210  	var stdout, stderr bytes.Buffer
   211  
   212  	if codes == nil || len(codes) == 0 {
   213  		log.Printf("No scan codes to send")
   214  		return nil
   215  	}
   216  
   217  	f, err := ioutil.TempFile("", "prltype")
   218  	if err != nil {
   219  		return err
   220  	}
   221  	defer os.Remove(f.Name())
   222  
   223  	script := []byte(Prltype)
   224  	_, err = f.Write(script)
   225  	if err != nil {
   226  		return err
   227  	}
   228  
   229  	args := prepend(vmName, codes)
   230  	args = prepend(f.Name(), args)
   231  	cmd := exec.Command("/usr/bin/python", args...)
   232  	cmd.Stdout = &stdout
   233  	cmd.Stderr = &stderr
   234  	err = cmd.Run()
   235  
   236  	stdoutString := strings.TrimSpace(stdout.String())
   237  	stderrString := strings.TrimSpace(stderr.String())
   238  
   239  	if _, ok := err.(*exec.ExitError); ok {
   240  		err = fmt.Errorf("prltype error: %s", stderrString)
   241  	}
   242  
   243  	log.Printf("stdout: %s", stdoutString)
   244  	log.Printf("stderr: %s", stderrString)
   245  
   246  	return err
   247  }
   248  
   249  func prepend(head string, tail []string) []string {
   250  	tmp := make([]string, len(tail)+1)
   251  	for i := 0; i < len(tail); i++ {
   252  		tmp[i+1] = tail[i]
   253  	}
   254  	tmp[0] = head
   255  	return tmp
   256  }
   257  
   258  func (d *Parallels9Driver) SetDefaultConfiguration(vmName string) error {
   259  	commands := make([][]string, 7)
   260  	commands[0] = []string{"set", vmName, "--cpus", "1"}
   261  	commands[1] = []string{"set", vmName, "--memsize", "512"}
   262  	commands[2] = []string{"set", vmName, "--startup-view", "same"}
   263  	commands[3] = []string{"set", vmName, "--on-shutdown", "close"}
   264  	commands[4] = []string{"set", vmName, "--on-window-close", "keep-running"}
   265  	commands[5] = []string{"set", vmName, "--auto-share-camera", "off"}
   266  	commands[6] = []string{"set", vmName, "--smart-guard", "off"}
   267  
   268  	for _, command := range commands {
   269  		err := d.Prlctl(command...)
   270  		if err != nil {
   271  			return err
   272  		}
   273  	}
   274  	return nil
   275  }
   276  
   277  func (d *Parallels9Driver) Mac(vmName string) (string, error) {
   278  	var stdout bytes.Buffer
   279  
   280  	cmd := exec.Command(d.PrlctlPath, "list", "-i", vmName)
   281  	cmd.Stdout = &stdout
   282  	if err := cmd.Run(); err != nil {
   283  		log.Printf("MAC address for NIC: nic0 on Virtual Machine: %s not found!\n", vmName)
   284  		return "", err
   285  	}
   286  
   287  	stdoutString := strings.TrimSpace(stdout.String())
   288  	re := regexp.MustCompile("net0.* mac=([0-9A-F]{12}) card=.*")
   289  	macMatch := re.FindAllStringSubmatch(stdoutString, 1)
   290  
   291  	if len(macMatch) != 1 {
   292  		return "", fmt.Errorf("MAC address for NIC: nic0 on Virtual Machine: %s not found!\n", vmName)
   293  	}
   294  
   295  	mac := macMatch[0][1]
   296  	log.Printf("Found MAC address for NIC: net0 - %s\n", mac)
   297  	return mac, nil
   298  }
   299  
   300  // Finds the IP address of a VM connected that uses DHCP by its MAC address
   301  //
   302  // Parses the file /Library/Preferences/Parallels/parallels_dhcp_leases
   303  // file contain a list of DHCP leases given by Parallels Desktop
   304  // Example line:
   305  // 10.211.55.181="1418921112,1800,001c42f593fb,ff42f593fb000100011c25b9ff001c42f593fb"
   306  // IP Address   ="Lease expiry, Lease time, MAC, MAC or DUID"
   307  func (d *Parallels9Driver) IpAddress(mac string) (string, error) {
   308  
   309  	if len(mac) != 12 {
   310  		return "", fmt.Errorf("Not a valid MAC address: %s. It should be exactly 12 digits.", mac)
   311  	}
   312  
   313  	leases, err := ioutil.ReadFile(d.dhcp_lease_file)
   314  	if err != nil {
   315  		return "", err
   316  	}
   317  
   318  	re := regexp.MustCompile("(.*)=\"(.*),(.*)," + strings.ToLower(mac) + ",.*\"")
   319  	mostRecentIp := ""
   320  	mostRecentLease := uint64(0)
   321  	for _, l := range re.FindAllStringSubmatch(string(leases), -1) {
   322  		ip := l[1]
   323  		expiry, _ := strconv.ParseUint(l[2], 10, 64)
   324  		leaseTime, _ := strconv.ParseUint(l[3], 10, 32)
   325  		log.Printf("Found lease: %s for MAC: %s, expiring at %d, leased for %d s.\n", ip, mac, expiry, leaseTime)
   326  		if mostRecentLease <= expiry-leaseTime {
   327  			mostRecentIp = ip
   328  			mostRecentLease = expiry - leaseTime
   329  		}
   330  	}
   331  
   332  	if len(mostRecentIp) == 0 {
   333  		return "", fmt.Errorf("IP lease not found for MAC address %s in: %s\n", mac, d.dhcp_lease_file)
   334  	}
   335  
   336  	log.Printf("Found IP lease: %s for MAC address %s\n", mostRecentIp, mac)
   337  	return mostRecentIp, nil
   338  }
   339  
   340  func (d *Parallels9Driver) ToolsIsoPath(k string) (string, error) {
   341  	appPath, err := getAppPath("com.parallels.desktop.console")
   342  	if err != nil {
   343  		return "", err
   344  	}
   345  
   346  	toolsPath := filepath.Join(appPath, "Contents", "Resources", "Tools", "prl-tools-"+k+".iso")
   347  	log.Printf("Parallels Tools path: '%s'", toolsPath)
   348  	return toolsPath, nil
   349  }