helpers/parallels/control.go (189 lines of code) (raw):
package parallels
// Large part of this source is taken from
// https://github.com/mitchellh/packer/blob/master/builder/parallels/common
import (
"bytes"
"errors"
"fmt"
"github.com/Sirupsen/logrus"
"io/ioutil"
"os/exec"
"regexp"
"runtime"
"strconv"
"strings"
"time"
)
type StatusType string
const (
NotFound StatusType = "notfound"
Invalid StatusType = "invalid"
Stopped StatusType = "stopped"
Suspended StatusType = "suspended"
Running StatusType = "running"
// TODO: more statuses
)
const (
prlctlPath = "prlctl"
dhcpLeases = "/Library/Preferences/Parallels/parallels_dhcp_leases"
)
func PrlctlOutput(args ...string) (string, error) {
if runtime.GOOS != "darwin" {
return "", fmt.Errorf("Parallels works only on \"darwin\" platform")
}
var stdout, stderr bytes.Buffer
logrus.Debugf("Executing PrlctlOutput: %#v", args)
cmd := exec.Command(prlctlPath, args...)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
stderrString := strings.TrimSpace(stderr.String())
if _, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf("PrlctlOutput error: %s", stderrString)
}
return stdout.String(), err
}
func Prlctl(args ...string) error {
_, err := PrlctlOutput(args...)
return err
}
func Exec(vmName string, args ...string) (string, error) {
args2 := append([]string{"exec", vmName}, args...)
return PrlctlOutput(args2...)
}
func Version() (string, error) {
out, err := PrlctlOutput("--version")
if err != nil {
return "", err
}
versionRe := regexp.MustCompile(`prlctl version (\d+\.\d+.\d+)`)
matches := versionRe.FindStringSubmatch(string(out))
if matches == nil {
return "", fmt.Errorf(
"Could not find Parallels Desktop version in output:\n%s", string(out))
}
version := matches[1]
logrus.Debugf("Parallels Desktop version: %s", version)
return version, nil
}
func Exist(name string) bool {
err := Prlctl("list", name, "--no-header", "--output", "status")
if err != nil {
return false
}
return true
}
func CreateTemplate(vmName, templateName string) error {
return Prlctl("clone", vmName, "--name", templateName, "--template", "--linked")
}
func CreateOsVM(vmName, templateName string) error {
return Prlctl("create", vmName, "--ostemplate", templateName)
}
func CreateSnapshot(vmName, snapshotName string) error {
return Prlctl("snapshot", vmName, "--name", snapshotName)
}
func GetDefaultSnapshot(vmName string) (string, error) {
output, err := PrlctlOutput("snapshot-list", vmName)
if err != nil {
return "", err
}
lines := strings.Split(output, "\n")
for _, line := range lines {
pos := strings.Index(line, " *")
if pos >= 0 {
snapshot := line[pos+2:]
snapshot = strings.TrimSpace(snapshot)
if len(snapshot) > 0 { // It uses UUID so it should be 38
return snapshot, nil
}
}
}
return "", errors.New("No snapshot")
}
func RevertToSnapshot(vmName, snapshotID string) error {
return Prlctl("snapshot-switch", vmName, "--id", snapshotID)
}
func Start(vmName string) error {
return Prlctl("start", vmName)
}
func Status(vmName string) (StatusType, error) {
output, err := PrlctlOutput("list", vmName, "--no-header", "--output", "status")
if err != nil {
return NotFound, err
}
return StatusType(strings.TrimSpace(output)), nil
}
func WaitForStatus(vmName string, vmStatus StatusType, seconds int) error {
var status StatusType
var err error
for i := 0; i < seconds; i++ {
status, err = Status(vmName)
if err != nil {
return err
}
if status == vmStatus {
return nil
}
time.Sleep(time.Second)
}
return errors.New("VM " + vmName + " is in " + string(status) + " where it should be in " + string(vmStatus))
}
func TryExec(vmName string, seconds int, cmd ...string) error {
var err error
for i := 0; i < seconds; i++ {
_, err = Exec(vmName, cmd...)
if err == nil {
return nil
}
time.Sleep(time.Second)
}
return err
}
func Kill(vmName string) error {
return Prlctl("stop", vmName, "--kill")
}
func Delete(vmName string) error {
return Prlctl("delete", vmName)
}
func Unregister(vmName string) error {
return Prlctl("unregister", vmName)
}
func Mac(vmName string) (string, error) {
output, err := PrlctlOutput("list", "-i", vmName)
if err != nil {
return "", err
}
stdoutString := strings.TrimSpace(output)
re := regexp.MustCompile("net0.* mac=([0-9A-F]{12}) card=.*")
macMatch := re.FindAllStringSubmatch(stdoutString, 1)
if len(macMatch) != 1 {
return "", fmt.Errorf("MAC address for NIC: nic0 on Virtual Machine: %s not found", vmName)
}
mac := macMatch[0][1]
logrus.Debugf("Found MAC address for NIC: net0 - %s\n", mac)
return mac, nil
}
// IPAddress finds the IP address of a VM connected that uses DHCP by its MAC address
//
// Parses the file /Library/Preferences/Parallels/parallels_dhcp_leases
// file contain a list of DHCP leases given by Parallels Desktop
// Example line:
// 10.211.55.181="1418921112,1800,001c42f593fb,ff42f593fb000100011c25b9ff001c42f593fb"
// IP Address ="Lease expiry, Lease time, MAC, MAC or DUID"
func IPAddress(mac string) (string, error) {
if len(mac) != 12 {
return "", fmt.Errorf("Not a valid MAC address: %s. It should be exactly 12 digits", mac)
}
leases, err := ioutil.ReadFile(dhcpLeases)
if err != nil {
return "", err
}
re := regexp.MustCompile("(.*)=\"(.*),(.*)," + strings.ToLower(mac) + ",.*\"")
mostRecentIP := ""
mostRecentLease := uint64(0)
for _, l := range re.FindAllStringSubmatch(string(leases), -1) {
ip := l[1]
expiry, _ := strconv.ParseUint(l[2], 10, 64)
leaseTime, _ := strconv.ParseUint(l[3], 10, 32)
logrus.Debugf("Found lease: %s for MAC: %s, expiring at %d, leased for %d s.\n", ip, mac, expiry, leaseTime)
if mostRecentLease <= expiry-leaseTime {
mostRecentIP = ip
mostRecentLease = expiry - leaseTime
}
}
if len(mostRecentIP) == 0 {
return "", fmt.Errorf("IP lease not found for MAC address %s in: %s", mac, dhcpLeases)
}
logrus.Debugf("Found IP lease: %s for MAC address %s\n", mostRecentIP, mac)
return mostRecentIP, nil
}