github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/vsphere/internal/vsphereclient/createvm.go (about) 1 // Copyright 2015-2017 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package vsphereclient 5 6 import ( 7 "context" 8 "crypto/rand" 9 "fmt" 10 "io" 11 "math/big" 12 "path" 13 "strconv" 14 "strings" 15 "time" 16 17 humanize "github.com/dustin/go-humanize" 18 "github.com/juju/clock" 19 "github.com/juju/errors" 20 "github.com/kr/pretty" 21 "github.com/vmware/govmomi/object" 22 "github.com/vmware/govmomi/ovf" 23 "github.com/vmware/govmomi/vim25/mo" 24 "github.com/vmware/govmomi/vim25/types" 25 26 "github.com/juju/juju/core/constraints" 27 ) 28 29 //go:generate go run ../../../../generate/filetoconst/filetoconst.go UbuntuOVF ubuntu.ovf ovf_ubuntu.go 2017 vsphereclient 30 31 // NetworkDevice defines a single network device attached to a newly created VM. 32 type NetworkDevice struct { 33 // Network is the name of the network the device should be connected to. 34 // If empty it will be connected to the default "VM Network" network. 35 Network string 36 // MAC is the hardware address of the network device. 37 MAC string 38 } 39 40 // That's a default network that's defined in OVF. 41 const defaultNetwork = "VM Network" 42 43 // CreateVirtualMachineParams contains the parameters required for creating 44 // a new virtual machine. 45 type CreateVirtualMachineParams struct { 46 // Name is the name to give the virtual machine. The VM name is used 47 // for its hostname also. 48 Name string 49 50 // Folder is the path of the VM folder, relative to the root VM folder, 51 // in which to create the VM. 52 Folder string 53 54 // VMDKDirectory is the datastore path in which VMDKs are stored for 55 // this controller. Within this directory there will be subdirectories 56 // for each series, and within those the VMDKs will be stored. 57 VMDKDirectory string 58 59 // Series is the name of the OS series that the image will run. 60 Series string 61 62 // ReadOVA returns the location of, and an io.ReadCloser for, 63 // the OVA from which to extract the VMDK. The location may be 64 // used for reporting progress. The ReadCloser must be closed 65 // by the caller when it is finished with it. 66 ReadOVA func() (location string, _ io.ReadCloser, _ error) 67 68 // OVASHA256 is the expected SHA-256 hash of the OVA. 69 OVASHA256 string 70 71 // UserData is the cloud-init user-data. 72 UserData string 73 74 // ComputeResource is the compute resource (host or cluster) to be used 75 // to create the VM. 76 ComputeResource *mo.ComputeResource 77 78 // Datastore is the name of the datastore in which to create the VM. 79 // If this is empty, any accessible datastore will be used. 80 Datastore string 81 82 // Metadata are metadata key/value pairs to apply to the VM as 83 // "extra config". 84 Metadata map[string]string 85 86 // Constraints contains the resource constraints for the virtual machine. 87 Constraints constraints.Value 88 89 // Networks contain a list of network devices the VM should have. 90 NetworkDevices []NetworkDevice 91 92 // UpdateProgress is a function that should be called before/during 93 // long-running operations to provide a progress reporting. 94 UpdateProgress func(string) 95 96 // UpdateProgressInterval is the amount of time to wait between calls 97 // to UpdateProgress. This should be lower when the operation is 98 // interactive (bootstrap), and higher when non-interactive. 99 UpdateProgressInterval time.Duration 100 101 // Clock is used for controlling the timing of progress updates. 102 Clock clock.Clock 103 104 // EnableDiskUUID controls whether the VMware disk should expose a 105 // consistent UUID to the guest OS. 106 EnableDiskUUID bool 107 } 108 109 // CreateVirtualMachine creates and powers on a new VM. 110 // 111 // This method imports an OVF template using the vSphere API. This process 112 // comprises the following steps: 113 // 1. Ensure the VMDK contained within the OVA archive (args.OVA) is 114 // stored in the datastore, in this controller's cache. If it is 115 // there already, we use it; otherwise we remove any existing VMDK 116 // for the same series, and upload the new one. 117 // 2. Call CreateImportSpec [0] with a pre-canned OVF, which validates 118 // the OVF descriptor against the hardware supported by the host system. 119 // If the validation succeeds,/the method returns an ImportSpec to use 120 // for importing the virtual machine. 121 // 3. Prepare all necessary parameters (CPU, memory, root disk, etc.), and 122 // call the ImportVApp method [0]. This method is responsible for actually 123 // creating the VM. This VM is temporary, and used only to convert the 124 // VMDK file into a disk type file. 125 // 4. Clone the temporary VM from step 3, to create the VM we will associate 126 // with the Juju machine. 127 // 5. If the user specified a root-disk constraint, extend the VMDK if its 128 // capacity is less than the specified constraint. 129 // 6. Power on the virtual machine. 130 // 131 // [0] https://www.vmware.com/support/developer/vc-sdk/visdk41pubs/ApiReference/ 132 // [1] https://www.vmware.com/support/developer/vc-sdk/visdk41pubs/ApiReference/vim.HttpNfcLease.html 133 func (c *Client) CreateVirtualMachine( 134 ctx context.Context, 135 args CreateVirtualMachineParams, 136 ) (_ *mo.VirtualMachine, resultErr error) { 137 138 // Locate the folder in which to create the VM. 139 finder, datacenter, err := c.finder(ctx) 140 if err != nil { 141 return nil, errors.Trace(err) 142 } 143 folders, err := datacenter.Folders(ctx) 144 if err != nil { 145 return nil, errors.Trace(err) 146 } 147 folderPath := path.Join(folders.VmFolder.InventoryPath, args.Folder) 148 vmFolder, err := finder.Folder(ctx, folderPath) 149 if err != nil { 150 return nil, errors.Trace(err) 151 } 152 153 // Select the datastore. 154 datastoreMo, err := c.selectDatastore(ctx, args) 155 if err != nil { 156 return nil, errors.Trace(err) 157 } 158 datastore := object.NewDatastore(c.client.Client, datastoreMo.Reference()) 159 datastore.DatacenterPath = datacenter.InventoryPath 160 datastore.SetInventoryPath(path.Join(folders.DatastoreFolder.InventoryPath, datastoreMo.Name)) 161 162 // Ensure the VMDK is present in the datastore, uploading it if it 163 // doesn't already exist. 164 resourcePool := object.NewResourcePool(c.client.Client, *args.ComputeResource.ResourcePool) 165 taskWaiter := &taskWaiter{args.Clock, args.UpdateProgress, args.UpdateProgressInterval} 166 vmdkDatastorePath, releaseVMDK, err := c.ensureVMDK(ctx, args, datastore, datacenter, taskWaiter) 167 if err != nil { 168 return nil, errors.Trace(err) 169 } 170 defer releaseVMDK() 171 172 // Import the VApp, creating a temporary VM. This is necessary to 173 // import the VMDK, which exists in the datastore as a not-a-disk 174 // file type. 175 args.UpdateProgress("creating import spec") 176 importSpec, err := c.createImportSpec(ctx, args, datastore, vmdkDatastorePath) 177 if err != nil { 178 return nil, errors.Annotate(err, "creating import spec") 179 } 180 importSpec.ConfigSpec.Name += ".tmp" 181 182 args.UpdateProgress(fmt.Sprintf("creating VM %q", args.Name)) 183 c.logger.Debugf("creating temporary VM in folder %s", vmFolder) 184 c.logger.Tracef("import spec: %s", pretty.Sprint(importSpec)) 185 lease, err := resourcePool.ImportVApp(ctx, importSpec, vmFolder, nil) 186 if err != nil { 187 return nil, errors.Annotatef(err, "failed to import vapp") 188 } 189 info, err := lease.Wait(ctx, nil) 190 if err != nil { 191 return nil, errors.Trace(err) 192 } 193 if err := lease.Complete(ctx); err != nil { 194 return nil, errors.Trace(err) 195 } 196 tempVM := object.NewVirtualMachine(c.client.Client, info.Entity) 197 defer func() { 198 if err := c.destroyVM(ctx, tempVM, taskWaiter); err != nil { 199 c.logger.Warningf("failed to delete temporary VM: %s", err) 200 } 201 }() 202 203 // Clone the temporary VM to import the VMDK, as mentioned above. 204 // After cloning the temporary VM, we must detach the original 205 // VMDK from the temporary VM to avoid deleting it when destroying 206 // the VM. 207 c.logger.Debugf("cloning VM") 208 vm, err := c.cloneVM(ctx, tempVM, args.Name, vmFolder, taskWaiter) 209 if err != nil { 210 return nil, errors.Trace(err) 211 } 212 args.UpdateProgress("VM cloned") 213 defer func() { 214 if resultErr == nil { 215 return 216 } 217 if err := c.destroyVM(ctx, vm, taskWaiter); err != nil { 218 c.logger.Warningf("failed to delete VM: %s", err) 219 } 220 }() 221 if _, err := c.detachDisk(ctx, tempVM, taskWaiter); err != nil { 222 return nil, errors.Trace(err) 223 } 224 if args.Constraints.RootDisk != nil { 225 // The user specified a root disk, so extend the VM's 226 // disk before powering the VM on. 227 args.UpdateProgress(fmt.Sprintf( 228 "extending disk to %s", 229 humanize.IBytes(*args.Constraints.RootDisk*1024*1024), 230 )) 231 if err := c.extendVMRootDisk( 232 ctx, vm, datacenter, 233 *args.Constraints.RootDisk, 234 taskWaiter, 235 ); err != nil { 236 return nil, errors.Trace(err) 237 } 238 } 239 240 // Finally, power on and return the VM. 241 args.UpdateProgress("powering on") 242 task, err := vm.PowerOn(ctx) 243 if err != nil { 244 return nil, errors.Trace(err) 245 } 246 taskInfo, err := taskWaiter.waitTask(ctx, task, "powering on VM") 247 if err != nil { 248 return nil, errors.Trace(err) 249 } 250 var res mo.VirtualMachine 251 if err := c.client.RetrieveOne(ctx, *taskInfo.Entity, nil, &res); err != nil { 252 return nil, errors.Trace(err) 253 } 254 return &res, nil 255 } 256 257 func (c *Client) extendVMRootDisk( 258 ctx context.Context, 259 vm *object.VirtualMachine, 260 datacenter *object.Datacenter, 261 sizeMB uint64, 262 taskWaiter *taskWaiter, 263 ) error { 264 var mo mo.VirtualMachine 265 if err := c.client.RetrieveOne(ctx, vm.Reference(), []string{"config.hardware"}, &mo); err != nil { 266 return errors.Trace(err) 267 } 268 for _, dev := range mo.Config.Hardware.Device { 269 dev, ok := dev.(*types.VirtualDisk) 270 if !ok { 271 continue 272 } 273 newCapacityInKB := int64(sizeMB) * 1024 274 if dev.CapacityInKB >= newCapacityInKB { 275 // The root disk is already bigger than the 276 // user-specified size, so leave it alone. 277 return nil 278 } 279 backing, ok := dev.Backing.(types.BaseVirtualDeviceFileBackingInfo) 280 if !ok { 281 continue 282 } 283 datastorePath := backing.GetVirtualDeviceFileBackingInfo().FileName 284 return errors.Trace(c.extendDisk( 285 ctx, datacenter, datastorePath, newCapacityInKB, taskWaiter, 286 )) 287 } 288 return errors.New("disk not found") 289 } 290 291 func (c *Client) createImportSpec( 292 ctx context.Context, 293 args CreateVirtualMachineParams, 294 datastore *object.Datastore, 295 vmdkDatastorePath string, 296 ) (*types.VirtualMachineImportSpec, error) { 297 cisp := types.OvfCreateImportSpecParams{ 298 EntityName: args.Name, 299 PropertyMapping: []types.KeyValue{ 300 {Key: "user-data", Value: args.UserData}, 301 {Key: "hostname", Value: args.Name}, 302 }, 303 } 304 305 ovfManager := ovf.NewManager(c.client.Client) 306 resourcePool := object.NewReference(c.client.Client, *args.ComputeResource.ResourcePool) 307 308 spec, err := ovfManager.CreateImportSpec(ctx, UbuntuOVF, resourcePool, datastore, cisp) 309 if err != nil { 310 return nil, errors.Trace(err) 311 } else if spec.Error != nil { 312 return nil, errors.New(spec.Error[0].LocalizedMessage) 313 } 314 importSpec := spec.ImportSpec.(*types.VirtualMachineImportSpec) 315 s := &spec.ImportSpec.(*types.VirtualMachineImportSpec).ConfigSpec 316 317 // Apply resource constraints. 318 if args.Constraints.HasCpuCores() { 319 s.NumCPUs = int32(*args.Constraints.CpuCores) 320 } 321 if args.Constraints.HasMem() { 322 s.MemoryMB = int64(*args.Constraints.Mem) 323 } 324 if args.Constraints.HasCpuPower() { 325 cpuPower := int64(*args.Constraints.CpuPower) 326 s.CpuAllocation = &types.ResourceAllocationInfo{ 327 Limit: &cpuPower, 328 Reservation: &cpuPower, 329 } 330 } 331 if s.Flags == nil { 332 s.Flags = &types.VirtualMachineFlagInfo{} 333 } 334 s.Flags.DiskUuidEnabled = &args.EnableDiskUUID 335 if err := c.addRootDisk(s, args, datastore, vmdkDatastorePath); err != nil { 336 return nil, errors.Trace(err) 337 } 338 339 // Apply metadata. Note that we do not have the ability set create or 340 // apply tags that will show up in vCenter, as that requires a separate 341 // vSphere Automation that we do not have an SDK for. 342 for k, v := range args.Metadata { 343 s.ExtraConfig = append(s.ExtraConfig, &types.OptionValue{Key: k, Value: v}) 344 } 345 346 networks, dvportgroupConfig, err := c.computeResourceNetworks(ctx, args.ComputeResource) 347 if err != nil { 348 return nil, errors.Trace(err) 349 } 350 351 for i, networkDevice := range args.NetworkDevices { 352 network := networkDevice.Network 353 if network == "" { 354 network = defaultNetwork 355 } 356 357 networkReference, err := findNetwork(networks, network) 358 if err != nil { 359 return nil, errors.Trace(err) 360 } 361 device, err := c.addNetworkDevice(ctx, s, networkReference, networkDevice.MAC, dvportgroupConfig) 362 if err != nil { 363 return nil, errors.Annotatef(err, "adding network device %d - network %s", i, network) 364 } 365 c.logger.Debugf("network device: %+v", device) 366 } 367 return importSpec, nil 368 } 369 370 func (c *Client) addRootDisk( 371 s *types.VirtualMachineConfigSpec, 372 args CreateVirtualMachineParams, 373 diskDatastore *object.Datastore, 374 vmdkDatastorePath string, 375 ) error { 376 for _, d := range s.DeviceChange { 377 deviceConfigSpec := d.GetVirtualDeviceConfigSpec() 378 existingDisk, ok := deviceConfigSpec.Device.(*types.VirtualDisk) 379 if !ok { 380 continue 381 } 382 ds := diskDatastore.Reference() 383 disk := &types.VirtualDisk{ 384 VirtualDevice: types.VirtualDevice{ 385 Key: existingDisk.VirtualDevice.Key, 386 ControllerKey: existingDisk.VirtualDevice.ControllerKey, 387 UnitNumber: existingDisk.VirtualDevice.UnitNumber, 388 Backing: &types.VirtualDiskFlatVer2BackingInfo{ 389 DiskMode: string(types.VirtualDiskModePersistent), 390 ThinProvisioned: types.NewBool(true), 391 VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{ 392 FileName: vmdkDatastorePath, 393 Datastore: &ds, 394 }, 395 }, 396 }, 397 } 398 deviceConfigSpec.Device = disk 399 deviceConfigSpec.FileOperation = "" // attach existing disk 400 } 401 return nil 402 } 403 404 func (c *Client) selectDatastore( 405 ctx context.Context, 406 args CreateVirtualMachineParams, 407 ) (*mo.Datastore, error) { 408 // Select a datastore. If the user specified one, use that; otherwise 409 // choose the first one in the list that is accessible. 410 refs := make([]types.ManagedObjectReference, len(args.ComputeResource.Datastore)) 411 for i, ds := range args.ComputeResource.Datastore { 412 refs[i] = ds.Reference() 413 } 414 var datastores []mo.Datastore 415 if err := c.client.Retrieve(ctx, refs, nil, &datastores); err != nil { 416 return nil, errors.Annotate(err, "retrieving datastore details") 417 } 418 if args.Datastore != "" { 419 for _, ds := range datastores { 420 if ds.Name == args.Datastore { 421 return &ds, nil 422 } 423 } 424 return nil, errors.Errorf("could not find datastore %q", args.Datastore) 425 } 426 for _, ds := range datastores { 427 if ds.Summary.Accessible { 428 c.logger.Debugf("using datastore %q", ds.Name) 429 return &ds, nil 430 } 431 } 432 return nil, errors.New("could not find an accessible datastore") 433 } 434 435 // addNetworkDevice adds an entry to the VirtualMachineConfigSpec's 436 // DeviceChange list, to create a NIC device connecting the machine 437 // to the specified network. 438 func (c *Client) addNetworkDevice( 439 ctx context.Context, 440 spec *types.VirtualMachineConfigSpec, 441 network *mo.Network, 442 mac string, 443 dvportgroupConfig map[types.ManagedObjectReference]types.DVPortgroupConfigInfo, 444 ) (*types.VirtualVmxnet3, error) { 445 var networkBacking types.BaseVirtualDeviceBackingInfo 446 if dvportgroupConfig, ok := dvportgroupConfig[network.Reference()]; !ok { 447 // It's not a distributed virtual portgroup, so return 448 // a backing info for a plain old network interface. 449 networkBacking = &types.VirtualEthernetCardNetworkBackingInfo{ 450 VirtualDeviceDeviceBackingInfo: types.VirtualDeviceDeviceBackingInfo{ 451 DeviceName: network.Name, 452 }, 453 } 454 } else { 455 // It's a distributed virtual portgroup, so retrieve the details of 456 // the distributed virtual switch, and return a backing info for 457 // connecting the VM to the portgroup. 458 var dvs mo.DistributedVirtualSwitch 459 if err := c.client.RetrieveOne( 460 ctx, *dvportgroupConfig.DistributedVirtualSwitch, nil, &dvs, 461 ); err != nil { 462 return nil, errors.Annotate(err, "retrieving distributed vSwitch details") 463 } 464 networkBacking = &types.VirtualEthernetCardDistributedVirtualPortBackingInfo{ 465 Port: types.DistributedVirtualSwitchPortConnection{ 466 SwitchUuid: dvs.Uuid, 467 PortgroupKey: dvportgroupConfig.Key, 468 }, 469 } 470 } 471 472 var networkDevice types.VirtualVmxnet3 473 wakeOnLan := true 474 networkDevice.WakeOnLanEnabled = &wakeOnLan 475 networkDevice.Backing = networkBacking 476 if mac != "" { 477 if !VerifyMAC(mac) { 478 return nil, fmt.Errorf("Invalid MAC address: %q", mac) 479 } 480 networkDevice.AddressType = "Manual" 481 networkDevice.MacAddress = mac 482 } 483 networkDevice.Connectable = &types.VirtualDeviceConnectInfo{ 484 StartConnected: true, 485 AllowGuestControl: true, 486 } 487 spec.DeviceChange = append(spec.DeviceChange, &types.VirtualDeviceConfigSpec{ 488 Operation: types.VirtualDeviceConfigSpecOperationAdd, 489 Device: &networkDevice, 490 }) 491 return &networkDevice, nil 492 } 493 494 // GenerateMAC generates a random hardware address that meets VMWare 495 // requirements for MAC address: 00:50:56:XX:YY:ZZ where XX is between 00 and 3f. 496 // https://pubs.vmware.com/vsphere-4-esx-vcenter/index.jsp?topic=/com.vmware.vsphere.server_configclassic.doc_41/esx_server_config/advanced_networking/c_setting_up_mac_addresses.html 497 func GenerateMAC() (string, error) { 498 c, err := rand.Int(rand.Reader, big.NewInt(0x3fffff)) 499 if err != nil { 500 return "", err 501 } 502 r := c.Uint64() 503 return fmt.Sprintf("00:50:56:%.2x:%.2x:%.2x", (r>>16)&0xff, (r>>8)&0xff, r&0xff), nil 504 } 505 506 // VerifyMAC verifies that the MAC is valid for VMWare. 507 func VerifyMAC(mac string) bool { 508 parts := strings.Split(mac, ":") 509 if len(parts) != 6 { 510 return false 511 } 512 if parts[0] != "00" || parts[1] != "50" || parts[2] != "56" { 513 return false 514 } 515 for i, part := range parts[3:] { 516 v, err := strconv.ParseUint(part, 16, 8) 517 if err != nil { 518 return false 519 } 520 if i == 0 && v > 0x3f { 521 // 4th byte must be <= 0x3f 522 return false 523 } 524 } 525 return true 526 } 527 528 func findNetwork(networks []mo.Network, name string) (*mo.Network, error) { 529 for _, n := range networks { 530 if n.Name == name { 531 return &n, nil 532 } 533 } 534 return nil, errors.NotFoundf("network %q", name) 535 } 536 537 // computeResourceNetworks returns the networks available to the compute 538 // resource, and the config info for the distributed virtual portgroup 539 // networks. Networks are returned with the distributed virtual portgroups 540 // first, then standard switch networks, and then finally opaque networks. 541 func (c *Client) computeResourceNetworks( 542 ctx context.Context, 543 computeResource *mo.ComputeResource, 544 ) ([]mo.Network, map[types.ManagedObjectReference]types.DVPortgroupConfigInfo, error) { 545 refsByType := make(map[string][]types.ManagedObjectReference) 546 for _, network := range computeResource.Network { 547 refsByType[network.Type] = append(refsByType[network.Type], network.Reference()) 548 } 549 var networks []mo.Network 550 if refs := refsByType["Network"]; len(refs) > 0 { 551 if err := c.client.Retrieve(ctx, refs, nil, &networks); err != nil { 552 return nil, nil, errors.Annotate(err, "retrieving network details") 553 } 554 } 555 var opaqueNetworks []mo.OpaqueNetwork 556 if refs := refsByType["OpaqueNetwork"]; len(refs) > 0 { 557 if err := c.client.Retrieve(ctx, refs, nil, &opaqueNetworks); err != nil { 558 return nil, nil, errors.Annotate(err, "retrieving opaque network details") 559 } 560 for _, on := range opaqueNetworks { 561 networks = append(networks, on.Network) 562 } 563 } 564 var dvportgroups []mo.DistributedVirtualPortgroup 565 var dvportgroupConfig map[types.ManagedObjectReference]types.DVPortgroupConfigInfo 566 if refs := refsByType["DistributedVirtualPortgroup"]; len(refs) > 0 { 567 if err := c.client.Retrieve(ctx, refs, nil, &dvportgroups); err != nil { 568 return nil, nil, errors.Annotate(err, "retrieving distributed virtual portgroup details") 569 } 570 dvportgroupConfig = make(map[types.ManagedObjectReference]types.DVPortgroupConfigInfo) 571 allnetworks := make([]mo.Network, len(dvportgroups)+len(networks)) 572 for i, d := range dvportgroups { 573 allnetworks[i] = d.Network 574 dvportgroupConfig[allnetworks[i].Reference()] = d.Config 575 } 576 copy(allnetworks[len(dvportgroups):], networks) 577 networks = allnetworks 578 } 579 return networks, dvportgroupConfig, nil 580 }