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 }