diff --git a/data/data/install.openshift.io_installconfigs.yaml b/data/data/install.openshift.io_installconfigs.yaml index 7d6cd00e6c0..8b0705af6cb 100644 --- a/data/data/install.openshift.io_installconfigs.yaml +++ b/data/data/install.openshift.io_installconfigs.yaml @@ -493,6 +493,47 @@ spec: description: Nutanix is the configuration used when installing on Nutanix. properties: + bootType: + description: BootType indicates the boot type (Legacy, UEFI + or SecureBoot) the Machine's VM uses to boot. If this + field is empty or omitted, the VM will use the default + boot type "Legacy" to boot. "SecureBoot" depends on "UEFI" + boot, i.e., enabling "SecureBoot" means that "UEFI" boot + is also enabled. + enum: + - "" + - Legacy + - UEFI + - SecureBoot + type: string + categories: + description: Categories optionally adds one or more prism + categories (each with key and value) for the Machine's + VM to associate with. All the category key and value pairs + specified must already exist in the prism central. + items: + description: NutanixCategory identifies a pair of prism + category key and value + properties: + key: + description: key is the prism category key name + maxLength: 64 + minLength: 1 + type: string + value: + description: value is the prism category value associated + with the key + maxLength: 64 + minLength: 1 + type: string + required: + - key + - value + type: object + type: array + x-kubernetes-list-map-keys: + - key + x-kubernetes-list-type: map coresPerSocket: description: 'NumCoresPerSocket is the number of cores per socket in a vm. The number of vCPUs on the vm will be @@ -520,6 +561,27 @@ spec: format: int64 type: integer type: object + project: + description: Project optionally identifies a Prism project + for the Machine's VM to associate with. + properties: + name: + description: name is the resource name in the PC + type: string + type: + description: Type is the identifier type to use for + this resource. + enum: + - uuid + - name + type: string + uuid: + description: uuid is the UUID of the resource in the + PC. + type: string + required: + - type + type: object type: object openstack: description: OpenStack is the configuration used when installing @@ -1171,6 +1233,46 @@ spec: description: Nutanix is the configuration used when installing on Nutanix. properties: + bootType: + description: BootType indicates the boot type (Legacy, UEFI + or SecureBoot) the Machine's VM uses to boot. If this field + is empty or omitted, the VM will use the default boot type + "Legacy" to boot. "SecureBoot" depends on "UEFI" boot, i.e., + enabling "SecureBoot" means that "UEFI" boot is also enabled. + enum: + - "" + - Legacy + - UEFI + - SecureBoot + type: string + categories: + description: Categories optionally adds one or more prism + categories (each with key and value) for the Machine's VM + to associate with. All the category key and value pairs + specified must already exist in the prism central. + items: + description: NutanixCategory identifies a pair of prism + category key and value + properties: + key: + description: key is the prism category key name + maxLength: 64 + minLength: 1 + type: string + value: + description: value is the prism category value associated + with the key + maxLength: 64 + minLength: 1 + type: string + required: + - key + - value + type: object + type: array + x-kubernetes-list-map-keys: + - key + x-kubernetes-list-type: map coresPerSocket: description: 'NumCoresPerSocket is the number of cores per socket in a vm. The number of vCPUs on the vm will be NumCPUs @@ -1197,6 +1299,26 @@ spec: format: int64 type: integer type: object + project: + description: Project optionally identifies a Prism project + for the Machine's VM to associate with. + properties: + name: + description: name is the resource name in the PC + type: string + type: + description: Type is the identifier type to use for this + resource. + enum: + - uuid + - name + type: string + uuid: + description: uuid is the UUID of the resource in the PC. + type: string + required: + - type + type: object type: object openstack: description: OpenStack is the configuration used when installing @@ -2586,6 +2708,46 @@ spec: used when installing on Nutanix for machine pools which do not define their own platform configuration. properties: + bootType: + description: BootType indicates the boot type (Legacy, UEFI + or SecureBoot) the Machine's VM uses to boot. If this field + is empty or omitted, the VM will use the default boot type + "Legacy" to boot. "SecureBoot" depends on "UEFI" boot, i.e., + enabling "SecureBoot" means that "UEFI" boot is also enabled. + enum: + - "" + - Legacy + - UEFI + - SecureBoot + type: string + categories: + description: Categories optionally adds one or more prism + categories (each with key and value) for the Machine's VM + to associate with. All the category key and value pairs + specified must already exist in the prism central. + items: + description: NutanixCategory identifies a pair of prism + category key and value + properties: + key: + description: key is the prism category key name + maxLength: 64 + minLength: 1 + type: string + value: + description: value is the prism category value associated + with the key + maxLength: 64 + minLength: 1 + type: string + required: + - key + - value + type: object + type: array + x-kubernetes-list-map-keys: + - key + x-kubernetes-list-type: map coresPerSocket: description: 'NumCoresPerSocket is the number of cores per socket in a vm. The number of vCPUs on the vm will be NumCPUs @@ -2612,6 +2774,26 @@ spec: format: int64 type: integer type: object + project: + description: Project optionally identifies a Prism project + for the Machine's VM to associate with. + properties: + name: + description: name is the resource name in the PC + type: string + type: + description: Type is the identifier type to use for this + resource. + enum: + - uuid + - name + type: string + uuid: + description: uuid is the UUID of the resource in the PC. + type: string + required: + - type + type: object type: object ingressVIP: description: 'DeprecatedIngressVIP is the virtual IP address for diff --git a/data/data/nutanix/cluster/main.tf b/data/data/nutanix/cluster/main.tf index f09dcaae2a8..16ac4f2d7c5 100644 --- a/data/data/nutanix/cluster/main.tf +++ b/data/data/nutanix/cluster/main.tf @@ -71,6 +71,16 @@ resource "nutanix_virtual_machine" "vm_master" { value = nutanix_category_value.ocp_category_value_owned.value } + dynamic "categories" { + for_each = (var.nutanix_control_plane_categories == null) ? {} : var.nutanix_control_plane_categories + content { + name = categories.key + value = categories.value + } + } + + project_reference = (length(var.nutanix_control_plane_project_uuid) != 0) ? { kind = "project", uuid = var.nutanix_control_plane_project_uuid } : null + guest_customization_cloud_init_user_data = base64encode(var.ignition_master) nic_list { subnet_uuid = var.nutanix_subnet_uuid diff --git a/data/data/nutanix/variables-nutanix.tf b/data/data/nutanix/variables-nutanix.tf index 4cc6c3a0ae0..b4d5673dc77 100644 --- a/data/data/nutanix/variables-nutanix.tf +++ b/data/data/nutanix/variables-nutanix.tf @@ -71,3 +71,21 @@ variable "nutanix_control_plane_num_cpus" { variable "nutanix_control_plane_cores_per_socket" { type = number } + +variable "nutanix_control_plane_project_uuid" { + type = string + default = null + description = "(optional) An existing prism-central project to be applied to control-plane vms." +} + +variable "nutanix_control_plane_categories" { + type = map(string) + + description = < 0 { + providerCfg.Categories = mpool.Categories + } + + return providerCfg, nil } // ConfigMasters sets the PublicIP flag and assigns a set of load balancers to the given machines diff --git a/pkg/asset/machines/worker.go b/pkg/asset/machines/worker.go index bb9172fa33e..2c808264036 100644 --- a/pkg/asset/machines/worker.go +++ b/pkg/asset/machines/worker.go @@ -565,6 +565,9 @@ func (w *Worker) Generate(dependencies asset.Parents) error { mpool := defaultNutanixMachinePoolPlatform() mpool.Set(ic.Platform.Nutanix.DefaultMachinePlatform) mpool.Set(pool.Platform.Nutanix) + if err = mpool.ValidateConfig(ic.Platform.Nutanix); err != nil { + return errors.Wrap(err, "failed to create master machine objects") + } pool.Platform.Nutanix = &mpool imageName := nutanixtypes.RHCOSImageName(clusterID.InfraID) diff --git a/pkg/tfvars/nutanix/nutanix.go b/pkg/tfvars/nutanix/nutanix.go index 577dd428247..153c74d6984 100644 --- a/pkg/tfvars/nutanix/nutanix.go +++ b/pkg/tfvars/nutanix/nutanix.go @@ -10,20 +10,22 @@ import ( ) type config struct { - PrismCentralAddress string `json:"nutanix_prism_central_address"` - Port string `json:"nutanix_prism_central_port"` - Username string `json:"nutanix_username"` - Password string `json:"nutanix_password"` - MemoryMiB int64 `json:"nutanix_control_plane_memory_mib"` - DiskSizeMiB int64 `json:"nutanix_control_plane_disk_mib"` - NumCPUs int64 `json:"nutanix_control_plane_num_cpus"` - NumCoresPerSocket int64 `json:"nutanix_control_plane_cores_per_socket"` - PrismElementUUID string `json:"nutanix_prism_element_uuid"` - SubnetUUID string `json:"nutanix_subnet_uuid"` - Image string `json:"nutanix_image"` - ImageURI string `json:"nutanix_image_uri"` - BootstrapIgnitionImage string `json:"nutanix_bootstrap_ignition_image"` - BootstrapIgnitionImageFilePath string `json:"nutanix_bootstrap_ignition_image_filepath"` + PrismCentralAddress string `json:"nutanix_prism_central_address"` + Port string `json:"nutanix_prism_central_port"` + Username string `json:"nutanix_username"` + Password string `json:"nutanix_password"` + MemoryMiB int64 `json:"nutanix_control_plane_memory_mib"` + DiskSizeMiB int64 `json:"nutanix_control_plane_disk_mib"` + NumCPUs int64 `json:"nutanix_control_plane_num_cpus"` + NumCoresPerSocket int64 `json:"nutanix_control_plane_cores_per_socket"` + ProjectUUID string `json:"nutanix_control_plane_project_uuid"` + Categories map[string]string `json:"nutanix_control_plane_categories"` + PrismElementUUID string `json:"nutanix_prism_element_uuid"` + SubnetUUID string `json:"nutanix_subnet_uuid"` + Image string `json:"nutanix_image"` + ImageURI string `json:"nutanix_image_uri"` + BootstrapIgnitionImage string `json:"nutanix_bootstrap_ignition_image"` + BootstrapIgnitionImageFilePath string `json:"nutanix_bootstrap_ignition_image_filepath"` } // TFVarsSources contains the parameters to be converted into Terraform variables @@ -63,5 +65,14 @@ func TFVars(sources TFVarsSources) ([]byte, error) { BootstrapIgnitionImage: bootstrapIgnitionImageName, BootstrapIgnitionImageFilePath: bootstrapIgnitionImagePath, } + + if controlPlaneConfig.Project.Type == machinev1.NutanixIdentifierUUID { + cfg.ProjectUUID = *controlPlaneConfig.Project.UUID + } + cfg.Categories = make(map[string]string, len(controlPlaneConfig.Categories)) + for _, category := range controlPlaneConfig.Categories { + cfg.Categories[category.Key] = category.Value + } + return json.MarshalIndent(cfg, "", " ") } diff --git a/pkg/types/nutanix/client.go b/pkg/types/nutanix/client.go index d68e43f6d7e..0743c2de22f 100644 --- a/pkg/types/nutanix/client.go +++ b/pkg/types/nutanix/client.go @@ -3,13 +3,14 @@ package nutanix import ( "context" "fmt" + "strconv" "time" nutanixclient "github.com/nutanix-cloud-native/prism-go-client" nutanixclientv3 "github.com/nutanix-cloud-native/prism-go-client/v3" ) -// CreateNutanixClient creates a Nutanix V3 Client +// CreateNutanixClient creates a Nutanix V3 Client. func CreateNutanixClient(ctx context.Context, prismCentral, port, username, password string) (*nutanixclientv3.Client, error) { ctx, cancel := context.WithTimeout(ctx, 60*time.Second) defer cancel() @@ -24,3 +25,12 @@ func CreateNutanixClient(ctx context.Context, prismCentral, port, username, pass return nutanixclientv3.NewV3Client(cred) } + +// CreateNutanixClientFromPlatform creates a Nutanix V3 clinet based on the platform configuration. +func CreateNutanixClientFromPlatform(platform *Platform) (*nutanixclientv3.Client, error) { + return CreateNutanixClient(context.TODO(), + platform.PrismCentral.Endpoint.Address, + strconv.Itoa(int(platform.PrismCentral.Endpoint.Port)), + platform.PrismCentral.Username, + platform.PrismCentral.Password) +} diff --git a/pkg/types/nutanix/machinepool.go b/pkg/types/nutanix/machinepool.go index 57792e9349e..b109beb3a51 100644 --- a/pkg/types/nutanix/machinepool.go +++ b/pkg/types/nutanix/machinepool.go @@ -1,5 +1,14 @@ package nutanix +import ( + "fmt" + + nutanixclientv3 "github.com/nutanix-cloud-native/prism-go-client/v3" + "k8s.io/apimachinery/pkg/util/validation/field" + + machinev1 "github.com/openshift/api/machine/v1" +) + // MachinePool stores the configuration for a machine pool installed // on Nutanix. type MachinePool struct { @@ -26,6 +35,25 @@ type MachinePool struct { // // +optional OSDisk `json:"osDisk,omitempty"` + + // BootType indicates the boot type (Legacy, UEFI or SecureBoot) the Machine's VM uses to boot. + // If this field is empty or omitted, the VM will use the default boot type "Legacy" to boot. + // "SecureBoot" depends on "UEFI" boot, i.e., enabling "SecureBoot" means that "UEFI" boot is also enabled. + // +kubebuilder:validation:Enum="";Legacy;UEFI;SecureBoot + // +optional + BootType machinev1.NutanixBootType `json:"bootType,omitempty"` + + // Project optionally identifies a Prism project for the Machine's VM to associate with. + // +optional + Project *machinev1.NutanixResourceIdentifier `json:"project,omitempty"` + + // Categories optionally adds one or more prism categories (each with key and value) for + // the Machine's VM to associate with. All the category key and value pairs specified must + // already exist in the prism central. + // +listType=map + // +listMapKey=key + // +optional + Categories []machinev1.NutanixCategory `json:"categories,omitempty"` } // OSDisk defines the disk for a virtual machine. @@ -57,4 +85,92 @@ func (p *MachinePool) Set(required *MachinePool) { if required.OSDisk.DiskSizeGiB != 0 { p.OSDisk.DiskSizeGiB = required.OSDisk.DiskSizeGiB } + + if len(required.BootType) != 0 { + p.BootType = required.BootType + } + + if required.Project != nil { + p.Project = required.Project + } + + if len(required.Categories) > 0 { + p.Categories = required.Categories + } +} + +// ValidateConfig validates the MachinePool configuration. +func (p *MachinePool) ValidateConfig(platform *Platform) error { + nc, err := CreateNutanixClientFromPlatform(platform) + if err != nil { + return fmt.Errorf("fail to create nutanix client. %w", err) + } + + errList := field.ErrorList{} + fldPath := field.NewPath("platform", "nutanix") + var errMsg string + + // validate BootType + if p.BootType != "" && p.BootType != machinev1.NutanixLegacyBoot && + p.BootType != machinev1.NutanixUEFIBoot && p.BootType != machinev1.NutanixSecureBoot { + errMsg = fmt.Sprintf("valid bootType: \"\", %q, %q, %q.", machinev1.NutanixLegacyBoot, machinev1.NutanixUEFIBoot, machinev1.NutanixSecureBoot) + errList = append(errList, field.Invalid(fldPath.Child("bootType"), p.BootType, errMsg)) + } + + // validate project if configured + if p.Project != nil { + switch p.Project.Type { + case machinev1.NutanixIdentifierName: + if p.Project.Name == nil || *p.Project.Name == "" { + errList = append(errList, field.Required(fldPath.Child("project", "name"), "missing projct name")) + } else { + projectName := *p.Project.Name + filter := fmt.Sprintf("name==%s", projectName) + res, err := nc.V3.ListProject(&nutanixclientv3.DSMetadata{ + Filter: &filter, + }) + switch { + case err != nil: + errMsg = fmt.Sprintf("failed to find project with name %q. error: %v", projectName, err) + errList = append(errList, field.Invalid(fldPath.Child("project", "name"), projectName, errMsg)) + case len(res.Entities) == 0: + errMsg = fmt.Sprintf("found no project with name %q.", projectName) + errList = append(errList, field.Invalid(fldPath.Child("project", "name"), projectName, errMsg)) + case len(res.Entities) > 1: + errMsg = fmt.Sprintf("found more than one (%v) projects with name %q.", len(res.Entities), projectName) + errList = append(errList, field.Invalid(fldPath.Child("project", "name"), projectName, errMsg)) + default: + p.Project.Type = machinev1.NutanixIdentifierUUID + p.Project.UUID = res.Entities[0].Metadata.UUID + } + } + case machinev1.NutanixIdentifierUUID: + if p.Project.UUID == nil || *p.Project.UUID == "" { + errList = append(errList, field.Required(fldPath.Child("project", "uuid"), "missing projct uuid")) + } else { + if _, err = nc.V3.GetProject(*p.Project.UUID); err != nil { + errMsg = fmt.Sprintf("failed to get the project with uuid %s. error: %v", *p.Project.UUID, err) + errList = append(errList, field.Invalid(fldPath.Child("project", "uuid"), *p.Project.UUID, errMsg)) + } + } + default: + errMsg = fmt.Sprintf("invalid project identifier type, valid types are: %q, %q.", machinev1.NutanixIdentifierName, machinev1.NutanixIdentifierUUID) + errList = append(errList, field.Invalid(fldPath.Child("project", "type"), p.Project.Type, errMsg)) + } + } + + // validate categories if configured + if len(p.Categories) > 0 { + for _, category := range p.Categories { + if _, err = nc.V3.GetCategoryValue(category.Key, category.Value); err != nil { + errMsg = fmt.Sprintf("Failed to find the category with key %q and value %q. error: %v", category.Key, category.Value, err) + errList = append(errList, field.Invalid(fldPath.Child("categories"), category, errMsg)) + } + } + } + + if len(errList) > 0 { + return fmt.Errorf(errList.ToAggregate().Error()) + } + return nil }