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