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  }