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 }