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  }