github.com/coreos/mantle@v0.13.0/platform/api/esx/api.go (about) 1 // Copyright (c) 2016 VMware, Inc. All Rights Reserved. 2 // Copyright 2017 CoreOS, Inc. 3 // 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at 7 // 8 // http://www.apache.org/licenses/LICENSE-2.0 9 // 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 16 package esx 17 18 import ( 19 "context" 20 "encoding/base64" 21 "errors" 22 "fmt" 23 "io/ioutil" 24 "net" 25 "net/url" 26 "path" 27 "strings" 28 "time" 29 30 "github.com/coreos/pkg/capnslog" 31 "github.com/vmware/govmomi" 32 "github.com/vmware/govmomi/find" 33 "github.com/vmware/govmomi/object" 34 "github.com/vmware/govmomi/ovf" 35 "github.com/vmware/govmomi/vim25/mo" 36 "github.com/vmware/govmomi/vim25/progress" 37 "github.com/vmware/govmomi/vim25/soap" 38 "github.com/vmware/govmomi/vim25/types" 39 40 "github.com/coreos/mantle/auth" 41 "github.com/coreos/mantle/platform" 42 "github.com/coreos/mantle/platform/conf" 43 ) 44 45 type Options struct { 46 *platform.Options 47 48 // Config file. Defaults to $HOME/.config/esx.json 49 ConfigPath string 50 // Profile name 51 Profile string 52 53 Server string 54 User string 55 Password string 56 BaseVMName string 57 } 58 59 var plog = capnslog.NewPackageLogger("github.com/coreos/mantle", "platform/api/esx") 60 61 type API struct { 62 options *Options 63 client *govmomi.Client 64 ctx context.Context 65 } 66 67 type ESXMachine struct { 68 Name string 69 IPAddress string 70 } 71 72 type ovfFileItem struct { 73 url *url.URL 74 item types.OvfFileItem 75 ch chan progress.Report 76 } 77 78 type serverResources struct { 79 finder *find.Finder 80 datacenter *object.Datacenter 81 resourcePool *object.ResourcePool 82 datastore *object.Datastore 83 network object.NetworkReference 84 } 85 86 func (a *API) getMachine(vm *object.VirtualMachine) (*ESXMachine, error) { 87 ctx := context.Background() 88 deadline, cancel := context.WithDeadline(ctx, time.Now().Add(1000*time.Second)) 89 defer cancel() 90 ip, err := vm.WaitForNetIP(deadline, false) 91 if err != nil { 92 return nil, fmt.Errorf("waiting for net ip: %v", err) 93 } 94 95 var ipaddr string 96 OUTER: 97 for _, ips := range ip { 98 for _, val := range ips { 99 addr := net.ParseIP(val) 100 if addr.IsLinkLocalMulticast() || addr.IsLinkLocalUnicast() { 101 continue 102 } 103 ipaddr = val 104 break OUTER 105 } 106 } 107 108 var mvm mo.VirtualMachine 109 err = vm.Properties(ctx, vm.Reference(), []string{"summary"}, &mvm) 110 if err != nil { 111 return nil, fmt.Errorf("getting machine reference: %v", err) 112 } 113 114 return &ESXMachine{ 115 Name: mvm.Summary.Config.Name, 116 IPAddress: ipaddr, 117 }, nil 118 } 119 120 func New(opts *Options) (*API, error) { 121 if opts.Server == "" || opts.User == "" || opts.Password == "" { 122 profiles, err := auth.ReadESXConfig(opts.ConfigPath) 123 if err != nil { 124 return nil, fmt.Errorf("couldn't read ESX config: %v", err) 125 } 126 127 if opts.Profile == "" { 128 opts.Profile = "default" 129 } 130 profile, ok := profiles[opts.Profile] 131 if !ok { 132 return nil, fmt.Errorf("no such profile %q", opts.Profile) 133 } 134 if opts.Server == "" { 135 opts.Server = profile.Server 136 } 137 if opts.User == "" { 138 opts.User = profile.User 139 } 140 if opts.Password == "" { 141 opts.Password = profile.Password 142 } 143 } 144 145 esxUrl := fmt.Sprintf("%s:%s@%s", opts.User, opts.Password, opts.Server) 146 u, err := soap.ParseURL(esxUrl) 147 if err != nil { 148 return nil, fmt.Errorf("parsing ESX URL: %v", err) 149 } 150 151 ctx := context.Background() 152 153 client, err := govmomi.NewClient(ctx, u, true) 154 if err != nil { 155 return nil, fmt.Errorf("connecting to ESX: %v", err) 156 } 157 158 return &API{ 159 options: opts, 160 client: client, 161 ctx: ctx, 162 }, nil 163 } 164 165 func getNetworkDevice(net object.NetworkReference) (types.BaseVirtualDevice, error) { 166 backing, err := net.EthernetCardBackingInfo(context.TODO()) 167 if err != nil { 168 return nil, err 169 } 170 171 device, err := object.EthernetCardTypes().CreateEthernetCard("e1000", backing) 172 if err != nil { 173 return nil, err 174 } 175 176 return device, nil 177 } 178 179 func (a *API) buildCloneSpec(baseVM *object.VirtualMachine, folder *object.Folder, network object.NetworkReference, resourcePool *object.ResourcePool, datastore *object.Datastore, userdata string) (*types.VirtualMachineCloneSpec, error) { 180 devices, err := baseVM.Device(a.ctx) 181 if err != nil { 182 return nil, fmt.Errorf("couldn't get base VM devices: %v", err) 183 } 184 var card *types.VirtualEthernetCard 185 for _, device := range devices { 186 if c, ok := device.(types.BaseVirtualEthernetCard); ok { 187 card = c.GetVirtualEthernetCard() 188 break 189 } 190 } 191 if card == nil { 192 return nil, fmt.Errorf("No network device found.") 193 } 194 195 netDev, err := getNetworkDevice(network) 196 if err != nil { 197 return nil, fmt.Errorf("couldn't get new network backing device: %v", err) 198 } 199 200 card.Backing = netDev.(types.BaseVirtualEthernetCard).GetVirtualEthernetCard().Backing 201 202 folderRef := folder.Reference() 203 poolRef := resourcePool.Reference() 204 datastoreRef := datastore.Reference() 205 206 cloneSpec := &types.VirtualMachineCloneSpec{ 207 Location: types.VirtualMachineRelocateSpec{ 208 DeviceChange: []types.BaseVirtualDeviceConfigSpec{ 209 &types.VirtualDeviceConfigSpec{ 210 Operation: types.VirtualDeviceConfigSpecOperationEdit, 211 Device: card, 212 }, 213 }, 214 Folder: &folderRef, 215 Pool: &poolRef, 216 Datastore: &datastoreRef, 217 }, 218 PowerOn: false, 219 Template: false, 220 } 221 222 return cloneSpec, nil 223 } 224 225 func (a *API) addSerialPort(vm *object.VirtualMachine) error { 226 devices, err := vm.Device(a.ctx) 227 if err != nil { 228 return fmt.Errorf("couldn't get devices for vm: %v", err) 229 } 230 231 d, err := devices.CreateSerialPort() 232 if err != nil { 233 return fmt.Errorf("couldn't create serial port: %v", err) 234 } 235 236 err = vm.AddDevice(a.ctx, d) 237 if err != nil { 238 return fmt.Errorf("couldn't add serial port to vm: %v", err) 239 } 240 241 // refresh devices 242 devices, err = vm.Device(a.ctx) 243 if err != nil { 244 return fmt.Errorf("couldn't get devices for vm: %v", err) 245 } 246 247 d, err = devices.FindSerialPort("") 248 if err != nil { 249 return fmt.Errorf("couldn't find serial port for vm: %v", err) 250 } 251 252 var mvm mo.VirtualMachine 253 err = vm.Properties(a.ctx, vm.Reference(), []string{"config.files.logDirectory"}, &mvm) 254 if err != nil { 255 return fmt.Errorf("couldn't get log directory: %v", err) 256 } 257 uri := path.Join(mvm.Config.Files.LogDirectory, "serial.out") 258 259 return vm.EditDevice(a.ctx, devices.ConnectSerialPort(d, uri, false, "")) 260 } 261 262 func (a *API) GetConsoleOutput(name string) (string, error) { 263 defaults, err := a.getServerDefaults() 264 if err != nil { 265 return "", fmt.Errorf("couldn't get server defaults: %v", err) 266 } 267 268 uri := fmt.Sprintf("%s/serial.out", name) 269 270 p := soap.DefaultDownload 271 272 f, _, err := defaults.datastore.Download(a.ctx, uri, &p) 273 if err != nil { 274 return "", fmt.Errorf("couldn't download console logs: %v", err) 275 } 276 defer f.Close() 277 278 buf, err := ioutil.ReadAll(f) 279 if err != nil { 280 return "", fmt.Errorf("couldn't read serial output: %v", err) 281 } 282 283 return string(buf), nil 284 } 285 286 func (a *API) CleanupDevice(name string) error { 287 defaults, err := a.getServerDefaults() 288 if err != nil { 289 return fmt.Errorf("couldn't get server defaults: %v", err) 290 } 291 292 _, err = defaults.finder.VirtualMachine(a.ctx, name) 293 if err == nil { 294 return fmt.Errorf("VM still exists") 295 } 296 297 fm := defaults.datastore.NewFileManager(defaults.datacenter, true) 298 299 // Remove the serial.out file 300 uri := fmt.Sprintf("%s/serial.out", name) 301 err = fm.DeleteFile(a.ctx, uri) 302 if err != nil && !strings.HasSuffix(err.Error(), "was not found") { 303 return fmt.Errorf("couldn't delete serial.out: %v", err) 304 } 305 306 // Remove the VM directory 307 err = fm.DeleteFile(a.ctx, name) 308 if err != nil && !strings.HasSuffix(err.Error(), "was not found") { 309 return fmt.Errorf("couldn't delete vm directory: %v", err) 310 } 311 312 return nil 313 } 314 315 func (a *API) CreateDevice(name string, conf *conf.Conf) (*ESXMachine, error) { 316 if a.options.BaseVMName == "" { 317 return nil, fmt.Errorf("Base VM Name must be supplied") 318 } 319 320 userdata := base64.StdEncoding.EncodeToString(conf.Bytes()) 321 322 defaults, err := a.getServerDefaults() 323 if err != nil { 324 return nil, fmt.Errorf("couldn't get server defaults: %v", err) 325 } 326 327 baseVM, err := defaults.finder.VirtualMachine(a.ctx, a.options.BaseVMName) 328 if err != nil { 329 return nil, fmt.Errorf("couldn't find base VM: %v", err) 330 } 331 332 folders, err := defaults.datacenter.Folders(a.ctx) 333 if err != nil { 334 return nil, fmt.Errorf("getting datacenter folders: %v", err) 335 } 336 folder := folders.VmFolder 337 338 cloneSpec, err := a.buildCloneSpec(baseVM, folder, defaults.network, defaults.resourcePool, defaults.datastore, userdata) 339 if err != nil { 340 return nil, fmt.Errorf("failed building clone spec: %v", err) 341 } 342 343 task, err := baseVM.Clone(a.ctx, folder, name, *cloneSpec) 344 if err != nil { 345 return nil, fmt.Errorf("couldn't clone base VM: %v", err) 346 } 347 348 err = task.Wait(a.ctx) 349 if err != nil { 350 return nil, fmt.Errorf("clone base VM operation failed: %v", err) 351 } 352 353 vm, err := defaults.finder.VirtualMachine(a.ctx, name) 354 if err != nil { 355 return nil, fmt.Errorf("couldn't find cloned VM: %v", err) 356 } 357 358 err = a.addSerialPort(vm) 359 if err != nil { 360 return nil, fmt.Errorf("adding serial port: %v", err) 361 } 362 363 err = a.updateOVFEnv(vm, userdata) 364 if err != nil { 365 return nil, fmt.Errorf("setting guestinfo settings: %v", err) 366 } 367 368 err = a.startVM(vm) 369 if err != nil { 370 return nil, fmt.Errorf("starting vm: %v", err) 371 } 372 373 mach, err := a.getMachine(vm) 374 if err != nil { 375 return nil, fmt.Errorf("getting machine info: %v", err) 376 } 377 378 return mach, nil 379 } 380 381 func (a *API) CreateBaseDevice(name, ovaPath string) error { 382 if ovaPath == "" { 383 return fmt.Errorf("ova path cannot be empty") 384 } 385 386 defaults, err := a.getServerDefaults() 387 if err != nil { 388 return fmt.Errorf("getting ESX defaults: %v", err) 389 } 390 391 arch, cisr, err := a.buildCreateImportSpecRequest(name, ovaPath, defaults.finder, defaults.network, defaults.resourcePool, defaults.datastore) 392 if err != nil { 393 return fmt.Errorf("building CreateImportSpecRequest: %v", err) 394 } 395 396 folders, err := defaults.datacenter.Folders(a.ctx) 397 if err != nil { 398 return fmt.Errorf("getting datacenter folders: %v", err) 399 } 400 folder := folders.VmFolder 401 402 entity, err := a.uploadToResourcePool(arch, defaults.resourcePool, cisr, folder) 403 if err != nil { 404 return fmt.Errorf("uploading disks to ResourcePool: %v", err) 405 } 406 407 // object.NewVirtualMachine returns a VirtualMachine object but we don't 408 // need to do anything with the returned object so ignore it 409 _ = object.NewVirtualMachine(a.client.Client, *entity) 410 411 return nil 412 } 413 414 func (a *API) TerminateDevice(name string) error { 415 defaults, err := a.getServerDefaults() 416 if err != nil { 417 return fmt.Errorf("couldn't get server defaults: %v", err) 418 } 419 420 vm, err := defaults.finder.VirtualMachine(a.ctx, name) 421 if err != nil { 422 return fmt.Errorf("couldn't find VM: %v", err) 423 } 424 425 return a.deleteDevice(vm) 426 } 427 428 func (a *API) deleteDevice(vm *object.VirtualMachine) error { 429 task, err := vm.PowerOff(a.ctx) 430 if err != nil { 431 return fmt.Errorf("powering off vm: %v", err) 432 } 433 434 // We don't check for errors on this task because it will throw an error 435 // if the VM is already in a powered off state 436 _ = task.Wait(a.ctx) 437 438 task, err = vm.Destroy(a.ctx) 439 if err != nil { 440 return fmt.Errorf("destroying vm: %v", vm) 441 } 442 443 return task.Wait(a.ctx) 444 } 445 446 func (a *API) buildCreateImportSpecRequest(name string, ovaPath string, finder *find.Finder, defaultNetwork object.NetworkReference, resourcePool *object.ResourcePool, datastore *object.Datastore) (*archive, *types.OvfCreateImportSpecResult, error) { 447 var nets []types.OvfNetworkMapping 448 nets = append(nets, types.OvfNetworkMapping{ 449 Name: "mantle", 450 Network: defaultNetwork.Reference(), 451 }) 452 453 arch := &archive{ovaPath} 454 envelope, err := arch.readEnvelope("*.ovf") 455 if err != nil { 456 return nil, nil, fmt.Errorf("reading envelope: %v", err) 457 } 458 459 ovfHandler := object.NewOvfManager(a.client.Client) 460 cisp := types.OvfCreateImportSpecParams{ 461 EntityName: name, 462 OvfManagerCommonParams: types.OvfManagerCommonParams{ 463 Locale: "US"}, 464 PropertyMapping: []types.KeyValue{}, 465 NetworkMapping: networkMap(finder, envelope), 466 } 467 468 descriptor, err := arch.readOvf("*.ovf") 469 if err != nil { 470 return nil, nil, fmt.Errorf("reading ovf: %v", err) 471 } 472 cisr, err := ovfHandler.CreateImportSpec(a.ctx, string(descriptor), resourcePool, datastore, cisp) 473 if err != nil { 474 return nil, nil, err 475 } 476 if cisr.Error != nil { 477 return nil, nil, errors.New(cisr.Error[0].LocalizedMessage) 478 } 479 return arch, cisr, nil 480 } 481 482 func (a *API) getServerDefaults() (serverResources, error) { 483 finder := find.NewFinder(a.client.Client, true) 484 datacenter, err := finder.DefaultDatacenter(a.ctx) 485 if err != nil { 486 return serverResources{}, err 487 } 488 finder.SetDatacenter(datacenter) 489 resourcePool, err := finder.DefaultResourcePool(a.ctx) 490 if err != nil { 491 return serverResources{}, err 492 } 493 datastore, err := finder.DefaultDatastore(a.ctx) 494 if err != nil { 495 return serverResources{}, err 496 } 497 498 defaultNetwork, err := finder.DefaultNetwork(a.ctx) 499 if err != nil { 500 return serverResources{}, err 501 } 502 503 return serverResources{ 504 finder: finder, 505 datacenter: datacenter, 506 resourcePool: resourcePool, 507 datastore: datastore, 508 network: defaultNetwork, 509 }, nil 510 } 511 512 func (a *API) uploadToResourcePool(arch *archive, resourcePool *object.ResourcePool, cisr *types.OvfCreateImportSpecResult, folder *object.Folder) (*types.ManagedObjectReference, error) { 513 lease, err := resourcePool.ImportVApp(a.ctx, cisr.ImportSpec, folder, nil) 514 if err != nil { 515 return nil, fmt.Errorf("importing vApp: %v", err) 516 } 517 518 info, err := lease.Wait(a.ctx) 519 if err != nil { 520 return nil, err 521 } 522 523 var items []ovfFileItem 524 525 for _, device := range info.DeviceUrl { 526 for _, item := range cisr.FileItem { 527 if device.ImportKey != item.DeviceId { 528 continue 529 } 530 531 u, err := a.client.Client.ParseURL(device.Url) 532 if err != nil { 533 return nil, err 534 } 535 536 i := ovfFileItem{ 537 url: u, 538 item: item, 539 ch: make(chan progress.Report), 540 } 541 542 items = append(items, i) 543 } 544 } 545 546 upd := newLeaseUpdater(a.client.Client, lease, items) 547 defer upd.Done() 548 549 for _, i := range items { 550 err = a.upload(arch, lease, i) 551 if err != nil { 552 return nil, err 553 } 554 } 555 556 err = lease.HttpNfcLeaseComplete(a.ctx) 557 if err != nil { 558 return nil, err 559 } 560 return &info.Entity, nil 561 } 562 563 func networkMap(finder *find.Finder, e *ovf.Envelope) (p []types.OvfNetworkMapping) { 564 if e.Network != nil { 565 for _, net := range e.Network.Networks { 566 if n, err := finder.Network(context.TODO(), net.Name); err == nil { 567 p = append(p, types.OvfNetworkMapping{ 568 Name: net.Name, 569 Network: n.Reference(), 570 }) 571 } 572 } 573 } 574 return 575 } 576 577 func (a *API) updateOVFEnv(vm *object.VirtualMachine, userdata string) error { 578 var property []types.VAppPropertySpec 579 580 var mvm mo.VirtualMachine 581 err := vm.Properties(a.ctx, vm.Reference(), []string{"config", "config.vAppConfig", "config.vAppConfig.property"}, &mvm) 582 if err != nil { 583 return fmt.Errorf("couldn't get config.vappconfig: %v", err) 584 } 585 586 for _, item := range mvm.Config.VAppConfig.(*types.VmConfigInfo).Property { 587 if item.Id == "guestinfo.coreos.config.data" { 588 property = append(property, types.VAppPropertySpec{ 589 ArrayUpdateSpec: types.ArrayUpdateSpec{ 590 Operation: types.ArrayUpdateOperationEdit, 591 }, 592 Info: &types.VAppPropertyInfo{ 593 Key: item.Key, 594 Id: item.Id, 595 DefaultValue: userdata, 596 }, 597 }) 598 } else if item.Id == "guestinfo.coreos.config.data.encoding" { 599 property = append(property, types.VAppPropertySpec{ 600 ArrayUpdateSpec: types.ArrayUpdateSpec{ 601 Operation: types.ArrayUpdateOperationEdit, 602 }, 603 Info: &types.VAppPropertyInfo{ 604 Key: item.Key, 605 Id: item.Id, 606 DefaultValue: "base64", 607 }, 608 }) 609 } 610 } 611 612 if len(property) != 2 { 613 return fmt.Errorf("couldn't find required vApp properties on vm") 614 } 615 616 task, err := vm.Reconfigure(a.ctx, types.VirtualMachineConfigSpec{ 617 VAppConfig: &types.VmConfigSpec{ 618 Property: property, 619 }, 620 }) 621 622 if err != nil { 623 return err 624 } 625 626 return task.Wait(a.ctx) 627 } 628 629 func (a *API) startVM(vm *object.VirtualMachine) error { 630 task, err := vm.PowerOn(a.ctx) 631 if err != nil { 632 return err 633 } 634 635 return task.Wait(a.ctx) 636 } 637 638 func (a *API) upload(arch *archive, lease *object.HttpNfcLease, ofi ovfFileItem) error { 639 item := ofi.item 640 file := item.Path 641 642 f, size, err := arch.open(file) 643 if err != nil { 644 return err 645 } 646 defer f.Close() 647 648 opts := soap.Upload{ 649 ContentLength: size, 650 Progress: nil, 651 } 652 653 // Non-disk files (such as .iso) use the PUT method. 654 // Overwrite: t header is also required in this case (ovftool does the same) 655 if item.Create { 656 opts.Method = "PUT" 657 opts.Headers = map[string]string{ 658 "Overwrite": "t", 659 } 660 } else { 661 opts.Method = "POST" 662 opts.Type = "application/x-vnd.vmware-streamVmdk" 663 } 664 665 return a.client.Client.Upload(f, ofi.url, &opts) 666 } 667 668 func (a *API) PreflightCheck() error { 669 var mgr mo.SessionManager 670 671 c := a.client.Client 672 673 return mo.RetrieveProperties(context.Background(), c, c.ServiceContent.PropertyCollector, *c.ServiceContent.SessionManager, &mgr) 674 }