diff --git a/apis/v1alpha3/vspheremachine_conversion.go b/apis/v1alpha3/vspheremachine_conversion.go index 79a0f1d548..2544d37029 100644 --- a/apis/v1alpha3/vspheremachine_conversion.go +++ b/apis/v1alpha3/vspheremachine_conversion.go @@ -46,6 +46,7 @@ func (src *VSphereMachine) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Network.Devices[i].DHCP6Overrides = restored.Spec.Network.Devices[i].DHCP6Overrides dst.Spec.Network.Devices[i].SkipIPAllocation = restored.Spec.Network.Devices[i].SkipIPAllocation } + dst.Spec.DataDisks = restored.Spec.DataDisks return nil } diff --git a/apis/v1alpha3/vspheremachinetemplate_conversion.go b/apis/v1alpha3/vspheremachinetemplate_conversion.go index 45f090e2c1..ba13cecc12 100644 --- a/apis/v1alpha3/vspheremachinetemplate_conversion.go +++ b/apis/v1alpha3/vspheremachinetemplate_conversion.go @@ -49,6 +49,7 @@ func (src *VSphereMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Template.Spec.Network.Devices[i].DHCP6Overrides = restored.Spec.Template.Spec.Network.Devices[i].DHCP6Overrides dst.Spec.Template.Spec.Network.Devices[i].SkipIPAllocation = restored.Spec.Template.Spec.Network.Devices[i].SkipIPAllocation } + dst.Spec.Template.Spec.DataDisks = restored.Spec.Template.Spec.DataDisks return nil } diff --git a/apis/v1alpha3/vspherevm_conversion.go b/apis/v1alpha3/vspherevm_conversion.go index 8bc24a37ae..95c144c9ec 100644 --- a/apis/v1alpha3/vspherevm_conversion.go +++ b/apis/v1alpha3/vspherevm_conversion.go @@ -46,6 +46,7 @@ func (src *VSphereVM) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Network.Devices[i].DHCP6Overrides = restored.Spec.Network.Devices[i].DHCP6Overrides dst.Spec.Network.Devices[i].SkipIPAllocation = restored.Spec.Network.Devices[i].SkipIPAllocation } + dst.Spec.DataDisks = restored.Spec.DataDisks return nil } diff --git a/apis/v1alpha3/zz_generated.conversion.go b/apis/v1alpha3/zz_generated.conversion.go index 966195ccbb..2d953c987c 100644 --- a/apis/v1alpha3/zz_generated.conversion.go +++ b/apis/v1alpha3/zz_generated.conversion.go @@ -1762,5 +1762,6 @@ func autoConvert_v1beta1_VirtualMachineCloneSpec_To_v1alpha3_VirtualMachineClone // WARNING: in.PciDevices requires manual conversion: does not exist in peer-type // WARNING: in.OS requires manual conversion: does not exist in peer-type // WARNING: in.HardwareVersion requires manual conversion: does not exist in peer-type + // WARNING: in.DataDisks requires manual conversion: does not exist in peer-type return nil } diff --git a/apis/v1alpha4/vspheremachine_conversion.go b/apis/v1alpha4/vspheremachine_conversion.go index 22a85aaa5a..ff5c085894 100644 --- a/apis/v1alpha4/vspheremachine_conversion.go +++ b/apis/v1alpha4/vspheremachine_conversion.go @@ -46,6 +46,7 @@ func (src *VSphereMachine) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Network.Devices[i].DHCP6Overrides = restored.Spec.Network.Devices[i].DHCP6Overrides dst.Spec.Network.Devices[i].SkipIPAllocation = restored.Spec.Network.Devices[i].SkipIPAllocation } + dst.Spec.DataDisks = restored.Spec.DataDisks return nil } diff --git a/apis/v1alpha4/vspheremachinetemplate_conversion.go b/apis/v1alpha4/vspheremachinetemplate_conversion.go index c0719758e8..bb2254e329 100644 --- a/apis/v1alpha4/vspheremachinetemplate_conversion.go +++ b/apis/v1alpha4/vspheremachinetemplate_conversion.go @@ -49,6 +49,7 @@ func (src *VSphereMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Template.Spec.Network.Devices[i].DHCP6Overrides = restored.Spec.Template.Spec.Network.Devices[i].DHCP6Overrides dst.Spec.Template.Spec.Network.Devices[i].SkipIPAllocation = restored.Spec.Template.Spec.Network.Devices[i].SkipIPAllocation } + dst.Spec.Template.Spec.DataDisks = restored.Spec.Template.Spec.DataDisks return nil } diff --git a/apis/v1alpha4/vspherevm_conversion.go b/apis/v1alpha4/vspherevm_conversion.go index 1f48120cca..7fd9a4aebb 100644 --- a/apis/v1alpha4/vspherevm_conversion.go +++ b/apis/v1alpha4/vspherevm_conversion.go @@ -46,6 +46,7 @@ func (src *VSphereVM) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Network.Devices[i].DHCP6Overrides = restored.Spec.Network.Devices[i].DHCP6Overrides dst.Spec.Network.Devices[i].SkipIPAllocation = restored.Spec.Network.Devices[i].SkipIPAllocation } + dst.Spec.DataDisks = restored.Spec.DataDisks return nil } diff --git a/apis/v1alpha4/zz_generated.conversion.go b/apis/v1alpha4/zz_generated.conversion.go index 147c1a9894..197b5b4667 100644 --- a/apis/v1alpha4/zz_generated.conversion.go +++ b/apis/v1alpha4/zz_generated.conversion.go @@ -1916,5 +1916,6 @@ func autoConvert_v1beta1_VirtualMachineCloneSpec_To_v1alpha4_VirtualMachineClone // WARNING: in.PciDevices requires manual conversion: does not exist in peer-type // WARNING: in.OS requires manual conversion: does not exist in peer-type // WARNING: in.HardwareVersion requires manual conversion: does not exist in peer-type + // WARNING: in.DataDisks requires manual conversion: does not exist in peer-type return nil } diff --git a/apis/v1beta1/types.go b/apis/v1beta1/types.go index cc0f3a1f6c..ce9d909ee8 100644 --- a/apis/v1beta1/types.go +++ b/apis/v1beta1/types.go @@ -203,6 +203,23 @@ type VirtualMachineCloneSpec struct { // Check the compatibility with the ESXi version before setting the value. // +optional HardwareVersion string `json:"hardwareVersion,omitempty"` + // DataDisks are additional disks to add to the VM that are not part of the VM's OVA template. + // +optional + // +listType=map + // +listMapKey=name + // +kubebuilder:validation:MaxItems=29 + DataDisks []VSphereDisk `json:"dataDisks,omitempty"` +} + +// VSphereDisk is an additional disk to add to the VM that is not part of the VM OVA template. +type VSphereDisk struct { + // Name is used to identify the disk definition. Name is required and needs to be unique so that it can be used to + // clearly identify purpose of the disk. + // +kubebuilder:validation:Required + Name string `json:"name,omitempty"` + // SizeGiB is the size of the disk in GiB. + // +kubebuilder:validation:Required + SizeGiB int32 `json:"sizeGiB"` } // VSphereMachineTemplateResource describes the data needed to create a VSphereMachine from a template. diff --git a/apis/v1beta1/zz_generated.deepcopy.go b/apis/v1beta1/zz_generated.deepcopy.go index 44d12a65fe..584c92f93c 100644 --- a/apis/v1beta1/zz_generated.deepcopy.go +++ b/apis/v1beta1/zz_generated.deepcopy.go @@ -820,6 +820,21 @@ func (in *VSphereDeploymentZoneStatus) DeepCopy() *VSphereDeploymentZoneStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VSphereDisk) DeepCopyInto(out *VSphereDisk) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSphereDisk. +func (in *VSphereDisk) DeepCopy() *VSphereDisk { + if in == nil { + return nil + } + out := new(VSphereDisk) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VSphereFailureDomain) DeepCopyInto(out *VSphereFailureDomain) { *out = *in @@ -1321,6 +1336,11 @@ func (in *VirtualMachineCloneSpec) DeepCopyInto(out *VirtualMachineCloneSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.DataDisks != nil { + in, out := &in.DataDisks, &out.DataDisks + *out = make([]VSphereDisk, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineCloneSpec. 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 6c35e7dd3f..ae8026b9d0 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 @@ -974,6 +974,31 @@ spec: CustomVMXKeys is a dictionary of advanced VMX options that can be set on VM Defaults to empty map type: object + dataDisks: + description: DataDisks are additional disks to add to the VM that + are not part of the VM's OVA template. + items: + description: VSphereDisk is an additional disk to add to the VM + that is not part of the VM OVA template. + properties: + name: + description: |- + Name is used to identify the disk definition. Name is required and needs to be unique so that it can be used to + clearly identify purpose of the disk. + type: string + sizeGiB: + description: SizeGiB is the size of the disk in GiB. + format: int32 + type: integer + required: + - name + - sizeGiB + type: object + maxItems: 29 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map datacenter: description: |- Datacenter is the name or inventory path of the datacenter in which the 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 1cf5a58c33..fb870f3760 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 @@ -844,6 +844,31 @@ spec: CustomVMXKeys is a dictionary of advanced VMX options that can be set on VM Defaults to empty map type: object + dataDisks: + description: DataDisks are additional disks to add to the + VM that are not part of the VM's OVA template. + items: + description: VSphereDisk is an additional disk to add to + the VM that is not part of the VM OVA template. + properties: + name: + description: |- + Name is used to identify the disk definition. Name is required and needs to be unique so that it can be used to + clearly identify purpose of the disk. + type: string + sizeGiB: + description: SizeGiB is the size of the disk in GiB. + format: int32 + type: integer + required: + - name + - sizeGiB + type: object + maxItems: 29 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map datacenter: description: |- Datacenter is the name or inventory path of the datacenter in which the 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 079466a2f4..817c5e8d76 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 @@ -1064,6 +1064,31 @@ spec: CustomVMXKeys is a dictionary of advanced VMX options that can be set on VM Defaults to empty map type: object + dataDisks: + description: DataDisks are additional disks to add to the VM that + are not part of the VM's OVA template. + items: + description: VSphereDisk is an additional disk to add to the VM + that is not part of the VM OVA template. + properties: + name: + description: |- + Name is used to identify the disk definition. Name is required and needs to be unique so that it can be used to + clearly identify purpose of the disk. + type: string + sizeGiB: + description: SizeGiB is the size of the disk in GiB. + format: int32 + type: integer + required: + - name + - sizeGiB + type: object + maxItems: 29 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map datacenter: description: |- Datacenter is the name or inventory path of the datacenter in which the diff --git a/pkg/services/govmomi/vcenter/clone.go b/pkg/services/govmomi/vcenter/clone.go index 91f64eb28d..7012cdc573 100644 --- a/pkg/services/govmomi/vcenter/clone.go +++ b/pkg/services/govmomi/vcenter/clone.go @@ -42,6 +42,11 @@ import ( const ( fullCloneDiskMoveType = types.VirtualMachineRelocateDiskMoveOptionsMoveAllDiskBackingsAndConsolidate linkCloneDiskMoveType = types.VirtualMachineRelocateDiskMoveOptionsCreateNewChildDiskBacking + + // maxUnitNumber constant is used to define the maximum number of devices that can be assigned to a virtual machine's controller. + // Not all controllers support up to 30, but the maximum is 30. + // xref: https://docs.vmware.com/en/VMware-vSphere/8.0/vsphere-vm-administration/GUID-5872D173-A076-42FE-8D0B-9DB0EB0E7362.html#:~:text=If%20you%20add%20a%20hard,values%20from%200%20to%2014. + maxUnitNumber = 30 ) // Clone kicks off a clone operation on vCenter to create a new virtual machine. This function does not wait for @@ -145,6 +150,16 @@ func Clone(ctx context.Context, vmCtx *capvcontext.VMContext, bootstrapData []by deviceSpecs = append(deviceSpecs, diskSpecs...) } + // Process all DataDisks definitions to dynamically create and add disks to the VM + if len(vmCtx.VSphereVM.Spec.DataDisks) > 0 { + dataDisks, err := createDataDisks(ctx, vmCtx.VSphereVM.Spec.DataDisks, devices) + if err != nil { + return errors.Wrapf(err, "error getting data disks") + } + log.V(4).Info("Adding the following data disks", "disks", dataDisks) + deviceSpecs = append(deviceSpecs, dataDisks...) + } + networkSpecs, err := getNetworkSpecs(ctx, vmCtx, devices) if err != nil { return errors.Wrapf(err, "error getting network specs for %q", ctx) @@ -390,6 +405,117 @@ func getDiskConfigSpec(disk *types.VirtualDisk, diskCloneCapacityKB int64) (type }, nil } +// createDataDisks parses through the list of VSphereDisk objects and generates the VirtualDeviceConfigSpec for each one. +func createDataDisks(ctx context.Context, dataDiskDefs []infrav1.VSphereDisk, devices object.VirtualDeviceList) ([]types.BaseVirtualDeviceConfigSpec, error) { + log := ctrl.LoggerFrom(ctx) + additionalDisks := []types.BaseVirtualDeviceConfigSpec{} + + disks := devices.SelectByType((*types.VirtualDisk)(nil)) + if len(disks) == 0 { + return nil, errors.Errorf("Invalid disk count: %d", len(disks)) + } + + // There is at least one disk + primaryDisk := disks[0].(*types.VirtualDisk) + + // Get the controller of the primary disk. + controller, ok := devices.FindByKey(primaryDisk.ControllerKey).(types.BaseVirtualController) + if !ok { + return nil, errors.Errorf("unable to find controller with key=%v", primaryDisk.ControllerKey) + } + + controllerKey := controller.GetVirtualController().Key + unitNumberAssigner, err := newUnitNumberAssigner(controller, devices) + if err != nil { + return nil, err + } + + for i, dataDisk := range dataDiskDefs { + log.V(2).Info("Adding disk", "name", dataDisk.Name, "spec", dataDisk) + + dev := &types.VirtualDisk{ + VirtualDevice: types.VirtualDevice{ + // Key needs to be unique and cannot match another new disk being added. So we'll use the index as an + // input to NewKey. NewKey() will always return same value since our new devices are not part of devices yet. + Key: devices.NewKey() - int32(i), + Backing: &types.VirtualDiskFlatVer2BackingInfo{ + DiskMode: string(types.VirtualDiskModePersistent), + ThinProvisioned: types.NewBool(true), + VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{ + FileName: "", + }, + }, + ControllerKey: controller.GetVirtualController().Key, + }, + CapacityInKB: int64(dataDisk.SizeGiB) * 1024 * 1024, + } + + vd := dev.GetVirtualDevice() + vd.ControllerKey = controllerKey + + // Assign unit number to the new disk. Should be next available slot on the controller. + unitNumber, err := unitNumberAssigner.assign() + if err != nil { + return nil, err + } + vd.UnitNumber = &unitNumber + + log.V(4).Info("Created device for data disk device", "name", dataDisk.Name, "spec", dataDisk, "device", dev) + + additionalDisks = append(additionalDisks, &types.VirtualDeviceConfigSpec{ + Device: dev, + Operation: types.VirtualDeviceConfigSpecOperationAdd, + FileOperation: types.VirtualDeviceConfigSpecFileOperationCreate, + }) + } + + return additionalDisks, nil +} + +type unitNumberAssigner struct { + used []bool + offset int32 +} + +func newUnitNumberAssigner(controller types.BaseVirtualController, existingDevices object.VirtualDeviceList) (*unitNumberAssigner, error) { + if controller == nil { + return nil, errors.New("controller parameter cannot be nil") + } + used := make([]bool, maxUnitNumber) + + // SCSIControllers also use a unit. + if scsiController, ok := controller.(types.BaseVirtualSCSIController); ok { + used[scsiController.GetVirtualSCSIController().ScsiCtlrUnitNumber] = true + } + controllerKey := controller.GetVirtualController().Key + + // Mark all unit numbers of existing devices as used + for _, device := range existingDevices { + d := device.GetVirtualDevice() + if d.ControllerKey == controllerKey && d.UnitNumber != nil { + used[*d.UnitNumber] = true + } + } + + // Set offset to 0, it will auto-increment on the first assignment. + return &unitNumberAssigner{used: used, offset: 0}, nil +} + +func (a *unitNumberAssigner) assign() (int32, error) { + if int(a.offset) > len(a.used) { + return -1, fmt.Errorf("all unit numbers are already in-use") + } + for i, isInUse := range a.used[a.offset:] { + unit := int32(i) + a.offset + if !isInUse { + a.used[unit] = true + a.offset++ + return unit, nil + } + } + return -1, fmt.Errorf("all unit numbers are already in-use") +} + const ethCardType = "vmxnet3" func getNetworkSpecs(ctx context.Context, vmCtx *capvcontext.VMContext, devices object.VirtualDeviceList) ([]types.BaseVirtualDeviceConfigSpec, error) {