github.phpd.cn/hashicorp/packer@v1.3.2/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 "strconv" 15 "strings" 16 "time" 17 18 vmwcommon "github.com/hashicorp/packer/builder/vmware/common" 19 "github.com/hashicorp/packer/communicator/ssh" 20 "github.com/hashicorp/packer/helper/multistep" 21 helperssh "github.com/hashicorp/packer/helper/ssh" 22 "github.com/hashicorp/packer/packer" 23 gossh "golang.org/x/crypto/ssh" 24 ) 25 26 // ESX5 driver talks to an ESXi5 hypervisor remotely over SSH to build 27 // virtual machines. This driver can only manage one machine at a time. 28 type ESX5Driver struct { 29 base vmwcommon.VmwareDriver 30 31 Host string 32 Port uint 33 Username string 34 Password string 35 PrivateKeyFile string 36 Datastore string 37 CacheDatastore string 38 CacheDirectory string 39 40 comm packer.Communicator 41 outputDir string 42 vmId string 43 } 44 45 func (d *ESX5Driver) Clone(dst, src string, linked bool) error { 46 return errors.New("Cloning is not supported with the ESX driver.") 47 } 48 49 func (d *ESX5Driver) CompactDisk(diskPathLocal string) error { 50 diskPath := d.datastorePath(diskPathLocal) 51 return d.sh("vmkfstools", "--punchzero", strconv.Quote(diskPath)) 52 } 53 54 func (d *ESX5Driver) CreateDisk(diskPathLocal string, size string, adapter_type string, typeId string) error { 55 diskPath := strconv.Quote(d.datastorePath(diskPathLocal)) 56 return d.sh("vmkfstools", "-c", size, "-d", typeId, "-a", adapter_type, diskPath) 57 } 58 59 func (d *ESX5Driver) IsRunning(string) (bool, error) { 60 state, err := d.run(nil, "vim-cmd", "vmsvc/power.getstate", d.vmId) 61 if err != nil { 62 return false, err 63 } 64 return strings.Contains(state, "Powered on"), nil 65 } 66 67 func (d *ESX5Driver) ReloadVM() error { 68 return d.sh("vim-cmd", "vmsvc/reload", d.vmId) 69 } 70 71 func (d *ESX5Driver) Start(vmxPathLocal string, headless bool) error { 72 for i := 0; i < 20; i++ { 73 //intentionally not checking for error since poweron may fail specially after initial VM registration 74 d.sh("vim-cmd", "vmsvc/power.on", d.vmId) 75 time.Sleep((time.Duration(i) * time.Second) + 1) 76 running, err := d.IsRunning(vmxPathLocal) 77 if err != nil { 78 return err 79 } 80 if running { 81 return nil 82 } 83 } 84 return errors.New("Retry limit exceeded") 85 } 86 87 func (d *ESX5Driver) Stop(vmxPathLocal string) error { 88 return d.sh("vim-cmd", "vmsvc/power.off", d.vmId) 89 } 90 91 func (d *ESX5Driver) Register(vmxPathLocal string) error { 92 vmxPath := filepath.ToSlash(filepath.Join(d.outputDir, filepath.Base(vmxPathLocal))) 93 if err := d.upload(vmxPath, vmxPathLocal); err != nil { 94 return err 95 } 96 r, err := d.run(nil, "vim-cmd", "solo/registervm", strconv.Quote(vmxPath)) 97 if err != nil { 98 return err 99 } 100 d.vmId = strings.TrimRight(r, "\n") 101 return nil 102 } 103 104 func (d *ESX5Driver) SuppressMessages(vmxPath string) error { 105 return nil 106 } 107 108 func (d *ESX5Driver) Unregister(vmxPathLocal string) error { 109 return d.sh("vim-cmd", "vmsvc/unregister", d.vmId) 110 } 111 112 func (d *ESX5Driver) Destroy() error { 113 return d.sh("vim-cmd", "vmsvc/destroy", d.vmId) 114 } 115 116 func (d *ESX5Driver) IsDestroyed() (bool, error) { 117 err := d.sh("test", "!", "-e", strconv.Quote(d.outputDir)) 118 if err != nil { 119 return false, err 120 } 121 return true, err 122 } 123 124 func (d *ESX5Driver) UploadISO(localPath string, checksum string, checksumType string) (string, error) { 125 finalPath := d.cachePath(localPath) 126 if err := d.mkdir(filepath.ToSlash(filepath.Dir(finalPath))); err != nil { 127 return "", err 128 } 129 130 log.Printf("Verifying checksum of %s", finalPath) 131 if d.verifyChecksum(checksumType, checksum, finalPath) { 132 log.Println("Initial checksum matched, no upload needed.") 133 return finalPath, nil 134 } 135 136 if err := d.upload(finalPath, localPath); err != nil { 137 return "", err 138 } 139 140 return finalPath, nil 141 } 142 143 func (d *ESX5Driver) RemoveCache(localPath string) error { 144 finalPath := d.cachePath(localPath) 145 log.Printf("Removing remote cache path %s (local %s)", finalPath, localPath) 146 return d.sh("rm", "-f", strconv.Quote(finalPath)) 147 } 148 149 func (d *ESX5Driver) ToolsIsoPath(string) string { 150 return "" 151 } 152 153 func (d *ESX5Driver) ToolsInstall() error { 154 return d.sh("vim-cmd", "vmsvc/tools.install", d.vmId) 155 } 156 157 func (d *ESX5Driver) Verify() error { 158 // Ensure that NetworkMapper is nil, since the mapping of device<->network 159 // is handled by ESX and thus can't be performed by packer unless we 160 // query things. 161 162 // FIXME: If we want to expose the network devices to the user, then we can 163 // probably use esxcli to enumerate the portgroup and switchId 164 d.base.NetworkMapper = nil 165 166 // Be safe/friendly and overwrite the rest of the utility functions with 167 // log functions despite the fact that these shouldn't be called anyways. 168 d.base.DhcpLeasesPath = func(device string) string { 169 log.Printf("Unexpected error, ESX5 driver attempted to call DhcpLeasesPath(%#v)\n", device) 170 return "" 171 } 172 d.base.DhcpConfPath = func(device string) string { 173 log.Printf("Unexpected error, ESX5 driver attempted to call DhcpConfPath(%#v)\n", device) 174 return "" 175 } 176 d.base.VmnetnatConfPath = func(device string) string { 177 log.Printf("Unexpected error, ESX5 driver attempted to call VmnetnatConfPath(%#v)\n", device) 178 return "" 179 } 180 181 checks := []func() error{ 182 d.connect, 183 d.checkSystemVersion, 184 d.checkGuestIPHackEnabled, 185 } 186 187 for _, check := range checks { 188 if err := check(); err != nil { 189 return err 190 } 191 } 192 return nil 193 } 194 195 func (d *ESX5Driver) HostIP(multistep.StateBag) (string, error) { 196 conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", d.Host, d.Port)) 197 if err != nil { 198 return "", err 199 } 200 defer conn.Close() 201 202 host, _, err := net.SplitHostPort(conn.LocalAddr().String()) 203 return host, err 204 } 205 206 func (d *ESX5Driver) GuestIP(multistep.StateBag) (string, error) { 207 // GuestIP is defined by the user as d.Host..but let's validate it just to be sure 208 conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", d.Host, d.Port)) 209 defer conn.Close() 210 if err != nil { 211 return "", err 212 } 213 214 host, _, err := net.SplitHostPort(conn.RemoteAddr().String()) 215 return host, err 216 } 217 218 func (d *ESX5Driver) HostAddress(multistep.StateBag) (string, error) { 219 // make a connection 220 conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", d.Host, d.Port)) 221 defer conn.Close() 222 if err != nil { 223 return "", err 224 } 225 226 // get the local address (the host) 227 host, _, err := net.SplitHostPort(conn.LocalAddr().String()) 228 if err != nil { 229 return "", fmt.Errorf("Unable to determine host address for ESXi: %v", err) 230 } 231 232 // iterate through all the interfaces.. 233 interfaces, err := net.Interfaces() 234 if err != nil { 235 return "", fmt.Errorf("Unable to enumerate host interfaces : %v", err) 236 } 237 238 for _, intf := range interfaces { 239 addrs, err := intf.Addrs() 240 if err != nil { 241 continue 242 } 243 244 // ..checking to see if any if it's addrs match the host address 245 for _, addr := range addrs { 246 if addr.String() == host { // FIXME: Is this the proper way to compare two HardwareAddrs? 247 return intf.HardwareAddr.String(), nil 248 } 249 } 250 } 251 252 // ..unfortunately nothing was found 253 return "", fmt.Errorf("Unable to locate interface matching host address in ESXi: %v", host) 254 } 255 256 func (d *ESX5Driver) GuestAddress(multistep.StateBag) (string, error) { 257 // list all the interfaces on the esx host 258 r, err := d.esxcli("network", "ip", "interface", "list") 259 if err != nil { 260 return "", fmt.Errorf("Could not retrieve network interfaces for ESXi: %v", err) 261 } 262 263 // rip out the interface name and the MAC address from the csv output 264 addrs := make(map[string]string) 265 for record, err := r.read(); record != nil && err == nil; record, err = r.read() { 266 if strings.ToUpper(record["Enabled"]) != "TRUE" { 267 continue 268 } 269 addrs[record["Name"]] = record["MAC Address"] 270 } 271 272 // list all the addresses on the esx host 273 r, err = d.esxcli("network", "ip", "interface", "ipv4", "get") 274 if err != nil { 275 return "", fmt.Errorf("Could not retrieve network addresses for ESXi: %v", err) 276 } 277 278 // figure out the interface name that matches the specified d.Host address 279 var intf string 280 intf = "" 281 for record, err := r.read(); record != nil && err == nil; record, err = r.read() { 282 if record["IPv4 Address"] == d.Host && record["Name"] != "" { 283 intf = record["Name"] 284 break 285 } 286 } 287 if intf == "" { 288 return "", fmt.Errorf("Unable to find matching address for ESXi guest") 289 } 290 291 // find the MAC address according to the interface name 292 result, ok := addrs[intf] 293 if !ok { 294 return "", fmt.Errorf("Unable to find address for ESXi guest interface") 295 } 296 297 // ..and we're good 298 return result, nil 299 } 300 301 func (d *ESX5Driver) VNCAddress(_ string, portMin, portMax uint) (string, uint, error) { 302 var vncPort uint 303 304 //Process ports ESXi is listening on to determine which are available 305 //This process does best effort to detect ports that are unavailable, 306 //it will ignore any ports listened to by only localhost 307 r, err := d.esxcli("network", "ip", "connection", "list") 308 if err != nil { 309 err = fmt.Errorf("Could not retrieve network information for ESXi: %v", err) 310 return "", 0, err 311 } 312 313 listenPorts := make(map[string]bool) 314 for record, err := r.read(); record != nil && err == nil; record, err = r.read() { 315 if record["State"] == "LISTEN" { 316 splitAddress := strings.Split(record["LocalAddress"], ":") 317 if splitAddress[0] != "127.0.0.1" { 318 port := splitAddress[len(splitAddress)-1] 319 log.Printf("ESXi listening on address %s, port %s unavailable for VNC", record["LocalAddress"], port) 320 listenPorts[port] = true 321 } 322 } 323 } 324 325 vncTimeout := time.Duration(15 * time.Second) 326 envTimeout := os.Getenv("PACKER_ESXI_VNC_PROBE_TIMEOUT") 327 if envTimeout != "" { 328 if parsedTimeout, err := time.ParseDuration(envTimeout); err != nil { 329 log.Printf("Error parsing PACKER_ESXI_VNC_PROBE_TIMEOUT. Falling back to default (15s). %s", err) 330 } else { 331 vncTimeout = parsedTimeout 332 } 333 } 334 335 for port := portMin; port <= portMax; port++ { 336 if _, ok := listenPorts[fmt.Sprintf("%d", port)]; ok { 337 log.Printf("Port %d in use", port) 338 continue 339 } 340 address := fmt.Sprintf("%s:%d", d.Host, port) 341 log.Printf("Trying address: %s...", address) 342 l, err := net.DialTimeout("tcp", address, vncTimeout) 343 344 if err != nil { 345 if e, ok := err.(*net.OpError); ok { 346 if e.Timeout() { 347 log.Printf("Timeout connecting to: %s (check firewall rules)", address) 348 } else { 349 vncPort = port 350 break 351 } 352 } 353 } else { 354 defer l.Close() 355 } 356 } 357 358 if vncPort == 0 { 359 err := fmt.Errorf("Unable to find available VNC port between %d and %d", 360 portMin, portMax) 361 return d.Host, vncPort, err 362 } 363 364 return d.Host, vncPort, nil 365 } 366 367 // UpdateVMX, adds the VNC port to the VMX data. 368 func (ESX5Driver) UpdateVMX(_, password string, port uint, data map[string]string) { 369 // Do not set remotedisplay.vnc.ip - this breaks ESXi. 370 data["remotedisplay.vnc.enabled"] = "TRUE" 371 data["remotedisplay.vnc.port"] = fmt.Sprintf("%d", port) 372 if len(password) > 0 { 373 data["remotedisplay.vnc.password"] = password 374 } 375 } 376 377 func (d *ESX5Driver) CommHost(state multistep.StateBag) (string, error) { 378 config := state.Get("config").(*Config) 379 sshc := config.SSHConfig.Comm 380 port := sshc.SSHPort 381 if sshc.Type == "winrm" { 382 port = sshc.WinRMPort 383 } 384 385 if address := config.CommConfig.Host(); address != "" { 386 return address, nil 387 } 388 389 r, err := d.esxcli("network", "vm", "list") 390 if err != nil { 391 return "", err 392 } 393 394 // The value in the Name field returned by 'esxcli network vm list' 395 // corresponds directly to the value of displayName set in the VMX file 396 var displayName string 397 if v, ok := state.GetOk("display_name"); ok { 398 displayName = v.(string) 399 } 400 record, err := r.find("Name", displayName) 401 if err != nil { 402 return "", err 403 } 404 wid := record["WorldID"] 405 if wid == "" { 406 return "", errors.New("VM WorldID not found") 407 } 408 409 r, err = d.esxcli("network", "vm", "port", "list", "-w", wid) 410 if err != nil { 411 return "", err 412 } 413 414 // Loop through interfaces 415 for { 416 record, err = r.read() 417 if err == io.EOF { 418 break 419 } 420 if err != nil { 421 return "", err 422 } 423 424 if record["IPAddress"] == "0.0.0.0" { 425 continue 426 } 427 // When multiple NICs are connected to the same network, choose 428 // one that has a route back. This Dial should ensure that. 429 conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", record["IPAddress"], port), 2*time.Second) 430 if err != nil { 431 if e, ok := err.(*net.OpError); ok { 432 if e.Timeout() { 433 log.Printf("Timeout connecting to %s", record["IPAddress"]) 434 continue 435 } else if strings.Contains(e.Error(), "connection refused") { 436 log.Printf("Connection refused when connecting to: %s", record["IPAddress"]) 437 continue 438 } 439 } 440 } else { 441 defer conn.Close() 442 address := record["IPAddress"] 443 return address, nil 444 } 445 } 446 return "", errors.New("No interface on the VM has an IP address ready") 447 } 448 449 //------------------------------------------------------------------- 450 // OutputDir implementation 451 //------------------------------------------------------------------- 452 453 func (d *ESX5Driver) DirExists() (bool, error) { 454 err := d.sh("test", "-e", strconv.Quote(d.outputDir)) 455 return err == nil, nil 456 } 457 458 func (d *ESX5Driver) ListFiles() ([]string, error) { 459 stdout, err := d.ssh("ls -1p "+d.outputDir, nil) 460 if err != nil { 461 return nil, err 462 } 463 464 files := make([]string, 0, 10) 465 reader := bufio.NewReader(stdout) 466 for { 467 line, _, err := reader.ReadLine() 468 if err == io.EOF { 469 break 470 } 471 if line[len(line)-1] == '/' { 472 continue 473 } 474 475 files = append(files, filepath.ToSlash(filepath.Join(d.outputDir, string(line)))) 476 } 477 478 return files, nil 479 } 480 481 func (d *ESX5Driver) MkdirAll() error { 482 return d.mkdir(d.outputDir) 483 } 484 485 func (d *ESX5Driver) Remove(path string) error { 486 return d.sh("rm", strconv.Quote(path)) 487 } 488 489 func (d *ESX5Driver) RemoveAll() error { 490 return d.sh("rm", "-rf", strconv.Quote(d.outputDir)) 491 } 492 493 func (d *ESX5Driver) SetOutputDir(path string) { 494 d.outputDir = d.datastorePath(path) 495 } 496 497 func (d *ESX5Driver) String() string { 498 return d.outputDir 499 } 500 501 func (d *ESX5Driver) datastorePath(path string) string { 502 dirPath := filepath.Dir(path) 503 return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.Datastore, dirPath, filepath.Base(path))) 504 } 505 506 func (d *ESX5Driver) cachePath(path string) string { 507 return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.CacheDatastore, d.CacheDirectory, filepath.Base(path))) 508 } 509 510 func (d *ESX5Driver) connect() error { 511 address := fmt.Sprintf("%s:%d", d.Host, d.Port) 512 513 auth := []gossh.AuthMethod{ 514 gossh.Password(d.Password), 515 gossh.KeyboardInteractive( 516 ssh.PasswordKeyboardInteractive(d.Password)), 517 } 518 519 if d.PrivateKeyFile != "" { 520 signer, err := helperssh.FileSigner(d.PrivateKeyFile) 521 if err != nil { 522 return err 523 } 524 525 auth = append(auth, gossh.PublicKeys(signer)) 526 } 527 528 sshConfig := &ssh.Config{ 529 Connection: ssh.ConnectFunc("tcp", address), 530 SSHConfig: &gossh.ClientConfig{ 531 User: d.Username, 532 Auth: auth, 533 HostKeyCallback: gossh.InsecureIgnoreHostKey(), 534 }, 535 } 536 537 comm, err := ssh.New(address, sshConfig) 538 if err != nil { 539 return err 540 } 541 542 d.comm = comm 543 return nil 544 } 545 546 func (d *ESX5Driver) checkSystemVersion() error { 547 r, err := d.esxcli("system", "version", "get") 548 if err != nil { 549 return err 550 } 551 552 record, err := r.read() 553 if err != nil { 554 return err 555 } 556 557 log.Printf("Connected to %s %s %s", record["Product"], 558 record["Version"], record["Build"]) 559 return nil 560 } 561 562 func (d *ESX5Driver) checkGuestIPHackEnabled() error { 563 r, err := d.esxcli("system", "settings", "advanced", "list", "-o", "/Net/GuestIPHack") 564 if err != nil { 565 return err 566 } 567 568 record, err := r.read() 569 if err != nil { 570 return err 571 } 572 573 if record["IntValue"] != "1" { 574 return errors.New( 575 "GuestIPHack is required, enable by running this on the ESX machine:\n" + 576 "esxcli system settings advanced set -o /Net/GuestIPHack -i 1") 577 } 578 579 return nil 580 } 581 582 func (d *ESX5Driver) mkdir(path string) error { 583 return d.sh("mkdir", "-p", strconv.Quote(path)) 584 } 585 586 func (d *ESX5Driver) upload(dst, src string) error { 587 f, err := os.Open(src) 588 if err != nil { 589 return err 590 } 591 defer f.Close() 592 return d.comm.Upload(dst, f, nil) 593 } 594 595 func (d *ESX5Driver) verifyChecksum(ctype string, hash string, file string) bool { 596 if ctype == "none" { 597 if err := d.sh("stat", strconv.Quote(file)); err != nil { 598 return false 599 } 600 } else { 601 stdin := bytes.NewBufferString(fmt.Sprintf("%s %s", hash, file)) 602 _, err := d.run(stdin, fmt.Sprintf("%ssum", ctype), "-c") 603 if err != nil { 604 return false 605 } 606 } 607 608 return true 609 } 610 611 func (d *ESX5Driver) ssh(command string, stdin io.Reader) (*bytes.Buffer, error) { 612 var stdout, stderr bytes.Buffer 613 614 cmd := &packer.RemoteCmd{ 615 Command: command, 616 Stdout: &stdout, 617 Stderr: &stderr, 618 Stdin: stdin, 619 } 620 621 err := d.comm.Start(cmd) 622 if err != nil { 623 return nil, err 624 } 625 626 cmd.Wait() 627 628 if cmd.ExitStatus != 0 { 629 err = fmt.Errorf("'%s'\n\nStdout: %s\n\nStderr: %s", 630 cmd.Command, stdout.String(), stderr.String()) 631 return nil, err 632 } 633 634 return &stdout, nil 635 } 636 637 func (d *ESX5Driver) run(stdin io.Reader, args ...string) (string, error) { 638 stdout, err := d.ssh(strings.Join(args, " "), stdin) 639 if err != nil { 640 return "", err 641 } 642 return stdout.String(), nil 643 } 644 645 func (d *ESX5Driver) sh(args ...string) error { 646 _, err := d.run(nil, args...) 647 return err 648 } 649 650 func (d *ESX5Driver) esxcli(args ...string) (*esxcliReader, error) { 651 stdout, err := d.ssh("esxcli --formatter csv "+strings.Join(args, " "), nil) 652 if err != nil { 653 return nil, err 654 } 655 r := csv.NewReader(bytes.NewReader(stdout.Bytes())) 656 r.TrailingComma = true 657 header, err := r.Read() 658 if err != nil { 659 return nil, err 660 } 661 return &esxcliReader{r, header}, nil 662 } 663 664 func (d *ESX5Driver) GetVmwareDriver() vmwcommon.VmwareDriver { 665 return d.base 666 } 667 668 type esxcliReader struct { 669 cr *csv.Reader 670 header []string 671 } 672 673 func (r *esxcliReader) read() (map[string]string, error) { 674 fields, err := r.cr.Read() 675 676 if err != nil { 677 return nil, err 678 } 679 680 record := map[string]string{} 681 for i, v := range fields { 682 record[r.header[i]] = v 683 } 684 685 return record, nil 686 } 687 688 func (r *esxcliReader) find(key, val string) (map[string]string, error) { 689 for { 690 record, err := r.read() 691 if err != nil { 692 return nil, err 693 } 694 if record[key] == val { 695 return record, nil 696 } 697 } 698 }