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