github.com/daniellockard/packer@v0.7.6-0.20141210173435-5a9390934716/builder/vmware/iso/driver_esx5.go (about) 1 package iso 2 3 import ( 4 "bufio" 5 "bytes" 6 gossh "code.google.com/p/go.crypto/ssh" 7 "encoding/csv" 8 "errors" 9 "fmt" 10 "github.com/mitchellh/multistep" 11 "github.com/mitchellh/packer/communicator/ssh" 12 "github.com/mitchellh/packer/packer" 13 "io" 14 "log" 15 "net" 16 "os" 17 "path/filepath" 18 "strings" 19 "time" 20 ) 21 22 // ESX5 driver talks to an ESXi5 hypervisor remotely over SSH to build 23 // virtual machines. This driver can only manage one machine at a time. 24 type ESX5Driver struct { 25 Host string 26 Port uint 27 Username string 28 Password string 29 Datastore string 30 CacheDatastore string 31 CacheDirectory string 32 33 comm packer.Communicator 34 outputDir string 35 vmId string 36 } 37 38 func (d *ESX5Driver) Clone(dst, src string) error { 39 return errors.New("Cloning is not supported with the ESX driver.") 40 } 41 42 func (d *ESX5Driver) CompactDisk(diskPathLocal string) error { 43 return nil 44 } 45 46 func (d *ESX5Driver) CreateDisk(diskPathLocal string, size string, typeId string) error { 47 diskPath := d.datastorePath(diskPathLocal) 48 return d.sh("vmkfstools", "-c", size, "-d", typeId, "-a", "lsilogic", diskPath) 49 } 50 51 func (d *ESX5Driver) IsRunning(string) (bool, error) { 52 state, err := d.run(nil, "vim-cmd", "vmsvc/power.getstate", d.vmId) 53 if err != nil { 54 return false, err 55 } 56 return strings.Contains(state, "Powered on"), nil 57 } 58 59 func (d *ESX5Driver) ReloadVM() error { 60 return d.sh("vim-cmd", "vmsvc/reload", d.vmId) 61 } 62 63 func (d *ESX5Driver) Start(vmxPathLocal string, headless bool) error { 64 for i := 0; i < 20; i++ { 65 err := d.sh("vim-cmd", "vmsvc/power.on", d.vmId) 66 if err != nil { 67 return err 68 } 69 time.Sleep((time.Duration(i) * time.Second) + 1) 70 running, err := d.IsRunning(vmxPathLocal) 71 if err != nil { 72 return err 73 } 74 if running { 75 return nil 76 } 77 } 78 return errors.New("Retry limit exceeded") 79 } 80 81 func (d *ESX5Driver) Stop(vmxPathLocal string) error { 82 return d.sh("vim-cmd", "vmsvc/power.off", d.vmId) 83 } 84 85 func (d *ESX5Driver) Register(vmxPathLocal string) error { 86 vmxPath := filepath.ToSlash(filepath.Join(d.outputDir, filepath.Base(vmxPathLocal))) 87 if err := d.upload(vmxPath, vmxPathLocal); err != nil { 88 return err 89 } 90 r, err := d.run(nil, "vim-cmd", "solo/registervm", vmxPath) 91 if err != nil { 92 return err 93 } 94 d.vmId = strings.TrimRight(r, "\n") 95 return nil 96 } 97 98 func (d *ESX5Driver) SuppressMessages(vmxPath string) error { 99 return nil 100 } 101 102 func (d *ESX5Driver) Unregister(vmxPathLocal string) error { 103 return d.sh("vim-cmd", "vmsvc/unregister", d.vmId) 104 } 105 106 func (d *ESX5Driver) UploadISO(localPath string, checksum string, checksumType string) (string, error) { 107 finalPath := d.cachePath(localPath) 108 if err := d.mkdir(filepath.ToSlash(filepath.Dir(finalPath))); err != nil { 109 return "", err 110 } 111 112 log.Printf("Verifying checksum of %s", finalPath) 113 if d.verifyChecksum(checksumType, checksum, finalPath) { 114 log.Println("Initial checksum matched, no upload needed.") 115 return finalPath, nil 116 } 117 118 if err := d.upload(finalPath, localPath); err != nil { 119 return "", err 120 } 121 122 return finalPath, nil 123 } 124 125 func (d *ESX5Driver) ToolsIsoPath(string) string { 126 return "" 127 } 128 129 func (d *ESX5Driver) ToolsInstall() error { 130 return d.sh("vim-cmd", "vmsvc/tools.install", d.vmId) 131 } 132 133 func (d *ESX5Driver) DhcpLeasesPath(string) string { 134 return "" 135 } 136 137 func (d *ESX5Driver) Verify() error { 138 checks := []func() error{ 139 d.connect, 140 d.checkSystemVersion, 141 d.checkGuestIPHackEnabled, 142 } 143 144 for _, check := range checks { 145 if err := check(); err != nil { 146 return err 147 } 148 } 149 150 return nil 151 } 152 153 func (d *ESX5Driver) HostIP() (string, error) { 154 conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", d.Host, d.Port)) 155 defer conn.Close() 156 if err != nil { 157 return "", err 158 } 159 160 host, _, err := net.SplitHostPort(conn.LocalAddr().String()) 161 return host, err 162 } 163 164 func (d *ESX5Driver) VNCAddress(portMin, portMax uint) (string, uint, error) { 165 var vncPort uint 166 167 //Process ports ESXi is listening on to determine which are available 168 //This process does best effort to detect ports that are unavailable, 169 //it will ignore any ports listened to by only localhost 170 r, err := d.esxcli("network", "ip", "connection", "list") 171 if err != nil { 172 err = fmt.Errorf("Could not retrieve network information for ESXi: %v", err) 173 return "", 0, err 174 } 175 176 listenPorts := make(map[string]bool) 177 for record, err := r.read(); record != nil && err == nil; record, err = r.read() { 178 if record["State"] == "LISTEN" { 179 splitAddress := strings.Split(record["LocalAddress"], ":") 180 if splitAddress[0] != "127.0.0.1" { 181 port := splitAddress[len(splitAddress)-1] 182 log.Printf("ESXi listening on address %s, port %s unavailable for VNC", record["LocalAddress"], port) 183 listenPorts[port] = true 184 } 185 } 186 } 187 188 for port := portMin; port <= portMax; port++ { 189 if _, ok := listenPorts[fmt.Sprintf("%d", port)]; ok { 190 log.Printf("Port %d in use", port) 191 continue 192 } 193 address := fmt.Sprintf("%s:%d", d.Host, port) 194 log.Printf("Trying address: %s...", address) 195 l, err := net.DialTimeout("tcp", address, 1*time.Second) 196 197 if err != nil { 198 if e, ok := err.(*net.OpError); ok { 199 if e.Timeout() { 200 log.Printf("Timeout connecting to: %s (check firewall rules)", address) 201 } else { 202 vncPort = port 203 break 204 } 205 } 206 } else { 207 defer l.Close() 208 } 209 } 210 211 if vncPort == 0 { 212 err := fmt.Errorf("Unable to find available VNC port between %d and %d", 213 portMin, portMax) 214 return d.Host, vncPort, err 215 } 216 217 return d.Host, vncPort, nil 218 } 219 220 func (d *ESX5Driver) SSHAddress(state multistep.StateBag) (string, error) { 221 config := state.Get("config").(*config) 222 223 if address, ok := state.GetOk("vm_address"); ok { 224 return address.(string), nil 225 } 226 227 r, err := d.esxcli("network", "vm", "list") 228 if err != nil { 229 return "", err 230 } 231 232 record, err := r.find("Name", config.VMName) 233 if err != nil { 234 return "", err 235 } 236 wid := record["WorldID"] 237 if wid == "" { 238 return "", errors.New("VM WorldID not found") 239 } 240 241 r, err = d.esxcli("network", "vm", "port", "list", "-w", wid) 242 if err != nil { 243 return "", err 244 } 245 246 record, err = r.read() 247 if err != nil { 248 return "", err 249 } 250 251 if record["IPAddress"] == "0.0.0.0" { 252 return "", errors.New("VM network port found, but no IP address") 253 } 254 255 address := fmt.Sprintf("%s:%d", record["IPAddress"], config.SSHPort) 256 state.Put("vm_address", address) 257 return address, nil 258 } 259 260 //------------------------------------------------------------------- 261 // OutputDir implementation 262 //------------------------------------------------------------------- 263 264 func (d *ESX5Driver) DirExists() (bool, error) { 265 err := d.sh("test", "-e", d.outputDir) 266 return err == nil, nil 267 } 268 269 func (d *ESX5Driver) ListFiles() ([]string, error) { 270 stdout, err := d.ssh("ls -1p "+d.outputDir, nil) 271 if err != nil { 272 return nil, err 273 } 274 275 files := make([]string, 0, 10) 276 reader := bufio.NewReader(stdout) 277 for { 278 line, _, err := reader.ReadLine() 279 if err == io.EOF { 280 break 281 } 282 if line[len(line)-1] == '/' { 283 continue 284 } 285 286 files = append(files, filepath.ToSlash(filepath.Join(d.outputDir, string(line)))) 287 } 288 289 return files, nil 290 } 291 292 func (d *ESX5Driver) MkdirAll() error { 293 return d.mkdir(d.outputDir) 294 } 295 296 func (d *ESX5Driver) Remove(path string) error { 297 return d.sh("rm", path) 298 } 299 300 func (d *ESX5Driver) RemoveAll() error { 301 return d.sh("rm", "-rf", d.outputDir) 302 } 303 304 func (d *ESX5Driver) SetOutputDir(path string) { 305 d.outputDir = d.datastorePath(path) 306 } 307 308 func (d *ESX5Driver) String() string { 309 return d.outputDir 310 } 311 312 func (d *ESX5Driver) datastorePath(path string) string { 313 baseDir := filepath.Base(filepath.Dir(path)) 314 return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.Datastore, baseDir, filepath.Base(path))) 315 } 316 317 func (d *ESX5Driver) cachePath(path string) string { 318 return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.CacheDatastore, d.CacheDirectory, filepath.Base(path))) 319 } 320 321 func (d *ESX5Driver) connect() error { 322 address := fmt.Sprintf("%s:%d", d.Host, d.Port) 323 324 auth := []gossh.AuthMethod{ 325 gossh.Password(d.Password), 326 gossh.KeyboardInteractive( 327 ssh.PasswordKeyboardInteractive(d.Password)), 328 } 329 330 // TODO(dougm) KeyPath support 331 sshConfig := &ssh.Config{ 332 Connection: ssh.ConnectFunc("tcp", address), 333 SSHConfig: &gossh.ClientConfig{ 334 User: d.Username, 335 Auth: auth, 336 }, 337 NoPty: true, 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 stdin := bytes.NewBufferString(fmt.Sprintf("%s %s", hash, file)) 400 _, err := d.run(stdin, fmt.Sprintf("%ssum", ctype), "-c") 401 if err != nil { 402 return false 403 } 404 return true 405 } 406 407 func (d *ESX5Driver) ssh(command string, stdin io.Reader) (*bytes.Buffer, error) { 408 var stdout, stderr bytes.Buffer 409 410 cmd := &packer.RemoteCmd{ 411 Command: command, 412 Stdout: &stdout, 413 Stderr: &stderr, 414 Stdin: stdin, 415 } 416 417 err := d.comm.Start(cmd) 418 if err != nil { 419 return nil, err 420 } 421 422 cmd.Wait() 423 424 if cmd.ExitStatus != 0 { 425 err = fmt.Errorf("'%s'\n\nStdout: %s\n\nStderr: %s", 426 cmd.Command, stdout.String(), stderr.String()) 427 return nil, err 428 } 429 430 return &stdout, nil 431 } 432 433 func (d *ESX5Driver) run(stdin io.Reader, args ...string) (string, error) { 434 stdout, err := d.ssh(strings.Join(args, " "), stdin) 435 if err != nil { 436 return "", err 437 } 438 return stdout.String(), nil 439 } 440 441 func (d *ESX5Driver) sh(args ...string) error { 442 _, err := d.run(nil, args...) 443 return err 444 } 445 446 func (d *ESX5Driver) esxcli(args ...string) (*esxcliReader, error) { 447 stdout, err := d.ssh("esxcli --formatter csv "+strings.Join(args, " "), nil) 448 if err != nil { 449 return nil, err 450 } 451 r := csv.NewReader(bytes.NewReader(stdout.Bytes())) 452 r.TrailingComma = true 453 header, err := r.Read() 454 if err != nil { 455 return nil, err 456 } 457 return &esxcliReader{r, header}, nil 458 } 459 460 type esxcliReader struct { 461 cr *csv.Reader 462 header []string 463 } 464 465 func (r *esxcliReader) read() (map[string]string, error) { 466 fields, err := r.cr.Read() 467 468 if err != nil { 469 return nil, err 470 } 471 472 record := map[string]string{} 473 for i, v := range fields { 474 record[r.header[i]] = v 475 } 476 477 return record, nil 478 } 479 480 func (r *esxcliReader) find(key, val string) (map[string]string, error) { 481 for { 482 record, err := r.read() 483 if err != nil { 484 return nil, err 485 } 486 if record[key] == val { 487 return record, nil 488 } 489 } 490 }