github.com/ttysteale/packer@v0.8.2-0.20150708160520-e5f8ea386ed8/builder/vmware/iso/driver_esx5.go (about)

     1  package iso
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"encoding/csv"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"log"
    11  	"net"
    12  	"os"
    13  	"path/filepath"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/mitchellh/multistep"
    18  	"github.com/mitchellh/packer/communicator/ssh"
    19  	"github.com/mitchellh/packer/packer"
    20  	gossh "golang.org/x/crypto/ssh"
    21  )
    22  
    23  // ESX5 driver talks to an ESXi5 hypervisor remotely over SSH to build
    24  // virtual machines. This driver can only manage one machine at a time.
    25  type ESX5Driver struct {
    26  	Host           string
    27  	Port           uint
    28  	Username       string
    29  	Password       string
    30  	Datastore      string
    31  	CacheDatastore string
    32  	CacheDirectory string
    33  
    34  	comm      packer.Communicator
    35  	outputDir string
    36  	vmId      string
    37  }
    38  
    39  func (d *ESX5Driver) Clone(dst, src string) error {
    40  	return errors.New("Cloning is not supported with the ESX driver.")
    41  }
    42  
    43  func (d *ESX5Driver) CompactDisk(diskPathLocal string) error {
    44  	return nil
    45  }
    46  
    47  func (d *ESX5Driver) CreateDisk(diskPathLocal string, size string, typeId string) error {
    48  	diskPath := d.datastorePath(diskPathLocal)
    49  	return d.sh("vmkfstools", "-c", size, "-d", typeId, "-a", "lsilogic", diskPath)
    50  }
    51  
    52  func (d *ESX5Driver) IsRunning(string) (bool, error) {
    53  	state, err := d.run(nil, "vim-cmd", "vmsvc/power.getstate", d.vmId)
    54  	if err != nil {
    55  		return false, err
    56  	}
    57  	return strings.Contains(state, "Powered on"), nil
    58  }
    59  
    60  func (d *ESX5Driver) ReloadVM() error {
    61  	return d.sh("vim-cmd", "vmsvc/reload", d.vmId)
    62  }
    63  
    64  func (d *ESX5Driver) Start(vmxPathLocal string, headless bool) error {
    65  	for i := 0; i < 20; i++ {
    66  		err := d.sh("vim-cmd", "vmsvc/power.on", d.vmId)
    67  		if err != nil {
    68  			return err
    69  		}
    70  		time.Sleep((time.Duration(i) * time.Second) + 1)
    71  		running, err := d.IsRunning(vmxPathLocal)
    72  		if err != nil {
    73  			return err
    74  		}
    75  		if running {
    76  			return nil
    77  		}
    78  	}
    79  	return errors.New("Retry limit exceeded")
    80  }
    81  
    82  func (d *ESX5Driver) Stop(vmxPathLocal string) error {
    83  	return d.sh("vim-cmd", "vmsvc/power.off", d.vmId)
    84  }
    85  
    86  func (d *ESX5Driver) Register(vmxPathLocal string) error {
    87  	vmxPath := filepath.ToSlash(filepath.Join(d.outputDir, filepath.Base(vmxPathLocal)))
    88  	if err := d.upload(vmxPath, vmxPathLocal); err != nil {
    89  		return err
    90  	}
    91  	r, err := d.run(nil, "vim-cmd", "solo/registervm", vmxPath)
    92  	if err != nil {
    93  		return err
    94  	}
    95  	d.vmId = strings.TrimRight(r, "\n")
    96  	return nil
    97  }
    98  
    99  func (d *ESX5Driver) SuppressMessages(vmxPath string) error {
   100  	return nil
   101  }
   102  
   103  func (d *ESX5Driver) Unregister(vmxPathLocal string) error {
   104  	return d.sh("vim-cmd", "vmsvc/unregister", d.vmId)
   105  }
   106  
   107  func (d *ESX5Driver) UploadISO(localPath string, checksum string, checksumType string) (string, error) {
   108  	finalPath := d.cachePath(localPath)
   109  	if err := d.mkdir(filepath.ToSlash(filepath.Dir(finalPath))); err != nil {
   110  		return "", err
   111  	}
   112  
   113  	log.Printf("Verifying checksum of %s", finalPath)
   114  	if d.verifyChecksum(checksumType, checksum, finalPath) {
   115  		log.Println("Initial checksum matched, no upload needed.")
   116  		return finalPath, nil
   117  	}
   118  
   119  	if err := d.upload(finalPath, localPath); err != nil {
   120  		return "", err
   121  	}
   122  
   123  	return finalPath, nil
   124  }
   125  
   126  func (d *ESX5Driver) ToolsIsoPath(string) string {
   127  	return ""
   128  }
   129  
   130  func (d *ESX5Driver) ToolsInstall() error {
   131  	return d.sh("vim-cmd", "vmsvc/tools.install", d.vmId)
   132  }
   133  
   134  func (d *ESX5Driver) DhcpLeasesPath(string) string {
   135  	return ""
   136  }
   137  
   138  func (d *ESX5Driver) Verify() error {
   139  	checks := []func() error{
   140  		d.connect,
   141  		d.checkSystemVersion,
   142  		d.checkGuestIPHackEnabled,
   143  	}
   144  
   145  	for _, check := range checks {
   146  		if err := check(); err != nil {
   147  			return err
   148  		}
   149  	}
   150  
   151  	return nil
   152  }
   153  
   154  func (d *ESX5Driver) HostIP() (string, error) {
   155  	conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", d.Host, d.Port))
   156  	defer conn.Close()
   157  	if err != nil {
   158  		return "", err
   159  	}
   160  
   161  	host, _, err := net.SplitHostPort(conn.LocalAddr().String())
   162  	return host, err
   163  }
   164  
   165  func (d *ESX5Driver) VNCAddress(portMin, portMax uint) (string, uint, error) {
   166  	var vncPort uint
   167  
   168  	//Process ports ESXi is listening on to determine which are available
   169  	//This process does best effort to detect ports that are unavailable,
   170  	//it will ignore any ports listened to by only localhost
   171  	r, err := d.esxcli("network", "ip", "connection", "list")
   172  	if err != nil {
   173  		err = fmt.Errorf("Could not retrieve network information for ESXi: %v", err)
   174  		return "", 0, err
   175  	}
   176  
   177  	listenPorts := make(map[string]bool)
   178  	for record, err := r.read(); record != nil && err == nil; record, err = r.read() {
   179  		if record["State"] == "LISTEN" {
   180  			splitAddress := strings.Split(record["LocalAddress"], ":")
   181  			if splitAddress[0] != "127.0.0.1" {
   182  				port := splitAddress[len(splitAddress)-1]
   183  				log.Printf("ESXi listening on address %s, port %s unavailable for VNC", record["LocalAddress"], port)
   184  				listenPorts[port] = true
   185  			}
   186  		}
   187  	}
   188  
   189  	for port := portMin; port <= portMax; port++ {
   190  		if _, ok := listenPorts[fmt.Sprintf("%d", port)]; ok {
   191  			log.Printf("Port %d in use", port)
   192  			continue
   193  		}
   194  		address := fmt.Sprintf("%s:%d", d.Host, port)
   195  		log.Printf("Trying address: %s...", address)
   196  		l, err := net.DialTimeout("tcp", address, 1*time.Second)
   197  
   198  		if err != nil {
   199  			if e, ok := err.(*net.OpError); ok {
   200  				if e.Timeout() {
   201  					log.Printf("Timeout connecting to: %s (check firewall rules)", address)
   202  				} else {
   203  					vncPort = port
   204  					break
   205  				}
   206  			}
   207  		} else {
   208  			defer l.Close()
   209  		}
   210  	}
   211  
   212  	if vncPort == 0 {
   213  		err := fmt.Errorf("Unable to find available VNC port between %d and %d",
   214  			portMin, portMax)
   215  		return d.Host, vncPort, err
   216  	}
   217  
   218  	return d.Host, vncPort, nil
   219  }
   220  
   221  func (d *ESX5Driver) CommHost(state multistep.StateBag) (string, error) {
   222  	config := state.Get("config").(*Config)
   223  
   224  	if address, ok := state.GetOk("vm_address"); ok {
   225  		return address.(string), nil
   226  	}
   227  
   228  	r, err := d.esxcli("network", "vm", "list")
   229  	if err != nil {
   230  		return "", err
   231  	}
   232  
   233  	record, err := r.find("Name", config.VMName)
   234  	if err != nil {
   235  		return "", err
   236  	}
   237  	wid := record["WorldID"]
   238  	if wid == "" {
   239  		return "", errors.New("VM WorldID not found")
   240  	}
   241  
   242  	r, err = d.esxcli("network", "vm", "port", "list", "-w", wid)
   243  	if err != nil {
   244  		return "", err
   245  	}
   246  
   247  	record, err = r.read()
   248  	if err != nil {
   249  		return "", err
   250  	}
   251  
   252  	if record["IPAddress"] == "0.0.0.0" {
   253  		return "", errors.New("VM network port found, but no IP address")
   254  	}
   255  
   256  	address := record["IPAddress"]
   257  	state.Put("vm_address", address)
   258  	return address, nil
   259  }
   260  
   261  //-------------------------------------------------------------------
   262  // OutputDir implementation
   263  //-------------------------------------------------------------------
   264  
   265  func (d *ESX5Driver) DirExists() (bool, error) {
   266  	err := d.sh("test", "-e", d.outputDir)
   267  	return err == nil, nil
   268  }
   269  
   270  func (d *ESX5Driver) ListFiles() ([]string, error) {
   271  	stdout, err := d.ssh("ls -1p "+d.outputDir, nil)
   272  	if err != nil {
   273  		return nil, err
   274  	}
   275  
   276  	files := make([]string, 0, 10)
   277  	reader := bufio.NewReader(stdout)
   278  	for {
   279  		line, _, err := reader.ReadLine()
   280  		if err == io.EOF {
   281  			break
   282  		}
   283  		if line[len(line)-1] == '/' {
   284  			continue
   285  		}
   286  
   287  		files = append(files, filepath.ToSlash(filepath.Join(d.outputDir, string(line))))
   288  	}
   289  
   290  	return files, nil
   291  }
   292  
   293  func (d *ESX5Driver) MkdirAll() error {
   294  	return d.mkdir(d.outputDir)
   295  }
   296  
   297  func (d *ESX5Driver) Remove(path string) error {
   298  	return d.sh("rm", path)
   299  }
   300  
   301  func (d *ESX5Driver) RemoveAll() error {
   302  	return d.sh("rm", "-rf", d.outputDir)
   303  }
   304  
   305  func (d *ESX5Driver) SetOutputDir(path string) {
   306  	d.outputDir = d.datastorePath(path)
   307  }
   308  
   309  func (d *ESX5Driver) String() string {
   310  	return d.outputDir
   311  }
   312  
   313  func (d *ESX5Driver) datastorePath(path string) string {
   314  	dirPath := filepath.Dir(path)
   315  	return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.Datastore, dirPath, filepath.Base(path)))
   316  }
   317  
   318  func (d *ESX5Driver) cachePath(path string) string {
   319  	return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.CacheDatastore, d.CacheDirectory, filepath.Base(path)))
   320  }
   321  
   322  func (d *ESX5Driver) connect() error {
   323  	address := fmt.Sprintf("%s:%d", d.Host, d.Port)
   324  
   325  	auth := []gossh.AuthMethod{
   326  		gossh.Password(d.Password),
   327  		gossh.KeyboardInteractive(
   328  			ssh.PasswordKeyboardInteractive(d.Password)),
   329  	}
   330  
   331  	// TODO(dougm) KeyPath support
   332  	sshConfig := &ssh.Config{
   333  		Connection: ssh.ConnectFunc("tcp", address),
   334  		SSHConfig: &gossh.ClientConfig{
   335  			User: d.Username,
   336  			Auth: auth,
   337  		},
   338  	}
   339  
   340  	comm, err := ssh.New(address, sshConfig)
   341  	if err != nil {
   342  		return err
   343  	}
   344  
   345  	d.comm = comm
   346  	return nil
   347  }
   348  
   349  func (d *ESX5Driver) checkSystemVersion() error {
   350  	r, err := d.esxcli("system", "version", "get")
   351  	if err != nil {
   352  		return err
   353  	}
   354  
   355  	record, err := r.read()
   356  	if err != nil {
   357  		return err
   358  	}
   359  
   360  	log.Printf("Connected to %s %s %s", record["Product"],
   361  		record["Version"], record["Build"])
   362  	return nil
   363  }
   364  
   365  func (d *ESX5Driver) checkGuestIPHackEnabled() error {
   366  	r, err := d.esxcli("system", "settings", "advanced", "list", "-o", "/Net/GuestIPHack")
   367  	if err != nil {
   368  		return err
   369  	}
   370  
   371  	record, err := r.read()
   372  	if err != nil {
   373  		return err
   374  	}
   375  
   376  	if record["IntValue"] != "1" {
   377  		return errors.New(
   378  			"GuestIPHack is required, enable by running this on the ESX machine:\n" +
   379  				"esxcli system settings advanced set -o /Net/GuestIPHack -i 1")
   380  	}
   381  
   382  	return nil
   383  }
   384  
   385  func (d *ESX5Driver) mkdir(path string) error {
   386  	return d.sh("mkdir", "-p", path)
   387  }
   388  
   389  func (d *ESX5Driver) upload(dst, src string) error {
   390  	f, err := os.Open(src)
   391  	if err != nil {
   392  		return err
   393  	}
   394  	defer f.Close()
   395  	return d.comm.Upload(dst, f, nil)
   396  }
   397  
   398  func (d *ESX5Driver) verifyChecksum(ctype string, hash string, file string) bool {
   399  	if ctype == "none" {
   400  		if err := d.sh("stat", file); err != nil {
   401  			return false
   402  		}
   403  	} else {
   404  		stdin := bytes.NewBufferString(fmt.Sprintf("%s  %s", hash, file))
   405  		_, err := d.run(stdin, fmt.Sprintf("%ssum", ctype), "-c")
   406  		if err != nil {
   407  			return false
   408  		}
   409  	}
   410  
   411  	return true
   412  }
   413  
   414  func (d *ESX5Driver) ssh(command string, stdin io.Reader) (*bytes.Buffer, error) {
   415  	var stdout, stderr bytes.Buffer
   416  
   417  	cmd := &packer.RemoteCmd{
   418  		Command: command,
   419  		Stdout:  &stdout,
   420  		Stderr:  &stderr,
   421  		Stdin:   stdin,
   422  	}
   423  
   424  	err := d.comm.Start(cmd)
   425  	if err != nil {
   426  		return nil, err
   427  	}
   428  
   429  	cmd.Wait()
   430  
   431  	if cmd.ExitStatus != 0 {
   432  		err = fmt.Errorf("'%s'\n\nStdout: %s\n\nStderr: %s",
   433  			cmd.Command, stdout.String(), stderr.String())
   434  		return nil, err
   435  	}
   436  
   437  	return &stdout, nil
   438  }
   439  
   440  func (d *ESX5Driver) run(stdin io.Reader, args ...string) (string, error) {
   441  	stdout, err := d.ssh(strings.Join(args, " "), stdin)
   442  	if err != nil {
   443  		return "", err
   444  	}
   445  	return stdout.String(), nil
   446  }
   447  
   448  func (d *ESX5Driver) sh(args ...string) error {
   449  	_, err := d.run(nil, args...)
   450  	return err
   451  }
   452  
   453  func (d *ESX5Driver) esxcli(args ...string) (*esxcliReader, error) {
   454  	stdout, err := d.ssh("esxcli --formatter csv "+strings.Join(args, " "), nil)
   455  	if err != nil {
   456  		return nil, err
   457  	}
   458  	r := csv.NewReader(bytes.NewReader(stdout.Bytes()))
   459  	r.TrailingComma = true
   460  	header, err := r.Read()
   461  	if err != nil {
   462  		return nil, err
   463  	}
   464  	return &esxcliReader{r, header}, nil
   465  }
   466  
   467  type esxcliReader struct {
   468  	cr     *csv.Reader
   469  	header []string
   470  }
   471  
   472  func (r *esxcliReader) read() (map[string]string, error) {
   473  	fields, err := r.cr.Read()
   474  
   475  	if err != nil {
   476  		return nil, err
   477  	}
   478  
   479  	record := map[string]string{}
   480  	for i, v := range fields {
   481  		record[r.header[i]] = v
   482  	}
   483  
   484  	return record, nil
   485  }
   486  
   487  func (r *esxcliReader) find(key, val string) (map[string]string, error) {
   488  	for {
   489  		record, err := r.read()
   490  		if err != nil {
   491  			return nil, err
   492  		}
   493  		if record[key] == val {
   494  			return record, nil
   495  		}
   496  	}
   497  }