diff --git a/api/v1alpha3/azuremachine_conversion.go b/api/v1alpha3/azuremachine_conversion.go index 5f48457cec2..8a62b4cc4b9 100644 --- a/api/v1alpha3/azuremachine_conversion.go +++ b/api/v1alpha3/azuremachine_conversion.go @@ -54,6 +54,10 @@ func (src *AzureMachine) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Image.ComputeGallery = restored.Spec.Image.ComputeGallery } + if restored.Spec.AdditionalCapabilities != nil { + dst.Spec.AdditionalCapabilities = restored.Spec.AdditionalCapabilities + } + dst.Spec.SubnetName = restored.Spec.SubnetName dst.Status.LongRunningOperationStates = restored.Status.LongRunningOperationStates diff --git a/api/v1alpha3/azuremachinetemplate_conversion.go b/api/v1alpha3/azuremachinetemplate_conversion.go index f2ceb54ddbd..f01b6924c7c 100644 --- a/api/v1alpha3/azuremachinetemplate_conversion.go +++ b/api/v1alpha3/azuremachinetemplate_conversion.go @@ -54,6 +54,10 @@ func (src *AzureMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Template.Spec.Image.ComputeGallery = restored.Spec.Template.Spec.Image.ComputeGallery } + if restored.Spec.Template.Spec.AdditionalCapabilities != nil { + dst.Spec.Template.Spec.AdditionalCapabilities = restored.Spec.Template.Spec.AdditionalCapabilities + } + dst.Spec.Template.Spec.SubnetName = restored.Spec.Template.Spec.SubnetName dst.Spec.Template.ObjectMeta = restored.Spec.Template.ObjectMeta diff --git a/api/v1alpha3/zz_generated.conversion.go b/api/v1alpha3/zz_generated.conversion.go index 0325968d850..7d1b0e804f3 100644 --- a/api/v1alpha3/zz_generated.conversion.go +++ b/api/v1alpha3/zz_generated.conversion.go @@ -899,6 +899,7 @@ func autoConvert_v1beta1_AzureMachineSpec_To_v1alpha3_AzureMachineSpec(in *v1bet } out.SSHPublicKey = in.SSHPublicKey out.AdditionalTags = *(*Tags)(unsafe.Pointer(&in.AdditionalTags)) + // WARNING: in.AdditionalCapabilities requires manual conversion: does not exist in peer-type out.AllocatePublicIP = in.AllocatePublicIP out.EnableIPForwarding = in.EnableIPForwarding out.AcceleratedNetworking = (*bool)(unsafe.Pointer(in.AcceleratedNetworking)) diff --git a/api/v1alpha4/azuremachine_conversion.go b/api/v1alpha4/azuremachine_conversion.go index c8cf37e76fa..14807cf7087 100644 --- a/api/v1alpha4/azuremachine_conversion.go +++ b/api/v1alpha4/azuremachine_conversion.go @@ -40,6 +40,10 @@ func (src *AzureMachine) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Image.ComputeGallery = restored.Spec.Image.ComputeGallery } + if restored.Spec.AdditionalCapabilities != nil { + dst.Spec.AdditionalCapabilities = restored.Spec.AdditionalCapabilities + } + return nil } @@ -85,3 +89,8 @@ func Convert_v1alpha4_AzureMarketplaceImage_To_v1beta1_AzureMarketplaceImage(in func Convert_v1beta1_Image_To_v1alpha4_Image(in *v1beta1.Image, out *Image, s apiconversion.Scope) error { return autoConvert_v1beta1_Image_To_v1alpha4_Image(in, out, s) } + +// Convert_v1beta1_AzureMachineSpec_To_v1alpha4_AzureMachineSpec is an autogenerated conversion function. +func Convert_v1beta1_AzureMachineSpec_To_v1alpha4_AzureMachineSpec(in *v1beta1.AzureMachineSpec, out *AzureMachineSpec, s apiconversion.Scope) error { + return autoConvert_v1beta1_AzureMachineSpec_To_v1alpha4_AzureMachineSpec(in, out, s) +} diff --git a/api/v1alpha4/azuremachinetemplate_conversion.go b/api/v1alpha4/azuremachinetemplate_conversion.go index 3b7e4e8ce9d..312f8ff6ff2 100644 --- a/api/v1alpha4/azuremachinetemplate_conversion.go +++ b/api/v1alpha4/azuremachinetemplate_conversion.go @@ -40,6 +40,10 @@ func (src *AzureMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Template.Spec.Image.ComputeGallery = restored.Spec.Template.Spec.Image.ComputeGallery } + if restored.Spec.Template.Spec.AdditionalCapabilities != nil { + dst.Spec.Template.Spec.AdditionalCapabilities = restored.Spec.Template.Spec.AdditionalCapabilities + } + dst.Spec.Template.ObjectMeta = restored.Spec.Template.ObjectMeta return nil diff --git a/api/v1alpha4/zz_generated.conversion.go b/api/v1alpha4/zz_generated.conversion.go index daed97869ed..9ad545f810b 100644 --- a/api/v1alpha4/zz_generated.conversion.go +++ b/api/v1alpha4/zz_generated.conversion.go @@ -167,11 +167,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta1.AzureMachineSpec)(nil), (*AzureMachineSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta1_AzureMachineSpec_To_v1alpha4_AzureMachineSpec(a.(*v1beta1.AzureMachineSpec), b.(*AzureMachineSpec), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*AzureMachineStatus)(nil), (*v1beta1.AzureMachineStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha4_AzureMachineStatus_To_v1beta1_AzureMachineStatus(a.(*AzureMachineStatus), b.(*v1beta1.AzureMachineStatus), scope) }); err != nil { @@ -462,6 +457,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1beta1.AzureMachineSpec)(nil), (*AzureMachineSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_AzureMachineSpec_To_v1alpha4_AzureMachineSpec(a.(*v1beta1.AzureMachineSpec), b.(*AzureMachineSpec), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*v1beta1.AzureMachineTemplateResource)(nil), (*AzureMachineTemplateResource)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_AzureMachineTemplateResource_To_v1alpha4_AzureMachineTemplateResource(a.(*v1beta1.AzureMachineTemplateResource), b.(*AzureMachineTemplateResource), scope) }); err != nil { @@ -1046,6 +1046,7 @@ func autoConvert_v1beta1_AzureMachineSpec_To_v1alpha4_AzureMachineSpec(in *v1bet out.DataDisks = *(*[]DataDisk)(unsafe.Pointer(&in.DataDisks)) out.SSHPublicKey = in.SSHPublicKey out.AdditionalTags = *(*Tags)(unsafe.Pointer(&in.AdditionalTags)) + // WARNING: in.AdditionalCapabilities requires manual conversion: does not exist in peer-type out.AllocatePublicIP = in.AllocatePublicIP out.EnableIPForwarding = in.EnableIPForwarding out.AcceleratedNetworking = (*bool)(unsafe.Pointer(in.AcceleratedNetworking)) @@ -1055,11 +1056,6 @@ func autoConvert_v1beta1_AzureMachineSpec_To_v1alpha4_AzureMachineSpec(in *v1bet return nil } -// Convert_v1beta1_AzureMachineSpec_To_v1alpha4_AzureMachineSpec is an autogenerated conversion function. -func Convert_v1beta1_AzureMachineSpec_To_v1alpha4_AzureMachineSpec(in *v1beta1.AzureMachineSpec, out *AzureMachineSpec, s conversion.Scope) error { - return autoConvert_v1beta1_AzureMachineSpec_To_v1alpha4_AzureMachineSpec(in, out, s) -} - func autoConvert_v1alpha4_AzureMachineStatus_To_v1beta1_AzureMachineStatus(in *AzureMachineStatus, out *v1beta1.AzureMachineStatus, s conversion.Scope) error { out.Ready = in.Ready out.Addresses = *(*[]corev1.NodeAddress)(unsafe.Pointer(&in.Addresses)) diff --git a/api/v1beta1/azuremachine_types.go b/api/v1beta1/azuremachine_types.go index 40af504fe63..f77d4afa0dd 100644 --- a/api/v1beta1/azuremachine_types.go +++ b/api/v1beta1/azuremachine_types.go @@ -86,6 +86,10 @@ type AzureMachineSpec struct { // +optional AdditionalTags Tags `json:"additionalTags,omitempty"` + // AdditionalCapabilities specifies additional capabilities enabled or disabled on the virtual machine. + // +optional + AdditionalCapabilities *AdditionalCapabilities `json:"additionalCapabilities,omitempty"` + // AllocatePublicIP allows the ability to create dynamic public ips for machines where this value is true. // +optional AllocatePublicIP bool `json:"allocatePublicIP,omitempty"` @@ -185,6 +189,15 @@ type AzureMachineStatus struct { LongRunningOperationStates Futures `json:"longRunningOperationStates,omitempty"` } +// AdditionalCapabilities enables or disables a capability on the virtual machine. +type AdditionalCapabilities struct { + // UltraSSDEnabled enables or disables Azure UltraSSD capability for the virtual machine. + // Defaults to true if Ultra SSD data disks are specified, + // otherwise it doesn't set the capability on the VM. + // +optional + UltraSSDEnabled *bool `json:"ultraSSDEnabled,omitempty"` +} + // +kubebuilder:object:root=true // +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" // +kubebuilder:printcolumn:name="Reason",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].reason" diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 2516b5c557b..9dfb74a0672 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -29,6 +29,26 @@ import ( "sigs.k8s.io/cluster-api/errors" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AdditionalCapabilities) DeepCopyInto(out *AdditionalCapabilities) { + *out = *in + if in.UltraSSDEnabled != nil { + in, out := &in.UltraSSDEnabled, &out.UltraSSDEnabled + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalCapabilities. +func (in *AdditionalCapabilities) DeepCopy() *AdditionalCapabilities { + if in == nil { + return nil + } + out := new(AdditionalCapabilities) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AddressRecord) DeepCopyInto(out *AddressRecord) { *out = *in @@ -583,6 +603,11 @@ func (in *AzureMachineSpec) DeepCopyInto(out *AzureMachineSpec) { (*out)[key] = val } } + if in.AdditionalCapabilities != nil { + in, out := &in.AdditionalCapabilities, &out.AdditionalCapabilities + *out = new(AdditionalCapabilities) + (*in).DeepCopyInto(*out) + } if in.AcceleratedNetworking != nil { in, out := &in.AcceleratedNetworking, &out.AcceleratedNetworking *out = new(bool) diff --git a/azure/scope/machine.go b/azure/scope/machine.go index be5b837705e..a1a51c7861b 100644 --- a/azure/scope/machine.go +++ b/azure/scope/machine.go @@ -164,6 +164,7 @@ func (m *MachineScope) VMSpec() azure.ResourceSpecGetter { SpotVMOptions: m.AzureMachine.Spec.SpotVMOptions, SecurityProfile: m.AzureMachine.Spec.SecurityProfile, AdditionalTags: m.AdditionalTags(), + AdditionalCapabilities: m.AzureMachine.Spec.AdditionalCapabilities, ProviderID: m.ProviderID(), } if m.cache != nil { diff --git a/azure/services/scalesets/scalesets.go b/azure/services/scalesets/scalesets.go index dafd7706897..d7640206276 100644 --- a/azure/services/scalesets/scalesets.go +++ b/azure/services/scalesets/scalesets.go @@ -335,17 +335,30 @@ func (s *Service) validateSpec(ctx context.Context) error { return azure.WithTerminalError(errors.Errorf("encryption at host is not supported for VM type %s", spec.Size)) } - // check the support for ultra disks based on location and vm size - for _, disks := range spec.DataDisks { - location := s.Scope.Location() - zones, err := s.resourceSKUCache.GetZones(ctx, location) - if err != nil { - return azure.WithTerminalError(errors.Wrapf(err, "failed to get the zones for location %s", location)) - } + // Fetch location and zone to check for their support of ultra disks. + location := s.Scope.Location() + zones, err := s.resourceSKUCache.GetZones(ctx, location) + if err != nil { + return azure.WithTerminalError(errors.Wrapf(err, "failed to get the zones for location %s", location)) + } + + for _, zone := range zones { + hasLocationCapability := sku.HasLocationCapability(resourceskus.UltraSSDAvailable, location, zone) + err := fmt.Errorf("vm size %s does not support ultra disks in location %s. select a different vm size or disable ultra disks", spec.Size, location) - for _, zone := range zones { - if disks.ManagedDisk != nil && disks.ManagedDisk.StorageAccountType == string(compute.StorageAccountTypesUltraSSDLRS) && !sku.HasLocationCapability(resourceskus.UltraSSDAvailable, location, zone) { - return azure.WithTerminalError(fmt.Errorf("vm size %s does not support ultra disks in location %s. select a different vm size or disable ultra disks", spec.Size, location)) + // Check support for ultra disks as data disks. + for _, disks := range spec.DataDisks { + if disks.ManagedDisk != nil && + disks.ManagedDisk.StorageAccountType == string(compute.StorageAccountTypesUltraSSDLRS) && + !hasLocationCapability { + return azure.WithTerminalError(err) + } + } + // Check support for ultra disks as persistent volumes. + if spec.AdditionalCapabilities != nil && spec.AdditionalCapabilities.UltraSSDEnabled != nil { + if *spec.AdditionalCapabilities.UltraSSDEnabled && + !hasLocationCapability { + return azure.WithTerminalError(err) } } } @@ -491,6 +504,8 @@ func (s *Service) buildVMSSFromSpec(ctx context.Context, vmssSpec azure.ScaleSet } } + // Provisionally detect whether there is any Data Disk defined which uses UltraSSDs. + // If that's the case, enable the UltraSSD capability. for _, dataDisk := range vmssSpec.DataDisks { if dataDisk.ManagedDisk != nil && dataDisk.ManagedDisk.StorageAccountType == string(compute.StorageAccountTypesUltraSSDLRS) { vmss.VirtualMachineScaleSetProperties.AdditionalCapabilities = &compute.AdditionalCapabilities{ @@ -499,6 +514,14 @@ func (s *Service) buildVMSSFromSpec(ctx context.Context, vmssSpec azure.ScaleSet } } + // Set Additional Capabilities if any is present on the spec. + if vmssSpec.AdditionalCapabilities != nil { + // Set UltraSSDEnabled if a specific value is set on the spec for it. + if vmssSpec.AdditionalCapabilities.UltraSSDEnabled != nil { + vmss.AdditionalCapabilities.UltraSSDEnabled = vmssSpec.AdditionalCapabilities.UltraSSDEnabled + } + } + if vmssSpec.TerminateNotificationTimeout != nil { vmss.VirtualMachineScaleSetProperties.VirtualMachineProfile.ScheduledEventsProfile = &compute.ScheduledEventsProfile{ TerminateNotificationProfile: &compute.TerminateNotificationProfile{ diff --git a/azure/services/scalesets/scalesets_test.go b/azure/services/scalesets/scalesets_test.go index b252c5e77a1..c3c9c6cea64 100644 --- a/azure/services/scalesets/scalesets_test.go +++ b/azure/services/scalesets/scalesets_test.go @@ -500,7 +500,7 @@ func TestReconcileVMSS(t *testing.T) { }, }, { - name: "fail to create a vm with ultra disk enabled", + name: "fail to create a vm with ultra disk implicitly enabled by data disk, when location not supported", expectedError: "reconcile error that cannot be recovered occurred: vm size VM_SIZE_USSD does not support ultra disks in location test-location. select a different vm size or disable ultra disks. Object will not be requeued", expect: func(g *WithT, s *mock_scalesets.MockScaleSetScopeMockRecorder, m *mock_scalesets.MockClientMockRecorder) { s.ScaleSetSpec().Return(azure.ScaleSetSpec{ @@ -519,6 +519,45 @@ func TestReconcileVMSS(t *testing.T) { s.Location().AnyTimes().Return("test-location") }, }, + { + name: "fail to create a vm with ultra disk explicitly enabled via additional capabilities, when location not supported", + expectedError: "reconcile error that cannot be recovered occurred: vm size VM_SIZE_USSD does not support ultra disks in location test-location. select a different vm size or disable ultra disks. Object will not be requeued", + expect: func(g *WithT, s *mock_scalesets.MockScaleSetScopeMockRecorder, m *mock_scalesets.MockClientMockRecorder) { + s.ScaleSetSpec().Return(azure.ScaleSetSpec{ + Name: defaultVMSSName, + Size: "VM_SIZE_USSD", + Capacity: 2, + SSHKeyData: "ZmFrZXNzaGtleQo=", + AdditionalCapabilities: &infrav1.AdditionalCapabilities{ + UltraSSDEnabled: to.BoolPtr(true), + }, + }) + s.Location().AnyTimes().Return("test-location") + }, + }, + { + name: "fail to create a vm with ultra disk explicitly enabled via additional capabilities, when location not supported", + expectedError: "reconcile error that cannot be recovered occurred: vm size VM_SIZE_USSD does not support ultra disks in location test-location. select a different vm size or disable ultra disks. Object will not be requeued", + expect: func(g *WithT, s *mock_scalesets.MockScaleSetScopeMockRecorder, m *mock_scalesets.MockClientMockRecorder) { + s.ScaleSetSpec().Return(azure.ScaleSetSpec{ + Name: defaultVMSSName, + Size: "VM_SIZE_USSD", + Capacity: 2, + SSHKeyData: "ZmFrZXNzaGtleQo=", + DataDisks: []infrav1.DataDisk{ + { + ManagedDisk: &infrav1.ManagedDiskParameters{ + StorageAccountType: "UltraSSD_LRS", + }, + }, + }, + AdditionalCapabilities: &infrav1.AdditionalCapabilities{ + UltraSSDEnabled: to.BoolPtr(false), + }, + }) + s.Location().AnyTimes().Return("test-location") + }, + }, } for _, tc := range testcases { diff --git a/azure/services/virtualmachines/spec.go b/azure/services/virtualmachines/spec.go index c245519094b..d77e12113ed 100644 --- a/azure/services/virtualmachines/spec.go +++ b/azure/services/virtualmachines/spec.go @@ -49,6 +49,7 @@ type VMSpec struct { SpotVMOptions *infrav1.SpotVMOptions SecurityProfile *infrav1.SecurityProfile AdditionalTags infrav1.Tags + AdditionalCapabilities *infrav1.AdditionalCapabilities SKU resourceskus.SKU Image *infrav1.Image BootstrapData string @@ -306,6 +307,9 @@ func (s *VMSpec) generateNICRefs() *[]compute.NetworkInterfaceReference { func (s *VMSpec) generateAdditionalCapabilities() *compute.AdditionalCapabilities { var capabilities *compute.AdditionalCapabilities + + // Provisionally detect whether there is any Data Disk defined which uses UltraSSDs. + // If that's the case, enable the UltraSSD capability. for _, dataDisk := range s.DataDisks { if dataDisk.ManagedDisk != nil && dataDisk.ManagedDisk.StorageAccountType == string(compute.StorageAccountTypesUltraSSDLRS) { capabilities = &compute.AdditionalCapabilities{ @@ -314,6 +318,18 @@ func (s *VMSpec) generateAdditionalCapabilities() *compute.AdditionalCapabilitie break } } + + // Set Additional Capabilities if any is present on the spec. + if s.AdditionalCapabilities != nil { + if capabilities == nil { + capabilities = &compute.AdditionalCapabilities{} + } + // Set UltraSSDEnabled if a specific value is set on the spec for it. + if s.AdditionalCapabilities.UltraSSDEnabled != nil { + capabilities.UltraSSDEnabled = s.AdditionalCapabilities.UltraSSDEnabled + } + } + return capabilities } diff --git a/azure/services/virtualmachines/spec_test.go b/azure/services/virtualmachines/spec_test.go index 60e03345843..f08a1248529 100644 --- a/azure/services/virtualmachines/spec_test.go +++ b/azure/services/virtualmachines/spec_test.go @@ -661,6 +661,184 @@ func TestParameters(t *testing.T) { }, expectedError: "reconcile error that cannot be recovered occurred: vm size Standard_D2v3 does not support ultra disks in location test-location. select a different vm size or disable ultra disks. Object will not be requeued", }, + { + name: "creates a vm with AdditionalCapabilities.UltraSSDEnabled false, if an ultra disk is specified as data disk but AdditionalCapabilities.UltraSSDEnabled is false", + spec: &VMSpec{ + Name: "my-ultra-ssd-vm", + Role: infrav1.Node, + NICIDs: []string{"my-nic"}, + SSHKeyData: "fakesshpublickey", + Size: "Standard_D2v3", + Location: "test-location", + Zone: "1", + Image: &infrav1.Image{ID: to.StringPtr("fake-image-id")}, + AdditionalCapabilities: &infrav1.AdditionalCapabilities{ + UltraSSDEnabled: to.BoolPtr(false), + }, + DataDisks: []infrav1.DataDisk{ + { + NameSuffix: "myDiskWithUltraDisk", + DiskSizeGB: 128, + Lun: to.Int32Ptr(1), + ManagedDisk: &infrav1.ManagedDiskParameters{ + StorageAccountType: "UltraSSD_LRS", + }, + }, + }, + SKU: validSKUWithUltraSSD, + }, + existing: nil, + expect: func(g *WithT, result interface{}) { + g.Expect(result).To(BeAssignableToTypeOf(compute.VirtualMachine{})) + g.Expect(result.(compute.VirtualMachine).AdditionalCapabilities.UltraSSDEnabled).To(Equal(to.BoolPtr(false))) + expectedDataDisks := &[]compute.DataDisk{ + { + Lun: to.Int32Ptr(1), + Name: to.StringPtr("my-ultra-ssd-vm_myDiskWithUltraDisk"), + CreateOption: "Empty", + DiskSizeGB: to.Int32Ptr(128), + ManagedDisk: &compute.ManagedDiskParameters{ + StorageAccountType: "UltraSSD_LRS", + }, + }, + } + g.Expect(gomockinternal.DiffEq(expectedDataDisks).Matches(result.(compute.VirtualMachine).StorageProfile.DataDisks)).To(BeTrue(), cmp.Diff(expectedDataDisks, result.(compute.VirtualMachine).StorageProfile.DataDisks)) + }, + expectedError: "", + }, + { + name: "creates a vm with AdditionalCapabilities.UltraSSDEnabled true, if an ultra disk is specified as data disk and no AdditionalCapabilities.UltraSSDEnabled is set", + spec: &VMSpec{ + Name: "my-ultra-ssd-vm", + Role: infrav1.Node, + NICIDs: []string{"my-nic"}, + SSHKeyData: "fakesshpublickey", + Size: "Standard_D2v3", + Location: "test-location", + Zone: "1", + Image: &infrav1.Image{ID: to.StringPtr("fake-image-id")}, + DataDisks: []infrav1.DataDisk{ + { + NameSuffix: "myDiskWithUltraDisk", + DiskSizeGB: 128, + Lun: to.Int32Ptr(1), + ManagedDisk: &infrav1.ManagedDiskParameters{ + StorageAccountType: "UltraSSD_LRS", + }, + }, + }, + SKU: validSKUWithUltraSSD, + }, + existing: nil, + expect: func(g *WithT, result interface{}) { + g.Expect(result).To(BeAssignableToTypeOf(compute.VirtualMachine{})) + g.Expect(result.(compute.VirtualMachine).AdditionalCapabilities.UltraSSDEnabled).To(Equal(to.BoolPtr(true))) + expectedDataDisks := &[]compute.DataDisk{ + { + Lun: to.Int32Ptr(1), + Name: to.StringPtr("my-ultra-ssd-vm_myDiskWithUltraDisk"), + CreateOption: "Empty", + DiskSizeGB: to.Int32Ptr(128), + ManagedDisk: &compute.ManagedDiskParameters{ + StorageAccountType: "UltraSSD_LRS", + }, + }, + } + g.Expect(gomockinternal.DiffEq(expectedDataDisks).Matches(result.(compute.VirtualMachine).StorageProfile.DataDisks)).To(BeTrue(), cmp.Diff(expectedDataDisks, result.(compute.VirtualMachine).StorageProfile.DataDisks)) + }, + expectedError: "", + }, + { + name: "creates a vm with AdditionalCapabilities.UltraSSDEnabled true, if an ultra disk is specified as data disk and AdditionalCapabilities.UltraSSDEnabled is true", + spec: &VMSpec{ + Name: "my-ultra-ssd-vm", + Role: infrav1.Node, + NICIDs: []string{"my-nic"}, + SSHKeyData: "fakesshpublickey", + Size: "Standard_D2v3", + Location: "test-location", + Zone: "1", + Image: &infrav1.Image{ID: to.StringPtr("fake-image-id")}, + AdditionalCapabilities: &infrav1.AdditionalCapabilities{ + UltraSSDEnabled: to.BoolPtr(true), + }, + DataDisks: []infrav1.DataDisk{ + { + NameSuffix: "myDiskWithUltraDisk", + DiskSizeGB: 128, + Lun: to.Int32Ptr(1), + ManagedDisk: &infrav1.ManagedDiskParameters{ + StorageAccountType: "UltraSSD_LRS", + }, + }, + }, + SKU: validSKUWithUltraSSD, + }, + existing: nil, + expect: func(g *WithT, result interface{}) { + g.Expect(result).To(BeAssignableToTypeOf(compute.VirtualMachine{})) + g.Expect(result.(compute.VirtualMachine).AdditionalCapabilities.UltraSSDEnabled).To(Equal(to.BoolPtr(true))) + expectedDataDisks := &[]compute.DataDisk{ + { + Lun: to.Int32Ptr(1), + Name: to.StringPtr("my-ultra-ssd-vm_myDiskWithUltraDisk"), + CreateOption: "Empty", + DiskSizeGB: to.Int32Ptr(128), + ManagedDisk: &compute.ManagedDiskParameters{ + StorageAccountType: "UltraSSD_LRS", + }, + }, + } + g.Expect(gomockinternal.DiffEq(expectedDataDisks).Matches(result.(compute.VirtualMachine).StorageProfile.DataDisks)).To(BeTrue(), cmp.Diff(expectedDataDisks, result.(compute.VirtualMachine).StorageProfile.DataDisks)) + }, + expectedError: "", + }, + { + name: "creates a vm with AdditionalCapabilities.UltraSSDEnabled true, if no ultra disk is specified as data disk and AdditionalCapabilities.UltraSSDEnabled is true", + spec: &VMSpec{ + Name: "my-ultra-ssd-vm", + Role: infrav1.Node, + NICIDs: []string{"my-nic"}, + SSHKeyData: "fakesshpublickey", + Size: "Standard_D2v3", + Location: "test-location", + Zone: "1", + Image: &infrav1.Image{ID: to.StringPtr("fake-image-id")}, + AdditionalCapabilities: &infrav1.AdditionalCapabilities{ + UltraSSDEnabled: to.BoolPtr(true), + }, + SKU: validSKUWithUltraSSD, + }, + existing: nil, + expect: func(g *WithT, result interface{}) { + g.Expect(result).To(BeAssignableToTypeOf(compute.VirtualMachine{})) + g.Expect(result.(compute.VirtualMachine).AdditionalCapabilities.UltraSSDEnabled).To(Equal(to.BoolPtr(true))) + }, + expectedError: "", + }, + { + name: "creates a vm with AdditionalCapabilities.UltraSSDEnabled false, if no ultra disk is specified as data disk and AdditionalCapabilities.UltraSSDEnabled is false", + spec: &VMSpec{ + Name: "my-ultra-ssd-vm", + Role: infrav1.Node, + NICIDs: []string{"my-nic"}, + SSHKeyData: "fakesshpublickey", + Size: "Standard_D2v3", + Location: "test-location", + Zone: "1", + Image: &infrav1.Image{ID: to.StringPtr("fake-image-id")}, + AdditionalCapabilities: &infrav1.AdditionalCapabilities{ + UltraSSDEnabled: to.BoolPtr(false), + }, + SKU: validSKUWithUltraSSD, + }, + existing: nil, + expect: func(g *WithT, result interface{}) { + g.Expect(result).To(BeAssignableToTypeOf(compute.VirtualMachine{})) + g.Expect(result.(compute.VirtualMachine).AdditionalCapabilities.UltraSSDEnabled).To(Equal(to.BoolPtr(false))) + }, + expectedError: "", + }, } for _, tc := range testcases { tc := tc diff --git a/azure/types.go b/azure/types.go index 3e0ad04199e..63172e8ef15 100644 --- a/azure/types.go +++ b/azure/types.go @@ -126,6 +126,7 @@ type ScaleSetSpec struct { UserAssignedIdentities []infrav1.UserAssignedIdentity SecurityProfile *infrav1.SecurityProfile SpotVMOptions *infrav1.SpotVMOptions + AdditionalCapabilities *infrav1.AdditionalCapabilities FailureDomains []string } diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachines.yaml index f7fa1fcc67f..b67c4c31104 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachines.yaml @@ -1050,6 +1050,17 @@ spec: is set to true with a VMSize that does not support it, Azure will return an error. type: boolean + additionalCapabilities: + description: AdditionalCapabilities specifies additional capabilities + enabled or disabled on the virtual machine. + properties: + ultraSSDEnabled: + description: UltraSSDEnabled enables or disables Azure UltraSSD + capability for the virtual machine. Defaults to true if Ultra + SSD data disks are specified, otherwise it doesn't set the capability + on the VM. + type: boolean + type: object additionalTags: additionalProperties: type: string diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinetemplates.yaml index 12fe01ab8d6..885a4f07bea 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremachinetemplates.yaml @@ -818,6 +818,17 @@ spec: If AcceleratedNetworking is set to true with a VMSize that does not support it, Azure will return an error. type: boolean + additionalCapabilities: + description: AdditionalCapabilities specifies additional capabilities + enabled or disabled on the virtual machine. + properties: + ultraSSDEnabled: + description: UltraSSDEnabled enables or disables Azure + UltraSSD capability for the virtual machine. Defaults + to true if Ultra SSD data disks are specified, otherwise + it doesn't set the capability on the VM. + type: boolean + type: object additionalTags: additionalProperties: type: string diff --git a/docs/book/src/topics/data-disks.md b/docs/book/src/topics/data-disks.md index 605de04efe0..16df63e7d4f 100644 --- a/docs/book/src/topics/data-disks.md +++ b/docs/book/src/topics/data-disks.md @@ -31,6 +31,22 @@ To check all available vm-sizes in a given region which supports availability zo ```bash az vm list-skus -l -z -s ``` + +Provided that the chosen region and zone support Ultra disks, Azure Machine objects having Ultra disks specified as Data disks will have their virtual machines created with the `AdditionalCapabilities.UltraSSDEnabled` additional capability set to `true`. This capability can also be manually set on the Azure Machine spec and will override the automatically chosen value (if any). + +See [Ultra disk](https://docs.microsoft.com/en-us/azure/virtual-machines/disks-types#ultra-disk) for ultra disk performance and GA scope. + +### Ultra disk support for Persistent Volumes +First, to check all available vm-sizes in a given region which supports availability zone that has the `UltraSSDAvailable` capability supported, execute following using Azure CLI: +```bash +az vm list-skus -l -z -s +``` + +Provided that the chosen region and zone support Ultra disks, Ultra disk based Persistent Volumes can be attached to Pods scheduled on specific Azure Machines, provided that the spec field `.spec.additionalCapabilities.ultraSSDEnabled` on those Machines has been set to `true`. +NOTE: A misconfiguration or lack this field on the targeted Node's Machine will result in the Pod using the PV be unable to reach the Running Phase. + +See [Use ultra disks dynamically with a storage class](https://docs.microsoft.com/en-us/azure/aks/use-ultra-disks#use-ultra-disks-dynamically-with-a-storage-class) for more information on how to configure an Ultra disk based StorageClass and PersistentVolumeClaim. + See [Ultra disk](https://docs.microsoft.com/en-us/azure/virtual-machines/disks-types#ultra-disk) for ultra disk performance and GA scope. ## Configuring partitions, file systems and mounts @@ -101,4 +117,4 @@ spec: - nameSuffix: mydisk diskSizeGB: 128 lun: 1 -```` \ No newline at end of file +````