github.com/yoctocloud/packer@v0.6.2-0.20160520224004-e11a0a18423f/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 err := d.sh("vim-cmd", "vmsvc/power.on", d.vmId) 69 if err != nil { 70 return err 71 } 72 time.Sleep((time.Duration(i) * time.Second) + 1) 73 running, err := d.IsRunning(vmxPathLocal) 74 if err != nil { 75 return err 76 } 77 if running { 78 return nil 79 } 80 } 81 return errors.New("Retry limit exceeded") 82 } 83 84 func (d *ESX5Driver) Stop(vmxPathLocal string) error { 85 return d.sh("vim-cmd", "vmsvc/power.off", d.vmId) 86 } 87 88 func (d *ESX5Driver) Register(vmxPathLocal string) error { 89 vmxPath := filepath.ToSlash(filepath.Join(d.outputDir, filepath.Base(vmxPathLocal))) 90 if err := d.upload(vmxPath, vmxPathLocal); err != nil { 91 return err 92 } 93 r, err := d.run(nil, "vim-cmd", "solo/registervm", vmxPath) 94 if err != nil { 95 return err 96 } 97 d.vmId = strings.TrimRight(r, "\n") 98 return nil 99 } 100 101 func (d *ESX5Driver) SuppressMessages(vmxPath string) error { 102 return nil 103 } 104 105 func (d *ESX5Driver) Unregister(vmxPathLocal string) error { 106 return d.sh("vim-cmd", "vmsvc/unregister", d.vmId) 107 } 108 109 func (d *ESX5Driver) Destroy() error { 110 return d.sh("vim-cmd", "vmsvc/destroy", d.vmId) 111 } 112 113 func (d *ESX5Driver) IsDestroyed() (bool, error) { 114 err := d.sh("test", "!", "-e", d.outputDir) 115 if err != nil { 116 return false, err 117 } 118 return true, err 119 } 120 121 func (d *ESX5Driver) UploadISO(localPath string, checksum string, checksumType string) (string, error) { 122 finalPath := d.cachePath(localPath) 123 if err := d.mkdir(filepath.ToSlash(filepath.Dir(finalPath))); err != nil { 124 return "", err 125 } 126 127 log.Printf("Verifying checksum of %s", finalPath) 128 if d.verifyChecksum(checksumType, checksum, finalPath) { 129 log.Println("Initial checksum matched, no upload needed.") 130 return finalPath, nil 131 } 132 133 if err := d.upload(finalPath, localPath); err != nil { 134 return "", err 135 } 136 137 return finalPath, nil 138 } 139 140 func (d *ESX5Driver) ToolsIsoPath(string) string { 141 return "" 142 } 143 144 func (d *ESX5Driver) ToolsInstall() error { 145 return d.sh("vim-cmd", "vmsvc/tools.install", d.vmId) 146 } 147 148 func (d *ESX5Driver) DhcpLeasesPath(string) string { 149 return "" 150 } 151 152 func (d *ESX5Driver) Verify() error { 153 checks := []func() error{ 154 d.connect, 155 d.checkSystemVersion, 156 d.checkGuestIPHackEnabled, 157 } 158 159 for _, check := range checks { 160 if err := check(); err != nil { 161 return err 162 } 163 } 164 165 return nil 166 } 167 168 func (d *ESX5Driver) HostIP() (string, error) { 169 conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", d.Host, d.Port)) 170 defer conn.Close() 171 if err != nil { 172 return "", err 173 } 174 175 host, _, err := net.SplitHostPort(conn.LocalAddr().String()) 176 return host, err 177 } 178 179 func (d *ESX5Driver) VNCAddress(portMin, portMax uint) (string, uint, error) { 180 var vncPort uint 181 182 //Process ports ESXi is listening on to determine which are available 183 //This process does best effort to detect ports that are unavailable, 184 //it will ignore any ports listened to by only localhost 185 r, err := d.esxcli("network", "ip", "connection", "list") 186 if err != nil { 187 err = fmt.Errorf("Could not retrieve network information for ESXi: %v", err) 188 return "", 0, err 189 } 190 191 listenPorts := make(map[string]bool) 192 for record, err := r.read(); record != nil && err == nil; record, err = r.read() { 193 if record["State"] == "LISTEN" { 194 splitAddress := strings.Split(record["LocalAddress"], ":") 195 if splitAddress[0] != "127.0.0.1" { 196 port := splitAddress[len(splitAddress)-1] 197 log.Printf("ESXi listening on address %s, port %s unavailable for VNC", record["LocalAddress"], port) 198 listenPorts[port] = true 199 } 200 } 201 } 202 203 for port := portMin; port <= portMax; port++ { 204 if _, ok := listenPorts[fmt.Sprintf("%d", port)]; ok { 205 log.Printf("Port %d in use", port) 206 continue 207 } 208 address := fmt.Sprintf("%s:%d", d.Host, port) 209 log.Printf("Trying address: %s...", address) 210 l, err := net.DialTimeout("tcp", address, 1*time.Second) 211 212 if err != nil { 213 if e, ok := err.(*net.OpError); ok { 214 if e.Timeout() { 215 log.Printf("Timeout connecting to: %s (check firewall rules)", address) 216 } else { 217 vncPort = port 218 break 219 } 220 } 221 } else { 222 defer l.Close() 223 } 224 } 225 226 if vncPort == 0 { 227 err := fmt.Errorf("Unable to find available VNC port between %d and %d", 228 portMin, portMax) 229 return d.Host, vncPort, err 230 } 231 232 return d.Host, vncPort, nil 233 } 234 235 func (d *ESX5Driver) CommHost(state multistep.StateBag) (string, error) { 236 config := state.Get("config").(*Config) 237 238 if address, ok := state.GetOk("vm_address"); ok { 239 return address.(string), nil 240 } 241 242 r, err := d.esxcli("network", "vm", "list") 243 if err != nil { 244 return "", err 245 } 246 247 record, err := r.find("Name", config.VMName) 248 if err != nil { 249 return "", err 250 } 251 wid := record["WorldID"] 252 if wid == "" { 253 return "", errors.New("VM WorldID not found") 254 } 255 256 r, err = d.esxcli("network", "vm", "port", "list", "-w", wid) 257 if err != nil { 258 return "", err 259 } 260 261 record, err = r.read() 262 if err != nil { 263 return "", err 264 } 265 266 if record["IPAddress"] == "0.0.0.0" { 267 return "", errors.New("VM network port found, but no IP address") 268 } 269 270 address := record["IPAddress"] 271 state.Put("vm_address", address) 272 return address, nil 273 } 274 275 //------------------------------------------------------------------- 276 // OutputDir implementation 277 //------------------------------------------------------------------- 278 279 func (d *ESX5Driver) DirExists() (bool, error) { 280 err := d.sh("test", "-e", d.outputDir) 281 return err == nil, nil 282 } 283 284 func (d *ESX5Driver) ListFiles() ([]string, error) { 285 stdout, err := d.ssh("ls -1p "+d.outputDir, nil) 286 if err != nil { 287 return nil, err 288 } 289 290 files := make([]string, 0, 10) 291 reader := bufio.NewReader(stdout) 292 for { 293 line, _, err := reader.ReadLine() 294 if err == io.EOF { 295 break 296 } 297 if line[len(line)-1] == '/' { 298 continue 299 } 300 301 files = append(files, filepath.ToSlash(filepath.Join(d.outputDir, string(line)))) 302 } 303 304 return files, nil 305 } 306 307 func (d *ESX5Driver) MkdirAll() error { 308 return d.mkdir(d.outputDir) 309 } 310 311 func (d *ESX5Driver) Remove(path string) error { 312 return d.sh("rm", path) 313 } 314 315 func (d *ESX5Driver) RemoveAll() error { 316 return d.sh("rm", "-rf", d.outputDir) 317 } 318 319 func (d *ESX5Driver) SetOutputDir(path string) { 320 d.outputDir = d.datastorePath(path) 321 } 322 323 func (d *ESX5Driver) String() string { 324 return d.outputDir 325 } 326 327 func (d *ESX5Driver) datastorePath(path string) string { 328 dirPath := filepath.Dir(path) 329 return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.Datastore, dirPath, filepath.Base(path))) 330 } 331 332 func (d *ESX5Driver) cachePath(path string) string { 333 return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.CacheDatastore, d.CacheDirectory, filepath.Base(path))) 334 } 335 336 func (d *ESX5Driver) connect() error { 337 address := fmt.Sprintf("%s:%d", d.Host, d.Port) 338 339 auth := []gossh.AuthMethod{ 340 gossh.Password(d.Password), 341 gossh.KeyboardInteractive( 342 ssh.PasswordKeyboardInteractive(d.Password)), 343 } 344 345 if d.PrivateKey != "" { 346 signer, err := commonssh.FileSigner(d.PrivateKey) 347 if err != nil { 348 return err 349 } 350 351 auth = append(auth, gossh.PublicKeys(signer)) 352 } 353 354 sshConfig := &ssh.Config{ 355 Connection: ssh.ConnectFunc("tcp", address), 356 SSHConfig: &gossh.ClientConfig{ 357 User: d.Username, 358 Auth: auth, 359 }, 360 } 361 362 comm, err := ssh.New(address, sshConfig) 363 if err != nil { 364 return err 365 } 366 367 d.comm = comm 368 return nil 369 } 370 371 func (d *ESX5Driver) checkSystemVersion() error { 372 r, err := d.esxcli("system", "version", "get") 373 if err != nil { 374 return err 375 } 376 377 record, err := r.read() 378 if err != nil { 379 return err 380 } 381 382 log.Printf("Connected to %s %s %s", record["Product"], 383 record["Version"], record["Build"]) 384 return nil 385 } 386 387 func (d *ESX5Driver) checkGuestIPHackEnabled() error { 388 r, err := d.esxcli("system", "settings", "advanced", "list", "-o", "/Net/GuestIPHack") 389 if err != nil { 390 return err 391 } 392 393 record, err := r.read() 394 if err != nil { 395 return err 396 } 397 398 if record["IntValue"] != "1" { 399 return errors.New( 400 "GuestIPHack is required, enable by running this on the ESX machine:\n" + 401 "esxcli system settings advanced set -o /Net/GuestIPHack -i 1") 402 } 403 404 return nil 405 } 406 407 func (d *ESX5Driver) mkdir(path string) error { 408 return d.sh("mkdir", "-p", path) 409 } 410 411 func (d *ESX5Driver) upload(dst, src string) error { 412 f, err := os.Open(src) 413 if err != nil { 414 return err 415 } 416 defer f.Close() 417 return d.comm.Upload(dst, f, nil) 418 } 419 420 func (d *ESX5Driver) verifyChecksum(ctype string, hash string, file string) bool { 421 if ctype == "none" { 422 if err := d.sh("stat", file); err != nil { 423 return false 424 } 425 } else { 426 stdin := bytes.NewBufferString(fmt.Sprintf("%s %s", hash, file)) 427 _, err := d.run(stdin, fmt.Sprintf("%ssum", ctype), "-c") 428 if err != nil { 429 return false 430 } 431 } 432 433 return true 434 } 435 436 func (d *ESX5Driver) ssh(command string, stdin io.Reader) (*bytes.Buffer, error) { 437 var stdout, stderr bytes.Buffer 438 439 cmd := &packer.RemoteCmd{ 440 Command: command, 441 Stdout: &stdout, 442 Stderr: &stderr, 443 Stdin: stdin, 444 } 445 446 err := d.comm.Start(cmd) 447 if err != nil { 448 return nil, err 449 } 450 451 cmd.Wait() 452 453 if cmd.ExitStatus != 0 { 454 err = fmt.Errorf("'%s'\n\nStdout: %s\n\nStderr: %s", 455 cmd.Command, stdout.String(), stderr.String()) 456 return nil, err 457 } 458 459 return &stdout, nil 460 } 461 462 func (d *ESX5Driver) run(stdin io.Reader, args ...string) (string, error) { 463 stdout, err := d.ssh(strings.Join(args, " "), stdin) 464 if err != nil { 465 return "", err 466 } 467 return stdout.String(), nil 468 } 469 470 func (d *ESX5Driver) sh(args ...string) error { 471 _, err := d.run(nil, args...) 472 return err 473 } 474 475 func (d *ESX5Driver) esxcli(args ...string) (*esxcliReader, error) { 476 stdout, err := d.ssh("esxcli --formatter csv "+strings.Join(args, " "), nil) 477 if err != nil { 478 return nil, err 479 } 480 r := csv.NewReader(bytes.NewReader(stdout.Bytes())) 481 r.TrailingComma = true 482 header, err := r.Read() 483 if err != nil { 484 return nil, err 485 } 486 return &esxcliReader{r, header}, nil 487 } 488 489 type esxcliReader struct { 490 cr *csv.Reader 491 header []string 492 } 493 494 func (r *esxcliReader) read() (map[string]string, error) { 495 fields, err := r.cr.Read() 496 497 if err != nil { 498 return nil, err 499 } 500 501 record := map[string]string{} 502 for i, v := range fields { 503 record[r.header[i]] = v 504 } 505 506 return record, nil 507 } 508 509 func (r *esxcliReader) find(key, val string) (map[string]string, error) { 510 for { 511 record, err := r.read() 512 if err != nil { 513 return nil, err 514 } 515 if record[key] == val { 516 return record, nil 517 } 518 } 519 }