github.com/openshift/installer@v1.4.17/pkg/types/nutanix/machinepool.go (about)

     1  package nutanix
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"time"
     7  
     8  	nutanixclientv3 "github.com/nutanix-cloud-native/prism-go-client/v3"
     9  	"k8s.io/apimachinery/pkg/api/resource"
    10  	"k8s.io/apimachinery/pkg/util/validation/field"
    11  
    12  	machinev1 "github.com/openshift/api/machine/v1"
    13  )
    14  
    15  // MachinePool stores the configuration for a machine pool installed
    16  // on Nutanix.
    17  type MachinePool struct {
    18  	// NumCPUs is the total number of virtual processor cores to assign a vm.
    19  	//
    20  	// +optional
    21  	NumCPUs int64 `json:"cpus,omitempty"`
    22  
    23  	// NumCoresPerSocket is the number of cores per socket in a vm. The number
    24  	// of vCPUs on the vm will be NumCPUs times NumCoresPerSocket.
    25  	// For example: 4 CPUs and 4 Cores per socket will result in 16 VPUs.
    26  	// The AHV scheduler treats socket and core allocation exactly the same
    27  	// so there is no benefit to configuring cores over CPUs.
    28  	//
    29  	// +optional
    30  	NumCoresPerSocket int64 `json:"coresPerSocket,omitempty"`
    31  
    32  	// Memory is the size of a VM's memory in MiB.
    33  	//
    34  	// +optional
    35  	MemoryMiB int64 `json:"memoryMiB,omitempty"`
    36  
    37  	// OSDisk defines the storage for instance.
    38  	//
    39  	// +optional
    40  	OSDisk `json:"osDisk,omitempty"`
    41  
    42  	// BootType indicates the boot type (Legacy, UEFI or SecureBoot) the Machine's VM uses to boot.
    43  	// If this field is empty or omitted, the VM will use the default boot type "Legacy" to boot.
    44  	// "SecureBoot" depends on "UEFI" boot, i.e., enabling "SecureBoot" means that "UEFI" boot is also enabled.
    45  	// +kubebuilder:validation:Enum="";Legacy;UEFI;SecureBoot
    46  	// +optional
    47  	BootType machinev1.NutanixBootType `json:"bootType,omitempty"`
    48  
    49  	// Project optionally identifies a Prism project for the Machine's VM to associate with.
    50  	// +optional
    51  	Project *machinev1.NutanixResourceIdentifier `json:"project,omitempty"`
    52  
    53  	// Categories optionally adds one or more prism categories (each with key and value) for
    54  	// the Machine's VM to associate with. All the category key and value pairs specified must
    55  	// already exist in the prism central.
    56  	// +listType=map
    57  	// +listMapKey=key
    58  	// +optional
    59  	Categories []machinev1.NutanixCategory `json:"categories,omitempty"`
    60  
    61  	// GPUs is a list of GPU devices to attach to the machine's VM.
    62  	// +listType=set
    63  	// +optional
    64  	GPUs []machinev1.NutanixGPU `json:"gpus"`
    65  
    66  	// DataDisks holds information of the data disks to attach to the Machine's VM
    67  	// +listType=set
    68  	// +optional
    69  	DataDisks []DataDisk `json:"dataDisks"`
    70  
    71  	// FailureDomains optionally configures a list of failure domain names
    72  	// that will be applied to the MachinePool
    73  	// +listType=set
    74  	// +optional
    75  	FailureDomains []string `json:"failureDomains,omitempty"`
    76  }
    77  
    78  // OSDisk defines the system disk for a Machine VM.
    79  type OSDisk struct {
    80  	// DiskSizeGiB defines the size of disk in GiB.
    81  	//
    82  	// +optional
    83  	DiskSizeGiB int64 `json:"diskSizeGiB,omitempty"`
    84  }
    85  
    86  // StorageResourceReference holds reference information of a storage resource (storage container, data source image, etc.)
    87  type StorageResourceReference struct {
    88  	// ReferenceName is the identifier of the storage resource configured in the FailureDomain.
    89  	// +optional
    90  	ReferenceName string `json:"referenceName,omitempty"`
    91  
    92  	// UUID is the UUID of the storage container resource in the Prism Element.
    93  	// +kubebuilder:validation:Required
    94  	UUID string `json:"uuid"`
    95  
    96  	// Name is the name of the storage container resource in the Prism Element.
    97  	// +optional
    98  	Name string `json:"name,omitempty"`
    99  }
   100  
   101  // StorageConfig specifies the storage configuration parameters for VM disks.
   102  type StorageConfig struct {
   103  	// diskMode specifies the disk mode.
   104  	// The valid values are Standard and Flash, and the default is Standard.
   105  	// +kubebuilder:default=Standard
   106  	// +kubebuilder:validation:Enum=Standard;Flash
   107  	DiskMode machinev1.NutanixDiskMode `json:"diskMode"`
   108  
   109  	// storageContainer refers to the storage_container used by the VM disk.
   110  	// +optional
   111  	StorageContainer *StorageResourceReference `json:"storageContainer,omitempty"`
   112  }
   113  
   114  // DataDisk defines a data disk for a Machine VM.
   115  type DataDisk struct {
   116  	// diskSize is size (in Quantity format) of the disk to attach to the VM.
   117  	// See https://pkg.go.dev/k8s.io/apimachinery/pkg/api/resource#Format for the Quantity format and example documentation.
   118  	// The minimum diskSize is 1GB.
   119  	// +kubebuilder:validation:Required
   120  	DiskSize resource.Quantity `json:"diskSize"`
   121  
   122  	// deviceProperties are the properties of the disk device.
   123  	// +optional
   124  	DeviceProperties *machinev1.NutanixVMDiskDeviceProperties `json:"deviceProperties,omitempty"`
   125  
   126  	// storageConfig are the storage configuration parameters of the VM disks.
   127  	// +optional
   128  	StorageConfig *StorageConfig `json:"storageConfig,omitempty"`
   129  
   130  	// dataSource refers to a data source image for the VM disk.
   131  	// +optional
   132  	DataSourceImage *StorageResourceReference `json:"dataSourceImage,omitempty"`
   133  }
   134  
   135  // Set sets the values from `required` to `p`.
   136  func (p *MachinePool) Set(required *MachinePool) {
   137  	if required == nil || p == nil {
   138  		return
   139  	}
   140  
   141  	if required.NumCPUs != 0 {
   142  		p.NumCPUs = required.NumCPUs
   143  	}
   144  
   145  	if required.NumCoresPerSocket != 0 {
   146  		p.NumCoresPerSocket = required.NumCoresPerSocket
   147  	}
   148  
   149  	if required.MemoryMiB != 0 {
   150  		p.MemoryMiB = required.MemoryMiB
   151  	}
   152  
   153  	if required.OSDisk.DiskSizeGiB != 0 {
   154  		p.OSDisk.DiskSizeGiB = required.OSDisk.DiskSizeGiB
   155  	}
   156  
   157  	if len(required.BootType) != 0 {
   158  		p.BootType = required.BootType
   159  	}
   160  
   161  	if required.Project != nil {
   162  		p.Project = required.Project
   163  	}
   164  
   165  	if len(required.Categories) > 0 {
   166  		p.Categories = required.Categories
   167  	}
   168  
   169  	if len(required.FailureDomains) > 0 {
   170  		p.FailureDomains = required.FailureDomains
   171  	}
   172  
   173  	if len(required.GPUs) > 0 {
   174  		p.GPUs = required.GPUs
   175  	}
   176  
   177  	if len(required.DataDisks) > 0 {
   178  		p.DataDisks = required.DataDisks
   179  	}
   180  }
   181  
   182  // ValidateConfig validates the MachinePool configuration.
   183  func (p *MachinePool) ValidateConfig(platform *Platform, role string) error {
   184  	nc, err := CreateNutanixClientFromPlatform(platform)
   185  	if err != nil {
   186  		return fmt.Errorf("fail to create nutanix client. %w", err)
   187  	}
   188  
   189  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   190  	defer cancel()
   191  
   192  	errList := field.ErrorList{}
   193  	fldPath := field.NewPath("platform", "nutanix")
   194  	var errMsg string
   195  
   196  	// validate BootType
   197  	if p.BootType != "" && p.BootType != machinev1.NutanixLegacyBoot &&
   198  		p.BootType != machinev1.NutanixUEFIBoot && p.BootType != machinev1.NutanixSecureBoot {
   199  		errMsg = fmt.Sprintf("valid bootType: \"\", %q, %q, %q.", machinev1.NutanixLegacyBoot, machinev1.NutanixUEFIBoot, machinev1.NutanixSecureBoot)
   200  		errList = append(errList, field.Invalid(fldPath.Child("bootType"), p.BootType, errMsg))
   201  	}
   202  
   203  	// validate project if configured
   204  	if p.Project != nil {
   205  		fldErr := p.validateProjectConfig(ctx, nc, fldPath)
   206  		if fldErr != nil {
   207  			errList = append(errList, fldErr)
   208  		}
   209  	}
   210  
   211  	// validate categories if configured
   212  	if len(p.Categories) > 0 {
   213  		for _, category := range p.Categories {
   214  			if _, err = nc.V3.GetCategoryValue(ctx, category.Key, category.Value); err != nil {
   215  				errMsg = fmt.Sprintf("Failed to find the category with key %q and value %q. error: %v", category.Key, category.Value, err)
   216  				errList = append(errList, field.Invalid(fldPath.Child("categories"), category, errMsg))
   217  			}
   218  		}
   219  	}
   220  
   221  	// validate FailureDomains if configured
   222  	for _, fdName := range p.FailureDomains {
   223  		_, err := platform.GetFailureDomainByName(fdName)
   224  		if err != nil {
   225  			errList = append(errList, field.Invalid(fldPath.Child("failureDomains"), fdName, fmt.Sprintf("The failure domain is not defined: %v", err)))
   226  		}
   227  	}
   228  
   229  	// validate GPUs if configured, currently only "worker" machines allow GPUs.
   230  	if len(p.GPUs) > 0 {
   231  		if role == "master" {
   232  			errList = append(errList, field.Forbidden(fldPath.Child("gpus"), "'gpus' are not supported for 'master' nodes, you can only configure it for 'worker' nodes."))
   233  		} else {
   234  			fldErrs := p.validateGPUsConfig(ctx, nc, platform, fldPath)
   235  			for _, fldErr := range fldErrs {
   236  				errList = append(errList, fldErr)
   237  			}
   238  		}
   239  	}
   240  
   241  	// validate DataDisks if configured, currently only "worker" machines allow DataDisks.
   242  	if len(p.DataDisks) > 0 {
   243  		if role == "master" {
   244  			errList = append(errList, field.Forbidden(fldPath.Child("gpus"), "'dataDisks' are not supported for 'master' nodes, you can only configure it for 'worker' nodes."))
   245  		} else {
   246  			fldErrs := p.validateDataDisksConfig(ctx, nc, platform, fldPath)
   247  			for _, fldErr := range fldErrs {
   248  				errList = append(errList, fldErr)
   249  			}
   250  		}
   251  	}
   252  
   253  	if len(errList) > 0 {
   254  		return fmt.Errorf(errList.ToAggregate().Error())
   255  	}
   256  	return nil
   257  }
   258  
   259  // validateProjectConfig validates the Project configuration in the machinePool.
   260  func (p *MachinePool) validateProjectConfig(ctx context.Context, nc *nutanixclientv3.Client, fldPath *field.Path) *field.Error {
   261  	if p.Project != nil {
   262  		switch p.Project.Type {
   263  		case machinev1.NutanixIdentifierName:
   264  			if p.Project.Name == nil || *p.Project.Name == "" {
   265  				return field.Required(fldPath.Child("project", "name"), "missing projct name")
   266  			}
   267  
   268  			projectName := *p.Project.Name
   269  			filter := fmt.Sprintf("name==%s", projectName)
   270  			res, err := nc.V3.ListProject(ctx, &nutanixclientv3.DSMetadata{
   271  				Filter: &filter,
   272  			})
   273  			switch {
   274  			case err != nil:
   275  				return field.Invalid(fldPath.Child("project", "name"), projectName,
   276  					fmt.Sprintf("failed to find project with name %q. error: %v", projectName, err))
   277  			case len(res.Entities) == 0:
   278  				return field.Invalid(fldPath.Child("project", "name"), projectName,
   279  					fmt.Sprintf("unable to find project with name %q.", projectName))
   280  			case len(res.Entities) > 1:
   281  				return field.Invalid(fldPath.Child("project", "name"), projectName,
   282  					fmt.Sprintf("found more than one (%v) projects with name %q.", len(res.Entities), projectName))
   283  			default:
   284  				p.Project.Type = machinev1.NutanixIdentifierUUID
   285  				p.Project.UUID = res.Entities[0].Metadata.UUID
   286  			}
   287  		case machinev1.NutanixIdentifierUUID:
   288  			if p.Project.UUID == nil || *p.Project.UUID == "" {
   289  				return field.Required(fldPath.Child("project", "uuid"), "missing projct uuid")
   290  			} else {
   291  				if _, err := nc.V3.GetProject(ctx, *p.Project.UUID); err != nil {
   292  					return field.Invalid(fldPath.Child("project", "uuid"), *p.Project.UUID,
   293  						fmt.Sprintf("failed to get the project with uuid %s. error: %v", *p.Project.UUID, err))
   294  				}
   295  			}
   296  		default:
   297  			return field.Invalid(fldPath.Child("project", "type"), p.Project.Type,
   298  				fmt.Sprintf("invalid project identifier type, valid types are: %q, %q.", machinev1.NutanixIdentifierName, machinev1.NutanixIdentifierUUID))
   299  		}
   300  	}
   301  
   302  	return nil
   303  }
   304  
   305  // validateGPUsConfig validates the GPUs configuration in the machinePool.
   306  func (p *MachinePool) validateGPUsConfig(ctx context.Context, nc *nutanixclientv3.Client, platform *Platform, fldPath *field.Path) (fldErrs []*field.Error) {
   307  	if len(p.GPUs) == 0 {
   308  		return fldErrs
   309  	}
   310  
   311  	peUUIDs := []string{}
   312  	for _, fdName := range p.FailureDomains {
   313  		if fd, err := platform.GetFailureDomainByName(fdName); err == nil {
   314  			peUUIDs = append(peUUIDs, fd.PrismElement.UUID)
   315  		}
   316  	}
   317  	if len(peUUIDs) == 0 {
   318  		peUUIDs = append(peUUIDs, platform.PrismElements[0].UUID)
   319  	}
   320  
   321  	for _, peUUID := range peUUIDs {
   322  		peGPUs, err := GetGPUsForPE(ctx, nc, peUUID)
   323  		if err != nil || len(peGPUs) == 0 {
   324  			err = fmt.Errorf("no available GPUs found in Prism Element cluster (uuid: %s): %w", peUUID, err)
   325  			fldErrs = append(fldErrs, field.InternalError(fldPath.Child("gpus"), err))
   326  			return fldErrs
   327  		}
   328  
   329  		for _, gpu := range p.GPUs {
   330  			switch gpu.Type {
   331  			case machinev1.NutanixGPUIdentifierDeviceID:
   332  				if gpu.DeviceID == nil {
   333  					fldErrs = append(fldErrs, field.Required(fldPath.Child("gpus", "deviceID"), "missing gpu deviceID"))
   334  				} else {
   335  					_, err := GetGPUFromList(ctx, nc, gpu, peGPUs)
   336  					if err != nil {
   337  						fldErrs = append(fldErrs, field.Invalid(fldPath.Child("gpus", "deviceID"), *gpu.DeviceID, err.Error()))
   338  					}
   339  				}
   340  			case machinev1.NutanixGPUIdentifierName:
   341  				if gpu.Name == nil || *gpu.Name == "" {
   342  					fldErrs = append(fldErrs, field.Required(fldPath.Child("gpus", "name"), "missing gpu name"))
   343  				} else {
   344  					_, err := GetGPUFromList(ctx, nc, gpu, peGPUs)
   345  					if err != nil {
   346  						fldErrs = append(fldErrs, field.Invalid(fldPath.Child("gpus", "name"), gpu.Name, err.Error()))
   347  					}
   348  				}
   349  			default:
   350  				errMsg := fmt.Sprintf("invalid gpu identifier type, the valid values: %q, %q.", machinev1.NutanixGPUIdentifierDeviceID, machinev1.NutanixGPUIdentifierName)
   351  				fldErrs = append(fldErrs, field.Invalid(fldPath.Child("gpus", "type"), gpu.Type, errMsg))
   352  			}
   353  		}
   354  	}
   355  
   356  	return fldErrs
   357  }
   358  
   359  // validateDataDisksConfig validates the DataDisks configuration in the machinePool.
   360  func (p *MachinePool) validateDataDisksConfig(ctx context.Context, nc *nutanixclientv3.Client, platform *Platform, fldPath *field.Path) (fldErrs []*field.Error) {
   361  	var err error
   362  	var errMsg string
   363  
   364  	for _, disk := range p.DataDisks {
   365  		// the minimum diskSize is 1Gi bytes
   366  		diskSizeBytes := disk.DiskSize.Value()
   367  		if diskSizeBytes < 1024*1024*1024 {
   368  			fldErrs = append(fldErrs, field.Invalid(fldPath.Child("dataDisks", "diskSize"), fmt.Sprintf("%v bytes", diskSizeBytes), "The minimum diskSize is 1Gi bytes."))
   369  		}
   370  
   371  		if disk.DeviceProperties != nil {
   372  			switch disk.DeviceProperties.DeviceType {
   373  			case machinev1.NutanixDiskDeviceTypeDisk:
   374  				switch disk.DeviceProperties.AdapterType {
   375  				case machinev1.NutanixDiskAdapterTypeSCSI, machinev1.NutanixDiskAdapterTypeIDE, machinev1.NutanixDiskAdapterTypePCI, machinev1.NutanixDiskAdapterTypeSATA, machinev1.NutanixDiskAdapterTypeSPAPR:
   376  					// valid configuration
   377  				default:
   378  					// invalid configuration
   379  					fldErrs = append(fldErrs, field.Invalid(fldPath.Child("deviceProperties", "adapterType"), disk.DeviceProperties.AdapterType,
   380  						fmt.Sprintf("invalid adapter type for the %q device type, the valid values: %q, %q, %q, %q, %q.",
   381  							machinev1.NutanixDiskDeviceTypeDisk, machinev1.NutanixDiskAdapterTypeSCSI, machinev1.NutanixDiskAdapterTypeIDE,
   382  							machinev1.NutanixDiskAdapterTypePCI, machinev1.NutanixDiskAdapterTypeSATA, machinev1.NutanixDiskAdapterTypeSPAPR)))
   383  				}
   384  			case machinev1.NutanixDiskDeviceTypeCDROM:
   385  				switch disk.DeviceProperties.AdapterType {
   386  				case machinev1.NutanixDiskAdapterTypeIDE, machinev1.NutanixDiskAdapterTypeSATA:
   387  					// valid configuration
   388  				default:
   389  					// invalid configuration
   390  					fldErrs = append(fldErrs, field.Invalid(fldPath.Child("deviceProperties", "adapterType"), disk.DeviceProperties.AdapterType,
   391  						fmt.Sprintf("invalid adapter type for the %q device type, the valid values: %q, %q.",
   392  							machinev1.NutanixDiskDeviceTypeCDROM, machinev1.NutanixDiskAdapterTypeIDE, machinev1.NutanixDiskAdapterTypeSATA)))
   393  				}
   394  			default:
   395  				fldErrs = append(fldErrs, field.Invalid(fldPath.Child("deviceProperties", "deviceType"), disk.DeviceProperties.DeviceType,
   396  					fmt.Sprintf("invalid device type, the valid types are: %q, %q.", machinev1.NutanixDiskDeviceTypeDisk, machinev1.NutanixDiskDeviceTypeCDROM)))
   397  			}
   398  
   399  			if disk.DeviceProperties.DeviceIndex < 0 {
   400  				fldErrs = append(fldErrs, field.Invalid(fldPath.Child("deviceProperties", "deviceIndex"),
   401  					disk.DeviceProperties.DeviceIndex, "invalid device index, the valid values are non-negative integers."))
   402  			}
   403  		}
   404  
   405  		if disk.StorageConfig != nil {
   406  			if disk.StorageConfig.DiskMode != machinev1.NutanixDiskModeStandard && disk.StorageConfig.DiskMode != machinev1.NutanixDiskModeFlash {
   407  				fldErrs = append(fldErrs, field.Invalid(fldPath.Child("storageConfig", "diskMode"), disk.StorageConfig.DiskMode,
   408  					fmt.Sprintf("invalid disk mode, the valid values: %q, %q.", machinev1.NutanixDiskModeStandard, machinev1.NutanixDiskModeFlash)))
   409  			}
   410  
   411  			storageContainerRef := disk.StorageConfig.StorageContainer
   412  			if storageContainerRef != nil {
   413  				if storageContainerRef.ReferenceName != "" {
   414  					for _, fdName := range p.FailureDomains {
   415  						_, err := platform.GetStorageContainerFromFailureDomain(fdName, storageContainerRef.ReferenceName)
   416  						if err != nil {
   417  							fldErrs = append(fldErrs, field.Invalid(fldPath.Child("storageConfig", "storageContainer", "referenceName"), storageContainerRef.ReferenceName,
   418  								fmt.Sprintf("not found storageContainer with the referenceName in the failureDomain %q configuration.", fdName)))
   419  						}
   420  					}
   421  				} else if storageContainerRef.UUID == "" {
   422  					fldErrs = append(fldErrs, field.Required(fldPath.Child("storageConfig", "storageContainer", "uuid"), "missing storageContainer uuid"))
   423  				}
   424  			}
   425  		}
   426  
   427  		if disk.DataSourceImage != nil {
   428  			dsImgRef := disk.DataSourceImage
   429  			if dsImgRef.ReferenceName != "" {
   430  				for _, fdName := range p.FailureDomains {
   431  					_, err = platform.GetDataSourceImageFromFailureDomain(fdName, disk.DataSourceImage.ReferenceName)
   432  					if err != nil {
   433  						fldErrs = append(fldErrs, field.Invalid(fldPath.Child("storageConfig", "dataSourceImage", "referenceName"), disk.DataSourceImage.ReferenceName,
   434  							fmt.Sprintf("not found datasource image with the referenceName in the failureDomain %q configuration.", fdName)))
   435  					}
   436  				}
   437  			} else {
   438  				switch {
   439  				case dsImgRef.UUID != "":
   440  					if _, err = nc.V3.GetImage(ctx, dsImgRef.UUID); err != nil {
   441  						errMsg = fmt.Sprintf("failed to find the dataSource image with uuid %s: %v", dsImgRef.UUID, err)
   442  						fldErrs = append(fldErrs, field.Invalid(fldPath.Child("dataDisks", "dataSourceImage", "uuid"), dsImgRef.UUID, errMsg))
   443  					}
   444  				case dsImgRef.Name != "":
   445  					if dsImgUUID, err := FindImageUUIDByName(ctx, nc, dsImgRef.Name); err != nil {
   446  						errMsg = fmt.Sprintf("failed to find the dataSource image with name %q: %v", dsImgRef.UUID, err)
   447  						fldErrs = append(fldErrs, field.Invalid(fldPath.Child("dataDisks", "dataSourceImage", "name"), dsImgRef.Name, errMsg))
   448  					} else {
   449  						dsImgRef.UUID = *dsImgUUID
   450  					}
   451  				default:
   452  					fldErrs = append(fldErrs, field.Required(fldPath.Child("dataDisks", "dataSourceImage"), "both the dataSourceImage's uuid and name are empty, you need to configure one."))
   453  				}
   454  			}
   455  		}
   456  	}
   457  
   458  	return fldErrs
   459  }