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