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  }