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