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