github.com/alloyci/alloy-runner@v1.0.1-0.20180222164613-925503ccafd6/helpers/parallels/control.go (about)

     1  package parallels
     2  
     3  // Large part of this source is taken from
     4  // https://github.com/mitchellh/packer/blob/master/builder/parallels/common
     5  
     6  import (
     7  	"bytes"
     8  	"errors"
     9  	"fmt"
    10  	"github.com/Sirupsen/logrus"
    11  	"io/ioutil"
    12  	"os/exec"
    13  	"regexp"
    14  	"runtime"
    15  	"strconv"
    16  	"strings"
    17  	"time"
    18  )
    19  
    20  type StatusType string
    21  
    22  const (
    23  	NotFound  StatusType = "notfound"
    24  	Invalid   StatusType = "invalid"
    25  	Stopped   StatusType = "stopped"
    26  	Suspended StatusType = "suspended"
    27  	Running   StatusType = "running"
    28  	// TODO: more statuses
    29  )
    30  
    31  const (
    32  	prlctlPath = "prlctl"
    33  	dhcpLeases = "/Library/Preferences/Parallels/parallels_dhcp_leases"
    34  )
    35  
    36  func PrlctlOutput(args ...string) (string, error) {
    37  	if runtime.GOOS != "darwin" {
    38  		return "", fmt.Errorf("Parallels works only on \"darwin\" platform")
    39  	}
    40  
    41  	var stdout, stderr bytes.Buffer
    42  
    43  	logrus.Debugf("Executing PrlctlOutput: %#v", args)
    44  	cmd := exec.Command(prlctlPath, args...)
    45  	cmd.Stdout = &stdout
    46  	cmd.Stderr = &stderr
    47  	err := cmd.Run()
    48  
    49  	stderrString := strings.TrimSpace(stderr.String())
    50  
    51  	if _, ok := err.(*exec.ExitError); ok {
    52  		err = fmt.Errorf("PrlctlOutput error: %s", stderrString)
    53  	}
    54  
    55  	return stdout.String(), err
    56  }
    57  
    58  func Prlctl(args ...string) error {
    59  	_, err := PrlctlOutput(args...)
    60  	return err
    61  }
    62  
    63  func Exec(vmName string, args ...string) (string, error) {
    64  	args2 := append([]string{"exec", vmName}, args...)
    65  	return PrlctlOutput(args2...)
    66  }
    67  
    68  func Version() (string, error) {
    69  	out, err := PrlctlOutput("--version")
    70  	if err != nil {
    71  		return "", err
    72  	}
    73  
    74  	versionRe := regexp.MustCompile(`prlctl version (\d+\.\d+.\d+)`)
    75  	matches := versionRe.FindStringSubmatch(string(out))
    76  	if matches == nil {
    77  		return "", fmt.Errorf(
    78  			"Could not find Parallels Desktop version in output:\n%s", string(out))
    79  	}
    80  
    81  	version := matches[1]
    82  	logrus.Debugf("Parallels Desktop version: %s", version)
    83  	return version, nil
    84  }
    85  
    86  func Exist(name string) bool {
    87  	err := Prlctl("list", name, "--no-header", "--output", "status")
    88  	if err != nil {
    89  		return false
    90  	}
    91  	return true
    92  }
    93  
    94  func CreateTemplate(vmName, templateName string) error {
    95  	return Prlctl("clone", vmName, "--name", templateName, "--template", "--linked")
    96  }
    97  
    98  func CreateOsVM(vmName, templateName string) error {
    99  	return Prlctl("create", vmName, "--ostemplate", templateName)
   100  }
   101  
   102  func CreateSnapshot(vmName, snapshotName string) error {
   103  	return Prlctl("snapshot", vmName, "--name", snapshotName)
   104  }
   105  
   106  func GetDefaultSnapshot(vmName string) (string, error) {
   107  	output, err := PrlctlOutput("snapshot-list", vmName)
   108  	if err != nil {
   109  		return "", err
   110  	}
   111  
   112  	lines := strings.Split(output, "\n")
   113  	for _, line := range lines {
   114  		pos := strings.Index(line, " *")
   115  		if pos >= 0 {
   116  			snapshot := line[pos+2:]
   117  			snapshot = strings.TrimSpace(snapshot)
   118  			if len(snapshot) > 0 { // It uses UUID so it should be 38
   119  				return snapshot, nil
   120  			}
   121  		}
   122  	}
   123  
   124  	return "", errors.New("No snapshot")
   125  }
   126  
   127  func RevertToSnapshot(vmName, snapshotID string) error {
   128  	return Prlctl("snapshot-switch", vmName, "--id", snapshotID)
   129  }
   130  
   131  func Start(vmName string) error {
   132  	return Prlctl("start", vmName)
   133  }
   134  
   135  func Status(vmName string) (StatusType, error) {
   136  	output, err := PrlctlOutput("list", vmName, "--no-header", "--output", "status")
   137  	if err != nil {
   138  		return NotFound, err
   139  	}
   140  	return StatusType(strings.TrimSpace(output)), nil
   141  }
   142  
   143  func WaitForStatus(vmName string, vmStatus StatusType, seconds int) error {
   144  	var status StatusType
   145  	var err error
   146  	for i := 0; i < seconds; i++ {
   147  		status, err = Status(vmName)
   148  		if err != nil {
   149  			return err
   150  		}
   151  		if status == vmStatus {
   152  			return nil
   153  		}
   154  		time.Sleep(time.Second)
   155  	}
   156  	return errors.New("VM " + vmName + " is in " + string(status) + " where it should be in " + string(vmStatus))
   157  }
   158  
   159  func TryExec(vmName string, seconds int, cmd ...string) error {
   160  	var err error
   161  	for i := 0; i < seconds; i++ {
   162  		_, err = Exec(vmName, cmd...)
   163  		if err == nil {
   164  			return nil
   165  		}
   166  		time.Sleep(time.Second)
   167  	}
   168  	return err
   169  }
   170  
   171  func Kill(vmName string) error {
   172  	return Prlctl("stop", vmName, "--kill")
   173  }
   174  
   175  func Delete(vmName string) error {
   176  	return Prlctl("delete", vmName)
   177  }
   178  
   179  func Unregister(vmName string) error {
   180  	return Prlctl("unregister", vmName)
   181  }
   182  
   183  func Mac(vmName string) (string, error) {
   184  	output, err := PrlctlOutput("list", "-i", vmName)
   185  	if err != nil {
   186  		return "", err
   187  	}
   188  
   189  	stdoutString := strings.TrimSpace(output)
   190  	re := regexp.MustCompile("net0.* mac=([0-9A-F]{12}) card=.*")
   191  	macMatch := re.FindAllStringSubmatch(stdoutString, 1)
   192  
   193  	if len(macMatch) != 1 {
   194  		return "", fmt.Errorf("MAC address for NIC: nic0 on Virtual Machine: %s not found", vmName)
   195  	}
   196  
   197  	mac := macMatch[0][1]
   198  	logrus.Debugf("Found MAC address for NIC: net0 - %s\n", mac)
   199  	return mac, nil
   200  }
   201  
   202  // IPAddress finds the IP address of a VM connected that uses DHCP by its MAC address
   203  //
   204  // Parses the file /Library/Preferences/Parallels/parallels_dhcp_leases
   205  // file contain a list of DHCP leases given by Parallels Desktop
   206  // Example line:
   207  // 10.211.55.181="1418921112,1800,001c42f593fb,ff42f593fb000100011c25b9ff001c42f593fb"
   208  // IP Address   ="Lease expiry, Lease time, MAC, MAC or DUID"
   209  func IPAddress(mac string) (string, error) {
   210  	if len(mac) != 12 {
   211  		return "", fmt.Errorf("Not a valid MAC address: %s. It should be exactly 12 digits", mac)
   212  	}
   213  
   214  	leases, err := ioutil.ReadFile(dhcpLeases)
   215  	if err != nil {
   216  		return "", err
   217  	}
   218  
   219  	re := regexp.MustCompile("(.*)=\"(.*),(.*)," + strings.ToLower(mac) + ",.*\"")
   220  	mostRecentIP := ""
   221  	mostRecentLease := uint64(0)
   222  	for _, l := range re.FindAllStringSubmatch(string(leases), -1) {
   223  		ip := l[1]
   224  		expiry, _ := strconv.ParseUint(l[2], 10, 64)
   225  		leaseTime, _ := strconv.ParseUint(l[3], 10, 32)
   226  		logrus.Debugf("Found lease: %s for MAC: %s, expiring at %d, leased for %d s.\n", ip, mac, expiry, leaseTime)
   227  		if mostRecentLease <= expiry-leaseTime {
   228  			mostRecentIP = ip
   229  			mostRecentLease = expiry - leaseTime
   230  		}
   231  	}
   232  
   233  	if len(mostRecentIP) == 0 {
   234  		return "", fmt.Errorf("IP lease not found for MAC address %s in: %s", mac, dhcpLeases)
   235  	}
   236  
   237  	logrus.Debugf("Found IP lease: %s for MAC address %s\n", mostRecentIP, mac)
   238  	return mostRecentIP, nil
   239  }