sigs.k8s.io/cluster-api-provider-azure@v1.17.0/azure/services/virtualmachines/spec.go (about) 1 /* 2 Copyright 2021 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package virtualmachines 18 19 import ( 20 "context" 21 "encoding/base64" 22 "fmt" 23 24 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5" 25 "github.com/pkg/errors" 26 "k8s.io/utils/ptr" 27 infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" 28 "sigs.k8s.io/cluster-api-provider-azure/azure" 29 "sigs.k8s.io/cluster-api-provider-azure/azure/converters" 30 "sigs.k8s.io/cluster-api-provider-azure/azure/services/resourceskus" 31 "sigs.k8s.io/cluster-api-provider-azure/util/generators" 32 ) 33 34 // VMSpec defines the specification for a Virtual Machine. 35 type VMSpec struct { 36 Name string 37 ResourceGroup string 38 Location string 39 ExtendedLocation *infrav1.ExtendedLocationSpec 40 ClusterName string 41 Role string 42 NICIDs []string 43 SSHKeyData string 44 Size string 45 AvailabilitySetID string 46 Zone string 47 Identity infrav1.VMIdentity 48 OSDisk infrav1.OSDisk 49 DataDisks []infrav1.DataDisk 50 UserAssignedIdentities []infrav1.UserAssignedIdentity 51 SpotVMOptions *infrav1.SpotVMOptions 52 SecurityProfile *infrav1.SecurityProfile 53 AdditionalTags infrav1.Tags 54 AdditionalCapabilities *infrav1.AdditionalCapabilities 55 DiagnosticsProfile *infrav1.Diagnostics 56 DisableExtensionOperations bool 57 CapacityReservationGroupID string 58 SKU resourceskus.SKU 59 Image *infrav1.Image 60 BootstrapData string 61 ProviderID string 62 } 63 64 // ResourceName returns the name of the virtual machine. 65 func (s *VMSpec) ResourceName() string { 66 return s.Name 67 } 68 69 // ResourceGroupName returns the name of the virtual machine. 70 func (s *VMSpec) ResourceGroupName() string { 71 return s.ResourceGroup 72 } 73 74 // OwnerResourceName is a no-op for virtual machines. 75 func (s *VMSpec) OwnerResourceName() string { 76 return "" 77 } 78 79 // Parameters returns the parameters for the virtual machine. 80 func (s *VMSpec) Parameters(ctx context.Context, existing interface{}) (params interface{}, err error) { 81 if existing != nil { 82 if _, ok := existing.(armcompute.VirtualMachine); !ok { 83 return nil, errors.Errorf("%T is not an armcompute.VirtualMachine", existing) 84 } 85 // vm already exists 86 return nil, nil 87 } 88 89 // VM got deleted outside of capz, do not recreate it as Machines are immutable. 90 if s.ProviderID != "" { 91 return nil, azure.VMDeletedError{ProviderID: s.ProviderID} 92 } 93 94 storageProfile, err := s.generateStorageProfile() 95 if err != nil { 96 return nil, err 97 } 98 99 securityProfile, err := s.generateSecurityProfile(storageProfile) 100 if err != nil { 101 return nil, err 102 } 103 104 osProfile, err := s.generateOSProfile() 105 if err != nil { 106 return nil, errors.Wrap(err, "failed to generate OS Profile") 107 } 108 109 priority, evictionPolicy, billingProfile, err := converters.GetSpotVMOptions(s.SpotVMOptions, s.OSDisk.DiffDiskSettings) 110 if err != nil { 111 return nil, errors.Wrap(err, "failed to get Spot VM options") 112 } 113 114 identity, err := converters.VMIdentityToVMSDK(s.Identity, s.UserAssignedIdentities) 115 if err != nil { 116 return nil, errors.Wrap(err, "failed to generate VM identity") 117 } 118 119 return armcompute.VirtualMachine{ 120 Plan: converters.ImageToPlan(s.Image), 121 Location: ptr.To(s.Location), 122 ExtendedLocation: converters.ExtendedLocationToComputeSDK(s.ExtendedLocation), 123 Tags: converters.TagsToMap(infrav1.Build(infrav1.BuildParams{ 124 ClusterName: s.ClusterName, 125 Lifecycle: infrav1.ResourceLifecycleOwned, 126 Name: ptr.To(s.Name), 127 Role: ptr.To(s.Role), 128 Additional: s.AdditionalTags, 129 })), 130 Properties: &armcompute.VirtualMachineProperties{ 131 AdditionalCapabilities: s.generateAdditionalCapabilities(), 132 AvailabilitySet: s.getAvailabilitySet(), 133 HardwareProfile: &armcompute.HardwareProfile{ 134 VMSize: ptr.To(armcompute.VirtualMachineSizeTypes(s.Size)), 135 }, 136 StorageProfile: storageProfile, 137 SecurityProfile: securityProfile, 138 OSProfile: osProfile, 139 NetworkProfile: &armcompute.NetworkProfile{ 140 NetworkInterfaces: s.generateNICRefs(), 141 }, 142 Priority: priority, 143 EvictionPolicy: evictionPolicy, 144 BillingProfile: billingProfile, 145 DiagnosticsProfile: converters.GetDiagnosticsProfile(s.DiagnosticsProfile), 146 CapacityReservation: s.getCapacityReservationProfile(), 147 }, 148 Identity: identity, 149 Zones: s.getZones(), 150 }, nil 151 } 152 153 // generateStorageProfile generates a pointer to an armcompute.StorageProfile which can utilized for VM creation. 154 func (s *VMSpec) generateStorageProfile() (*armcompute.StorageProfile, error) { 155 osDisk := &armcompute.OSDisk{ 156 Name: ptr.To(azure.GenerateOSDiskName(s.Name)), 157 OSType: ptr.To(armcompute.OperatingSystemTypes(s.OSDisk.OSType)), 158 CreateOption: ptr.To(armcompute.DiskCreateOptionTypesFromImage), 159 DiskSizeGB: s.OSDisk.DiskSizeGB, 160 } 161 if s.OSDisk.CachingType != "" { 162 osDisk.Caching = ptr.To(armcompute.CachingTypes(s.OSDisk.CachingType)) 163 } 164 storageProfile := &armcompute.StorageProfile{ 165 OSDisk: osDisk, 166 } 167 168 // Checking if the requested VM size has at least 2 vCPUS 169 vCPUCapability, err := s.SKU.HasCapabilityWithCapacity(resourceskus.VCPUs, resourceskus.MinimumVCPUS) 170 if err != nil { 171 return nil, azure.WithTerminalError(errors.Wrap(err, "failed to validate the vCPU capability")) 172 } 173 if !vCPUCapability { 174 return nil, azure.WithTerminalError(errors.New("VM size should be bigger or equal to at least 2 vCPUs")) 175 } 176 177 // Checking if the requested VM size has at least 2 Gi of memory 178 MemoryCapability, err := s.SKU.HasCapabilityWithCapacity(resourceskus.MemoryGB, resourceskus.MinimumMemory) 179 if err != nil { 180 return nil, azure.WithTerminalError(errors.Wrap(err, "failed to validate the memory capability")) 181 } 182 183 if !MemoryCapability { 184 return nil, azure.WithTerminalError(errors.New("VM memory should be bigger or equal to at least 2Gi")) 185 } 186 187 // enable ephemeral OS 188 if s.OSDisk.DiffDiskSettings != nil { 189 if !s.SKU.HasCapability(resourceskus.EphemeralOSDisk) { 190 return nil, azure.WithTerminalError(fmt.Errorf("VM size %s does not support ephemeral os. Select a different VM size or disable ephemeral os", s.Size)) 191 } 192 193 storageProfile.OSDisk.DiffDiskSettings = &armcompute.DiffDiskSettings{ 194 Option: ptr.To(armcompute.DiffDiskOptions(s.OSDisk.DiffDiskSettings.Option)), 195 } 196 197 if s.OSDisk.DiffDiskSettings.Placement != nil { 198 storageProfile.OSDisk.DiffDiskSettings.Placement = ptr.To(armcompute.DiffDiskPlacement(*s.OSDisk.DiffDiskSettings.Placement)) 199 } 200 } 201 202 if s.OSDisk.ManagedDisk != nil { 203 storageProfile.OSDisk.ManagedDisk = &armcompute.ManagedDiskParameters{} 204 if s.OSDisk.ManagedDisk.StorageAccountType != "" { 205 storageProfile.OSDisk.ManagedDisk.StorageAccountType = ptr.To(armcompute.StorageAccountTypes(s.OSDisk.ManagedDisk.StorageAccountType)) 206 } 207 if s.OSDisk.ManagedDisk.DiskEncryptionSet != nil { 208 storageProfile.OSDisk.ManagedDisk.DiskEncryptionSet = &armcompute.DiskEncryptionSetParameters{ID: ptr.To(s.OSDisk.ManagedDisk.DiskEncryptionSet.ID)} 209 } 210 if s.OSDisk.ManagedDisk.SecurityProfile != nil { 211 if _, exists := s.SKU.GetCapability(resourceskus.ConfidentialComputingType); !exists { 212 return nil, azure.WithTerminalError(fmt.Errorf("VM size %s does not support confidential computing. Select a different VM size or remove the security profile of the OS disk", s.Size)) 213 } 214 215 storageProfile.OSDisk.ManagedDisk.SecurityProfile = &armcompute.VMDiskSecurityProfile{} 216 217 if s.OSDisk.ManagedDisk.SecurityProfile.DiskEncryptionSet != nil { 218 storageProfile.OSDisk.ManagedDisk.SecurityProfile.DiskEncryptionSet = &armcompute.DiskEncryptionSetParameters{ID: ptr.To(s.OSDisk.ManagedDisk.SecurityProfile.DiskEncryptionSet.ID)} 219 } 220 if s.OSDisk.ManagedDisk.SecurityProfile.SecurityEncryptionType != "" { 221 storageProfile.OSDisk.ManagedDisk.SecurityProfile.SecurityEncryptionType = ptr.To(armcompute.SecurityEncryptionTypes(string(s.OSDisk.ManagedDisk.SecurityProfile.SecurityEncryptionType))) 222 } 223 } 224 } 225 226 dataDisks := make([]*armcompute.DataDisk, len(s.DataDisks)) 227 for i, disk := range s.DataDisks { 228 dataDisks[i] = &armcompute.DataDisk{ 229 CreateOption: ptr.To(armcompute.DiskCreateOptionTypesEmpty), 230 DiskSizeGB: ptr.To[int32](disk.DiskSizeGB), 231 Lun: disk.Lun, 232 Name: ptr.To(azure.GenerateDataDiskName(s.Name, disk.NameSuffix)), 233 } 234 if disk.CachingType != "" { 235 dataDisks[i].Caching = ptr.To(armcompute.CachingTypes(disk.CachingType)) 236 } 237 238 if disk.ManagedDisk != nil { 239 dataDisks[i].ManagedDisk = &armcompute.ManagedDiskParameters{ 240 StorageAccountType: ptr.To(armcompute.StorageAccountTypes(disk.ManagedDisk.StorageAccountType)), 241 } 242 243 if disk.ManagedDisk.DiskEncryptionSet != nil { 244 dataDisks[i].ManagedDisk.DiskEncryptionSet = &armcompute.DiskEncryptionSetParameters{ID: ptr.To(disk.ManagedDisk.DiskEncryptionSet.ID)} 245 } 246 247 // check the support for ultra disks based on location and vm size 248 if disk.ManagedDisk.StorageAccountType == string(armcompute.StorageAccountTypesUltraSSDLRS) && !s.SKU.HasLocationCapability(resourceskus.UltraSSDAvailable, s.Location, s.Zone) { 249 return nil, azure.WithTerminalError(fmt.Errorf("VM size %s does not support ultra disks in location %s. Select a different VM size or disable ultra disks", s.Size, s.Location)) 250 } 251 } 252 } 253 storageProfile.DataDisks = dataDisks 254 255 imageRef, err := converters.ImageToSDK(s.Image) 256 if err != nil { 257 return nil, err 258 } 259 260 storageProfile.ImageReference = imageRef 261 262 return storageProfile, nil 263 } 264 265 func (s *VMSpec) generateOSProfile() (*armcompute.OSProfile, error) { 266 sshKey, err := base64.StdEncoding.DecodeString(s.SSHKeyData) 267 if err != nil { 268 return nil, errors.Wrap(err, "failed to decode ssh public key") 269 } 270 271 osProfile := &armcompute.OSProfile{ 272 ComputerName: ptr.To(s.Name), 273 AdminUsername: ptr.To(azure.DefaultUserName), 274 CustomData: ptr.To(s.BootstrapData), 275 AllowExtensionOperations: ptr.To(!s.DisableExtensionOperations), 276 } 277 278 switch s.OSDisk.OSType { 279 case string(armcompute.OperatingSystemTypesWindows): 280 // Cloudbase-init is used to generate a password. 281 // https://cloudbase-init.readthedocs.io/en/latest/plugins.html#setting-password-main 282 // 283 // We generate a random password here in case of failure 284 // but the password on the VM will NOT be the same as created here. 285 // Access is provided via SSH public key that is set during deployment 286 // Azure also provides a way to reset user passwords in the case of need. 287 osProfile.AdminPassword = ptr.To(generators.SudoRandomPassword(123)) 288 osProfile.WindowsConfiguration = &armcompute.WindowsConfiguration{ 289 EnableAutomaticUpdates: ptr.To(false), 290 } 291 default: 292 osProfile.LinuxConfiguration = &armcompute.LinuxConfiguration{ 293 DisablePasswordAuthentication: ptr.To(true), 294 SSH: &armcompute.SSHConfiguration{ 295 PublicKeys: []*armcompute.SSHPublicKey{ 296 { 297 Path: ptr.To(fmt.Sprintf("/home/%s/.ssh/authorized_keys", azure.DefaultUserName)), 298 KeyData: ptr.To(string(sshKey)), 299 }, 300 }, 301 }, 302 } 303 } 304 305 return osProfile, nil 306 } 307 308 func (s *VMSpec) generateSecurityProfile(storageProfile *armcompute.StorageProfile) (*armcompute.SecurityProfile, error) { 309 if s.SecurityProfile == nil { 310 return nil, nil 311 } 312 313 securityProfile := &armcompute.SecurityProfile{} 314 315 if storageProfile.OSDisk.ManagedDisk != nil && 316 storageProfile.OSDisk.ManagedDisk.SecurityProfile != nil && 317 ptr.Deref(storageProfile.OSDisk.ManagedDisk.SecurityProfile.SecurityEncryptionType, "") != "" { 318 if s.SecurityProfile.EncryptionAtHost != nil && *s.SecurityProfile.EncryptionAtHost && 319 *storageProfile.OSDisk.ManagedDisk.SecurityProfile.SecurityEncryptionType == armcompute.SecurityEncryptionTypesDiskWithVMGuestState { 320 return nil, azure.WithTerminalError(errors.Errorf("encryption at host is not supported when securityEncryptionType is set to %s", armcompute.SecurityEncryptionTypesDiskWithVMGuestState)) 321 } 322 323 if s.SecurityProfile.SecurityType != infrav1.SecurityTypesConfidentialVM { 324 return nil, azure.WithTerminalError(errors.Errorf("securityType should be set to %s when securityEncryptionType is set", infrav1.SecurityTypesConfidentialVM)) 325 } 326 327 if s.SecurityProfile.UefiSettings == nil { 328 return nil, azure.WithTerminalError(errors.New("vTpmEnabled should be true when securityEncryptionType is set")) 329 } 330 331 if ptr.Deref(storageProfile.OSDisk.ManagedDisk.SecurityProfile.SecurityEncryptionType, "") == armcompute.SecurityEncryptionTypesDiskWithVMGuestState && 332 !*s.SecurityProfile.UefiSettings.SecureBootEnabled { 333 return nil, azure.WithTerminalError(errors.Errorf("secureBootEnabled should be true when securityEncryptionType is set to %s", armcompute.SecurityEncryptionTypesDiskWithVMGuestState)) 334 } 335 336 if s.SecurityProfile.UefiSettings.VTpmEnabled != nil && !*s.SecurityProfile.UefiSettings.VTpmEnabled { 337 return nil, azure.WithTerminalError(errors.New("vTpmEnabled should be true when securityEncryptionType is set")) 338 } 339 340 securityProfile.SecurityType = ptr.To(armcompute.SecurityTypesConfidentialVM) 341 342 securityProfile.UefiSettings = &armcompute.UefiSettings{ 343 SecureBootEnabled: s.SecurityProfile.UefiSettings.SecureBootEnabled, 344 VTpmEnabled: s.SecurityProfile.UefiSettings.VTpmEnabled, 345 } 346 347 return securityProfile, nil 348 } 349 350 if s.SecurityProfile.EncryptionAtHost != nil { 351 if !s.SKU.HasCapability(resourceskus.EncryptionAtHost) && *s.SecurityProfile.EncryptionAtHost { 352 return nil, azure.WithTerminalError(errors.Errorf("encryption at host is not supported for VM type %s", s.Size)) 353 } 354 355 securityProfile.EncryptionAtHost = s.SecurityProfile.EncryptionAtHost 356 } 357 358 hasTrustedLaunchDisabled := s.SKU.HasCapability(resourceskus.TrustedLaunchDisabled) 359 360 if s.SecurityProfile.UefiSettings != nil { 361 securityProfile.UefiSettings = &armcompute.UefiSettings{} 362 363 if s.SecurityProfile.UefiSettings.SecureBootEnabled != nil && *s.SecurityProfile.UefiSettings.SecureBootEnabled { 364 if hasTrustedLaunchDisabled { 365 return nil, azure.WithTerminalError(errors.Errorf("secure boot is not supported for VM type %s", s.Size)) 366 } 367 368 if s.SecurityProfile.SecurityType != infrav1.SecurityTypesTrustedLaunch { 369 return nil, azure.WithTerminalError(errors.Errorf("securityType should be set to %s when secureBootEnabled is true", infrav1.SecurityTypesTrustedLaunch)) 370 } 371 372 securityProfile.SecurityType = ptr.To(armcompute.SecurityTypesTrustedLaunch) 373 securityProfile.UefiSettings.SecureBootEnabled = ptr.To(true) 374 } 375 376 if s.SecurityProfile.UefiSettings.VTpmEnabled != nil && *s.SecurityProfile.UefiSettings.VTpmEnabled { 377 if hasTrustedLaunchDisabled { 378 return nil, azure.WithTerminalError(errors.Errorf("vTPM is not supported for VM type %s", s.Size)) 379 } 380 381 if s.SecurityProfile.SecurityType != infrav1.SecurityTypesTrustedLaunch { 382 return nil, azure.WithTerminalError(errors.Errorf("securityType should be set to %s when vTpmEnabled is true", infrav1.SecurityTypesTrustedLaunch)) 383 } 384 385 securityProfile.SecurityType = ptr.To(armcompute.SecurityTypesTrustedLaunch) 386 securityProfile.UefiSettings.VTpmEnabled = ptr.To(true) 387 } 388 } 389 390 return securityProfile, nil 391 } 392 393 func (s *VMSpec) generateNICRefs() []*armcompute.NetworkInterfaceReference { 394 nicRefs := make([]*armcompute.NetworkInterfaceReference, len(s.NICIDs)) 395 for i, id := range s.NICIDs { 396 primary := i == 0 397 nicRefs[i] = &armcompute.NetworkInterfaceReference{ 398 ID: ptr.To(id), 399 Properties: &armcompute.NetworkInterfaceReferenceProperties{ 400 Primary: ptr.To(primary), 401 }, 402 } 403 } 404 return nicRefs 405 } 406 407 func (s *VMSpec) generateAdditionalCapabilities() *armcompute.AdditionalCapabilities { 408 var capabilities *armcompute.AdditionalCapabilities 409 410 // Provisionally detect whether there is any Data Disk defined which uses UltraSSDs. 411 // If that's the case, enable the UltraSSD capability. 412 for _, dataDisk := range s.DataDisks { 413 if dataDisk.ManagedDisk != nil && dataDisk.ManagedDisk.StorageAccountType == string(armcompute.StorageAccountTypesUltraSSDLRS) { 414 capabilities = &armcompute.AdditionalCapabilities{ 415 UltraSSDEnabled: ptr.To(true), 416 } 417 break 418 } 419 } 420 421 // Set Additional Capabilities if any is present on the spec. 422 if s.AdditionalCapabilities != nil { 423 if capabilities == nil { 424 capabilities = &armcompute.AdditionalCapabilities{} 425 } 426 // Set UltraSSDEnabled if a specific value is set on the spec for it. 427 if s.AdditionalCapabilities.UltraSSDEnabled != nil { 428 capabilities.UltraSSDEnabled = s.AdditionalCapabilities.UltraSSDEnabled 429 } 430 } 431 432 return capabilities 433 } 434 435 func (s *VMSpec) getAvailabilitySet() *armcompute.SubResource { 436 var as *armcompute.SubResource 437 if s.AvailabilitySetID != "" { 438 as = &armcompute.SubResource{ID: &s.AvailabilitySetID} 439 } 440 return as 441 } 442 443 func (s *VMSpec) getZones() []*string { 444 var zones []*string 445 if s.Zone != "" { 446 zones = []*string{ptr.To(s.Zone)} 447 } 448 return zones 449 } 450 451 func (s *VMSpec) getCapacityReservationProfile() *armcompute.CapacityReservationProfile { 452 var crf *armcompute.CapacityReservationProfile 453 if s.CapacityReservationGroupID != "" { 454 crf = &armcompute.CapacityReservationProfile{ 455 CapacityReservationGroup: &armcompute.SubResource{ID: &s.CapacityReservationGroupID}, 456 } 457 } 458 return crf 459 }