diff --git a/apis/v1beta1/types.go b/apis/v1beta1/types.go index 1a12b8de6f..728f26a457 100644 --- a/apis/v1beta1/types.go +++ b/apis/v1beta1/types.go @@ -251,13 +251,24 @@ type PCIDeviceSpec struct { // DeviceID is the device ID of a virtual machine's PCI, in integer. // Defaults to the eponymous property value in the template from which the // virtual machine is cloned. + // Mutually exclusive with VGPUProfile as VGPUProfile and DeviceID + VendorID + // are two independent ways to define PCI devices. // +kubebuilder:validation:Required DeviceID *int32 `json:"deviceId,omitempty"` // VendorId is the vendor ID of a virtual machine's PCI, in integer. // Defaults to the eponymous property value in the template from which the // virtual machine is cloned. + // Mutually exclusive with VGPUProfile as VGPUProfile and DeviceID + VendorID + // are two independent ways to define PCI devices. // +kubebuilder:validation:Required VendorID *int32 `json:"vendorId,omitempty"` + // VGPUProfile is the profile name of a virtual machine's vGPU, in string. + // Defaults to the eponymous property value in the template from which the + // virtual machine is cloned. + // Mutually exclusive with DeviceID and VendorID as VGPUProfile and DeviceID + VendorID + // are two independent ways to define PCI devices. + // +kubebuilder:validation:Required + VGPUProfile string `json:"vGPUProfile,omitempty"` // CustomLabel is the hardware label of a virtual machine's PCI device. // Defaults to the eponymous property value in the template from which the // virtual machine is cloned. diff --git a/config/default/crd/bases/infrastructure.cluster.x-k8s.io_vspheremachines.yaml b/config/default/crd/bases/infrastructure.cluster.x-k8s.io_vspheremachines.yaml index bc1ec1541e..3f9b8fb56c 100644 --- a/config/default/crd/bases/infrastructure.cluster.x-k8s.io_vspheremachines.yaml +++ b/config/default/crd/bases/infrastructure.cluster.x-k8s.io_vspheremachines.yaml @@ -1370,13 +1370,25 @@ spec: DeviceID is the device ID of a virtual machine's PCI, in integer. Defaults to the eponymous property value in the template from which the virtual machine is cloned. + Mutually exclusive with VGPUProfile as VGPUProfile and DeviceID + VendorID + are two independent ways to define PCI devices. format: int32 type: integer + vGPUProfile: + description: |- + VGPUProfile is the profile name of a virtual machine's vGPU, in string. + Defaults to the eponymous property value in the template from which the + virtual machine is cloned. + Mutually exclusive with DeviceID and VendorID as VGPUProfile and DeviceID + VendorID + are two independent ways to define PCI devices. + type: string vendorId: description: |- VendorId is the vendor ID of a virtual machine's PCI, in integer. Defaults to the eponymous property value in the template from which the virtual machine is cloned. + Mutually exclusive with VGPUProfile as VGPUProfile and DeviceID + VendorID + are two independent ways to define PCI devices. format: int32 type: integer type: object diff --git a/config/default/crd/bases/infrastructure.cluster.x-k8s.io_vspheremachinetemplates.yaml b/config/default/crd/bases/infrastructure.cluster.x-k8s.io_vspheremachinetemplates.yaml index ca4bae3640..9d72178886 100644 --- a/config/default/crd/bases/infrastructure.cluster.x-k8s.io_vspheremachinetemplates.yaml +++ b/config/default/crd/bases/infrastructure.cluster.x-k8s.io_vspheremachinetemplates.yaml @@ -1245,13 +1245,25 @@ spec: DeviceID is the device ID of a virtual machine's PCI, in integer. Defaults to the eponymous property value in the template from which the virtual machine is cloned. + Mutually exclusive with VGPUProfile as VGPUProfile and DeviceID + VendorID + are two independent ways to define PCI devices. format: int32 type: integer + vGPUProfile: + description: |- + VGPUProfile is the profile name of a virtual machine's vGPU, in string. + Defaults to the eponymous property value in the template from which the + virtual machine is cloned. + Mutually exclusive with DeviceID and VendorID as VGPUProfile and DeviceID + VendorID + are two independent ways to define PCI devices. + type: string vendorId: description: |- VendorId is the vendor ID of a virtual machine's PCI, in integer. Defaults to the eponymous property value in the template from which the virtual machine is cloned. + Mutually exclusive with VGPUProfile as VGPUProfile and DeviceID + VendorID + are two independent ways to define PCI devices. format: int32 type: integer type: object diff --git a/config/default/crd/bases/infrastructure.cluster.x-k8s.io_vspherevms.yaml b/config/default/crd/bases/infrastructure.cluster.x-k8s.io_vspherevms.yaml index 3f42eea904..f7c8474262 100644 --- a/config/default/crd/bases/infrastructure.cluster.x-k8s.io_vspherevms.yaml +++ b/config/default/crd/bases/infrastructure.cluster.x-k8s.io_vspherevms.yaml @@ -1458,13 +1458,25 @@ spec: DeviceID is the device ID of a virtual machine's PCI, in integer. Defaults to the eponymous property value in the template from which the virtual machine is cloned. + Mutually exclusive with VGPUProfile as VGPUProfile and DeviceID + VendorID + are two independent ways to define PCI devices. format: int32 type: integer + vGPUProfile: + description: |- + VGPUProfile is the profile name of a virtual machine's vGPU, in string. + Defaults to the eponymous property value in the template from which the + virtual machine is cloned. + Mutually exclusive with DeviceID and VendorID as VGPUProfile and DeviceID + VendorID + are two independent ways to define PCI devices. + type: string vendorId: description: |- VendorId is the vendor ID of a virtual machine's PCI, in integer. Defaults to the eponymous property value in the template from which the virtual machine is cloned. + Mutually exclusive with VGPUProfile as VGPUProfile and DeviceID + VendorID + are two independent ways to define PCI devices. format: int32 type: integer type: object diff --git a/docs/gpu-vgpu.md b/docs/gpu-vgpu.md new file mode 100644 index 0000000000..81d1ea19d1 --- /dev/null +++ b/docs/gpu-vgpu.md @@ -0,0 +1,107 @@ +# GPU enabled clusters using vGPU + +## Overview + +You can choose to create a cluster with both worker and control plane nodes having vGPU devices attached to them. + +Before we begin, a few important things to note: + +- [NVIDIA GPU Operator](https://github.com/NVIDIA/gpu-operator) is used to expose the GPU PCI devices to the workloads running on the cluster. +- The OVA templates used for cluster creation should have the VMX version (Virtual Hardware) set to 17 or higher. This is necessary because Dynamic DirectPath I/O was introduced in this version, which enables the Assignable Hardware intelligence for passthrough devices. +- Since we need the VMX version to be >=17, this way of provisioning clusters with PCI passthrough devices works for vSphere 7.0 and above. This is the ESXi/VMX version [compatibility list](https://kb.vmware.com/s/article/2007240). +- UEFI boot mode is recommended for the OVAs used for cluster creation. +- Most of the setup is similar to [GPU enabled clusters via PCI Passthrough](https://github.com/kubernetes-sigs/cluster-api-provider-vsphere/blob/main/docs/gpu-pci.md#create-the-cluster). + +## An example GPU enabled cluster + +Let's create a CAPV cluster with vGPU enabled nodes. + +### Prerequisites + +- Refer the [NVIDIA Virtual GPU Software Quick Start Guide](https://docs.nvidia.com/grid/latest/grid-software-quick-start-guide/index.html) to download and install the vGPU software and configure vGPU licensing. + +- Ensure vGPU compatibility for your vSphere installation and the GPU devices using the [VMware Compatibility Guide - Shared Pass-through Graphics](https://www.vmware.com/resources/compatibility/search.php?deviceCategory=vgpu) + +- Enable Shared Passthrough for the GPU device on the ESXi Host + - Browse to a host in the vSphere Client navigator. + - On the **Configure** tab, expand **Hardware** and click **Graphics**. + - Under **GRAPHICS DEVICES**, select the GPU device to be used for vGPU, click **EDIT...** and select **Shared Direct**. Repeat this for additional GPU devices as needed. + - Select **HOST GRAPHICS**, click **EDIT...** and select **Shared Direct** and select a shared passthrough GPU assignment policy, for example **Group VMs on GPU until full (GPU consolidation)**. + +- Build an OVA template + We can build a custom OVA template using the [image-builder](https://github.com/kubernetes-sigs/image-builder) project. We will build a Ubuntu 20.04 OVA with UEFI boot mode. More documentation on how to use image-builder can be found in the [image-builder book](https://image-builder.sigs.k8s.io/capi/providers/vsphere.html) + - Clone the repo locally and go to the `./images/capi/` directory. + - Create a `packer-vars.json` file with the following content. + + ```shell + $ cat packer-vars.json + { + "vmx_version": 17 + } + ``` + + - Run the make file target associated to ubuntu 20.04 UEFI OVA as follows: + + ```shell + > PACKER_VAR_FILES=packer-vars.json make build-node-ova-vsphere-ubuntu-2004-efi + ``` + +### Source the vGPU profile(s) for the GPU device + +See "2. Choosing the vGPU Profile for the Virtual Machine" at [Using GPUs with Virtual Machines on vSphere](https://blogs.vmware.com/apps/2018/09/using-gpus-with-virtual-machines-on-vsphere-part-3-installing-the-nvidia-grid-technology.html) to see what vGPU profiles are available for your GPU device. + +We are using NVIDIA Tesla V100 32GB cards for this example and will use the `grid_v100d-4c` vGPU profile for this card that allocates 4GB GPU memory to the worker node's vGPU device. + +### Create the cluster template + +```shell +$ make dev-flavors +go run ./packaging/flavorgen --output-dir /home/user/.cluster-api/overrides/infrastructure-vsphere/v0.0.0 +``` + +Edit the generated Cluster template (`cluster-template.yaml`) to set the values for the `pciDevices` array. Here we are editing the VSphereMachineTemplate object for the worker nodes. This will create a worker node with a single NVIDIA 16GB vGPU device attached to the VM. + +```yaml +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: VSphereMachineTemplate +metadata: + name: ${CLUSTER_NAME}-worker + namespace: '${NAMESPACE}' +spec: + template: + spec: + cloneMode: linkedClone + datacenter: '${VSPHERE_DATACENTER}' + datastore: '${VSPHERE_DATASTORE}' + diskGiB: 25 + folder: '${VSPHERE_FOLDER}' + memoryMiB: 8192 + network: + devices: + - dhcp4: true + networkName: '${VSPHERE_NETWORK}' + numCPUs: 2 + os: Linux + powerOffMode: trySoft + resourcePool: '${VSPHERE_RESOURCE_POOL}' + server: '${VSPHERE_SERVER}' + storagePolicyName: '${VSPHERE_STORAGE_POLICY}' + template: '${VSPHERE_TEMPLATE}' + thumbprint: '${VSPHERE_TLS_THUMBPRINT}' + pciDevices: + - vGPUProfile: "grid_t4-1a" # value from above +``` + +Set the required values for the other fields and the cluster template is ready for use. +The similar changes can be made to a template generated using `clusterctl generate cluster` command as well. + +### Create the cluster + +Set the size of the GPU nodes appropriately, since the Nvidia gpu-operator requires additional CPU and memory to install the device drivers on the VMs. + +Note: For GPU nodes (PCI Passthrough or vGPU), all memory of the nodes must be reserved. CAPV will automatically do this for nodes that have a PCI Passthrough GPU or a vGPU device in the spec. See "Memory Reservation" at [Using GPUs with Virtual Machines on vSphere](https://blogs.vmware.com/apps/2018/09/using-gpus-with-virtual-machines-on-vsphere-part-2-vmdirectpath-i-o.html) + +Apply the manifest from the previous step to your management cluster to have CAPV create a workload cluster with worker nodes that have vGPUs. + +From this point on, the setup is exactly the same as [GPU enabled clusters via PCI Passthrough](./gpu-pci.md#create-the-cluster). diff --git a/internal/webhooks/vspheremachine.go b/internal/webhooks/vspheremachine.go index 420df4733d..328c7c1361 100644 --- a/internal/webhooks/vspheremachine.go +++ b/internal/webhooks/vspheremachine.go @@ -92,6 +92,8 @@ func (webhook *VSphereMachineWebhook) ValidateCreate(_ context.Context, raw runt allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "guestSoftPowerOffTimeout"), spec.GuestSoftPowerOffTimeout, "should be greater than 0")) } } + pciErrs := validatePCIDevices(spec.PciDevices) + allErrs = append(allErrs, pciErrs...) return nil, AggregateObjErrors(obj.GroupVersionKind().GroupKind(), obj.Name, allErrs) } @@ -160,3 +162,20 @@ func (webhook *VSphereMachineWebhook) ValidateUpdate(_ context.Context, oldRaw r func (webhook *VSphereMachineWebhook) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { return nil, nil } + +func validatePCIDevices(devices []infrav1.PCIDeviceSpec) field.ErrorList { + var allErrs field.ErrorList + + for i, device := range devices { + if device.VGPUProfile != "" && device.DeviceID == nil && device.VendorID == nil { + // Valid case for vGPU. + continue + } + if device.VGPUProfile == "" && device.DeviceID != nil && device.VendorID != nil { + // Valid case for PCI Passthrough. + continue + } + allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "template", "spec", "pciDevices", fmt.Sprintf("%d", i)), device, "should have either deviceId + vendorId or vGPUProfile set")) + } + return allErrs +} diff --git a/internal/webhooks/vspheremachine_test.go b/internal/webhooks/vspheremachine_test.go index b5ef77df7f..64b5c327f9 100644 --- a/internal/webhooks/vspheremachine_test.go +++ b/internal/webhooks/vspheremachine_test.go @@ -22,6 +22,7 @@ import ( . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" infrav1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1" ) @@ -48,52 +49,86 @@ func TestVSphereMachine_ValidateCreate(t *testing.T) { }{ { name: "preferredAPIServerCIDR set on creation ", - vsphereMachine: createVSphereMachine("foo.com", nil, "192.168.0.1/32", []string{}, infrav1.VirtualMachinePowerOpModeTrySoft, nil), + vsphereMachine: createVSphereMachine("foo.com", nil, "192.168.0.1/32", []string{}, infrav1.VirtualMachinePowerOpModeTrySoft, nil, nil), wantErr: true, }, { name: "IPs are not in CIDR format", - vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3"}, infrav1.VirtualMachinePowerOpModeTrySoft, nil), + vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3"}, infrav1.VirtualMachinePowerOpModeTrySoft, nil, nil), wantErr: true, }, { name: "IPs are not valid IPs in CIDR format", - vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"/32", "192.168.0.644/33"}, infrav1.VirtualMachinePowerOpModeTrySoft, nil), + vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"/32", "192.168.0.644/33"}, infrav1.VirtualMachinePowerOpModeTrySoft, nil, nil), wantErr: true, }, { name: "guestSoftPowerOffTimeout should not be set with powerOffMode set to hard", - vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeHard, &metav1.Duration{Duration: infrav1.GuestSoftPowerOffDefaultTimeout}), + vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeHard, &metav1.Duration{Duration: infrav1.GuestSoftPowerOffDefaultTimeout}, nil), wantErr: true, }, { name: "guestSoftPowerOffTimeout should not be set with powerOffMode set to soft", - vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeSoft, &metav1.Duration{Duration: infrav1.GuestSoftPowerOffDefaultTimeout}), + vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeSoft, &metav1.Duration{Duration: infrav1.GuestSoftPowerOffDefaultTimeout}, nil), wantErr: true, }, { name: "guestSoftPowerOffTimeout should not be negative", - vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeHard, &metav1.Duration{Duration: -1234}), + vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeHard, &metav1.Duration{Duration: -1234}, nil), + wantErr: true, + }, + + { + name: "empty pciDevice", + vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, nil, []infrav1.PCIDeviceSpec{{VGPUProfile: ""}}), + wantErr: true, + }, + { + name: "incorrect pciDevice", + vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, nil, []infrav1.PCIDeviceSpec{{VGPUProfile: "vgpu", DeviceID: ptr.To[int32](1)}}), + wantErr: true, + }, + { + name: "incorrect pciDevice", + vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, nil, []infrav1.PCIDeviceSpec{{VGPUProfile: "vgpu", DeviceID: ptr.To[int32](1), VendorID: ptr.To[int32](1)}}), wantErr: true, }, + { + name: "incomplete pciDevice", + vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, nil, []infrav1.PCIDeviceSpec{{DeviceID: ptr.To[int32](1)}}), + wantErr: true, + }, + { + name: "incomplete pciDevice", + vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, nil, []infrav1.PCIDeviceSpec{{VendorID: ptr.To[int32](1)}}), + wantErr: true, + }, + { + name: "successful VSphereMachine creation with PCI device", + vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, nil, []infrav1.PCIDeviceSpec{{DeviceID: ptr.To[int32](1), VendorID: ptr.To[int32](1)}}), + }, + { + name: "successful VSphereMachine creation with vgpu", + vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, nil, []infrav1.PCIDeviceSpec{{VGPUProfile: "vgpu"}}), + }, { name: "successful VSphereMachine creation", - vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, nil), + vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, nil, nil), wantErr: false, }, { name: "successful VSphereMachine creation with powerOffMode set to hard", - vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeHard, nil), + vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeHard, nil, nil), wantErr: false, }, { name: "successful VSphereMachine creation with powerOffMode set to soft", - vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil), + vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil, nil), wantErr: false, }, { name: "successful VSphereMachine creation with powerOffMode set to trySoft and non-default timeout", - vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, &metav1.Duration{Duration: 1234}), + vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, &metav1.Duration{Duration: 1234}, nil), wantErr: false, }, } @@ -121,50 +156,56 @@ func TestVSphereMachine_ValidateUpdate(t *testing.T) { }{ { name: "ProviderID can be updated", - oldVSphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil), - vsphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil), + oldVSphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil, nil), + vsphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil, nil), wantErr: false, }, { name: "updating ips can be done", - oldVSphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil), - vsphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32", "192.168.0.10/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil), + oldVSphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil, nil), + vsphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32", "192.168.0.10/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil, nil), wantErr: false, }, { name: "updating non-existing IP with invalid ips can not be done", - oldVSphereMachine: createVSphereMachine("foo.com", nil, "", nil, infrav1.VirtualMachinePowerOpModeSoft, nil), - vsphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"/32", "192.168.0.10/33"}, infrav1.VirtualMachinePowerOpModeSoft, nil), + oldVSphereMachine: createVSphereMachine("foo.com", nil, "", nil, infrav1.VirtualMachinePowerOpModeSoft, nil, nil), + vsphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"/32", "192.168.0.10/33"}, infrav1.VirtualMachinePowerOpModeSoft, nil, nil), wantErr: true, }, { name: "updating existing IP with invalid ips can not be done", - oldVSphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil), - vsphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"/32", "192.168.0.10/33"}, infrav1.VirtualMachinePowerOpModeSoft, nil), + oldVSphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil, nil), + vsphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"/32", "192.168.0.10/33"}, infrav1.VirtualMachinePowerOpModeSoft, nil, nil), wantErr: true, }, { name: "updating server cannot be done", - oldVSphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil), - vsphereMachine: createVSphereMachine("bar.com", &someProviderID, "", []string{"192.168.0.1/32", "192.168.0.10/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil), + oldVSphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil, nil), + vsphereMachine: createVSphereMachine("bar.com", &someProviderID, "", []string{"192.168.0.1/32", "192.168.0.10/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil, nil), + wantErr: true, + }, + { + name: "updating pci devices cannot be done", + oldVSphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil, []infrav1.PCIDeviceSpec{{VGPUProfile: "vgpu"}}), + vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil, []infrav1.PCIDeviceSpec{{VGPUProfile: "new-vgpu"}}), wantErr: true, }, { name: "powerOffMode cannot be updated when new powerOffMode is not valid", - oldVSphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, nil), - vsphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeHard, &metav1.Duration{Duration: infrav1.GuestSoftPowerOffDefaultTimeout}), + oldVSphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, nil, nil), + vsphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeHard, &metav1.Duration{Duration: infrav1.GuestSoftPowerOffDefaultTimeout}, nil), wantErr: true, }, { name: "powerOffMode can be updated to hard", - oldVSphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, &metav1.Duration{Duration: infrav1.GuestSoftPowerOffDefaultTimeout}), - vsphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeHard, nil), + oldVSphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, &metav1.Duration{Duration: infrav1.GuestSoftPowerOffDefaultTimeout}, nil), + vsphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeHard, nil, nil), wantErr: false, }, { name: "powerOffMode can be updated to soft", - oldVSphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, &metav1.Duration{Duration: infrav1.GuestSoftPowerOffDefaultTimeout}), - vsphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil), + oldVSphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, &metav1.Duration{Duration: infrav1.GuestSoftPowerOffDefaultTimeout}, nil), + vsphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil, nil), wantErr: false, }, } @@ -181,7 +222,7 @@ func TestVSphereMachine_ValidateUpdate(t *testing.T) { } } -func createVSphereMachine(server string, providerID *string, preferredAPIServerCIDR string, ips []string, powerOffMode infrav1.VirtualMachinePowerOpMode, guestSoftPowerOffTimeout *metav1.Duration) *infrav1.VSphereMachine { +func createVSphereMachine(server string, providerID *string, preferredAPIServerCIDR string, ips []string, powerOffMode infrav1.VirtualMachinePowerOpMode, guestSoftPowerOffTimeout *metav1.Duration, pciDevices []infrav1.PCIDeviceSpec) *infrav1.VSphereMachine { VSphereMachine := &infrav1.VSphereMachine{ Spec: infrav1.VSphereMachineSpec{ VirtualMachineCloneSpec: infrav1.VirtualMachineCloneSpec{ @@ -190,6 +231,7 @@ func createVSphereMachine(server string, providerID *string, preferredAPIServerC PreferredAPIServerCIDR: preferredAPIServerCIDR, Devices: []infrav1.NetworkDeviceSpec{}, }, + PciDevices: pciDevices, }, ProviderID: providerID, PowerOffMode: powerOffMode, diff --git a/internal/webhooks/vspheremachinetemplate.go b/internal/webhooks/vspheremachinetemplate.go index a88af09892..8a1c865481 100644 --- a/internal/webhooks/vspheremachinetemplate.go +++ b/internal/webhooks/vspheremachinetemplate.go @@ -84,6 +84,9 @@ func (webhook *VSphereMachineTemplateWebhook) ValidateCreate(_ context.Context, allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "template", "spec", "guestSoftPowerOffTimeout"), spec.GuestSoftPowerOffTimeout, "should be greater than 0")) } } + pciErrs := validatePCIDevices(spec.PciDevices) + allErrs = append(allErrs, pciErrs...) + return nil, AggregateObjErrors(obj.GroupVersionKind().GroupKind(), obj.Name, allErrs) } diff --git a/internal/webhooks/vspheremachinetemplate_test.go b/internal/webhooks/vspheremachinetemplate_test.go index 20f6eddacf..95ccd7042e 100644 --- a/internal/webhooks/vspheremachinetemplate_test.go +++ b/internal/webhooks/vspheremachinetemplate_test.go @@ -37,37 +37,70 @@ func TestVSphereMachineTemplate_ValidateCreate(t *testing.T) { }{ { name: "preferredAPIServerCIDR set on creation ", - vsphereMachine: createVSphereMachineTemplate("foo.com", "", nil, "192.168.0.1/32", []string{}), + vsphereMachine: createVSphereMachineTemplate("foo.com", "", nil, "192.168.0.1/32", []string{}, nil), wantErr: true, }, { name: "ProviderID set on creation", - vsphereMachine: createVSphereMachineTemplate("foo.com", "", &someProviderID, "", []string{}), + vsphereMachine: createVSphereMachineTemplate("foo.com", "", &someProviderID, "", []string{}, nil), wantErr: true, }, { name: "IPs are not in CIDR format", - vsphereMachine: createVSphereMachineTemplate("foo.com", "", nil, "", []string{"192.168.0.1/32", "192.168.0.3"}), + vsphereMachine: createVSphereMachineTemplate("foo.com", "", nil, "", []string{"192.168.0.1/32", "192.168.0.3"}, nil), wantErr: true, }, { name: "successful VSphereMachine creation", - vsphereMachine: createVSphereMachineTemplate("foo.com", "", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}), + vsphereMachine: createVSphereMachineTemplate("foo.com", "", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil), wantErr: true, }, { name: "incomplete hardware version", - vsphereMachine: createVSphereMachineTemplate("foo.com", "vmx-", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}), + vsphereMachine: createVSphereMachineTemplate("foo.com", "vmx-", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil), wantErr: true, }, { name: "incorrect hardware version", - vsphereMachine: createVSphereMachineTemplate("foo.com", "vmx-0", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}), + vsphereMachine: createVSphereMachineTemplate("foo.com", "vmx-0", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil), wantErr: true, }, + { + name: "empty pciDevice", + vsphereMachine: createVSphereMachineTemplate("foo.com", "vmx-17", nil, "", []string{}, []infrav1.PCIDeviceSpec{{VGPUProfile: ""}}), + wantErr: true, + }, + { + name: "incorrect pciDevice", + vsphereMachine: createVSphereMachineTemplate("foo.com", "vmx-17", nil, "", []string{}, []infrav1.PCIDeviceSpec{{VGPUProfile: "vgpu", DeviceID: ptr.To[int32](1)}}), + wantErr: true, + }, + { + name: "incorrect pciDevice", + vsphereMachine: createVSphereMachineTemplate("foo.com", "vmx-17", nil, "", []string{}, []infrav1.PCIDeviceSpec{{VGPUProfile: "vgpu", DeviceID: ptr.To[int32](1), VendorID: ptr.To[int32](1)}}), + wantErr: true, + }, + { + name: "incomplete pciDevice", + vsphereMachine: createVSphereMachineTemplate("foo.com", "vmx-17", nil, "", []string{}, []infrav1.PCIDeviceSpec{{DeviceID: ptr.To[int32](1)}}), + wantErr: true, + }, + { + name: "incomplete pciDevice", + vsphereMachine: createVSphereMachineTemplate("foo.com", "vmx-17", nil, "", []string{}, []infrav1.PCIDeviceSpec{{VendorID: ptr.To[int32](1)}}), + wantErr: true, + }, + { + name: "successful VSphereMachine creation with PCI device", + vsphereMachine: createVSphereMachineTemplate("foo.com", "vmx-17", nil, "", []string{}, []infrav1.PCIDeviceSpec{{DeviceID: ptr.To[int32](1), VendorID: ptr.To[int32](1)}}), + }, + { + name: "successful VSphereMachine creation with vgpu", + vsphereMachine: createVSphereMachineTemplate("foo.com", "vmx-17", nil, "", []string{}, []infrav1.PCIDeviceSpec{{VGPUProfile: "vgpu"}}), + }, { name: "successful VSphereMachine creation with hardware version set", - vsphereMachine: createVSphereMachineTemplate("foo.com", "vmx-17", nil, "", []string{}), + vsphereMachine: createVSphereMachineTemplate("foo.com", "vmx-17", nil, "", []string{}, nil), }, } for _, tc := range tests { @@ -94,36 +127,43 @@ func TestVSphereMachineTemplate_ValidateUpdate(t *testing.T) { }{ { name: "ProviderID cannot be updated", - oldVSphereMachine: createVSphereMachineTemplate("foo.com", "", nil, "", []string{"192.168.0.1/32"}), - vsphereMachine: createVSphereMachineTemplate("foo.com", "", &someProviderID, "", []string{"192.168.0.1/32"}), + oldVSphereMachine: createVSphereMachineTemplate("foo.com", "", nil, "", []string{"192.168.0.1/32"}, nil), + vsphereMachine: createVSphereMachineTemplate("foo.com", "", &someProviderID, "", []string{"192.168.0.1/32"}, nil), req: &admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{DryRun: ptr.To(false)}}, wantErr: true, }, { name: "ip addresses cannot be updated", - oldVSphereMachine: createVSphereMachineTemplate("foo.com", "", nil, "", []string{"192.168.0.1/32"}), - vsphereMachine: createVSphereMachineTemplate("foo.com", "", &someProviderID, "", []string{"192.168.0.1/32", "192.168.0.10/32"}), + oldVSphereMachine: createVSphereMachineTemplate("foo.com", "", nil, "", []string{"192.168.0.1/32"}, nil), + vsphereMachine: createVSphereMachineTemplate("foo.com", "", &someProviderID, "", []string{"192.168.0.1/32", "192.168.0.10/32"}, nil), req: &admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{DryRun: ptr.To(false)}}, wantErr: true, }, { name: "server cannot be updated", - oldVSphereMachine: createVSphereMachineTemplate("foo.com", "", nil, "", []string{"192.168.0.1/32"}), - vsphereMachine: createVSphereMachineTemplate("baz.com", "", &someProviderID, "", []string{"192.168.0.1/32", "192.168.0.10/32"}), + oldVSphereMachine: createVSphereMachineTemplate("foo.com", "", nil, "", []string{"192.168.0.1/32"}, nil), + vsphereMachine: createVSphereMachineTemplate("baz.com", "", &someProviderID, "", []string{"192.168.0.1/32", "192.168.0.10/32"}, nil), req: &admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{DryRun: ptr.To(false)}}, wantErr: true, }, { name: "hardware version cannot be updated", - oldVSphereMachine: createVSphereMachineTemplate("foo.com", "vmx-16", nil, "", []string{"192.168.0.1/32"}), - vsphereMachine: createVSphereMachineTemplate("baz.com", "vmx-17", nil, "", []string{"192.168.0.1/32"}), + oldVSphereMachine: createVSphereMachineTemplate("foo.com", "vmx-16", nil, "", []string{"192.168.0.1/32"}, nil), + vsphereMachine: createVSphereMachineTemplate("baz.com", "vmx-17", nil, "", []string{"192.168.0.1/32"}, nil), + req: &admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{DryRun: ptr.To(false)}}, + wantErr: true, + }, + { + name: "pci devices cannot be updated", + oldVSphereMachine: createVSphereMachineTemplate("foo.com", "vmx-16", nil, "", []string{"192.168.0.1/32"}, []infrav1.PCIDeviceSpec{{VGPUProfile: "vgpu"}}), + vsphereMachine: createVSphereMachineTemplate("foo.com", "vmx-16", nil, "", []string{"192.168.0.1/32"}, []infrav1.PCIDeviceSpec{{VGPUProfile: "new-vgpu"}}), req: &admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{DryRun: ptr.To(false)}}, wantErr: true, }, { name: "with hardware version set and not updated", - oldVSphereMachine: createVSphereMachineTemplate("foo.com", "vmx-16", nil, "", []string{"192.168.0.1/32"}), - vsphereMachine: createVSphereMachineTemplate("foo.com", "vmx-16", nil, "", []string{"192.168.0.1/32"}), + oldVSphereMachine: createVSphereMachineTemplate("foo.com", "vmx-16", nil, "", []string{"192.168.0.1/32"}, nil), + vsphereMachine: createVSphereMachineTemplate("foo.com", "vmx-16", nil, "", []string{"192.168.0.1/32"}, nil), req: &admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{DryRun: ptr.To(false)}}, wantErr: false, // explicitly calling out that this is a valid scenario. }, @@ -145,7 +185,7 @@ func TestVSphereMachineTemplate_ValidateUpdate(t *testing.T) { } } -func createVSphereMachineTemplate(server, hwVersion string, providerID *string, preferredAPIServerCIDR string, ips []string) *infrav1.VSphereMachineTemplate { +func createVSphereMachineTemplate(server, hwVersion string, providerID *string, preferredAPIServerCIDR string, ips []string, pciDevices []infrav1.PCIDeviceSpec) *infrav1.VSphereMachineTemplate { vsphereMachineTemplate := &infrav1.VSphereMachineTemplate{ Spec: infrav1.VSphereMachineTemplateSpec{ Template: infrav1.VSphereMachineTemplateResource{ @@ -158,6 +198,7 @@ func createVSphereMachineTemplate(server, hwVersion string, providerID *string, Devices: []infrav1.NetworkDeviceSpec{}, }, HardwareVersion: hwVersion, + PciDevices: pciDevices, }, }, }, diff --git a/pkg/services/govmomi/pci/device.go b/pkg/services/govmomi/pci/device.go index f92670bf9d..7c41cb194e 100644 --- a/pkg/services/govmomi/pci/device.go +++ b/pkg/services/govmomi/pci/device.go @@ -76,7 +76,12 @@ func ConstructDeviceSpecs(pciDeviceSpecs []infrav1.PCIDeviceSpec) []types.BaseVi return pciDevices } -func createBackingInfo(spec infrav1.PCIDeviceSpec) *types.VirtualPCIPassthroughDynamicBackingInfo { +func createBackingInfo(spec infrav1.PCIDeviceSpec) types.BaseVirtualDeviceBackingInfo { + if spec.VGPUProfile != "" { + return &types.VirtualPCIPassthroughVmiopBackingInfo{ + Vgpu: spec.VGPUProfile, + } + } return &types.VirtualPCIPassthroughDynamicBackingInfo{ AllowedDevice: []types.VirtualPCIPassthroughAllowedDevice{ { @@ -89,5 +94,8 @@ func createBackingInfo(spec infrav1.PCIDeviceSpec) *types.VirtualPCIPassthroughD } func constructKey(pciDeviceSpec infrav1.PCIDeviceSpec) string { + if pciDeviceSpec.VGPUProfile != "" { + return pciDeviceSpec.VGPUProfile + } return fmt.Sprintf("%d-%d", *pciDeviceSpec.DeviceID, *pciDeviceSpec.VendorID) } diff --git a/pkg/services/govmomi/pci/device_test.go b/pkg/services/govmomi/pci/device_test.go index 74f57245c8..5f62089552 100644 --- a/pkg/services/govmomi/pci/device_test.go +++ b/pkg/services/govmomi/pci/device_test.go @@ -72,30 +72,36 @@ func Test_CalculateDevicesToBeAdded(t *testing.T) { inputs := []input{ { name: "when adding a single PCI device of each type", - expectedLen: 2, + expectedLen: 3, pciDeviceSpecs: []infrav1.PCIDeviceSpec{ {DeviceID: ptr.To[int32](1234), VendorID: ptr.To[int32](5678)}, {DeviceID: ptr.To[int32](4321), VendorID: ptr.To[int32](8765)}, + {VGPUProfile: "grid_t4-1a"}, }, assertFunc: func(g *gomega.WithT, actual []infrav1.PCIDeviceSpec) { g.Expect(*actual[0].DeviceID).To(gomega.Equal(int32(1234))) g.Expect(*actual[0].VendorID).To(gomega.Equal(int32(5678))) g.Expect(*actual[1].DeviceID).To(gomega.Equal(int32(4321))) g.Expect(*actual[1].VendorID).To(gomega.Equal(int32(8765))) + g.Expect(actual[2].VGPUProfile).To(gomega.Equal("grid_t4-1a")) }, }, { name: "when adding multiple PCI devices of a type", - expectedLen: 2, + expectedLen: 4, pciDeviceSpecs: []infrav1.PCIDeviceSpec{ {DeviceID: ptr.To[int32](1234), VendorID: ptr.To[int32](5678)}, {DeviceID: ptr.To[int32](1234), VendorID: ptr.To[int32](5678)}, + {VGPUProfile: "grid_t4-1a"}, + {VGPUProfile: "grid_t4-1a"}, }, assertFunc: func(g *gomega.WithT, actual []infrav1.PCIDeviceSpec) { g.Expect(*actual[0].DeviceID).To(gomega.Equal(int32(1234))) g.Expect(*actual[0].VendorID).To(gomega.Equal(int32(5678))) g.Expect(*actual[1].DeviceID).To(gomega.Equal(int32(1234))) g.Expect(*actual[1].VendorID).To(gomega.Equal(int32(5678))) + g.Expect(actual[2].VGPUProfile).To(gomega.Equal("grid_t4-1a")) + g.Expect(actual[3].VGPUProfile).To(gomega.Equal("grid_t4-1a")) }, }, } @@ -112,8 +118,9 @@ func Test_CalculateDevicesToBeAdded(t *testing.T) { pciDeviceSpecs: []infrav1.PCIDeviceSpec{ {DeviceID: ptr.To[int32](1234), VendorID: ptr.To[int32](5678)}, {DeviceID: ptr.To[int32](4321), VendorID: ptr.To[int32](8765)}, + {VGPUProfile: "grid_t4-1a"}, }, - existingDeviceSpecIndexes: []int{0, 1}, + existingDeviceSpecIndexes: []int{0, 1, 2}, }, { name: "when adding multiple PCI devices of a type", @@ -121,8 +128,10 @@ func Test_CalculateDevicesToBeAdded(t *testing.T) { pciDeviceSpecs: []infrav1.PCIDeviceSpec{ {DeviceID: ptr.To[int32](1234), VendorID: ptr.To[int32](5678)}, {DeviceID: ptr.To[int32](1234), VendorID: ptr.To[int32](5678)}, + {VGPUProfile: "grid_t4-1a"}, + {VGPUProfile: "grid_t4-1a"}, }, - existingDeviceSpecIndexes: []int{0, 1}, + existingDeviceSpecIndexes: []int{0, 1, 2, 3}, }, } for _, tt := range inputs { @@ -134,24 +143,27 @@ func Test_CalculateDevicesToBeAdded(t *testing.T) { inputs := []input{ { name: "when adding a single PCI device of each type", - expectedLen: 1, + expectedLen: 2, pciDeviceSpecs: []infrav1.PCIDeviceSpec{ {DeviceID: ptr.To[int32](1234), VendorID: ptr.To[int32](5678)}, {DeviceID: ptr.To[int32](4321), VendorID: ptr.To[int32](8765)}, + {VGPUProfile: "grid_t4-1a"}, }, existingDeviceSpecIndexes: []int{0}, assertFunc: func(g *gomega.WithT, actual []infrav1.PCIDeviceSpec) { g.Expect(*actual[0].DeviceID).To(gomega.Equal(int32(4321))) g.Expect(*actual[0].VendorID).To(gomega.Equal(int32(8765))) + g.Expect(actual[1].VGPUProfile).To(gomega.Equal("grid_t4-1a")) }, }, { name: "when adding multiple PCI devices of a type", - expectedLen: 2, + expectedLen: 3, pciDeviceSpecs: []infrav1.PCIDeviceSpec{ {DeviceID: ptr.To[int32](1234), VendorID: ptr.To[int32](5678)}, {DeviceID: ptr.To[int32](1234), VendorID: ptr.To[int32](5678)}, {DeviceID: ptr.To[int32](4321), VendorID: ptr.To[int32](8765)}, + {VGPUProfile: "grid_t4-1a"}, }, existingDeviceSpecIndexes: []int{0}, assertFunc: func(g *gomega.WithT, actual []infrav1.PCIDeviceSpec) { @@ -159,6 +171,7 @@ func Test_CalculateDevicesToBeAdded(t *testing.T) { g.Expect(*actual[0].VendorID).To(gomega.Equal(int32(5678))) g.Expect(*actual[1].DeviceID).To(gomega.Equal(int32(4321))) g.Expect(*actual[1].VendorID).To(gomega.Equal(int32(8765))) + g.Expect(actual[2].VGPUProfile).To(gomega.Equal("grid_t4-1a")) }, }, } diff --git a/pkg/services/govmomi/vcenter/clone.go b/pkg/services/govmomi/vcenter/clone.go index e216790081..91f64eb28d 100644 --- a/pkg/services/govmomi/vcenter/clone.go +++ b/pkg/services/govmomi/vcenter/clone.go @@ -69,7 +69,7 @@ func Clone(ctx context.Context, vmCtx *capvcontext.VMContext, bootstrapData []by } } if vmCtx.VSphereVM.Spec.CustomVMXKeys != nil { - log.Info("Applied custom vmx keys o VM clone spec") + log.Info("Applied custom VMX keys to VM clone spec") if err := extraConfig.SetCustomVMXKeys(vmCtx.VSphereVM.Spec.CustomVMXKeys); err != nil { return err } @@ -152,10 +152,6 @@ func Clone(ctx context.Context, vmCtx *capvcontext.VMContext, bootstrapData []by deviceSpecs = append(deviceSpecs, networkSpecs...) - if err != nil { - return errors.Wrapf(err, "error getting network specs for %q", ctx) - } - numCPUs := vmCtx.VSphereVM.Spec.NumCPUs if numCPUs < 2 { numCPUs = 2