/* * Copyright 2014 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package vmwarevsphere import ( "archive/tar" "fmt" "io" "io/ioutil" "net/http" "os" "path" "path/filepath" "strings" "time" log "github.com/Sirupsen/logrus" "github.com/codegangsta/cli" "github.com/docker/machine/drivers" "github.com/docker/machine/drivers/vmwarevsphere/errors" "github.com/docker/machine/provider" "github.com/docker/machine/ssh" "github.com/docker/machine/state" "github.com/docker/machine/utils" ) const ( DATASTORE_DIR = "boot2docker-iso" isoFilename = "boot2docker-1.5.0-GH747.iso" B2D_ISO_NAME = isoFilename DEFAULT_CPU_NUMBER = 2 B2D_USER = "docker" B2D_PASS = "tcuser" ) type Driver struct { MachineName string SSHUser string SSHPort int CPU int Memory int DiskSize int Boot2DockerURL string IP string Username string Password string Network string Datastore string Datacenter string Pool string HostIP string StorePath string ISO string CaCertPath string PrivateKeyPath string SwarmMaster bool SwarmHost string SwarmDiscovery string storePath string } func init() { drivers.Register("vmwarevsphere", &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.IntFlag{ EnvVar: "VSPHERE_CPU_COUNT", Name: "vmwarevsphere-cpu-count", Usage: "vSphere CPU number for docker VM", Value: 2, }, cli.IntFlag{ EnvVar: "VSPHERE_MEMORY_SIZE", Name: "vmwarevsphere-memory-size", Usage: "vSphere size of memory for docker VM (in MB)", Value: 2048, }, cli.IntFlag{ EnvVar: "VSPHERE_DISK_SIZE", Name: "vmwarevsphere-disk-size", Usage: "vSphere size of disk for docker VM (in MB)", Value: 20000, }, cli.StringFlag{ EnvVar: "VSPHERE_BOOT2DOCKER_URL", Name: "vmwarevsphere-boot2docker-url", Usage: "vSphere URL for boot2docker image", }, cli.StringFlag{ EnvVar: "VSPHERE_VCENTER", Name: "vmwarevsphere-vcenter", Usage: "vSphere IP/hostname for vCenter", }, cli.StringFlag{ EnvVar: "VSPHERE_USERNAME", Name: "vmwarevsphere-username", Usage: "vSphere username", }, cli.StringFlag{ EnvVar: "VSPHERE_PASSWORD", Name: "vmwarevsphere-password", Usage: "vSphere password", }, cli.StringFlag{ EnvVar: "VSPHERE_NETWORK", Name: "vmwarevsphere-network", Usage: "vSphere network where the docker VM will be attached", }, cli.StringFlag{ EnvVar: "VSPHERE_DATASTORE", Name: "vmwarevsphere-datastore", Usage: "vSphere datastore for docker VM", }, cli.StringFlag{ EnvVar: "VSPHERE_DATACENTER", Name: "vmwarevsphere-datacenter", Usage: "vSphere datacenter for docker VM", }, cli.StringFlag{ EnvVar: "VSPHERE_POOL", Name: "vmwarevsphere-pool", Usage: "vSphere resource pool for docker VM", }, cli.StringFlag{ EnvVar: "VSPHERE_COMPUTE_IP", Name: "vmwarevsphere-compute-ip", Usage: "vSphere compute host IP where the docker VM will be instantiated", }, } } 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) GetProviderType() provider.ProviderType { return provider.Local } func (d *Driver) DriverName() string { return "vmwarevsphere" } func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { d.SSHUser = "docker" d.SSHPort = 22 d.CPU = flags.Int("vmwarevsphere-cpu-count") d.Memory = flags.Int("vmwarevsphere-memory-size") d.DiskSize = flags.Int("vmwarevsphere-disk-size") d.Boot2DockerURL = flags.String("vmwarevsphere-boot2docker-url") d.IP = flags.String("vmwarevsphere-vcenter") d.Username = flags.String("vmwarevsphere-username") d.Password = flags.String("vmwarevsphere-password") d.Network = flags.String("vmwarevsphere-network") d.Datastore = flags.String("vmwarevsphere-datastore") d.Datacenter = flags.String("vmwarevsphere-datacenter") d.Pool = flags.String("vmwarevsphere-pool") d.HostIP = flags.String("vmwarevsphere-compute-ip") d.SwarmMaster = flags.Bool("swarm-master") d.SwarmHost = flags.String("swarm-host") d.SwarmDiscovery = flags.String("swarm-discovery") imgPath := utils.GetMachineCacheDir() commonIsoPath := filepath.Join(imgPath, isoFilename) d.ISO = path.Join(commonIsoPath) return nil } func (d *Driver) GetURL() (string, error) { ip, _ := d.GetIP() if ip == "" { return "", nil } return fmt.Sprintf("tcp://%s:2376", ip), nil } func (d *Driver) GetIP() (string, error) { status, err := d.GetState() if status != state.Running { return "", errors.NewInvalidStateError(d.MachineName) } vcConn := NewVcConn(d) rawIp, err := vcConn.VmFetchIp() if err != nil { return "", err } ip := strings.Trim(strings.Split(rawIp, "\n")[0], " ") return ip, nil } func (d *Driver) GetState() (state.State, error) { vcConn := NewVcConn(d) stdout, err := vcConn.VmInfo() if err != nil { return state.None, err } if strings.Contains(stdout, "poweredOn") { return state.Running, nil } else if strings.Contains(stdout, "poweredOff") { return state.Stopped, nil } return state.None, nil } func (d *Driver) PreCreateCheck() error { return nil } // the current implementation does the following: // 1. check whether the docker directory contains the boot2docker ISO // 2. generate an SSH keypair and bundle it in a tar. // 3. create a virtual machine with the boot2docker ISO mounted; // 4. reconfigure the virtual machine network and disk size; func (d *Driver) Create() error { if err := d.checkVsphereConfig(); err != nil { return err } var ( isoURL string ) b2dutils := utils.NewB2dUtils("", "") imgPath := utils.GetMachineCacheDir() commonIsoPath := filepath.Join(imgPath, isoFilename) // just in case boot2docker.iso has been manually deleted if _, err := os.Stat(imgPath); os.IsNotExist(err) { if err := os.Mkdir(imgPath, 0700); err != nil { return err } } if d.Boot2DockerURL != "" { isoURL = d.Boot2DockerURL log.Infof("Downloading boot2docker.iso from %s...", isoURL) if err := b2dutils.DownloadISO(d.storePath, isoFilename, isoURL); err != nil { return err } } else { // TODO: until vmw tools are merged into b2d master // we will use the iso from the vmware team //// todo: check latest release URL, download if it's new //// until then always use "latest" //isoURL, err = b2dutils.GetLatestBoot2DockerReleaseURL() //if err != nil { // log.Warnf("Unable to check for the latest release: %s", err) //} // see https://github.com/boot2docker/boot2docker/pull/747 isoURL := "https://github.com/cloudnativeapps/boot2docker/releases/download/1.5.0-GH747/boot2docker-1.5.0-GH747.iso" if _, err := os.Stat(commonIsoPath); os.IsNotExist(err) { log.Infof("Downloading boot2docker.iso to %s...", commonIsoPath) // just in case boot2docker.iso has been manually deleted if _, err := os.Stat(imgPath); os.IsNotExist(err) { if err := os.Mkdir(imgPath, 0700); err != nil { return err } } if err := b2dutils.DownloadISO(imgPath, isoFilename, isoURL); err != nil { return err } } isoDest := filepath.Join(d.storePath, isoFilename) if err := utils.CopyFile(commonIsoPath, isoDest); err != nil { return err } } log.Infof("Generating SSH Keypair...") if err := ssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil { return err } vcConn := NewVcConn(d) log.Infof("Uploading Boot2docker ISO ...") if err := vcConn.DatastoreMkdir(DATASTORE_DIR); err != nil { return err } if _, err := os.Stat(d.ISO); os.IsNotExist(err) { log.Errorf("Unable to find boot2docker ISO at %s", d.ISO) return errors.NewIncompleteVsphereConfigError(d.ISO) } if err := vcConn.DatastoreUpload(d.ISO); err != nil { return err } isoPath := fmt.Sprintf("%s/%s", DATASTORE_DIR, isoFilename) if err := vcConn.VmCreate(isoPath); err != nil { return err } log.Infof("Configuring the virtual machine %s... ", d.MachineName) if err := vcConn.VmDiskCreate(); err != nil { return err } if err := vcConn.VmAttachNetwork(); err != nil { return err } if err := d.Start(); err != nil { return err } // Generate a tar keys bundle if err := d.generateKeyBundle(); err != nil { return err } // Copy SSH keys bundle if err := vcConn.GuestUpload(B2D_USER, B2D_PASS, path.Join(d.storePath, "userdata.tar"), "/home/docker/userdata.tar"); err != nil { return err } // Expand tar file. if err := vcConn.GuestStart(B2D_USER, B2D_PASS, "/usr/bin/sudo", "/bin/mv /home/docker/userdata.tar /var/lib/boot2docker/userdata.tar && /usr/bin/sudo tar xf /var/lib/boot2docker/userdata.tar -C /home/docker/ > /var/log/userdata.log 2>&1 && /usr/bin/sudo chown -R docker:staff /home/docker"); err != nil { return err } return nil } func (d *Driver) Start() error { machineState, err := d.GetState() if err != nil { return err } switch machineState { case state.Running: log.Infof("VM %s has already been started", d.MachineName) return nil case state.Stopped: // TODO add transactional or error handling in the following steps vcConn := NewVcConn(d) err := vcConn.VmPowerOn() if err != nil { return err } // this step waits for the vm to start and fetch its ip address; // this guarantees that the opem-vmtools has started working... _, err = vcConn.VmFetchIp() if err != nil { return err } return nil } return errors.NewInvalidStateError(d.MachineName) } func (d *Driver) Stop() error { vcConn := NewVcConn(d) if err := vcConn.VmShutdown(); err != nil { return err } return nil } func (d *Driver) Remove() error { machineState, err := d.GetState() if err != nil { return err } if machineState == state.Running { if err = d.Kill(); err != nil { return fmt.Errorf("can't stop VM: %s", err) } } vcConn := NewVcConn(d) if err = vcConn.VmDestroy(); err != nil { return err } return nil } func (d *Driver) Restart() error { if err := d.Stop(); err != nil { return err } // Check for 120 seconds for the machine to stop for i := 1; i <= 60; i++ { machineState, err := d.GetState() if err != nil { return err } if machineState == state.Running { log.Debugf("Not there yet %d/%d", i, 60) time.Sleep(2 * time.Second) continue } if machineState == state.Stopped { break } } machineState, err := d.GetState() // If the VM is still running after 120 seconds just kill it. if machineState == state.Running { if err = d.Kill(); err != nil { return fmt.Errorf("can't stop VM: %s", err) } } return d.Start() } func (d *Driver) Kill() error { vcConn := NewVcConn(d) if err := vcConn.VmPowerOff(); err != nil { return err } return nil } func (d *Driver) Upgrade() error { return fmt.Errorf("upgrade is not supported for vsphere driver at this moment") } func (d *Driver) publicSSHKeyPath() string { return d.GetSSHKeyPath() + ".pub" } func (d *Driver) checkVsphereConfig() error { if d.IP == "" { return errors.NewIncompleteVsphereConfigError("vSphere IP") } if d.Username == "" { return errors.NewIncompleteVsphereConfigError("vSphere username") } if d.Password == "" { return errors.NewIncompleteVsphereConfigError("vSphere password") } if d.Network == "" { return errors.NewIncompleteVsphereConfigError("vSphere network") } if d.Datastore == "" { return errors.NewIncompleteVsphereConfigError("vSphere datastore") } if d.Datacenter == "" { return errors.NewIncompleteVsphereConfigError("vSphere datacenter") } return nil } // Download boot2docker ISO image for the given tag and save it at dest. func downloadISO(dir, file, url string) error { rsp, err := http.Get(url) if err != nil { return err } defer rsp.Body.Close() // Download to a temp file first then rename it to avoid partial download. f, err := ioutil.TempFile(dir, file+".tmp") if err != nil { return err } defer os.Remove(f.Name()) if _, err := io.Copy(f, rsp.Body); err != nil { // TODO: display download progress? return err } if err := f.Close(); err != nil { return err } if err := os.Rename(f.Name(), path.Join(dir, file)); err != nil { return err } return nil } // 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 io.WriteCloser = 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 }