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