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