github.com/daniellockard/packer@v0.7.6-0.20141210173435-5a9390934716/builder/vmware/iso/driver_esx5.go (about)

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