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 }