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 }