diff --git a/pkg/apis/metal3/v1alpha1/baremetalhost_types.go b/pkg/apis/metal3/v1alpha1/baremetalhost_types.go index b6830ac92d..7f6182a3ea 100644 --- a/pkg/apis/metal3/v1alpha1/baremetalhost_types.go +++ b/pkg/apis/metal3/v1alpha1/baremetalhost_types.go @@ -1,6 +1,7 @@ package v1alpha1 import ( + "github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -86,6 +87,10 @@ const ( // learn about the hardware components available there StateInspecting ProvisioningState = "inspecting" + // StateCleaning means Ironic are running the referenced CleanSteps via + // manual cleaning for RAID and BIOS configuration + StateCleaning ProvisioningState = "cleaning" + // StatePowerManagementError means something went wrong trying to // power the server on or off. StatePowerManagementError ProvisioningState = "power management error" @@ -104,6 +109,49 @@ type BMCDetails struct { CredentialsName string `json:"credentialsName"` } +type RAIDVolume struct { + // Size (Integer) of the logical disk to be created in GiB. If unspecified, "MAX" will be used. + SizeGB *int `json:"sizeGB"` + + // RAID level for the logical disk. + RAIDLevel string `json:"raidLevel" required:"true"` + + // Name of the volume. Should be unique within the Node. If not specified, volume name will be auto-generated. + VolumeName string `json:"volumeName,omitempty"` + + // Set to true if this logical disk can share physical disks with other logical disks. + SharePhysicalDisks *bool `json:"sharePhysicalDisks,omitempty"` + + // If this is not specified, disk type will not be a criterion to find backing physical disks + DiskType string `json:"diskType,omitempty"` + + // If this is not specified, interface type will not be a criterion to find backing physical disks. + InterfaceType string `json:"interfaceType,omitempty"` + + // Integer, number of disks to use for the logical disk. Defaults to minimum number of disks required + // for the particular RAID level. + NumberOfPhysicalDisks int `json:"numberOfPhysicalDisks,omitempty"` + + // The name of the controller as read by the RAID interface. + Controller string `json:"controller,omitempty"` + + // A list of physical disks to use as read by the RAID interface. + PhysicalDisks []string `json:"physicalDisks,omitempty"` +} + +// RAIDConfig contains the configuration that are required to config RAID in Bare Metal server +type RAIDConfig struct { + + // The logical disk that will be root volume + RootVolume *RAIDVolume `json:"rootVolume,omitempty"` + + // The list of logical disks + Volumes []RAIDVolume `json:"volumes,omitempty"` +} + +// BIOSConfig contains the configuration that are required to config BIOS in Bare Metal server +type BIOSConfig map[string]interface{} + // BareMetalHostSpec defines the desired state of BareMetalHost type BareMetalHostSpec struct { // Important: Run "operator-sdk generate k8s" to regenerate code @@ -118,6 +166,12 @@ type BareMetalHostSpec struct { // How do we connect to the BMC? BMC BMCDetails `json:"bmc,omitempty"` + // RAID configuration for bare metal server + RAID RAIDConfig `json:"raid,omitempty"` + + // BIOS configurations for bare metal server + BIOS BIOSConfig `json:"bios,omitempty"` + // What is the name of the hardware profile for this host? It // should only be necessary to set this when inspection cannot // automatically determine the profile. @@ -351,6 +405,9 @@ type BareMetalHostStatus struct { // The hardware discovered to exist on the host. HardwareDetails *HardwareDetails `json:"hardware,omitempty"` + // The executed CleanSteps on the host. + CleanSteps []nodes.CleanStep `json:"cleanSteps,omitempty"` + // Information tracked by the provisioner. Provisioning ProvisionStatus `json:"provisioning"` @@ -529,6 +586,18 @@ func (host *BareMetalHost) NeedsHardwareInspection() bool { return host.Status.HardwareDetails == nil } +// NeedManualCleaning looks at the state of the host to determine +// if Clean Steps are needed to be run +func (host *BareMetalHost) NeedsManualCleaning(cleanSteps []nodes.CleanStep) bool { + if host.Status.CleanSteps != nil { + return false + } else if len(cleanSteps) > 0 { + // Never run manual cleaning if RAID or BIOS is not configured + return true + } + return false +} + // NeedsProvisioning compares the settings with the provisioning // status and returns true when more work is needed or false // otherwise. diff --git a/pkg/apis/metal3/v1alpha1/baremetalhost_types_test.go b/pkg/apis/metal3/v1alpha1/baremetalhost_types_test.go index 21748a0e23..24e78f6b81 100644 --- a/pkg/apis/metal3/v1alpha1/baremetalhost_types_test.go +++ b/pkg/apis/metal3/v1alpha1/baremetalhost_types_test.go @@ -1,6 +1,7 @@ package v1alpha1 import ( + "github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes" "testing" "time" @@ -151,6 +152,107 @@ func TestHostNeedsHardwareInspection(t *testing.T) { } } +func TestHostNeedsManualCleaning(t *testing.T) { + testCases := []struct { + Scenario string + Host BareMetalHost + CleanSteps [] nodes.CleanStep + Expected bool + }{ + + { + Scenario: "without cleanSteps in host status and incoming cleanSteps", + Host: BareMetalHost{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myhost", + Namespace: "myns", + }, + }, + CleanSteps: []nodes.CleanStep{ + { + Interface: "fakeInterface", + Step: "fakeStep", + Args: map[string]interface{}{ + "fakeKey": "fakeValue", + }, + }, + }, + Expected: true, + }, + + { + Scenario: "with cleanSteps in host status and incoming cleanSteps", + Host: BareMetalHost{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myhost", + Namespace: "myns", + }, + Status: BareMetalHostStatus{ + CleanSteps: []nodes.CleanStep{ + { + Interface: "myInterface", + Step: "myStep", + }, + }, + }, + }, + CleanSteps: []nodes.CleanStep{ + { + Interface: "fakeInterface", + Step: "fakeStep", + Args: map[string]interface{}{ + "fakeKey": "fakeValue", + }, + }, + }, + Expected: false, + }, + + { + Scenario: "with cleanSteps in host status and no incoming cleanSteps", + Host: BareMetalHost{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myhost", + Namespace: "myns", + }, + Status: BareMetalHostStatus{ + CleanSteps: []nodes.CleanStep{ + { + Interface: "myInterface", + Step: "myStep", + }, + }, + }, + }, + CleanSteps: []nodes.CleanStep{}, + Expected: false, + }, + + { + Scenario: "without cleanSteps in host status and no incoming cleanSteps", + Host: BareMetalHost{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myhost", + Namespace: "myns", + }, + }, + CleanSteps: []nodes.CleanStep{}, + Expected: false, + }, + } + for _, tc := range testCases { + t.Run(tc.Scenario, func(t *testing.T) { + actual := tc.Host.NeedsManualCleaning(tc.CleanSteps) + if tc.Expected && !actual { + t.Error("expected to need manual cleaning") + } + if !tc.Expected && actual { + t.Error("did not expect to need manual cleaning") + } + }) + } +} + func TestHostNeedsProvisioning(t *testing.T) { testCases := []struct { Scenario string diff --git a/pkg/bmc/access.go b/pkg/bmc/access.go index 5f4ab10dfc..62aa41a069 100644 --- a/pkg/bmc/access.go +++ b/pkg/bmc/access.go @@ -3,6 +3,7 @@ package bmc import ( "net" "net/url" + "reflect" "strings" "github.com/pkg/errors" @@ -34,6 +35,28 @@ type AccessDetails interface { // Boot interface to set BootInterface() string + + // Return the map of supported BIOS configuration keys + GetBIOSConfigDetails() map[string]VendorBIOSConfigSpec +} + +type VendorBIOSConfigSpec struct { + VendorKey string + ValueType reflect.Kind + SupportedValues []interface{} +} + +func (v *VendorBIOSConfigSpec) IsSupportedConfigValue (value interface{}) (isSupport bool) { + if v.SupportedValues == nil { + return true + } else { + for _, supportedValue := range v.SupportedValues { + if value == supportedValue { + return true + } + } + } + return false } func getTypeHostPort(address string) (bmcType, host, port, path string, err error) { diff --git a/pkg/bmc/access_test.go b/pkg/bmc/access_test.go index e4b80294c4..5667fe95af 100644 --- a/pkg/bmc/access_test.go +++ b/pkg/bmc/access_test.go @@ -1,6 +1,7 @@ package bmc import ( + "reflect" "testing" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" @@ -211,6 +212,16 @@ func TestIPMIBootInterface(t *testing.T) { } } +func TestIPMIBiosConfigDetails(t *testing.T) { + acc, err := NewAccessDetails("ipmi://192.168.122.1:6233") + if err != nil { + t.Fatalf("unexpected parse error: %v", err) + } + if acc.GetBIOSConfigDetails() != nil { + t.Fatal("expected BIOS configuration details to be nil") + } +} + func TestLibvirtNeedsMAC(t *testing.T) { acc, err := NewAccessDetails("libvirt://192.168.122.1:6233/") if err != nil { @@ -242,6 +253,16 @@ func TestLibvirtBootInterface(t *testing.T) { } } +func TestLibvirtBiosConfigDetails(t *testing.T) { + acc, err := NewAccessDetails("libvirt://192.168.122.1:6233/") + if err != nil { + t.Fatalf("unexpected parse error: %v", err) + } + if acc.GetBIOSConfigDetails() != nil { + t.Fatal("expected BIOS configuration details to be nil") + } +} + func TestParseIDRACURL(t *testing.T) { T, H, P, A, err := getTypeHostPort("idrac://192.168.122.1") if err != nil { @@ -469,6 +490,17 @@ func TestIDRACBootInterface(t *testing.T) { } } +func TestIRACBiosConfigDetails(t *testing.T) { + acc, err := NewAccessDetails("idrac://192.168.122.1") + if err != nil { + t.Fatalf("unexpected parse error: %v", err) + } + if acc.GetBIOSConfigDetails() != nil { + t.Fatal("expected BIOS configuration details to be nil") + } +} + + func TestParseIRMCURL(t *testing.T) { T, H, P, A, err := getTypeHostPort("irmc://192.168.122.1") if err != nil { @@ -613,6 +645,17 @@ func TestIRMCBootInterface(t *testing.T) { } } +func TestIRMCBiosConfigDetails(t *testing.T) { + acc, err := NewAccessDetails("irmc://192.168.122.1") + if err != nil { + t.Fatalf("unexpected parse error: %v", err) + } + if !reflect.DeepEqual(acc.GetBIOSConfigDetails(), iRMCBiosConfigDetails) { + t.Fatal("unexpected BIOS configuration details") + } +} + + func TestUnknownType(t *testing.T) { acc, err := NewAccessDetails("foo://192.168.122.1") if err == nil || acc != nil { diff --git a/pkg/bmc/idrac.go b/pkg/bmc/idrac.go index c907f63f28..edc8db5411 100644 --- a/pkg/bmc/idrac.go +++ b/pkg/bmc/idrac.go @@ -54,3 +54,9 @@ func (a *iDracAccessDetails) DriverInfo(bmcCreds Credentials) map[string]interfa func (a *iDracAccessDetails) BootInterface() string { return "ipxe" } + +// GetBIOSConfigDetails return the mapping of supported BIOS configuration keys between Metal3 and iDRAC. +// If the user use the key that does not belong to this map, this key wil be marked as unsupported in iDRAC. +func (a *iDracAccessDetails) GetBIOSConfigDetails() map[string]VendorBIOSConfigSpec { + return nil +} diff --git a/pkg/bmc/ipmi.go b/pkg/bmc/ipmi.go index 3e4191e274..d9c6743e78 100644 --- a/pkg/bmc/ipmi.go +++ b/pkg/bmc/ipmi.go @@ -49,3 +49,10 @@ func (a *ipmiAccessDetails) DriverInfo(bmcCreds Credentials) map[string]interfac func (a *ipmiAccessDetails) BootInterface() string { return "ipxe" } + +// GetBIOSConfigDetails return the mapping of supported BIOS configuration keys between Metal3 and IPMI. +// If the user use the key that does not belong to this map, this key wil be marked as unsupported in IPMI. +func (a *ipmiAccessDetails) GetBIOSConfigDetails() map[string] VendorBIOSConfigSpec { + return nil +} + diff --git a/pkg/bmc/irmc.go b/pkg/bmc/irmc.go index 0e901505cb..2a4b24f9a5 100644 --- a/pkg/bmc/irmc.go +++ b/pkg/bmc/irmc.go @@ -1,5 +1,7 @@ package bmc +import "reflect" + type iRMCAccessDetails struct { bmcType string portNum string @@ -42,3 +44,90 @@ func (a *iRMCAccessDetails) DriverInfo(bmcCreds Credentials) map[string]interfac func (a *iRMCAccessDetails) BootInterface() string { return "pxe" } +// GetBIOSConfigDetails return the mapping of supported BIOS configuration keys between Metal3 and iRMC. +// If the user use the key that does not belong to this map, this key wil be marked as unsupported in iRMC +func (a *iRMCAccessDetails) GetBIOSConfigDetails() map[string]VendorBIOSConfigSpec { + return iRMCBiosConfigDetails +} + +var iRMCBiosConfigDetails = map[string]VendorBIOSConfigSpec { + // Specifies from which drives can be booted + "bootOptionFilter": { + VendorKey: "boot_option_filter", + ValueType: reflect.String, + SupportedValues: []interface{}{"UefiAndLegacy", "LegacyOnly", "UefiOnly"}, + }, + // The UEFI FW checks the controller health status. + "checkControllersHealthStatusEnabled": { + VendorKey: "check_controllers_health_status_enabled", + ValueType: reflect.Bool, + }, + // The number of active processor cores 1…n. Option 0 indicates that all + // available processor cores are active. + "cpuActiveProcessorCores": { + VendorKey: "cpu_active_processor_cores", + ValueType: reflect.Int, + }, + // The processor loads the requested cache line and the adjacent cache line + "cpuAdjacentCacheLinePrefetchEnabled": { + VendorKey: "cpu_adjacent_cache_line_prefetch_enabled", + ValueType: reflect.Bool, + }, + // Supports the virtualization of platform hardware and several software + // environments, based on Virtual Machine Extensions to support the use of + // several software environments using virtual computers + "cpuVtEnabled": { + VendorKey: "cpu_vt_enabled", + ValueType: reflect.Bool, + }, + // The system BIOS can be written. Flash BIOS update is possible + "flashWriteEnabled": { + VendorKey: "flash_write_enabled", + ValueType: reflect.Bool, + }, + // Hyper-threading technology allows a single physical processor core to + // appear as several logical processors. + "hyperThreadingEnabled": { + VendorKey: "hyper_threading_enabled", + ValueType: reflect.Bool, + }, + // Boot Options will not be removed from “Boot Option Priority” list + "keepVoidBootOptionsEnabled":{ + VendorKey: "keep_void_boot_options_enabled", + ValueType: reflect.Bool, + }, + // Specifies whether the Compatibility Support Module (CSM) is executed + "launchCsmEnabled": { + VendorKey: "launch_csm_enabled", + ValueType: reflect.Bool, + }, + // Prevents the OS from overruling any energy efficiency policy setting of the setup + "osEnergyPerformanceOverrideEnabled":{ + VendorKey: "os_energy_performance_override_enabled", + ValueType: reflect.Bool, + }, + // Active State Power Management (ASPM) is used to power-manage the PCI + // Express links, thus consuming less power + "pciAspmSupport": { + VendorKey: "pci_aspm_support", + ValueType: reflect.String, + SupportedValues: []interface{}{"Disabled", "Auto", "L0Limited", "L1only", "L0Force"}, + }, + // Specifies if memory resources above the 4GB address boundary can be assigned to PCI devices + "pciAbove4gDecodingEnabled": { + VendorKey: "pci_above_4g_decoding_enabled", + ValueType: reflect.Bool, + }, + // Specifies whether the switch on sources for the system are managed by + // the BIOS or the ACPI operating system + "powerOnSource": { + VendorKey: "power_on_source", + ValueType: reflect.String, + SupportedValues: []interface{}{"BiosControlled", "AcpiControlled"}, + }, + // Single Root IO Virtualization Support is active + "singleRootIoVirtualizationSupportEnabled": { + VendorKey: "single_root_io_virtualization_support_enabled", + ValueType: reflect.Bool, + }, + } diff --git a/pkg/controller/baremetalhost/baremetalhost_controller.go b/pkg/controller/baremetalhost/baremetalhost_controller.go index 6648a9d0c2..108d4ee9b4 100644 --- a/pkg/controller/baremetalhost/baremetalhost_controller.go +++ b/pkg/controller/baremetalhost/baremetalhost_controller.go @@ -4,6 +4,8 @@ import ( "context" "flag" "fmt" + "github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes" + "reflect" "strings" "time" @@ -123,6 +125,7 @@ type reconcileInfo struct { host *metal3v1alpha1.BareMetalHost request reconcile.Request bmcCredsSecret *corev1.Secret + cleanSteps []nodes.CleanStep events []corev1.Event errorMessage string } @@ -260,6 +263,19 @@ func (r *ReconcileBareMetalHost) Reconcile(request reconcile.Request) (result re } } + info := &reconcileInfo{ + host: host, + request: request, + bmcCredsSecret: bmcCredsSecret, + } + + prov, err := r.provisionerFactory(host, *bmcCreds, info.publishEvent) + if err != nil { + return reconcile.Result{}, errors.Wrap(err, "failed to create provisioner") + } + + info.cleanSteps = r.buildAndValidateCleanSteps(request, host, prov) + // Pick the action to perform var actionName metal3v1alpha1.ProvisioningState switch { @@ -267,6 +283,8 @@ func (r *ReconcileBareMetalHost) Reconcile(request reconcile.Request) (result re actionName = metal3v1alpha1.StateRegistering case host.WasExternallyProvisioned(): actionName = metal3v1alpha1.StateExternallyProvisioned + case host.NeedsManualCleaning(info.cleanSteps): + actionName = metal3v1alpha1.StateCleaning case host.NeedsHardwareInspection(): actionName = metal3v1alpha1.StateInspecting case host.NeedsHardwareProfile(): @@ -294,20 +312,14 @@ func (r *ReconcileBareMetalHost) Reconcile(request reconcile.Request) (result re return reconcile.Result{Requeue: true}, nil } - info := &reconcileInfo{ - log: reqLogger.WithValues("provisioningState", actionName), - host: host, - request: request, - bmcCredsSecret: bmcCredsSecret, - } - prov, err := r.provisionerFactory(host, *bmcCreds, info.publishEvent) - if err != nil { - return reconcile.Result{}, errors.Wrap(err, "failed to create provisioner") - } + // Update info object with log for provisioningState + info.log = reqLogger.WithValues("provisioningState", actionName) switch actionName { case metal3v1alpha1.StateRegistering: result, err = r.actionRegistering(prov, info) + case metal3v1alpha1.StateCleaning: + result, err = r.actionCleaning(prov, info) case metal3v1alpha1.StateInspecting: result, err = r.actionInspecting(prov, info) case metal3v1alpha1.StateMatchProfile: @@ -504,6 +516,44 @@ func (r *ReconcileBareMetalHost) actionRegistering(prov provisioner.Provisioner, return result, nil } +// Run manual cleaning to execute configured Clean Steps for RAID and BIOS +func (r *ReconcileBareMetalHost) actionCleaning(prov provisioner.Provisioner, info *reconcileInfo) (result reconcile.Result, err error) { + var provResult provisioner.Result + + info.log.Info("Manual cleaning") + provResult, err = prov.ManualCleaning(info.cleanSteps) + if err != nil { + return result, errors.Wrap(err, "Manual cleaning failed") + } + + if provResult.ErrorMessage != "" { + info.host.Status.Provisioning.State = metal3v1alpha1.StateRegistrationError + if info.host.SetErrorMessage(provResult.ErrorMessage) { + info.publishEvent("RegistrationError", provResult.ErrorMessage) + result.Requeue = true + } + return result, nil + } + + if provResult.Dirty { + // Go back into the queue and wait for the ManualCleaning() method + // to return false, indicating that it has no more work to + // do. + info.host.ClearError() + result.Requeue = true + result.RequeueAfter = provResult.RequeueAfter + return result, nil + } + + // If the provisioner had no work, ensure the CleanSteps has been saved into host status. + if reflect.DeepEqual(info.host.Status.CleanSteps, info.cleanSteps) { + info.log.Info("updating CleanSteps in status") + info.host.Status.CleanSteps = info.cleanSteps + } + + return result, nil +} + // Ensure we have the information about the hardware on the host. func (r *ReconcileBareMetalHost) actionInspecting(prov provisioner.Provisioner, info *reconcileInfo) (result reconcile.Result, err error) { var provResult provisioner.Result @@ -900,3 +950,158 @@ func (r *ReconcileBareMetalHost) publishEvent(request reconcile.Request, event c func hostHasFinalizer(host *metal3v1alpha1.BareMetalHost) bool { return utils.StringInList(host.Finalizers, metal3v1alpha1.BareMetalHostFinalizer) } + +func(r *ReconcileBareMetalHost) buildAndValidateCleanSteps( + request reconcile.Request, host *metal3v1alpha1.BareMetalHost, prov provisioner.Provisioner) (cleanSteps []nodes.CleanStep) { + reqLogger := log.WithValues( + "Request.Namespace", request.Namespace, "Request.Name", request.Name) + + // Retrieve RAID configuration in form of provisioner's clean step + raidCleanStep, err := r.ValidateAndBuildRAIDCleanStep(&host.Spec.RAID) + if err != nil { + reqLogger.Info("Failed to retrieve RAID configuration", err) + } else if raidCleanStep != nil { + // ‘create_configuration’ doesn’t remove existing disks. It is recommended + // to add ‘delete_configuration’ before ‘create_configuration’ to make sure + // that only the desired logical disks exist in the system after manual cleaning. + cleanSteps = append( + cleanSteps, + nodes.CleanStep{ + Interface: "raid", + Step: "delete_configuration", + Args: nil, + }, + *raidCleanStep, + ) + } + + // Retrieve BIOS settings in form of provisioner's clean step + biosCleanStep, err := r.ValidateAndBuildBIOSCleanStep(prov, host.Spec.BIOS) + if err != nil { + reqLogger.Info("Failed to retrieve BIOS configuration") + } else if biosCleanStep != nil { + cleanSteps = append(cleanSteps, *biosCleanStep) + } + return cleanSteps +} + +// ValidateAndBuildRAIDCleanSteps verify the given RAID configuration +func (r *ReconcileBareMetalHost) ValidateAndBuildRAIDCleanStep(raidConfig *metal3v1alpha1.RAIDConfig) (cleanStep *nodes.CleanStep, err error){ + if &raidConfig == nil { + return nil, nil + } + logicalDisks := []map[string]interface{}{} + if raidConfig.RootVolume != nil { + err = r.validateRAIDVolumeConfig(raidConfig.RootVolume) + if err != nil { + return nil, err + } + logicalDisks = append(logicalDisks, r.buildRAIDVolumeArgs(raidConfig.RootVolume, true)) + } + for _, volume := range raidConfig.Volumes { + err = r.validateRAIDVolumeConfig(&volume) + if err != nil { + return nil, err + } + logicalDisks = append(logicalDisks, r.buildRAIDVolumeArgs(&volume, false)) + } + if len(logicalDisks) > 0 { + cleanStep = &nodes.CleanStep{ + Interface: "raid", + Step: "create_configuration", + Args: map[string]interface{}{"logical_disks": logicalDisks}, + } + } + return cleanStep, nil +} + +// A private method to validate incoming RAID configuration. +func (r *ReconcileBareMetalHost) validateRAIDVolumeConfig(volumeConfig *metal3v1alpha1.RAIDVolume) (err error) { + // The config without RAIDLevel will be marked as invalid + if volumeConfig.RAIDLevel == "" { + return errors.New("Missing the RAID level of disk 'raidLevel' in RAID config") + } + return nil +} + +// A private method to build the Args in CleanStep for RAID configuration +func (r *ReconcileBareMetalHost) buildRAIDVolumeArgs( + volumeConfig *metal3v1alpha1.RAIDVolume, isRootVolume bool) map[string]interface{} { + volume := map[string]interface{}{} + if isRootVolume { + volume["is_root_volume"] = isRootVolume + } + if volumeConfig.SizeGB != nil { + volume["size_gb"] = volumeConfig.SizeGB + } else { + volume["size_gb"] = "MAX" + } + if volumeConfig.RAIDLevel != "" { + volume["raid_level"] = volumeConfig.RAIDLevel + } + if volumeConfig.VolumeName != "" { + volume["volume_name"] = volumeConfig.VolumeName + } + if volumeConfig.SharePhysicalDisks != nil { + volume["share_physical_disks"] = volumeConfig.SharePhysicalDisks + } + if volumeConfig.DiskType != "" { + volume["disk_type"] = volumeConfig.DiskType + } + if volumeConfig.InterfaceType != "" { + volume["interface_type"] = volumeConfig.InterfaceType + } + if volumeConfig.NumberOfPhysicalDisks > 0 { + volume["number_of_physical_disks"] = volumeConfig.NumberOfPhysicalDisks + } + if volumeConfig.Controller != "" { + volume["controller"] = volumeConfig.Controller + } + if volumeConfig.PhysicalDisks != nil { + volume["physical_disks"] = volumeConfig.PhysicalDisks + } + return volume +} + +// ValidateAndBuildBIOSCleanSteps verify the given BIOS configuration +func (r *ReconcileBareMetalHost) ValidateAndBuildBIOSCleanStep( + prov provisioner.Provisioner, biosConfig metal3v1alpha1.BIOSConfig) (cleanStep *nodes.CleanStep, err error){ + if biosConfig == nil { + return nil, nil + } + accessDetails := prov.GetAccessDetails() + biosConfigDetails := accessDetails.GetBIOSConfigDetails() + if biosConfigDetails == nil { + return nil, errors.New(fmt.Sprintf( + "'%s' driver does not support BIOS configuration", accessDetails.Driver())) + } + biosSettings := []map[string]interface{}{} + for key, value := range biosConfig{ + valueType := reflect.TypeOf(value).Kind() + configSpec, ok := biosConfigDetails[key] + if !ok || value == nil { + return nil, errors.New(fmt.Sprintf( + "Invalid BIOS configuration: {%s: %v}", key, value)) + } else if configSpec.ValueType != valueType { + return nil, errors.New(fmt.Sprintf( + "Expected value to be '%s' but got type '%s' with value '%v'", configSpec.ValueType.String(), valueType, value)) + } else if !configSpec.IsSupportedConfigValue(value){ + return nil, errors.New(fmt.Sprintf( + "The BIOS config '%s' does not support value '%v'", key, value)) + } else { + biosSettings = append(biosSettings, map[string]interface{} { + "name": configSpec.VendorKey, + "value": value, + }) + } + } + if len(biosSettings) > 0 { + cleanStep = &nodes.CleanStep{ + Interface: "bios", + Step: "apply_configuration", + Args: map[string]interface{}{"settings": biosSettings}, + } + } + + return cleanStep, nil +} diff --git a/pkg/provisioner/demo/demo.go b/pkg/provisioner/demo/demo.go index 98a89584f7..4ada8d18c6 100644 --- a/pkg/provisioner/demo/demo.go +++ b/pkg/provisioner/demo/demo.go @@ -1,6 +1,8 @@ package demo import ( + "github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes" + "github.com/pkg/errors" "time" "github.com/go-logr/logr" @@ -47,6 +49,8 @@ const ( type demoProvisioner struct { // the host to be managed by this provisioner host *metal3v1alpha1.BareMetalHost + // access parameters for the BMC + bmcAccess bmc.AccessDetails // the bmc credentials bmcCreds bmc.Credentials // a logger configured for this host @@ -57,8 +61,13 @@ type demoProvisioner struct { // New returns a new Ironic Provisioner func New(host *metal3v1alpha1.BareMetalHost, bmcCreds bmc.Credentials, publisher provisioner.EventPublisher) (provisioner.Provisioner, error) { + bmcAccess, err := bmc.NewAccessDetails(host.Spec.BMC.Address) + if err != nil { + return nil, errors.Wrap(err, "failed to parse BMC address information") + } p := &demoProvisioner{ host: host, + bmcAccess: bmcAccess, bmcCreds: bmcCreds, log: log.WithValues("host", host.Name), publisher: publisher, @@ -171,6 +180,19 @@ func (p *demoProvisioner) InspectHardware() (result provisioner.Result, details return } +func (p *demoProvisioner) ManualCleaning(cleanSteps []nodes.CleanStep) (result provisioner.Result, err error) { + hostName := p.host.ObjectMeta.Name + switch hostName { + default: + p.host.Status.CleanSteps = cleanSteps + return result,nil + } +} + +func (p *demoProvisioner) GetAccessDetails() (bmc.AccessDetails){ + return p.bmcAccess +} + // UpdateHardwareState fetches the latest hardware state of the server // and updates the HardwareDetails field of the host with details. It // is expected to do this in the least expensive way possible, such as diff --git a/pkg/provisioner/fixture/fixture.go b/pkg/provisioner/fixture/fixture.go index 9ba2f055a8..6e53dd6f3d 100644 --- a/pkg/provisioner/fixture/fixture.go +++ b/pkg/provisioner/fixture/fixture.go @@ -1,6 +1,8 @@ package fixture import ( + "github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes" + "github.com/pkg/errors" "time" "github.com/go-logr/logr" @@ -14,12 +16,15 @@ import ( var log = logf.Log.WithName("fixture") var deprovisionRequeueDelay = time.Second * 10 var provisionRequeueDelay = time.Second * 10 +var requeueDelay = time.Second * 10 // Provisioner implements the provisioning.Provisioner interface // and uses Ironic to manage the host. type fixtureProvisioner struct { // the host to be managed by this provisioner host *metal3v1alpha1.BareMetalHost + // access parameters for the BMC + bmcAccess bmc.AccessDetails // the bmc credentials bmcCreds bmc.Credentials // a logger configured for this host @@ -32,8 +37,13 @@ type fixtureProvisioner struct { // New returns a new Ironic Provisioner func New(host *metal3v1alpha1.BareMetalHost, bmcCreds bmc.Credentials, publisher provisioner.EventPublisher) (provisioner.Provisioner, error) { + bmcAccess, err := bmc.NewAccessDetails(host.Spec.BMC.Address) + if err != nil { + return nil, errors.Wrap(err, "failed to parse BMC address information") + } p := &fixtureProvisioner{ host: host, + bmcAccess: bmcAccess, bmcCreds: bmcCreds, log: log.WithValues("host", host.Name), publisher: publisher, @@ -122,6 +132,23 @@ func (p *fixtureProvisioner) InspectHardware() (result provisioner.Result, detai return } +func (p *fixtureProvisioner) ManualCleaning(cleanSteps []nodes.CleanStep) (result provisioner.Result, err error) { + p.log.Info("Manual cleaning with ", "steps", cleanSteps) + + if p.host.Status.CleanSteps == nil { + p.publisher("ManualCleaningComplete", "ManualCleaning completed") + p.log.Info("moving to done") + p.host.Status.CleanSteps = cleanSteps + result.Dirty = true + result.RequeueAfter = requeueDelay + } + return result, nil +} + +func (p *fixtureProvisioner) GetAccessDetails() (bmc.AccessDetails){ + return p.bmcAccess +} + // UpdateHardwareState fetches the latest hardware state of the server // and updates the HardwareDetails field of the host with details. It // is expected to do this in the least expensive way possible, such as diff --git a/pkg/provisioner/ironic/ironic.go b/pkg/provisioner/ironic/ironic.go index 71740782bf..adb2eeceb0 100644 --- a/pkg/provisioner/ironic/ironic.go +++ b/pkg/provisioner/ironic/ironic.go @@ -375,6 +375,10 @@ func (p *ironicProvisioner) ValidateManagementAccess(credentialsChanged bool) (r } } +func (p *ironicProvisioner) GetAccessDetails()(bmc.AccessDetails){ + return p.bmcAccess +} + func (p *ironicProvisioner) changeNodeProvisionState(ironicNode *nodes.Node, opts nodes.ProvisionStateOpts) (result provisioner.Result, err error) { p.log.Info("changing provisioning state", "current", ironicNode.ProvisionState, @@ -585,6 +589,54 @@ func (p *ironicProvisioner) InspectHardware() (result provisioner.Result, detail return } +//Enter cleaning state via manual cleaning +func (p *ironicProvisioner) ManualCleaning(cleanSteps []nodes.CleanStep) (result provisioner.Result, error error){ + p.log.Info("Manual cleaning ", "status", p.host.OperationalStatus()) + + ironicNode, err := p.findExistingHost() + if err != nil { + err = errors.Wrap(err, "failed to find existing host") + return + } + if ironicNode == nil { + return result, fmt.Errorf("no ironic node for host") + } + + result.RequeueAfter = provisionRequeueDelay + + switch nodes.ProvisionState(ironicNode.ProvisionState) { + case nodes.Manageable: + return p.startManualCleaning(ironicNode, cleanSteps) + + case nodes.CleanFail: + if ironicNode.LastError == "" { + p.log.Info("failed but error message not available") + result.Dirty = true + return result, nil + } + p.log.Info("found error", "msg", ironicNode.LastError) + result.ErrorMessage = fmt.Sprintf("Manual Cleaning failed") + return result, nil + //p.log.Info("Reconfiguration from previous failure") + //return p.startManualCleaning(ironicNode, cleanSteps) + default: + // wait states like cleaning and clean wait + p.log.Info("Cleaning is still in process", + "state", ironicNode.ProvisionState, + "CleanSteps", cleanSteps) + result.Dirty = true + return result, nil + } + return +} + +func (p *ironicProvisioner) startManualCleaning( + ironicNode *nodes.Node, cleanSteps []nodes.CleanStep) (result provisioner.Result, err error){ + p.log.Info("Manual cleaning with ", "steps", cleanSteps) + cleanOpts := nodes.ProvisionStateOpts{Target: nodes.TargetClean, CleanSteps: cleanSteps} + return p.changeNodeProvisionState(ironicNode, cleanOpts) +} + // UpdateHardwareState fetches the latest hardware state of the server // and updates the HardwareDetails field of the host with details. It // is expected to do this in the least expensive way possible, such as diff --git a/pkg/provisioner/provisioner.go b/pkg/provisioner/provisioner.go index 02cd876453..375cce77a1 100644 --- a/pkg/provisioner/provisioner.go +++ b/pkg/provisioner/provisioner.go @@ -1,6 +1,7 @@ package provisioner import ( + "github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes" "time" metal3v1alpha1 "github.com/metal3-io/baremetal-operator/pkg/apis/metal3/v1alpha1" @@ -33,12 +34,18 @@ type Provisioner interface { // credentials is correct. ValidateManagementAccess(credentialsChanged bool) (result Result, err error) + // Get AccessDetails from provisioner + GetAccessDetails() (bmc.AccessDetails) + // InspectHardware updates the HardwareDetails field of the host with // details of devices discovered on the hardware. It may be called // multiple times, and should return true for its dirty flag until the // inspection is completed. InspectHardware() (result Result, details *metal3v1alpha1.HardwareDetails, err error) + // ManualCleaning execute the Clean Steps for extra configuration such as RAID, BIOS. + ManualCleaning(cleanSteps []nodes.CleanStep) (result Result, err error) + // UpdateHardwareState fetches the latest hardware state of the // server and updates the HardwareDetails field of the host with // details. It is expected to do this in the least expensive way