github.com/mmcquillan/packer@v1.1.1-0.20171009221028-c85cf0483a5d/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  	commonssh "github.com/hashicorp/packer/common/ssh"
    18  	"github.com/hashicorp/packer/communicator/ssh"
    19  	"github.com/hashicorp/packer/packer"
    20  	"github.com/mitchellh/multistep"
    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  		//intentionally not checking for error since poweron may fail specially after initial VM registration
    69  		d.sh("vim-cmd", "vmsvc/power.on", d.vmId)
    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  	if err != nil {
   169  		return "", err
   170  	}
   171  	defer conn.Close()
   172  
   173  	host, _, err := net.SplitHostPort(conn.LocalAddr().String())
   174  	return host, err
   175  }
   176  
   177  func (d *ESX5Driver) VNCAddress(_ string, 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  	vncTimeout := time.Duration(15 * time.Second)
   202  	envTimeout := os.Getenv("PACKER_ESXI_VNC_PROBE_TIMEOUT")
   203  	if envTimeout != "" {
   204  		if parsedTimeout, err := time.ParseDuration(envTimeout); err != nil {
   205  			log.Printf("Error parsing PACKER_ESXI_VNC_PROBE_TIMEOUT. Falling back to default (15s). %s", err)
   206  		} else {
   207  			vncTimeout = parsedTimeout
   208  		}
   209  	}
   210  
   211  	for port := portMin; port <= portMax; port++ {
   212  		if _, ok := listenPorts[fmt.Sprintf("%d", port)]; ok {
   213  			log.Printf("Port %d in use", port)
   214  			continue
   215  		}
   216  		address := fmt.Sprintf("%s:%d", d.Host, port)
   217  		log.Printf("Trying address: %s...", address)
   218  		l, err := net.DialTimeout("tcp", address, vncTimeout)
   219  
   220  		if err != nil {
   221  			if e, ok := err.(*net.OpError); ok {
   222  				if e.Timeout() {
   223  					log.Printf("Timeout connecting to: %s (check firewall rules)", address)
   224  				} else {
   225  					vncPort = port
   226  					break
   227  				}
   228  			}
   229  		} else {
   230  			defer l.Close()
   231  		}
   232  	}
   233  
   234  	if vncPort == 0 {
   235  		err := fmt.Errorf("Unable to find available VNC port between %d and %d",
   236  			portMin, portMax)
   237  		return d.Host, vncPort, err
   238  	}
   239  
   240  	return d.Host, vncPort, nil
   241  }
   242  
   243  // UpdateVMX, adds the VNC port to the VMX data.
   244  func (ESX5Driver) UpdateVMX(_, password string, port uint, data map[string]string) {
   245  	// Do not set remotedisplay.vnc.ip - this breaks ESXi.
   246  	data["remotedisplay.vnc.enabled"] = "TRUE"
   247  	data["remotedisplay.vnc.port"] = fmt.Sprintf("%d", port)
   248  	if len(password) > 0 {
   249  		data["remotedisplay.vnc.password"] = password
   250  	}
   251  }
   252  
   253  func (d *ESX5Driver) CommHost(state multistep.StateBag) (string, error) {
   254  	config := state.Get("config").(*Config)
   255  	sshc := config.SSHConfig.Comm
   256  	port := sshc.SSHPort
   257  	if sshc.Type == "winrm" {
   258  		port = sshc.WinRMPort
   259  	}
   260  
   261  	if address := config.CommConfig.Host(); address != "" {
   262  		return address, nil
   263  	}
   264  
   265  	r, err := d.esxcli("network", "vm", "list")
   266  	if err != nil {
   267  		return "", err
   268  	}
   269  
   270  	record, err := r.find("Name", config.VMName)
   271  	if err != nil {
   272  		return "", err
   273  	}
   274  	wid := record["WorldID"]
   275  	if wid == "" {
   276  		return "", errors.New("VM WorldID not found")
   277  	}
   278  
   279  	r, err = d.esxcli("network", "vm", "port", "list", "-w", wid)
   280  	if err != nil {
   281  		return "", err
   282  	}
   283  
   284  	// Loop through interfaces
   285  	for {
   286  		record, err = r.read()
   287  		if err == io.EOF {
   288  			break
   289  		}
   290  		if err != nil {
   291  			return "", err
   292  		}
   293  
   294  		if record["IPAddress"] == "0.0.0.0" {
   295  			continue
   296  		}
   297  		// When multiple NICs are connected to the same network, choose
   298  		// one that has a route back. This Dial should ensure that.
   299  		conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", record["IPAddress"], port), 2*time.Second)
   300  		if err != nil {
   301  			if e, ok := err.(*net.OpError); ok {
   302  				if e.Timeout() {
   303  					log.Printf("Timeout connecting to %s", record["IPAddress"])
   304  					continue
   305  				}
   306  			}
   307  		} else {
   308  			defer conn.Close()
   309  			address := record["IPAddress"]
   310  			return address, nil
   311  		}
   312  	}
   313  	return "", errors.New("No interface on the VM has an IP address ready")
   314  }
   315  
   316  //-------------------------------------------------------------------
   317  // OutputDir implementation
   318  //-------------------------------------------------------------------
   319  
   320  func (d *ESX5Driver) DirExists() (bool, error) {
   321  	err := d.sh("test", "-e", d.outputDir)
   322  	return err == nil, nil
   323  }
   324  
   325  func (d *ESX5Driver) ListFiles() ([]string, error) {
   326  	stdout, err := d.ssh("ls -1p "+d.outputDir, nil)
   327  	if err != nil {
   328  		return nil, err
   329  	}
   330  
   331  	files := make([]string, 0, 10)
   332  	reader := bufio.NewReader(stdout)
   333  	for {
   334  		line, _, err := reader.ReadLine()
   335  		if err == io.EOF {
   336  			break
   337  		}
   338  		if line[len(line)-1] == '/' {
   339  			continue
   340  		}
   341  
   342  		files = append(files, filepath.ToSlash(filepath.Join(d.outputDir, string(line))))
   343  	}
   344  
   345  	return files, nil
   346  }
   347  
   348  func (d *ESX5Driver) MkdirAll() error {
   349  	return d.mkdir(d.outputDir)
   350  }
   351  
   352  func (d *ESX5Driver) Remove(path string) error {
   353  	return d.sh("rm", path)
   354  }
   355  
   356  func (d *ESX5Driver) RemoveAll() error {
   357  	return d.sh("rm", "-rf", d.outputDir)
   358  }
   359  
   360  func (d *ESX5Driver) SetOutputDir(path string) {
   361  	d.outputDir = d.datastorePath(path)
   362  }
   363  
   364  func (d *ESX5Driver) String() string {
   365  	return d.outputDir
   366  }
   367  
   368  func (d *ESX5Driver) datastorePath(path string) string {
   369  	dirPath := filepath.Dir(path)
   370  	return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.Datastore, dirPath, filepath.Base(path)))
   371  }
   372  
   373  func (d *ESX5Driver) cachePath(path string) string {
   374  	return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.CacheDatastore, d.CacheDirectory, filepath.Base(path)))
   375  }
   376  
   377  func (d *ESX5Driver) connect() error {
   378  	address := fmt.Sprintf("%s:%d", d.Host, d.Port)
   379  
   380  	auth := []gossh.AuthMethod{
   381  		gossh.Password(d.Password),
   382  		gossh.KeyboardInteractive(
   383  			ssh.PasswordKeyboardInteractive(d.Password)),
   384  	}
   385  
   386  	if d.PrivateKey != "" {
   387  		signer, err := commonssh.FileSigner(d.PrivateKey)
   388  		if err != nil {
   389  			return err
   390  		}
   391  
   392  		auth = append(auth, gossh.PublicKeys(signer))
   393  	}
   394  
   395  	sshConfig := &ssh.Config{
   396  		Connection: ssh.ConnectFunc("tcp", address),
   397  		SSHConfig: &gossh.ClientConfig{
   398  			User:            d.Username,
   399  			Auth:            auth,
   400  			HostKeyCallback: gossh.InsecureIgnoreHostKey(),
   401  		},
   402  	}
   403  
   404  	comm, err := ssh.New(address, sshConfig)
   405  	if err != nil {
   406  		return err
   407  	}
   408  
   409  	d.comm = comm
   410  	return nil
   411  }
   412  
   413  func (d *ESX5Driver) checkSystemVersion() error {
   414  	r, err := d.esxcli("system", "version", "get")
   415  	if err != nil {
   416  		return err
   417  	}
   418  
   419  	record, err := r.read()
   420  	if err != nil {
   421  		return err
   422  	}
   423  
   424  	log.Printf("Connected to %s %s %s", record["Product"],
   425  		record["Version"], record["Build"])
   426  	return nil
   427  }
   428  
   429  func (d *ESX5Driver) checkGuestIPHackEnabled() error {
   430  	r, err := d.esxcli("system", "settings", "advanced", "list", "-o", "/Net/GuestIPHack")
   431  	if err != nil {
   432  		return err
   433  	}
   434  
   435  	record, err := r.read()
   436  	if err != nil {
   437  		return err
   438  	}
   439  
   440  	if record["IntValue"] != "1" {
   441  		return errors.New(
   442  			"GuestIPHack is required, enable by running this on the ESX machine:\n" +
   443  				"esxcli system settings advanced set -o /Net/GuestIPHack -i 1")
   444  	}
   445  
   446  	return nil
   447  }
   448  
   449  func (d *ESX5Driver) mkdir(path string) error {
   450  	return d.sh("mkdir", "-p", path)
   451  }
   452  
   453  func (d *ESX5Driver) upload(dst, src string) error {
   454  	f, err := os.Open(src)
   455  	if err != nil {
   456  		return err
   457  	}
   458  	defer f.Close()
   459  	return d.comm.Upload(dst, f, nil)
   460  }
   461  
   462  func (d *ESX5Driver) verifyChecksum(ctype string, hash string, file string) bool {
   463  	if ctype == "none" {
   464  		if err := d.sh("stat", file); err != nil {
   465  			return false
   466  		}
   467  	} else {
   468  		stdin := bytes.NewBufferString(fmt.Sprintf("%s  %s", hash, file))
   469  		_, err := d.run(stdin, fmt.Sprintf("%ssum", ctype), "-c")
   470  		if err != nil {
   471  			return false
   472  		}
   473  	}
   474  
   475  	return true
   476  }
   477  
   478  func (d *ESX5Driver) ssh(command string, stdin io.Reader) (*bytes.Buffer, error) {
   479  	var stdout, stderr bytes.Buffer
   480  
   481  	cmd := &packer.RemoteCmd{
   482  		Command: command,
   483  		Stdout:  &stdout,
   484  		Stderr:  &stderr,
   485  		Stdin:   stdin,
   486  	}
   487  
   488  	err := d.comm.Start(cmd)
   489  	if err != nil {
   490  		return nil, err
   491  	}
   492  
   493  	cmd.Wait()
   494  
   495  	if cmd.ExitStatus != 0 {
   496  		err = fmt.Errorf("'%s'\n\nStdout: %s\n\nStderr: %s",
   497  			cmd.Command, stdout.String(), stderr.String())
   498  		return nil, err
   499  	}
   500  
   501  	return &stdout, nil
   502  }
   503  
   504  func (d *ESX5Driver) run(stdin io.Reader, args ...string) (string, error) {
   505  	stdout, err := d.ssh(strings.Join(args, " "), stdin)
   506  	if err != nil {
   507  		return "", err
   508  	}
   509  	return stdout.String(), nil
   510  }
   511  
   512  func (d *ESX5Driver) sh(args ...string) error {
   513  	_, err := d.run(nil, args...)
   514  	return err
   515  }
   516  
   517  func (d *ESX5Driver) esxcli(args ...string) (*esxcliReader, error) {
   518  	stdout, err := d.ssh("esxcli --formatter csv "+strings.Join(args, " "), nil)
   519  	if err != nil {
   520  		return nil, err
   521  	}
   522  	r := csv.NewReader(bytes.NewReader(stdout.Bytes()))
   523  	r.TrailingComma = true
   524  	header, err := r.Read()
   525  	if err != nil {
   526  		return nil, err
   527  	}
   528  	return &esxcliReader{r, header}, nil
   529  }
   530  
   531  type esxcliReader struct {
   532  	cr     *csv.Reader
   533  	header []string
   534  }
   535  
   536  func (r *esxcliReader) read() (map[string]string, error) {
   537  	fields, err := r.cr.Read()
   538  
   539  	if err != nil {
   540  		return nil, err
   541  	}
   542  
   543  	record := map[string]string{}
   544  	for i, v := range fields {
   545  		record[r.header[i]] = v
   546  	}
   547  
   548  	return record, nil
   549  }
   550  
   551  func (r *esxcliReader) find(key, val string) (map[string]string, error) {
   552  	for {
   553  		record, err := r.read()
   554  		if err != nil {
   555  			return nil, err
   556  		}
   557  		if record[key] == val {
   558  			return record, nil
   559  		}
   560  	}
   561  }