docs/drivers/vmwarefusion/fusion_darwin.go

475 lines
12 KiB
Go

/*
* Copyright 2014 VMware, Inc. All rights reserved. Licensed under the Apache v2 License.
*/
package vmwarefusion
import (
"archive/tar"
"fmt"
"io/ioutil"
"os"
"regexp"
"runtime"
"strings"
"text/template"
"time"
"github.com/codegangsta/cli"
"github.com/docker/machine/drivers"
"github.com/docker/machine/log"
"github.com/docker/machine/ssh"
"github.com/docker/machine/state"
"github.com/docker/machine/utils"
)
const (
B2DUser = "docker"
B2DPass = "tcuser"
isoFilename = "boot2docker.iso"
)
// Driver for VMware Fusion
type Driver struct {
*drivers.BaseDriver
Memory int
DiskSize int
CPU int
ISO string
Boot2DockerURL string
CPUS int
}
func init() {
drivers.Register("vmwarefusion", &drivers.RegisteredDriver{
New: NewDriver,
GetCreateFlags: GetCreateFlags,
})
}
// GetCreateFlags registers the flags this driver adds to
// "docker hosts create"
func GetCreateFlags() []cli.Flag {
return []cli.Flag{
cli.StringFlag{
EnvVar: "FUSION_BOOT2DOCKER_URL",
Name: "vmwarefusion-boot2docker-url",
Usage: "Fusion URL for boot2docker image",
},
cli.IntFlag{
EnvVar: "FUSION_CPU_COUNT",
Name: "vmwarefusion-cpu-count",
Usage: "number of CPUs for the machine (-1 to use the number of CPUs available)",
Value: 1,
},
cli.IntFlag{
EnvVar: "FUSION_MEMORY_SIZE",
Name: "vmwarefusion-memory-size",
Usage: "Fusion size of memory for host VM (in MB)",
Value: 1024,
},
cli.IntFlag{
EnvVar: "FUSION_DISK_SIZE",
Name: "vmwarefusion-disk-size",
Usage: "Fusion size of disk for host VM (in MB)",
Value: 20000,
},
}
}
func NewDriver(machineName string, storePath string, caCert string, privateKey string) (drivers.Driver, error) {
inner := drivers.NewBaseDriver(machineName, storePath, caCert, privateKey)
return &Driver{BaseDriver: inner}, nil
}
func (d *Driver) GetSSHHostname() (string, error) {
return d.GetIP()
}
func (d *Driver) GetSSHUsername() string {
if d.SSHUser == "" {
d.SSHUser = "docker"
}
return d.SSHUser
}
func (d *Driver) DriverName() string {
return "vmwarefusion"
}
func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error {
d.Memory = flags.Int("vmwarefusion-memory-size")
d.CPU = flags.Int("vmwarefusion-cpu-count")
d.DiskSize = flags.Int("vmwarefusion-disk-size")
d.Boot2DockerURL = flags.String("vmwarefusion-boot2docker-url")
d.ISO = d.ResolveStorePath(isoFilename)
d.SwarmMaster = flags.Bool("swarm-master")
d.SwarmHost = flags.String("swarm-host")
d.SwarmDiscovery = flags.String("swarm-discovery")
d.SSHUser = "docker"
d.SSHPort = 22
// We support a maximum of 16 cpu to be consistent with Virtual Hardware 10
// specs.
if d.CPU > 16 {
d.CPU = 16
}
return nil
}
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
}
func (d *Driver) GetIP() (string, error) {
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
}
func (d *Driver) GetState() (state.State, error) {
// VMRUN only tells use if the vm is running or not
if stdout, _, _ := vmrun("list"); strings.Contains(stdout, d.vmxPath()) {
return state.Running, nil
}
return state.Stopped, nil
}
func (d *Driver) PreCreateCheck() error {
return nil
}
func (d *Driver) Create() error {
b2dutils := utils.NewB2dUtils("", "")
if err := b2dutils.CopyIsoToMachineDir(d.Boot2DockerURL, d.MachineName); err != nil {
return err
}
log.Infof("Creating SSH key...")
if err := ssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil {
return err
}
log.Infof("Creating VM...")
if err := os.MkdirAll(d.ResolveStorePath("."), 0755); err != nil {
return err
}
if _, err := os.Stat(d.vmxPath()); err == nil {
return ErrMachineExist
}
// Generate vmx config file from template
vmxt := template.Must(template.New("vmx").Parse(vmx))
vmxfile, err := os.Create(d.vmxPath())
if err != nil {
return err
}
vmxt.Execute(vmxfile, d)
// Generate vmdk file
diskImg := d.ResolveStorePath(fmt.Sprintf("%s.vmdk", d.MachineName))
if _, err := os.Stat(diskImg); err != nil {
if !os.IsNotExist(err) {
return err
}
if err := vdiskmanager(diskImg, d.DiskSize); err != nil {
return err
}
}
log.Infof("Starting %s...", d.MachineName)
vmrun("start", d.vmxPath(), "nogui")
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)
break
}
}
if ip == "" {
return fmt.Errorf("Machine didn't return an IP after 120 seconds, aborting")
}
// we got an IP, let's copy ssh keys over
d.IPAddress = ip
// Generate a tar keys bundle
if err := d.generateKeyBundle(); err != nil {
return err
}
// Test if /var/lib/boot2docker exists
vmrun("-gu", B2DUser, "-gp", B2DPass, "directoryExistsInGuest", d.vmxPath(), "/var/lib/boot2docker")
// Copy SSH keys bundle
vmrun("-gu", B2DUser, "-gp", B2DPass, "CopyFileFromHostToGuest", d.vmxPath(), d.ResolveStorePath("userdata.tar"), "/home/docker/userdata.tar")
// Expand tar file.
vmrun("-gu", B2DUser, "-gp", B2DPass, "runScriptInGuest", d.vmxPath(), "/bin/sh", "sudo /bin/mv /home/docker/userdata.tar /var/lib/boot2docker/userdata.tar && sudo tar xf /var/lib/boot2docker/userdata.tar -C /home/docker/ > /var/log/userdata.log 2>&1 && sudo chown -R docker:staff /home/docker")
// Enable Shared Folders
vmrun("-gu", B2DUser, "-gp", B2DPass, "enableSharedFolders", d.vmxPath())
var shareName, shareDir string // TODO configurable at some point
switch runtime.GOOS {
case "darwin":
shareName = "Users"
shareDir = "/Users"
// TODO "linux" and "windows"
}
if shareDir != "" {
if _, err := os.Stat(shareDir); err != nil && !os.IsNotExist(err) {
return err
} else if !os.IsNotExist(err) {
// add shared folder, create mountpoint and mount it.
vmrun("-gu", B2DUser, "-gp", B2DPass, "addSharedFolder", d.vmxPath(), shareName, shareDir)
vmrun("-gu", B2DUser, "-gp", B2DPass, "runScriptInGuest", d.vmxPath(), "/bin/sh", "sudo mkdir "+shareDir+" && sudo mount -t vmhgfs .host:/"+shareName+" "+shareDir)
}
}
return nil
}
func (d *Driver) Start() error {
log.Infof("Starting %s...", d.MachineName)
vmrun("start", d.vmxPath(), "nogui")
log.Debugf("Mounting Shared Folders...")
var shareName, shareDir string // TODO configurable at some point
switch runtime.GOOS {
case "darwin":
shareName = "Users"
shareDir = "/Users"
// TODO "linux" and "windows"
}
if shareDir != "" {
if _, err := os.Stat(shareDir); err != nil && !os.IsNotExist(err) {
return err
} else if !os.IsNotExist(err) {
// create mountpoint and mount shared folder
vmrun("-gu", B2DUser, "-gp", B2DPass, "runScriptInGuest", d.vmxPath(), "/bin/sh", "sudo mkdir "+shareDir+" && sudo mount -t vmhgfs .host:/"+shareName+" "+shareDir)
}
}
return nil
}
func (d *Driver) Stop() error {
log.Infof("Gracefully shutting down %s...", d.MachineName)
vmrun("stop", d.vmxPath(), "nogui")
return nil
}
func (d *Driver) Remove() error {
s, _ := d.GetState()
if s == state.Running {
if err := d.Kill(); err != nil {
return fmt.Errorf("Error stopping VM before deletion")
}
}
log.Infof("Deleting %s...", d.MachineName)
vmrun("deleteVM", d.vmxPath(), "nogui")
return nil
}
func (d *Driver) Restart() error {
log.Infof("Gracefully restarting %s...", d.MachineName)
vmrun("reset", d.vmxPath(), "nogui")
return nil
}
func (d *Driver) Kill() error {
log.Infof("Forcibly halting %s...", d.MachineName)
vmrun("stop", d.vmxPath(), "hard nogui")
return nil
}
func (d *Driver) Upgrade() error {
return fmt.Errorf("VMware Fusion does not currently support the upgrade operation")
}
func (d *Driver) vmxPath() string {
return d.ResolveStorePath(fmt.Sprintf("%s.vmx", d.MachineName))
}
func (d *Driver) vmdkPath() string {
return d.ResolveStorePath(fmt.Sprintf("%s.vmdk", d.MachineName))
}
func (d *Driver) getIPfromDHCPLease() (string, error) {
var vmxfh *os.File
var dhcpfh *os.File
var vmxcontent []byte
var dhcpcontent []byte
var macaddr string
var err error
var lastipmatch string
var currentip string
var lastleaseendtime time.Time
var currentleadeendtime time.Time
// DHCP lease table for NAT vmnet interface
var dhcpfile = "/var/db/vmware/vmnet-dhcpd-vmnet8.leases"
if vmxfh, err = os.Open(d.vmxPath()); err != nil {
return "", err
}
defer vmxfh.Close()
if vmxcontent, err = ioutil.ReadAll(vmxfh); err != nil {
return "", err
}
// Look for generatedAddress as we're passing a VMX with addressType = "generated".
vmxparse := regexp.MustCompile(`^ethernet0.generatedAddress\s*=\s*"(.*?)"\s*$`)
for _, line := range strings.Split(string(vmxcontent), "\n") {
if matches := vmxparse.FindStringSubmatch(line); matches == nil {
continue
} else {
macaddr = strings.ToLower(matches[1])
}
}
if macaddr == "" {
return "", fmt.Errorf("couldn't find MAC address in VMX file %s", d.vmxPath())
}
log.Debugf("MAC address in VMX: %s", macaddr)
if dhcpfh, err = os.Open(dhcpfile); err != nil {
return "", err
}
defer dhcpfh.Close()
if dhcpcontent, err = ioutil.ReadAll(dhcpfh); err != nil {
return "", err
}
// Get the IP from the lease table.
leaseip := regexp.MustCompile(`^lease (.+?) {$`)
// Get the lease end date time.
leaseend := regexp.MustCompile(`^\s*ends \d (.+?);$`)
// Get the MAC address associated.
leasemac := regexp.MustCompile(`^\s*hardware ethernet (.+?);$`)
for _, line := range strings.Split(string(dhcpcontent), "\n") {
if matches := leaseip.FindStringSubmatch(line); matches != nil {
lastipmatch = matches[1]
continue
}
if matches := leaseend.FindStringSubmatch(line); matches != nil {
lastleaseendtime, _ = time.Parse("2006/01/02 15:04:05", matches[1])
continue
}
if matches := leasemac.FindStringSubmatch(line); matches != nil && matches[1] == macaddr && currentleadeendtime.Before(lastleaseendtime) {
currentip = lastipmatch
currentleadeendtime = lastleaseendtime
}
}
if currentip == "" {
return "", fmt.Errorf("IP not found for MAC %s in DHCP leases", macaddr)
}
log.Debugf("IP found in DHCP lease table: %s", currentip)
return currentip, nil
}
func (d *Driver) publicSSHKeyPath() string {
return d.GetSSHKeyPath() + ".pub"
}
// Make a boot2docker userdata.tar key bundle
func (d *Driver) generateKeyBundle() error {
log.Debugf("Creating Tar key bundle...")
magicString := "boot2docker, this is vmware speaking"
tf, err := os.Create(d.ResolveStorePath("userdata.tar"))
if err != nil {
return err
}
defer tf.Close()
var fileWriter = tf
tw := tar.NewWriter(fileWriter)
defer tw.Close()
// magicString first so we can figure out who originally wrote the tar.
file := &tar.Header{Name: magicString, Size: int64(len(magicString))}
if err := tw.WriteHeader(file); err != nil {
return err
}
if _, err := tw.Write([]byte(magicString)); err != nil {
return err
}
// .ssh/key.pub => authorized_keys
file = &tar.Header{Name: ".ssh", Typeflag: tar.TypeDir, Mode: 0700}
if err := tw.WriteHeader(file); err != nil {
return err
}
pubKey, err := ioutil.ReadFile(d.publicSSHKeyPath())
if err != nil {
return err
}
file = &tar.Header{Name: ".ssh/authorized_keys", Size: int64(len(pubKey)), Mode: 0644}
if err := tw.WriteHeader(file); err != nil {
return err
}
if _, err := tw.Write([]byte(pubKey)); err != nil {
return err
}
file = &tar.Header{Name: ".ssh/authorized_keys2", Size: int64(len(pubKey)), Mode: 0644}
if err := tw.WriteHeader(file); err != nil {
return err
}
if _, err := tw.Write([]byte(pubKey)); err != nil {
return err
}
if err := tw.Close(); err != nil {
return err
}
return nil
}