/* * Copyright 2014 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package vmwarefusion import ( "archive/tar" "fmt" "io/ioutil" "os" "path" "path/filepath" "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 { MachineName string IPAddress string Memory int DiskSize int CPU int ISO string Boot2DockerURL string CaCertPath string PrivateKeyPath string SwarmMaster bool SwarmHost string SwarmDiscovery string CPUS int SSHUser string SSHPort int storePath string } 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) { return &Driver{MachineName: machineName, storePath: storePath, CaCertPath: caCert, PrivateKeyPath: privateKey}, nil } func (d *Driver) AuthorizePort(ports []*drivers.Port) error { return nil } func (d *Driver) DeauthorizePort(ports []*drivers.Port) error { return nil } func (d *Driver) GetMachineName() string { return d.MachineName } func (d *Driver) GetSSHHostname() (string, error) { return d.GetIP() } func (d *Driver) GetSSHKeyPath() string { return filepath.Join(d.storePath, "id_rsa") } func (d *Driver) GetSSHPort() (int, error) { if d.SSHPort == 0 { d.SSHPort = 22 } return d.SSHPort, nil } 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 = path.Join(d.storePath, 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.storePath, 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 := filepath.Join(d.storePath, 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(), path.Join(d.storePath, "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 path.Join(d.storePath, fmt.Sprintf("%s.vmx", d.MachineName)) } func (d *Driver) vmdkPath() string { return path.Join(d.storePath, 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(path.Join(d.storePath, "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 }