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 }