github.com/mmcquillan/packer@v1.1.1-0.20171009221028-c85cf0483a5d/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 = getFirtsMACAddress(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 getFirtsMACAddress(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 compation 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); 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 log.Printf("Parallels Desktop version: %s", version) 279 return version, nil 280 } 281 282 // SendKeyScanCodes sends the specified scancodes as key events to the VM. 283 // It is performed using "Prltype" script (refer to "prltype.go"). 284 func (d *Parallels9Driver) SendKeyScanCodes(vmName string, codes ...string) error { 285 var stdout, stderr bytes.Buffer 286 287 if codes == nil || len(codes) == 0 { 288 log.Printf("No scan codes to send") 289 return nil 290 } 291 292 f, err := ioutil.TempFile("", "prltype") 293 if err != nil { 294 return err 295 } 296 defer os.Remove(f.Name()) 297 298 script := []byte(Prltype) 299 _, err = f.Write(script) 300 if err != nil { 301 return err 302 } 303 304 args := prepend(vmName, codes) 305 args = prepend(f.Name(), args) 306 cmd := exec.Command("/usr/bin/python", args...) 307 cmd.Stdout = &stdout 308 cmd.Stderr = &stderr 309 err = cmd.Run() 310 311 stdoutString := strings.TrimSpace(stdout.String()) 312 stderrString := strings.TrimSpace(stderr.String()) 313 314 if _, ok := err.(*exec.ExitError); ok { 315 err = fmt.Errorf("prltype error: %s", stderrString) 316 } 317 318 log.Printf("stdout: %s", stdoutString) 319 log.Printf("stderr: %s", stderrString) 320 321 return err 322 } 323 324 func prepend(head string, tail []string) []string { 325 tmp := make([]string, len(tail)+1) 326 for i := 0; i < len(tail); i++ { 327 tmp[i+1] = tail[i] 328 } 329 tmp[0] = head 330 return tmp 331 } 332 333 // SetDefaultConfiguration applies pre-defined default settings to the VM config. 334 func (d *Parallels9Driver) SetDefaultConfiguration(vmName string) error { 335 commands := make([][]string, 7) 336 commands[0] = []string{"set", vmName, "--cpus", "1"} 337 commands[1] = []string{"set", vmName, "--memsize", "512"} 338 commands[2] = []string{"set", vmName, "--startup-view", "same"} 339 commands[3] = []string{"set", vmName, "--on-shutdown", "close"} 340 commands[4] = []string{"set", vmName, "--on-window-close", "keep-running"} 341 commands[5] = []string{"set", vmName, "--auto-share-camera", "off"} 342 commands[6] = []string{"set", vmName, "--smart-guard", "off"} 343 344 for _, command := range commands { 345 err := d.Prlctl(command...) 346 if err != nil { 347 return err 348 } 349 } 350 return nil 351 } 352 353 // MAC returns the MAC address of the VM's first network interface. 354 func (d *Parallels9Driver) MAC(vmName string) (string, error) { 355 var stdout bytes.Buffer 356 357 cmd := exec.Command(d.PrlctlPath, "list", "-i", vmName) 358 cmd.Stdout = &stdout 359 if err := cmd.Run(); err != nil { 360 log.Printf("MAC address for NIC: nic0 on Virtual Machine: %s not found!\n", vmName) 361 return "", err 362 } 363 364 stdoutString := strings.TrimSpace(stdout.String()) 365 re := regexp.MustCompile("net0.* mac=([0-9A-F]{12}) card=.*") 366 macMatch := re.FindAllStringSubmatch(stdoutString, 1) 367 368 if len(macMatch) != 1 { 369 return "", fmt.Errorf("MAC address for NIC: nic0 on Virtual Machine: %s not found!\n", vmName) 370 } 371 372 mac := macMatch[0][1] 373 log.Printf("Found MAC address for NIC: net0 - %s\n", mac) 374 return mac, nil 375 } 376 377 // IPAddress finds the IP address of a VM connected that uses DHCP by its MAC address 378 // 379 // Parses the file /Library/Preferences/Parallels/parallels_dhcp_leases 380 // file contain a list of DHCP leases given by Parallels Desktop 381 // Example line: 382 // 10.211.55.181="1418921112,1800,001c42f593fb,ff42f593fb000100011c25b9ff001c42f593fb" 383 // IP Address ="Lease expiry, Lease time, MAC, MAC or DUID" 384 func (d *Parallels9Driver) IPAddress(mac string) (string, error) { 385 386 if len(mac) != 12 { 387 return "", fmt.Errorf("Not a valid MAC address: %s. It should be exactly 12 digits.", mac) 388 } 389 390 leases, err := ioutil.ReadFile(d.dhcpLeaseFile) 391 if err != nil { 392 return "", err 393 } 394 395 re := regexp.MustCompile("(.*)=\"(.*),(.*)," + strings.ToLower(mac) + ",.*\"") 396 mostRecentIP := "" 397 mostRecentLease := uint64(0) 398 for _, l := range re.FindAllStringSubmatch(string(leases), -1) { 399 ip := l[1] 400 expiry, _ := strconv.ParseUint(l[2], 10, 64) 401 leaseTime, _ := strconv.ParseUint(l[3], 10, 32) 402 log.Printf("Found lease: %s for MAC: %s, expiring at %d, leased for %d s.\n", ip, mac, expiry, leaseTime) 403 if mostRecentLease <= expiry-leaseTime { 404 mostRecentIP = ip 405 mostRecentLease = expiry - leaseTime 406 } 407 } 408 409 if len(mostRecentIP) == 0 { 410 return "", fmt.Errorf("IP lease not found for MAC address %s in: %s\n", mac, d.dhcpLeaseFile) 411 } 412 413 log.Printf("Found IP lease: %s for MAC address %s\n", mostRecentIP, mac) 414 return mostRecentIP, nil 415 } 416 417 // ToolsISOPath returns a full path to the Parallels Tools ISO for the specified guest 418 // OS type. The following OS types are supported: "win", "lin", "mac", "other". 419 func (d *Parallels9Driver) ToolsISOPath(k string) (string, error) { 420 appPath, err := getAppPath("com.parallels.desktop.console") 421 if err != nil { 422 return "", err 423 } 424 425 toolsPath := filepath.Join(appPath, "Contents", "Resources", "Tools", "prl-tools-"+k+".iso") 426 log.Printf("Parallels Tools path: '%s'", toolsPath) 427 return toolsPath, nil 428 }