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