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