github.com/ttysteale/packer@v0.8.2-0.20150708160520-e5f8ea386ed8/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 "github.com/mitchellh/packer/communicator/ssh" 19 "github.com/mitchellh/packer/packer" 20 gossh "golang.org/x/crypto/ssh" 21 ) 22 23 // ESX5 driver talks to an ESXi5 hypervisor remotely over SSH to build 24 // virtual machines. This driver can only manage one machine at a time. 25 type ESX5Driver struct { 26 Host string 27 Port uint 28 Username string 29 Password string 30 Datastore string 31 CacheDatastore string 32 CacheDirectory string 33 34 comm packer.Communicator 35 outputDir string 36 vmId string 37 } 38 39 func (d *ESX5Driver) Clone(dst, src string) error { 40 return errors.New("Cloning is not supported with the ESX driver.") 41 } 42 43 func (d *ESX5Driver) CompactDisk(diskPathLocal string) error { 44 return nil 45 } 46 47 func (d *ESX5Driver) CreateDisk(diskPathLocal string, size string, typeId string) error { 48 diskPath := d.datastorePath(diskPathLocal) 49 return d.sh("vmkfstools", "-c", size, "-d", typeId, "-a", "lsilogic", diskPath) 50 } 51 52 func (d *ESX5Driver) IsRunning(string) (bool, error) { 53 state, err := d.run(nil, "vim-cmd", "vmsvc/power.getstate", d.vmId) 54 if err != nil { 55 return false, err 56 } 57 return strings.Contains(state, "Powered on"), nil 58 } 59 60 func (d *ESX5Driver) ReloadVM() error { 61 return d.sh("vim-cmd", "vmsvc/reload", d.vmId) 62 } 63 64 func (d *ESX5Driver) Start(vmxPathLocal string, headless bool) error { 65 for i := 0; i < 20; i++ { 66 err := d.sh("vim-cmd", "vmsvc/power.on", d.vmId) 67 if err != nil { 68 return err 69 } 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) UploadISO(localPath string, checksum string, checksumType string) (string, error) { 108 finalPath := d.cachePath(localPath) 109 if err := d.mkdir(filepath.ToSlash(filepath.Dir(finalPath))); err != nil { 110 return "", err 111 } 112 113 log.Printf("Verifying checksum of %s", finalPath) 114 if d.verifyChecksum(checksumType, checksum, finalPath) { 115 log.Println("Initial checksum matched, no upload needed.") 116 return finalPath, nil 117 } 118 119 if err := d.upload(finalPath, localPath); err != nil { 120 return "", err 121 } 122 123 return finalPath, nil 124 } 125 126 func (d *ESX5Driver) ToolsIsoPath(string) string { 127 return "" 128 } 129 130 func (d *ESX5Driver) ToolsInstall() error { 131 return d.sh("vim-cmd", "vmsvc/tools.install", d.vmId) 132 } 133 134 func (d *ESX5Driver) DhcpLeasesPath(string) string { 135 return "" 136 } 137 138 func (d *ESX5Driver) Verify() error { 139 checks := []func() error{ 140 d.connect, 141 d.checkSystemVersion, 142 d.checkGuestIPHackEnabled, 143 } 144 145 for _, check := range checks { 146 if err := check(); err != nil { 147 return err 148 } 149 } 150 151 return nil 152 } 153 154 func (d *ESX5Driver) HostIP() (string, error) { 155 conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", d.Host, d.Port)) 156 defer conn.Close() 157 if err != nil { 158 return "", err 159 } 160 161 host, _, err := net.SplitHostPort(conn.LocalAddr().String()) 162 return host, err 163 } 164 165 func (d *ESX5Driver) VNCAddress(portMin, portMax uint) (string, uint, error) { 166 var vncPort uint 167 168 //Process ports ESXi is listening on to determine which are available 169 //This process does best effort to detect ports that are unavailable, 170 //it will ignore any ports listened to by only localhost 171 r, err := d.esxcli("network", "ip", "connection", "list") 172 if err != nil { 173 err = fmt.Errorf("Could not retrieve network information for ESXi: %v", err) 174 return "", 0, err 175 } 176 177 listenPorts := make(map[string]bool) 178 for record, err := r.read(); record != nil && err == nil; record, err = r.read() { 179 if record["State"] == "LISTEN" { 180 splitAddress := strings.Split(record["LocalAddress"], ":") 181 if splitAddress[0] != "127.0.0.1" { 182 port := splitAddress[len(splitAddress)-1] 183 log.Printf("ESXi listening on address %s, port %s unavailable for VNC", record["LocalAddress"], port) 184 listenPorts[port] = true 185 } 186 } 187 } 188 189 for port := portMin; port <= portMax; port++ { 190 if _, ok := listenPorts[fmt.Sprintf("%d", port)]; ok { 191 log.Printf("Port %d in use", port) 192 continue 193 } 194 address := fmt.Sprintf("%s:%d", d.Host, port) 195 log.Printf("Trying address: %s...", address) 196 l, err := net.DialTimeout("tcp", address, 1*time.Second) 197 198 if err != nil { 199 if e, ok := err.(*net.OpError); ok { 200 if e.Timeout() { 201 log.Printf("Timeout connecting to: %s (check firewall rules)", address) 202 } else { 203 vncPort = port 204 break 205 } 206 } 207 } else { 208 defer l.Close() 209 } 210 } 211 212 if vncPort == 0 { 213 err := fmt.Errorf("Unable to find available VNC port between %d and %d", 214 portMin, portMax) 215 return d.Host, vncPort, err 216 } 217 218 return d.Host, vncPort, nil 219 } 220 221 func (d *ESX5Driver) CommHost(state multistep.StateBag) (string, error) { 222 config := state.Get("config").(*Config) 223 224 if address, ok := state.GetOk("vm_address"); ok { 225 return address.(string), nil 226 } 227 228 r, err := d.esxcli("network", "vm", "list") 229 if err != nil { 230 return "", err 231 } 232 233 record, err := r.find("Name", config.VMName) 234 if err != nil { 235 return "", err 236 } 237 wid := record["WorldID"] 238 if wid == "" { 239 return "", errors.New("VM WorldID not found") 240 } 241 242 r, err = d.esxcli("network", "vm", "port", "list", "-w", wid) 243 if err != nil { 244 return "", err 245 } 246 247 record, err = r.read() 248 if err != nil { 249 return "", err 250 } 251 252 if record["IPAddress"] == "0.0.0.0" { 253 return "", errors.New("VM network port found, but no IP address") 254 } 255 256 address := record["IPAddress"] 257 state.Put("vm_address", address) 258 return address, nil 259 } 260 261 //------------------------------------------------------------------- 262 // OutputDir implementation 263 //------------------------------------------------------------------- 264 265 func (d *ESX5Driver) DirExists() (bool, error) { 266 err := d.sh("test", "-e", d.outputDir) 267 return err == nil, nil 268 } 269 270 func (d *ESX5Driver) ListFiles() ([]string, error) { 271 stdout, err := d.ssh("ls -1p "+d.outputDir, nil) 272 if err != nil { 273 return nil, err 274 } 275 276 files := make([]string, 0, 10) 277 reader := bufio.NewReader(stdout) 278 for { 279 line, _, err := reader.ReadLine() 280 if err == io.EOF { 281 break 282 } 283 if line[len(line)-1] == '/' { 284 continue 285 } 286 287 files = append(files, filepath.ToSlash(filepath.Join(d.outputDir, string(line)))) 288 } 289 290 return files, nil 291 } 292 293 func (d *ESX5Driver) MkdirAll() error { 294 return d.mkdir(d.outputDir) 295 } 296 297 func (d *ESX5Driver) Remove(path string) error { 298 return d.sh("rm", path) 299 } 300 301 func (d *ESX5Driver) RemoveAll() error { 302 return d.sh("rm", "-rf", d.outputDir) 303 } 304 305 func (d *ESX5Driver) SetOutputDir(path string) { 306 d.outputDir = d.datastorePath(path) 307 } 308 309 func (d *ESX5Driver) String() string { 310 return d.outputDir 311 } 312 313 func (d *ESX5Driver) datastorePath(path string) string { 314 dirPath := filepath.Dir(path) 315 return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.Datastore, dirPath, filepath.Base(path))) 316 } 317 318 func (d *ESX5Driver) cachePath(path string) string { 319 return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.CacheDatastore, d.CacheDirectory, filepath.Base(path))) 320 } 321 322 func (d *ESX5Driver) connect() error { 323 address := fmt.Sprintf("%s:%d", d.Host, d.Port) 324 325 auth := []gossh.AuthMethod{ 326 gossh.Password(d.Password), 327 gossh.KeyboardInteractive( 328 ssh.PasswordKeyboardInteractive(d.Password)), 329 } 330 331 // TODO(dougm) KeyPath support 332 sshConfig := &ssh.Config{ 333 Connection: ssh.ConnectFunc("tcp", address), 334 SSHConfig: &gossh.ClientConfig{ 335 User: d.Username, 336 Auth: auth, 337 }, 338 } 339 340 comm, err := ssh.New(address, sshConfig) 341 if err != nil { 342 return err 343 } 344 345 d.comm = comm 346 return nil 347 } 348 349 func (d *ESX5Driver) checkSystemVersion() error { 350 r, err := d.esxcli("system", "version", "get") 351 if err != nil { 352 return err 353 } 354 355 record, err := r.read() 356 if err != nil { 357 return err 358 } 359 360 log.Printf("Connected to %s %s %s", record["Product"], 361 record["Version"], record["Build"]) 362 return nil 363 } 364 365 func (d *ESX5Driver) checkGuestIPHackEnabled() error { 366 r, err := d.esxcli("system", "settings", "advanced", "list", "-o", "/Net/GuestIPHack") 367 if err != nil { 368 return err 369 } 370 371 record, err := r.read() 372 if err != nil { 373 return err 374 } 375 376 if record["IntValue"] != "1" { 377 return errors.New( 378 "GuestIPHack is required, enable by running this on the ESX machine:\n" + 379 "esxcli system settings advanced set -o /Net/GuestIPHack -i 1") 380 } 381 382 return nil 383 } 384 385 func (d *ESX5Driver) mkdir(path string) error { 386 return d.sh("mkdir", "-p", path) 387 } 388 389 func (d *ESX5Driver) upload(dst, src string) error { 390 f, err := os.Open(src) 391 if err != nil { 392 return err 393 } 394 defer f.Close() 395 return d.comm.Upload(dst, f, nil) 396 } 397 398 func (d *ESX5Driver) verifyChecksum(ctype string, hash string, file string) bool { 399 if ctype == "none" { 400 if err := d.sh("stat", file); err != nil { 401 return false 402 } 403 } else { 404 stdin := bytes.NewBufferString(fmt.Sprintf("%s %s", hash, file)) 405 _, err := d.run(stdin, fmt.Sprintf("%ssum", ctype), "-c") 406 if err != nil { 407 return false 408 } 409 } 410 411 return true 412 } 413 414 func (d *ESX5Driver) ssh(command string, stdin io.Reader) (*bytes.Buffer, error) { 415 var stdout, stderr bytes.Buffer 416 417 cmd := &packer.RemoteCmd{ 418 Command: command, 419 Stdout: &stdout, 420 Stderr: &stderr, 421 Stdin: stdin, 422 } 423 424 err := d.comm.Start(cmd) 425 if err != nil { 426 return nil, err 427 } 428 429 cmd.Wait() 430 431 if cmd.ExitStatus != 0 { 432 err = fmt.Errorf("'%s'\n\nStdout: %s\n\nStderr: %s", 433 cmd.Command, stdout.String(), stderr.String()) 434 return nil, err 435 } 436 437 return &stdout, nil 438 } 439 440 func (d *ESX5Driver) run(stdin io.Reader, args ...string) (string, error) { 441 stdout, err := d.ssh(strings.Join(args, " "), stdin) 442 if err != nil { 443 return "", err 444 } 445 return stdout.String(), nil 446 } 447 448 func (d *ESX5Driver) sh(args ...string) error { 449 _, err := d.run(nil, args...) 450 return err 451 } 452 453 func (d *ESX5Driver) esxcli(args ...string) (*esxcliReader, error) { 454 stdout, err := d.ssh("esxcli --formatter csv "+strings.Join(args, " "), nil) 455 if err != nil { 456 return nil, err 457 } 458 r := csv.NewReader(bytes.NewReader(stdout.Bytes())) 459 r.TrailingComma = true 460 header, err := r.Read() 461 if err != nil { 462 return nil, err 463 } 464 return &esxcliReader{r, header}, nil 465 } 466 467 type esxcliReader struct { 468 cr *csv.Reader 469 header []string 470 } 471 472 func (r *esxcliReader) read() (map[string]string, error) { 473 fields, err := r.cr.Read() 474 475 if err != nil { 476 return nil, err 477 } 478 479 record := map[string]string{} 480 for i, v := range fields { 481 record[r.header[i]] = v 482 } 483 484 return record, nil 485 } 486 487 func (r *esxcliReader) find(key, val string) (map[string]string, error) { 488 for { 489 record, err := r.read() 490 if err != nil { 491 return nil, err 492 } 493 if record[key] == val { 494 return record, nil 495 } 496 } 497 }