minikube/vendor/github.com/Parallels/docker-machine-parallels/parallels_darwin.go

742 lines
18 KiB
Go

package parallels
import (
"errors"
"fmt"
"io/ioutil"
"net"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"time"
"github.com/docker/machine/libmachine/drivers"
"github.com/docker/machine/libmachine/log"
"github.com/docker/machine/libmachine/mcnflag"
"github.com/docker/machine/libmachine/mcnutils"
"github.com/docker/machine/libmachine/ssh"
"github.com/docker/machine/libmachine/state"
"github.com/hashicorp/go-version"
)
const (
isoFilename = "boot2docker.iso"
shareFolderName = "Users"
shareFolderPath = "/Users"
minDiskSize = 32
defaultCPU = 1
defaultMemory = 1024
defaultVideoSize = 64
defaultBoot2DockerURL = ""
defaultNoShare = false
defaultDiskSize = 20000
defaultSSHPort = 22
defaultSSHUser = "docker"
)
var (
reMachineNotFound = regexp.MustCompile(`Failed to get VM config: The virtual machine could not be found..*`)
reParallelsVersion = regexp.MustCompile(`.* (\d+\.\d+\.\d+).*`)
reParallelsEdition = regexp.MustCompile(`edition="(.+)"`)
errMachineExist = errors.New("machine already exists")
errMachineNotExist = errors.New("machine does not exist")
errSharedNotConnected = errors.New("Your Mac host is not connected to Shared network. Please, enable this option: 'Parallels Desktop' -> 'Preferences' -> 'Network' -> 'Shared' -> 'Connect Mac to this network'")
v10, _ = version.NewVersion("10.0.0")
v11, _ = version.NewVersion("11.0.0")
)
// Driver for Parallels Desktop
type Driver struct {
*drivers.BaseDriver
CPU int
Memory int
VideoSize int
DiskSize int
Boot2DockerURL string
NoShare bool
}
// NewDriver creates a new Parallels Desktop driver with default settings
func NewDriver(hostName, storePath string) drivers.Driver {
return &Driver{
BaseDriver: &drivers.BaseDriver{
MachineName: hostName,
StorePath: storePath,
SSHUser: defaultSSHUser,
SSHPort: defaultSSHPort,
},
CPU: defaultCPU,
Memory: defaultMemory,
VideoSize: defaultVideoSize,
DiskSize: defaultDiskSize,
Boot2DockerURL: defaultBoot2DockerURL,
NoShare: defaultNoShare,
}
}
// Create a host using the driver's config
func (d *Driver) Create() error {
var (
err error
)
b2dutils := mcnutils.NewB2dUtils(d.StorePath)
if err = b2dutils.CopyIsoToMachineDir(d.Boot2DockerURL, d.MachineName); err != nil {
return err
}
log.Infof("Creating SSH key...")
sshKeyPath := d.GetSSHKeyPath()
log.Debugf("SSH key: %s", sshKeyPath)
if err = ssh.GenerateSSHKey(sshKeyPath); err != nil {
return err
}
log.Infof("Creating Parallels Desktop VM...")
ver, err := getParallelsVersion()
if err != nil {
return err
}
distribution := "boot2docker"
if ver.LessThan(v11) {
distribution = "linux-2.6"
}
absStorePath, _ := filepath.Abs(d.ResolveStorePath("."))
if err = prlctl("create", d.MachineName,
"--distribution", distribution,
"--dst", absStorePath,
"--no-hdd"); err != nil {
return err
}
cpus := d.CPU
if cpus < 1 {
cpus = int(runtime.NumCPU())
}
if cpus > 32 {
cpus = 32
}
videoSize := d.VideoSize
if videoSize < 2 {
videoSize = defaultVideoSize
}
if err = prlctl("set", d.MachineName,
"--select-boot-device", "off",
"--cpus", fmt.Sprintf("%d", cpus),
"--memsize", fmt.Sprintf("%d", d.Memory),
"--videosize", fmt.Sprintf("%d", videoSize),
"--cpu-hotplug", "off",
"--on-window-close", "keep-running",
"--longer-battery-life", "on",
"--3d-accelerate", "off",
"--device-bootorder", "cdrom0"); err != nil {
return err
}
absISOPath, _ := filepath.Abs(d.ResolveStorePath(isoFilename))
if err = prlctl("set", d.MachineName,
"--device-set", "cdrom0",
"--iface", "sata",
"--position", "0",
"--image", absISOPath,
"--connect"); err != nil {
return err
}
initialDiskSize := minDiskSize
// Fix for [GH-67]. Create a bigger disk on Parallels Desktop 13.0.*
constraints, _ := version.NewConstraint(">= 13.0.0, < 13.1.0")
if constraints.Check(ver) {
initialDiskSize = 1891
}
// Create a small plain disk. It will be converted and expanded later
if err = prlctl("set", d.MachineName,
"--device-add", "hdd",
"--iface", "sata",
"--position", "1",
"--image", d.diskPath(),
"--type", "plain",
"--size", fmt.Sprintf("%d", initialDiskSize)); err != nil {
return err
}
if err = d.generateDiskImage(d.DiskSize); err != nil {
return err
}
// For Parallels Desktop >= 11.0.0
if ver.Compare(v11) >= 0 {
// Enable headless mode
if err = prlctl("set", d.MachineName,
"--startup-view", "headless"); err != nil {
return err
}
// Don't share any additional folders
if err = prlctl("set", d.MachineName,
"--shf-host-defined", "off"); err != nil {
return err
}
// Enable time sync, don't touch timezone (this part is buggy)
if err = prlctl("set", d.MachineName, "--time-sync", "on"); err != nil {
return err
}
if err = prlctl("set", d.MachineName,
"--disable-timezone-sync", "on"); err != nil {
return err
}
} else {
// Disable time sync feature because it has an issue with timezones.
if err = prlctl("set", d.MachineName, "--time-sync", "off"); err != nil {
return err
}
}
// Configure Shared Folders
if err = prlctl("set", d.MachineName,
"--shf-host", "on",
"--shared-cloud", "off",
"--shared-profile", "off",
"--smart-mount", "off"); err != nil {
return err
}
if !d.NoShare {
if err = prlctl("set", d.MachineName,
"--shf-host-add", shareFolderName,
"--path", shareFolderPath); err != nil {
return err
}
}
log.Infof("Starting Parallels Desktop VM...")
// Don't use Start() since it expects to have a dhcp lease already
if err = prlctl("start", d.MachineName); err != nil {
return err
}
var ip string
log.Infof("Waiting for VM to come online...")
for i := 1; i <= 60; i++ {
ip, err = d.getIPfromDHCPLease()
if err != nil {
log.Debugf("Not there yet %d/%d, error: %s", i, 60, err)
time.Sleep(2 * time.Second)
continue
}
if ip != "" {
log.Debugf("Got an ip: %s", ip)
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ip, d.SSHPort), time.Duration(2*time.Second))
if err != nil {
log.Debugf("SSH Daemon not responding yet: %s", err)
time.Sleep(2 * time.Second)
continue
}
conn.Close()
break
}
}
if ip == "" {
return fmt.Errorf("Machine didn't return an IP after 120 seconds, aborting")
}
d.IPAddress = ip
if err := d.Start(); err != nil {
return err
}
return nil
}
// DriverName returns the name of the driver as it is registered
func (d *Driver) DriverName() string {
return "parallels"
}
// GetIP returns an IP or hostname that this host is available at
// e.g. 1.2.3.4 or docker-host-d60b70a14d3a.cloudapp.net
func (d *Driver) GetIP() (string, error) {
// Assume that Parallels Desktop hosts don't have IPs unless they are running
s, err := d.GetState()
if err != nil {
return "", err
}
if s != state.Running {
return "", drivers.ErrHostIsNotRunning
}
ip, err := d.getIPfromDHCPLease()
if err != nil {
return "", err
}
return ip, nil
}
// GetSSHHostname returns hostname for use with ssh
func (d *Driver) GetSSHHostname() (string, error) {
return d.GetIP()
}
// GetURL returns a Docker compatible host URL for connecting to this host
// e.g. tcp://1.2.3.4:2376
func (d *Driver) GetURL() (string, error) {
ip, err := d.GetIP()
if err != nil {
return "", err
}
if ip == "" {
return "", nil
}
return fmt.Sprintf("tcp://%s:2376", ip), nil
}
// GetState returns the state that the host is in (running, stopped, etc)
func (d *Driver) GetState() (state.State, error) {
stdout, stderr, err := prlctlOutErr("list", d.MachineName, "--output", "status", "--no-header")
if err != nil {
if reMachineNotFound.FindString(stderr) != "" {
return state.Error, errMachineNotExist
}
return state.Error, err
}
switch stdout {
case "running\n":
return state.Running, nil
case "paused\n":
return state.Paused, nil
case "suspended\n":
return state.Saved, nil
case "stopping\n":
return state.Stopping, nil
case "stopped\n":
return state.Stopped, nil
}
return state.None, nil
}
// Kill stops a host forcefully
func (d *Driver) Kill() error {
return prlctl("stop", d.MachineName, "--kill")
}
// PreCreateCheck allows for pre-create operations to make sure a driver is ready for creation
func (d *Driver) PreCreateCheck() error {
// Check platform type
if runtime.GOOS != "darwin" {
return fmt.Errorf("Driver \"parallels\" works only on OS X!")
}
// Check Parallels Desktop version
ver, err := getParallelsVersion()
if err != nil {
return err
}
if ver.LessThan(v10) {
return fmt.Errorf("Driver \"parallels\" supports only Parallels Desktop 10 and higher. You use: Parallels Desktop %s.", ver)
}
if ver.LessThan(v11) {
log.Debugf("Found Parallels Desktop version: %s", ver)
log.Infof("Driver \"parallels\" integration with Parallels Desktop 10 is maintained by open source community.")
log.Infof("For Parallels supported configuration you should use it with Parallels Desktop 11 or later (Pro or Business edition).")
return nil
}
// Check Parallels Desktop edition
edit, err := getParallelsEdition()
if err != nil {
return err
}
log.Debugf("Found Parallels Desktop version: %d, edition: %s", ver, edit)
switch edit {
case "pro", "business":
break
default:
return fmt.Errorf("Docker Machine can be used only with Parallels Desktop Pro or Business edition. You use: %s edition", edit)
}
// Check whether the host is connected to Shared network
ok, err := isSharedConnected()
if err != nil {
return err
}
if !ok {
return errSharedNotConnected
}
// Downloading boot2docker to cache should be done here to make sure
// that a download failure will not leave a machine half created.
b2dutils := mcnutils.NewB2dUtils(d.StorePath)
if err := b2dutils.UpdateISOCache(d.Boot2DockerURL); err != nil {
return err
}
return nil
}
// Remove a host
func (d *Driver) Remove() error {
s, err := d.GetState()
if err != nil {
if err == errMachineNotExist {
log.Infof("machine does not exist, assuming it has been removed already")
return nil
}
return err
}
if s == state.Running {
if err := d.Kill(); err != nil {
return err
}
}
return prlctl("delete", d.MachineName)
}
// Restart a host. This may just call Stop(); Start() if the provider does not
// have any special restart behaviour.
func (d *Driver) Restart() error {
if err := d.Stop(); err != nil {
return err
}
return d.Start()
}
// GetCreateFlags registers the flags this driver adds to
// "docker hosts create"
func (d *Driver) GetCreateFlags() []mcnflag.Flag {
return []mcnflag.Flag{
mcnflag.IntFlag{
EnvVar: "PARALLELS_MEMORY_SIZE",
Name: "parallels-memory",
Usage: "Size of memory for host in MB",
Value: defaultMemory,
},
mcnflag.IntFlag{
EnvVar: "PARALLELS_CPU_COUNT",
Name: "parallels-cpu-count",
Usage: "number of CPUs for the machine (-1 to use the number of CPUs available)",
Value: defaultCPU,
},
mcnflag.IntFlag{
EnvVar: "PARALLELS_VIDEO_SIZE",
Name: "parallels-video-size",
Usage: "Size of video for host in MB",
Value: defaultVideoSize,
},
mcnflag.IntFlag{
EnvVar: "PARALLELS_DISK_SIZE",
Name: "parallels-disk-size",
Usage: "Size of disk for host in MB",
Value: defaultDiskSize,
},
mcnflag.StringFlag{
EnvVar: "PARALLELS_BOOT2DOCKER_URL",
Name: "parallels-boot2docker-url",
Usage: "The URL of the boot2docker image. Defaults to the latest available version",
Value: defaultBoot2DockerURL,
},
mcnflag.BoolFlag{
Name: "parallels-no-share",
Usage: "Disable the mount of your home directory",
},
}
}
// SetConfigFromFlags configures the driver with the object that was returned
// by RegisterCreateFlags
func (d *Driver) SetConfigFromFlags(opts drivers.DriverOptions) error {
d.CPU = opts.Int("parallels-cpu-count")
d.Memory = opts.Int("parallels-memory")
d.VideoSize = opts.Int("parallels-video-size")
d.DiskSize = opts.Int("parallels-disk-size")
d.Boot2DockerURL = opts.String("parallels-boot2docker-url")
d.SetSwarmConfigFromFlags(opts)
d.SSHUser = defaultSSHUser
d.SSHPort = defaultSSHPort
d.NoShare = opts.Bool("parallels-no-share")
return nil
}
// Start a host
func (d *Driver) Start() error {
// Check whether the host is connected to Shared network
ok, err := isSharedConnected()
if err != nil {
return err
}
if !ok {
return errSharedNotConnected
}
s, err := d.GetState()
if err != nil {
return err
}
switch s {
case state.Stopped, state.Saved, state.Paused:
if err = prlctl("start", d.MachineName); err != nil {
return err
}
log.Infof("Waiting for VM to start...")
case state.Running:
break
default:
log.Infof("VM not in restartable state")
}
if err = drivers.WaitForSSH(d); err != nil {
return err
}
d.IPAddress, err = d.GetIP()
if err != nil {
return err
}
// Mount Share Folder
if !d.NoShare {
if err := d.mountShareFolder(shareFolderName, shareFolderPath); err != nil {
return err
}
}
return nil
}
// Stop a host gracefully
func (d *Driver) Stop() error {
if err := prlctl("stop", d.MachineName); err != nil {
return err
}
for {
s, err := d.GetState()
if err != nil {
return err
}
if s == state.Running {
time.Sleep(1 * time.Second)
} else {
break
}
}
return nil
}
func (d *Driver) getIPfromDHCPLease() (string, error) {
DHCPLeaseFile := "/Library/Preferences/Parallels/parallels_dhcp_leases"
stdout, _, err := prlctlOutErr("list", "-i", d.MachineName)
macRe := regexp.MustCompile("net0.* mac=([0-9A-F]{12}) card=.*")
macMatch := macRe.FindAllStringSubmatch(stdout, 1)
if len(macMatch) != 1 {
return "", fmt.Errorf("MAC address for NIC: nic0 on Virtual Machine: %s not found!\n", d.MachineName)
}
mac := macMatch[0][1]
if len(mac) != 12 {
return "", fmt.Errorf("Not a valid MAC address: %s. It should be exactly 12 digits.", mac)
}
leases, err := ioutil.ReadFile(DHCPLeaseFile)
if err != nil {
return "", err
}
ipRe := regexp.MustCompile("(.*)=\"(.*),(.*)," + strings.ToLower(mac) + ",.*\"")
mostRecentIP := ""
mostRecentLease := uint64(0)
for _, l := range ipRe.FindAllStringSubmatch(string(leases), -1) {
ip := l[1]
expiry, _ := strconv.ParseUint(l[2], 10, 64)
leaseTime, _ := strconv.ParseUint(l[3], 10, 32)
log.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\n", mac, DHCPLeaseFile)
}
log.Debugf("Found IP lease: %s for MAC address %s\n", mostRecentIP, mac)
return mostRecentIP, nil
}
func (d *Driver) diskPath() string {
absDiskPath, _ := filepath.Abs(d.ResolveStorePath("disk.hdd"))
return absDiskPath
}
func (d *Driver) mountShareFolder(shareName string, mountPoint string) error {
// Check the host path is available
if _, err := os.Stat(mountPoint); err != nil {
if os.IsNotExist(err) {
log.Infof("Host path '%s' does not exist. Skipping mount to VM...", mountPoint)
return nil
}
return err
}
// Ensure that share is available on the guest side
checkCmd := "sudo modprobe prl_fs && grep -w " + shareName + " /proc/fs/prl_fs/sf_list"
if _, err := drivers.RunSSHCommandFromDriver(d, checkCmd); err != nil {
log.Infof("Shared folder '%s' is unavailable. Skipping mount to VM...", shareName)
return nil
}
// Mount shared folder
mountCmd := "sudo mkdir -p " + mountPoint + " && sudo mount -t prl_fs " + shareName + " " + mountPoint
if _, err := drivers.RunSSHCommandFromDriver(d, mountCmd); err != nil {
return fmt.Errorf("Error mounting shared folder: %s", err)
}
return nil
}
// Make a boot2docker VM disk image.
func (d *Driver) generateDiskImage(size int) error {
tarBuf, err := mcnutils.MakeDiskImage(d.publicSSHKeyPath())
if err != nil {
return err
}
minSizeBytes := int64(minDiskSize) << 20 // usually won't fit in 32-bit int (max 2GB)
//Expand the initial image if needed
if bufLen := int64(tarBuf.Len()); bufLen > minSizeBytes {
bufLenMBytes := bufLen>>20 + 1
if err = prldisktool("resize",
"--hdd", d.diskPath(),
"--size", fmt.Sprintf("%d", bufLenMBytes)); err != nil {
return err
}
}
// Find hds file
hdsList, err := filepath.Glob(d.diskPath() + "/*.hds")
if err != nil {
return err
}
if len(hdsList) == 0 {
return fmt.Errorf("Could not find *.hds image in %s", d.diskPath())
}
hdsPath := hdsList[0]
log.Debugf("HDS image path: %s", hdsPath)
// Write tar to the hds file
hds, err := os.OpenFile(hdsPath, os.O_WRONLY, 0644)
if err != nil {
return err
}
defer hds.Close()
hds.Seek(0, os.SEEK_SET)
_, err = hds.Write(tarBuf.Bytes())
if err != nil {
return err
}
hds.Close()
// Convert image to expanding type and resize it
if err := prldisktool("convert", "--expanding", "--merge",
"--hdd", d.diskPath()); err != nil {
return err
}
if err := prldisktool("resize",
"--hdd", d.diskPath(),
"--size", fmt.Sprintf("%d", size)); err != nil {
return err
}
return nil
}
func (d *Driver) publicSSHKeyPath() string {
return d.GetSSHKeyPath() + ".pub"
}
func detectCmdInPath(cmd string) string {
if path, err := exec.LookPath(cmd); err == nil {
return path
}
return cmd
}
// Detects Parallels Desktop major version
func getParallelsVersion() (*version.Version, error) {
stdout, _, err := prlctlOutErr("--version")
if err != nil {
return nil, err
}
// Parse Parallels Desktop version
verRaw := reParallelsVersion.FindStringSubmatch(string(stdout))
if verRaw == nil {
return nil, fmt.Errorf("Parallels Desktop version could not be fetched: %s", stdout)
}
ver, err := version.NewVersion(verRaw[1])
if err != nil {
return nil, err
}
return ver, nil
}
// Detects Parallels Desktop edition
func getParallelsEdition() (string, error) {
stdout, _, err := prlsrvctlOutErr("info", "--license")
if err != nil {
return "", err
}
// Parse Parallels Desktop version
res := reParallelsEdition.FindStringSubmatch(string(stdout))
if res == nil {
return "", fmt.Errorf("Parallels Desktop edition could not be fetched!")
}
return res[1], nil
}
// Checks whether the host is connected to Shared network
func isSharedConnected() (bool, error) {
stdout, _, err := prlsrvctlOutErr("net", "info", "Shared")
if err != nil {
return false, err
}
reSharedIsConnected := regexp.MustCompile(`Bound To:.*`)
return reSharedIsConnected.MatchString(stdout), nil
}