diff --git a/PROJECT b/PROJECT index 45eb6fec..919b6885 100644 --- a/PROJECT +++ b/PROJECT @@ -90,4 +90,15 @@ resources: webhooks: validation: true webhookVersion: v1 +- api: + crdVersion: v1 + controller: true + domain: ironcore.dev + group: metal + kind: BMCSettings + path: github.com/ironcore-dev/metal-operator/api/v1alpha1 + version: v1alpha1 + webhooks: + validation: true + webhookVersion: v1 version: "3" diff --git a/api/v1alpha1/bmc_types.go b/api/v1alpha1/bmc_types.go index 7f466a11..797eeded 100644 --- a/api/v1alpha1/bmc_types.go +++ b/api/v1alpha1/bmc_types.go @@ -23,6 +23,13 @@ const ( // BMCSpec defines the desired state of BMC // +kubebuilder:validation:XValidation:rule="has(self.access) != has(self.endpointRef)",message="exactly one of access or endpointRef needs to be set" type BMCSpec struct { + + // bmcUUID is the unique identifier for the BMC as defined in REDFISH API. + // +kubebuilder:validation:Optional + // +optional + // This field is optional and can be omitted, controller will choose the first avaialbe Manager + BMCUUID string `json:"bmcUUID,omitempty"` + // EndpointRef is a reference to the Kubernetes object that contains the endpoint information for the BMC. // This reference is typically used to locate the BMC endpoint within the cluster. // +optional @@ -49,6 +56,10 @@ type BMCSpec struct { // This field is optional and can be omitted if console access is not required. // +optional ConsoleProtocol *ConsoleProtocol `json:"consoleProtocol,omitempty"` + + // BMCSettingRef is a reference to a BMCSettings object that specifies + // the BMC configuration for this BMC. + BMCSettingRef *v1.LocalObjectReference `json:"bmcSettingsRef,omitempty"` } // InlineEndpoint defines inline network access configuration for the BMC. diff --git a/api/v1alpha1/bmcsettings_types.go b/api/v1alpha1/bmcsettings_types.go new file mode 100644 index 00000000..da537ed4 --- /dev/null +++ b/api/v1alpha1/bmcsettings_types.go @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// BMCSettings refer's to Out-Of-Band_management like IDrac for Dell, iLo for HPE etc Settings + +// BMCSettingsSpec defines the desired state of BMCSettings. +type BMCSettingsSpec struct { + // Version contains BMC version this settings applies to + // +required + Version string `json:"version"` + + // SettingsMap contains bmc settings as map + // +optional + SettingsMap map[string]string `json:"settings,omitempty"` + + // BMCRef is a reference to a specific BMC to apply setting to. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="serverRef is immutable" + BMCRef *corev1.LocalObjectReference `json:"BMCRef,omitempty"` + + // ServerMaintenancePolicy is maintenance policy to be enforced on the server when applying setting. + // ServerMaintenancePolicyOwnerApproval is asking for User approval for changing BMC settings + // note: User approval is only enforced for server's which are reserved state + // ServerMaintenancePolicyEnforced will will bypass user approval and apply setting directly + ServerMaintenancePolicy ServerMaintenancePolicy `json:"serverMaintenancePolicy,omitempty"` + + // ServerMaintenanceRefs are references to a ServerMaintenance objects that Controller has requested for the each of the related server. + ServerMaintenanceRefs []ServerMaintenanceRefItem `json:"serverMaintenanceRefs,omitempty"` +} + +type ServerMaintenanceRefItem struct { + ServerMaintenanceRef *corev1.ObjectReference `json:"serverMaintenanceRef,omitempty"` +} + +// ServerMaintenanceState specifies the current state of the server maintenance. +type BMCSettingsState string + +const ( + // BMCSettingsStatePending specifies that the BMC maintenance is waiting + BMCSettingsStatePending BMCSettingsState = "Pending" + // BMCSettingsStateInProgress specifies that the BMC setting changes are in progress + BMCSettingsStateInProgress BMCSettingsState = "InProgress" + // BMCSettingsStateApplied specifies that the BMC maintenance has been completed. + BMCSettingsStateApplied BMCSettingsState = "Applied" + // BMCSettingsStateFailed specifies that the BMC maintenance has failed. + BMCSettingsStateFailed BMCSettingsState = "Failed" +) + +// BMCSettingsStatus defines the observed state of BMCSettings. +type BMCSettingsStatus struct { + // State represents the current state of the BMC configuration task. + State BMCSettingsState `json:"state,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster +// +kubebuilder:printcolumn:name="BMCVersion",type=string,JSONPath=`.spec.bmcSettings.version` +// +kubebuilder:printcolumn:name="State",type=string,JSONPath=`.status.state` +// +kubebuilder:printcolumn:name="BMCRef",type=string,JSONPath=`.spec.BMCRef.name` +// +kubebuilder:printcolumn:name="ServerRef",type=string,JSONPath=`.spec.serverRef.name` + +// BMCSettings is the Schema for the BMCSettings API. +type BMCSettings struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec BMCSettingsSpec `json:"spec,omitempty"` + Status BMCSettingsStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// BMCSettingsList contains a list of BMCSettings. +type BMCSettingsList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []BMCSettings `json:"items"` +} + +func init() { + SchemeBuilder.Register(&BMCSettings{}, &BMCSettingsList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 67e98a5e..364ab087 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -397,6 +397,114 @@ func (in *BMCSecretList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BMCSettings) DeepCopyInto(out *BMCSettings) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BMCSettings. +func (in *BMCSettings) DeepCopy() *BMCSettings { + if in == nil { + return nil + } + out := new(BMCSettings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BMCSettings) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BMCSettingsList) DeepCopyInto(out *BMCSettingsList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]BMCSettings, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BMCSettingsList. +func (in *BMCSettingsList) DeepCopy() *BMCSettingsList { + if in == nil { + return nil + } + out := new(BMCSettingsList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BMCSettingsList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BMCSettingsSpec) DeepCopyInto(out *BMCSettingsSpec) { + *out = *in + if in.SettingsMap != nil { + in, out := &in.SettingsMap, &out.SettingsMap + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.BMCRef != nil { + in, out := &in.BMCRef, &out.BMCRef + *out = new(v1.LocalObjectReference) + **out = **in + } + if in.ServerMaintenanceRefs != nil { + in, out := &in.ServerMaintenanceRefs, &out.ServerMaintenanceRefs + *out = make([]ServerMaintenanceRefItem, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BMCSettingsSpec. +func (in *BMCSettingsSpec) DeepCopy() *BMCSettingsSpec { + if in == nil { + return nil + } + out := new(BMCSettingsSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BMCSettingsStatus) DeepCopyInto(out *BMCSettingsStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BMCSettingsStatus. +func (in *BMCSettingsStatus) DeepCopy() *BMCSettingsStatus { + if in == nil { + return nil + } + out := new(BMCSettingsStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BMCSpec) DeepCopyInto(out *BMCSpec) { *out = *in @@ -417,6 +525,11 @@ func (in *BMCSpec) DeepCopyInto(out *BMCSpec) { *out = new(ConsoleProtocol) **out = **in } + if in.BMCSettingRef != nil { + in, out := &in.BMCSettingRef, &out.BMCSettingRef + *out = new(v1.LocalObjectReference) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BMCSpec. @@ -987,6 +1100,26 @@ func (in *ServerMaintenanceList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerMaintenanceRefItem) DeepCopyInto(out *ServerMaintenanceRefItem) { + *out = *in + if in.ServerMaintenanceRef != nil { + in, out := &in.ServerMaintenanceRef, &out.ServerMaintenanceRef + *out = new(v1.ObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerMaintenanceRefItem. +func (in *ServerMaintenanceRefItem) DeepCopy() *ServerMaintenanceRefItem { + if in == nil { + return nil + } + out := new(ServerMaintenanceRefItem) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServerMaintenanceSpec) DeepCopyInto(out *ServerMaintenanceSpec) { *out = *in diff --git a/bmc/bmc.go b/bmc/bmc.go index 459154e8..0014deed 100644 --- a/bmc/bmc.go +++ b/bmc/bmc.go @@ -5,6 +5,7 @@ package bmc import ( "context" + "encoding/json" "fmt" "net/http" @@ -24,6 +25,14 @@ const ( ManufacturerHPE Manufacturer = "HPE" ) +type SettingAttributeValueTypes string + +const ( + TypeInteger SettingAttributeValueTypes = "integer" + TypeString SettingAttributeValueTypes = "string" + TypeEnumerations SettingAttributeValueTypes = "enumeration" +) + // BMC defines an interface for interacting with a Baseboard Management Controller. type BMC interface { // PowerOn powers on the system. @@ -51,7 +60,10 @@ type BMC interface { GetSystems(ctx context.Context) ([]Server, error) // GetManager returns the manager - GetManager() (*Manager, error) + GetManager(UUID string) (*redfish.Manager, error) + + // Reset performs a reset on the Manager. + ResetManager(ctx context.Context, UUID string, resetType redfish.ResetType) error GetBootOrder(ctx context.Context, systemUUID string) ([]string, error) @@ -59,12 +71,22 @@ type BMC interface { GetBiosPendingAttributeValues(ctx context.Context, systemUUID string) (redfish.SettingsAttributes, error) + GetBMCAttributeValues(ctx context.Context, UUID string, attributes []string) (redfish.SettingsAttributes, error) + + GetBMCPendingAttributeValues(ctx context.Context, UUID string) (result redfish.SettingsAttributes, err error) + CheckBiosAttributes(attrs redfish.SettingsAttributes) (reset bool, err error) + CheckBMCAttributes(UUID string, attrs redfish.SettingsAttributes) (reset bool, err error) + SetBiosAttributesOnReset(ctx context.Context, systemUUID string, attributes redfish.SettingsAttributes) (err error) + SetBMCAttributesImediately(ctx context.Context, UUID string, attributes redfish.SettingsAttributes) (err error) + GetBiosVersion(ctx context.Context, systemUUID string) (string, error) + GetBMCVersion(ctx context.Context, UUID string) (string, error) + SetBootOrder(ctx context.Context, systemUUID string, order []string) error GetStorages(ctx context.Context, systemUUID string) ([]Storage, error) @@ -84,6 +106,27 @@ type BMC interface { WaitForServerPowerState(ctx context.Context, systemUUID string, powerState redfish.PowerState) error } +type OEMManagerInterface interface { + GetOEMBMCSettingAttribute(attributes []string) (redfish.SettingsAttributes, error) + GetBMCPendingAttributeValues() (redfish.SettingsAttributes, error) + CheckBMCAttributes(attributes redfish.SettingsAttributes) (bool, error) + GetObjFromUri(uri string, respObj any) ([]string, error) + UpdateBMCAttributesApplyAt(attrs redfish.SettingsAttributes, applyTime common.ApplyTime) error +} + +type OEMInterface interface { + GetUpdateRequestBody( + parameters *redfish.SimpleUpdateParameters, + ) *oem.SimpleUpdateRequestBody + GetUpdateTaskMonitorURI( + response *http.Response, + ) (string, error) + GetTaskMonitorDetails( + ctx context.Context, + taskMonitorResponse *http.Response, + ) (*redfish.Task, error) +} + type Entity struct { // ID uniquely identifies the resource. ID string `json:"Id"` @@ -118,8 +161,8 @@ type RegistryEntry struct { Attributes []RegistryEntryAttributes } -// BiosRegistry describes the Message Registry file locator Resource. -type BiosRegistry struct { +// Registry describes the Message Registry file locator Resource. +type Registry struct { common.Entity // ODataContext is the odata context. ODataContext string `json:"@odata.context"` @@ -257,19 +300,21 @@ type Manager struct { PowerState string State string MACAddress string + OemLinks json.RawMessage } -type OEMInterface interface { - GetUpdateRequestBody( - parameters *redfish.SimpleUpdateParameters, - ) *oem.SimpleUpdateRequestBody - GetUpdateTaskMonitorURI( - response *http.Response, - ) (string, error) - GetTaskMonitorDetails( - ctx context.Context, - taskMonitorResponse *http.Response, - ) (*redfish.Task, error) +func NewOEMManager(ooem *redfish.Manager, service *gofish.Service) (OEMManagerInterface, error) { + var OEMManager OEMManagerInterface + switch ooem.Manufacturer { + case string(ManufacturerDell): + OEMManager = &oem.DellIdracManager{ + BMC: ooem, + Service: service, + } + default: + return nil, fmt.Errorf("unsupported manufacturer: %v", ooem.Manufacturer) + } + return OEMManager, nil } func NewOEM(manufacturer string, service *gofish.Service) (OEMInterface, error) { diff --git a/bmc/common/helpers.go b/bmc/common/helpers.go new file mode 100644 index 00000000..6a4552f5 --- /dev/null +++ b/bmc/common/helpers.go @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package common + +import ( + "errors" + "fmt" + + "github.com/stmcginnis/gofish/redfish" +) + +// todo: merge with checkBiosAttribues after #298 +func CheckAttribues( + attrs redfish.SettingsAttributes, + filtered map[string]redfish.Attribute, +) (reset bool, err error) { + reset = false + var errs []error + //TODO: add more types like maps and Enumerations + for name, value := range attrs { + entryAttribute, ok := filtered[name] + if !ok { + errs = append(errs, fmt.Errorf("attribute %s not found or immutable/hidden", name)) + continue + } + if entryAttribute.ResetRequired { + reset = true + } + switch entryAttribute.Type { + case redfish.IntegerAttributeType: + if _, ok := value.(int); !ok { + errs = append( + errs, + fmt.Errorf( + "attribute '%s's' value '%v' has wrong type. needed '%s' for '%v'", + name, + value, + entryAttribute.Type, + entryAttribute, + )) + } + case redfish.StringAttributeType: + if _, ok := value.(string); !ok { + errs = append( + errs, + fmt.Errorf( + "attribute '%s's' value '%v' has wrong type. needed '%s' for '%v'", + name, + value, + entryAttribute.Type, + entryAttribute, + )) + } + case redfish.EnumerationAttributeType: + if _, ok := value.(string); !ok { + errs = append( + errs, + fmt.Errorf( + "attribute '%s's' value '%v' has wrong type. needed '%s' for '%v'", + name, + value, + entryAttribute.Type, + entryAttribute, + )) + break + } + var validEnum bool + for _, attrValue := range entryAttribute.Value { + if attrValue.ValueName == value.(string) { + validEnum = true + break + } + } + if !validEnum { + errs = append(errs, fmt.Errorf("attribute %s value is unknown. needed %v", name, entryAttribute.Value)) + } + default: + errs = append( + errs, + fmt.Errorf( + "attribute '%s's' value '%v' has wrong type. needed '%s' for '%v'", + name, + value, + entryAttribute.Type, + entryAttribute, + )) + } + } + return reset, errors.Join(errs...) +} diff --git a/bmc/mockup.go b/bmc/mockup.go index ebb0add8..89694800 100644 --- a/bmc/mockup.go +++ b/bmc/mockup.go @@ -13,6 +13,9 @@ type RedfishMockUps struct { BIOSUpgradingVersion string BIOSUpgradeTaskIndex int BIOSUpgradeTaskStatus []redfish.Task + + BMCSettingAttr map[string]map[string]any + PendingBMCSetting map[string]map[string]any } func (r *RedfishMockUps) InitializeDefaults() { @@ -20,6 +23,10 @@ func (r *RedfishMockUps) InitializeDefaults() { "abc": {"type": "string", "reboot": false, "value": "bar"}, "fooreboot": {"type": "integer", "reboot": true, "value": 123}, } + r.BMCSettingAttr = map[string]map[string]any{ + "abc": {"type": redfish.StringAttributeType, "reboot": false, "value": "bar"}, + "fooreboot": {"type": redfish.IntegerAttributeType, "reboot": true, "value": 123}, + } r.PendingBIOSSetting = map[string]map[string]any{} r.BIOSVersion = "" r.BIOSUpgradingVersion = "" @@ -55,6 +62,8 @@ func (r *RedfishMockUps) InitializeDefaults() { PercentComplete: 100, }, } + + r.PendingBMCSetting = map[string]map[string]any{} } func (r *RedfishMockUps) ResetBIOSSettings() { @@ -76,6 +85,18 @@ func (r *RedfishMockUps) ResetBIOSVersionUpdate() { r.BIOSVersion = "" } +func (r *RedfishMockUps) ResetPendingBMCSetting() { + r.PendingBMCSetting = map[string]map[string]any{} +} + +func (r *RedfishMockUps) ResetBMCSettings() { + r.BMCSettingAttr = map[string]map[string]any{ + "abc": {"type": redfish.StringAttributeType, "reboot": false, "value": "bar"}, + "fooreboot": {"type": redfish.IntegerAttributeType, "reboot": true, "value": 123}, + } + r.PendingBMCSetting = map[string]map[string]any{} +} + func InitMockUp() { UnitTestMockUps = &RedfishMockUps{} UnitTestMockUps.InitializeDefaults() diff --git a/bmc/oem/dell.go b/bmc/oem/dell.go index 71410795..4fde4d8f 100644 --- a/bmc/oem/dell.go +++ b/bmc/oem/dell.go @@ -6,11 +6,16 @@ package oem import ( "context" "encoding/json" + "errors" "fmt" "io" + "maps" "net/http" + "strings" + helpers "github.com/ironcore-dev/metal-operator/bmc/common" "github.com/stmcginnis/gofish" + "github.com/stmcginnis/gofish/common" "github.com/stmcginnis/gofish/redfish" ) @@ -60,3 +65,305 @@ func (r *Dell) GetTaskMonitorDetails(ctx context.Context, taskMonitorResponse *h return task, nil } + +type DellIdracManager struct { + BMC *redfish.Manager + Service *gofish.Service +} + +type DellAttributes struct { + Id string + Attributes redfish.SettingsAttributes + Settings common.Settings `json:"@Redfish.Settings"` + Etag string +} + +type DellManagerLinksOEM struct { + DellLinkAttributes common.Links `json:"DellAttributes"` + DellAttributesCount int `json:"DellAttributes@odata.count"` +} + +func (d *DellIdracManager) GetObjFromUri( + uri string, + respObj any, +) ([]string, error) { + resp, err := d.BMC.GetClient().Get(uri) + if err != nil { + return nil, err + } + defer resp.Body.Close() // nolint: errcheck + + rawBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + err = json.Unmarshal(rawBody, &respObj) + if err != nil { + return nil, err + } + return resp.Header["Etag"], nil +} + +func (d *DellIdracManager) getCurrentBMCSettingAttribute() ([]DellAttributes, error) { + + type temp struct { + DellOEMData DellManagerLinksOEM `json:"Dell"` + } + + tempData := &temp{} + err := json.Unmarshal(d.BMC.OemLinks, tempData) + if err != nil { + return nil, err + } + + // get all current attributes values for dell manager + BMCDellAttributes := []DellAttributes{} + var errs []error + for _, data := range tempData.DellOEMData.DellLinkAttributes { + BMCDellAttribute := &DellAttributes{} + eTag, err := d.GetObjFromUri(data.String(), BMCDellAttribute) + if err != nil { + errs = append(errs, err) + } + if eTag != nil { + BMCDellAttribute.Etag = eTag[0] + } + BMCDellAttributes = append(BMCDellAttributes, *BMCDellAttribute) + } + if len(errs) > 0 { + return nil, errors.Join(errs...) + } + + return BMCDellAttributes, nil + +} + +func (d *DellIdracManager) getFilteredBMCRegistryAttributes( + readOnly bool, + immutable bool, +) ( + filtered map[string]redfish.Attribute, + err error, +) { + // from the registriesAttribure, get the attributes which can be changed. + registries, err := d.Service.Registries() + if err != nil { + return nil, err + } + bmcRegistryAttribute := &redfish.AttributeRegistry{} + for _, registry := range registries { + if strings.Contains(registry.ID, "ManagerAttributeRegistry") { + _, err = d.GetObjFromUri(registry.Location[0].URI, bmcRegistryAttribute) + if err != nil { + return nil, err + } + break + } + } + // filter out immutable, readonly and hidden attributes + filteredAttr := make(map[string]redfish.Attribute) + for _, entry := range bmcRegistryAttribute.RegistryEntries.Attributes { + if entry.Immutable == immutable && entry.ReadOnly == readOnly && !entry.Hidden { + filteredAttr[entry.AttributeName] = entry + } + } + + return filteredAttr, nil +} + +func (d *DellIdracManager) GetOEMBMCSettingAttribute( + attributes []string, +) (redfish.SettingsAttributes, error) { + + BMCDellAttributes, err := d.getCurrentBMCSettingAttribute() + if err != nil { + return nil, err + } + + // merge al the current attributes to single map, to help fetch it later + var mergedBMCAttributes = make(redfish.SettingsAttributes) + for _, BMCattributeValue := range BMCDellAttributes { + for k, v := range BMCattributeValue.Attributes { + if _, ok := mergedBMCAttributes[k]; !ok { + mergedBMCAttributes[k] = v + } else { + return nil, + fmt.Errorf("duplicate attributes in BMC settings are not supported duplicate key %v. in attribute %v", + k, + BMCDellAttributes, + ) + } + } + } + + filteredAttr, err := d.getFilteredBMCRegistryAttributes(false, false) + if err != nil { + return nil, err + } + + if len(filteredAttr) == 0 { + return nil, fmt.Errorf("'ManagerAttributeRegistry' not found") + } + + // from the gives attributes to change, find the ones which can be changed and get current value for them + result := make(redfish.SettingsAttributes, len(attributes)) + var errs []error + for _, name := range attributes { + if entry, ok := filteredAttr[name]; ok { + // enumerations current setting comtains display name. + // need to be checked with the actual value rather than the display value + // as the settings provided will have actual values. + // replace display values with actual values + if strings.ToLower(string(entry.Type)) == string(redfish.EnumerationAttributeType) { + for _, attrValue := range entry.Value { + if attrValue.ValueDisplayName == mergedBMCAttributes[name] { + result[name] = attrValue.ValueName + break + } + } + if _, ok := result[name]; !ok { + errs = append( + errs, + fmt.Errorf( + "current setting '%v' for key '%v' not found in possible values for it (%v)", + mergedBMCAttributes[name], + name, + entry.Value, + )) + } + } else { + result[name] = mergedBMCAttributes[name] + } + } else { + // possible error in settings key + errs = append(errs, fmt.Errorf("setting key '%v' not found in possible settings", name)) + } + } + if len(errs) > 0 { + return result, fmt.Errorf( + "some errors found in the settings '%v'.\nPossible settings %v", + errs, + maps.Keys(filteredAttr), + ) + } + + return result, nil +} + +func (d *DellIdracManager) UpdateBMCAttributesApplyAt( + attrs redfish.SettingsAttributes, + applyTime common.ApplyTime, +) error { + + BMCattributeValues, err := d.getCurrentBMCSettingAttribute() + if err != nil { + return err + } + + payloads := make(map[string]redfish.SettingsAttributes, len(BMCattributeValues)) + for key, value := range attrs { + for _, eachAttr := range BMCattributeValues { + if _, ok := eachAttr.Attributes[key]; ok { + if data, ok := payloads[eachAttr.Settings.SettingsObject.String()]; ok { + data[key] = value + } else { + payloads[eachAttr.Settings.SettingsObject.String()] = make(redfish.SettingsAttributes) + payloads[eachAttr.Settings.SettingsObject.String()][key] = value + } + // keys cant be duplicate. Hence, break once its already found in one of idrac settings sub types + break + } + } + } + + // If there are any allowed updates, try to send updates to the system and + // return the result. + if len(payloads) > 0 { + var errs []error + // for each sub type, apply the settings + for settingPath, payload := range payloads { + // fetch the etag required for settingPath + etag, err := func() ([]string, error) { + resp, err := d.BMC.GetClient().Get(settingPath) + if err != nil { + return nil, err + } + defer resp.Body.Close() // nolint: errcheck + return resp.Header["Etag"], nil + }() + + if err != nil { + errs = append(errs, fmt.Errorf("failed to get Etag for %v. error %v", settingPath, err)) + continue + } + + data := map[string]interface{}{"Attributes": payload} + if applyTime != "" { + data["@Redfish.SettingsApplyTime"] = map[string]string{"ApplyTime": string(applyTime)} + } + var header = make(map[string]string) + if etag != nil { + header["If-Match"] = etag[0] + } + + err = func() error { + resp, err := d.BMC.GetClient().PatchWithHeaders(settingPath, data, header) + if err != nil { + return err + } + defer resp.Body.Close() // nolint: errcheck + return nil + }() + if err != nil { + errs = append(errs, fmt.Errorf("failed to patch settings at %v. error %v", settingPath, err)) + continue + } + } + + if len(errs) > 0 { + return fmt.Errorf("some settings failed to apply %v", errs) + } + } + return nil +} + +func (d *DellIdracManager) GetBMCPendingAttributeValues() (redfish.SettingsAttributes, error) { + + BMCattributeValues, err := d.getCurrentBMCSettingAttribute() + if err != nil { + return nil, err + } + + var mergedPendingBMCAttributes = make(redfish.SettingsAttributes) + var tBMCSetting struct { + Attributes redfish.SettingsAttributes `json:"Attributes"` + } + + for _, BMCattributeValue := range BMCattributeValues { + _, err := d.GetObjFromUri(BMCattributeValue.Settings.SettingsObject.String(), &tBMCSetting) + if err != nil { + return nil, err + } + for k, v := range tBMCSetting.Attributes { + if _, ok := mergedPendingBMCAttributes[k]; !ok { + mergedPendingBMCAttributes[k] = v + } else { + return nil, fmt.Errorf("duplicate pending attributes in Idrac settings are not supported %v", k) + } + } + } + + return mergedPendingBMCAttributes, nil +} + +func (d *DellIdracManager) CheckBMCAttributes(attributes redfish.SettingsAttributes) (bool, error) { + filteredAttr, err := d.getFilteredBMCRegistryAttributes(false, false) + if err != nil { + return false, err + } + if len(filteredAttr) == 0 { + return false, nil + } + return helpers.CheckAttribues(attributes, filteredAttr) +} diff --git a/bmc/redfish.go b/bmc/redfish.go index 1c34d640..7850a270 100644 --- a/bmc/redfish.go +++ b/bmc/redfish.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "net/http" + "slices" "strings" "time" @@ -196,7 +197,7 @@ func (r *RedfishBMC) SetPXEBootOnce(ctx context.Context, systemUUID string) erro return nil } -func (r *RedfishBMC) GetManager() (*Manager, error) { +func (r *RedfishBMC) GetManager(bmcUUID string) (*redfish.Manager, error) { if r.client == nil { return nil, fmt.Errorf("no client found") } @@ -204,21 +205,62 @@ func (r *RedfishBMC) GetManager() (*Manager, error) { if err != nil { return nil, fmt.Errorf("failed to get managers: %w", err) } + if len(managers) == 0 { + return nil, fmt.Errorf("zero managers found") + } + + if len(bmcUUID) == 0 { + // take the first one available + return managers[0], nil + } + for _, m := range managers { - // TODO: always take the first for now. - return &Manager{ - UUID: m.UUID, - Manufacturer: m.Manufacturer, - State: string(m.Status.State), - PowerState: string(m.PowerState), - SerialNumber: m.SerialNumber, - FirmwareVersion: m.FirmwareVersion, - SKU: m.PartNumber, - Model: m.Model, - }, nil + if bmcUUID == m.UUID { + return m, nil + } + } + return nil, fmt.Errorf("matching managers not found for UUID %v", bmcUUID) +} + +func (r *RedfishBMC) getOEMManager(bmcUUID string) (OEMManagerInterface, error) { + manager, err := r.GetManager(bmcUUID) + if err != nil { + return nil, fmt.Errorf("not able to Manager %v", err) + } + + // some vendors (like Dell) does not publich this. get through the system + if manager.Manufacturer == "" { + manufacturer, err := r.getSystemManufacturer() + if err != nil { + return nil, fmt.Errorf("not able to determine manufacturer: %v", err) + } + manager.Manufacturer = manufacturer + } + + // togo: improve. as of now use first one similar to r.GetManager() + oemManager, err := NewOEMManager(manager, r.client.Service) + if err != nil { + return nil, fmt.Errorf("not able create oem Manager: %v", err) + } + + return oemManager, nil +} + +func (r *RedfishBMC) ResetManager(ctx context.Context, bmcUUID string, resetType redfish.ResetType) error { + + manager, err := r.GetManager(bmcUUID) + if err != nil { + return fmt.Errorf("failed to get managers: %w", err) + } + if len(manager.SupportedResetTypes) > 0 && !slices.Contains(manager.SupportedResetTypes, resetType) { + return fmt.Errorf("reset type of %v is not supported for manager %v", resetType, manager.UUID) } - return nil, err + err = manager.Reset(resetType) + if err != nil { + return fmt.Errorf("failed to reset managers %v with error: %w", manager.UUID, err) + } + return nil } // GetSystemInfo retrieves information about the system using Redfish. @@ -284,6 +326,14 @@ func (r *RedfishBMC) GetBiosVersion(ctx context.Context, systemUUID string) (str return system.BIOSVersion, nil } +func (r *RedfishBMC) GetBMCVersion(ctx context.Context, bmcUUID string) (string, error) { + manager, err := r.GetManager(bmcUUID) + if err != nil { + return "", err + } + return manager.FirmwareVersion, nil +} + func (r *RedfishBMC) GetBiosAttributeValues( ctx context.Context, systemUUID string, @@ -316,6 +366,25 @@ func (r *RedfishBMC) GetBiosAttributeValues( return result, err } +func (r *RedfishBMC) GetBMCAttributeValues( + ctx context.Context, + bmcUUID string, + attributes []string, +) ( + result redfish.SettingsAttributes, + err error, +) { + if len(attributes) == 0 { + return nil, nil + } + oemManager, err := r.getOEMManager(bmcUUID) + if err != nil { + return nil, err + } + + return oemManager.GetOEMBMCSettingAttribute(attributes) +} + func (r *RedfishBMC) GetBiosPendingAttributeValues( ctx context.Context, systemUUID string, @@ -384,6 +453,21 @@ func (r *RedfishBMC) GetEntityFromUri(uri string, client common.Client, entity a return json.Unmarshal(RespRawBody, &entity) } +func (r *RedfishBMC) GetBMCPendingAttributeValues( + ctx context.Context, + bmcUUID string, +) ( + result redfish.SettingsAttributes, + err error, +) { + oemManager, err := r.getOEMManager(bmcUUID) + if err != nil { + return nil, err + } + + return oemManager.GetBMCPendingAttributeValues() +} + // SetBiosAttributesOnReset sets given bios attributes. func (r *RedfishBMC) SetBiosAttributesOnReset( ctx context.Context, @@ -406,6 +490,21 @@ func (r *RedfishBMC) SetBiosAttributesOnReset( return bios.UpdateBiosAttributesApplyAt(attrs, common.OnResetApplyTime) } +func (r *RedfishBMC) SetBMCAttributesImediately( + ctx context.Context, + bmcUUID string, + attributes redfish.SettingsAttributes, +) (err error) { + if len(attributes) == 0 { + return nil + } + oemManager, err := r.getOEMManager(bmcUUID) + if err != nil { + return err + } + return oemManager.UpdateBMCAttributesApplyAt(attributes, common.ImmediateApplyTime) +} + // SetBootOrder sets bios boot order func (r *RedfishBMC) SetBootOrder(ctx context.Context, systemUUID string, bootOrder []string) error { system, err := r.getSystemByUUID(ctx, systemUUID) @@ -429,7 +528,7 @@ func (r *RedfishBMC) getFilteredBiosRegistryAttributes( err error, ) { registries, err := r.client.Service.Registries() - biosRegistry := &BiosRegistry{} + biosRegistry := &Registry{} for _, registry := range registries { if strings.Contains(registry.ID, "BiosAttributeRegistry") { err = registry.Get(r.client, registry.Location[0].URI, biosRegistry) @@ -451,7 +550,6 @@ func (r *RedfishBMC) getFilteredBiosRegistryAttributes( // CheckBiosAttributes checks if the attributes need to reboot when changed and are the correct type. func (r *RedfishBMC) CheckBiosAttributes(attrs redfish.SettingsAttributes) (reset bool, err error) { reset = false - // filter out immutable, readonly and hidden attributes filtered, err := r.getFilteredBiosRegistryAttributes(false, false) if err != nil { return reset, err @@ -538,6 +636,29 @@ func (r *RedfishBMC) checkAttribues( return reset, errors.Join(errs...) } +func (r *RedfishBMC) getSystemManufacturer() (string, error) { + systems, err := r.client.Service.Systems() + if err != nil { + return "", err + } + if len(systems) > 0 { + return systems[0].Manufacturer, nil + } + + return "", fmt.Errorf("no system found to determine the Manufacturer") +} + +// check if the arrtibutes need to reboot when changed, and are correct type. +// supported attrType, bmc and bios +func (r *RedfishBMC) CheckBMCAttributes(bmcUUID string, attrs redfish.SettingsAttributes) (reset bool, err error) { + oemManager, err := r.getOEMManager(bmcUUID) + if err != nil { + return false, err + } + + return oemManager.CheckBMCAttributes(attrs) +} + func (r *RedfishBMC) GetStorages(ctx context.Context, systemUUID string) ([]Storage, error) { system, err := r.getSystemByUUID(ctx, systemUUID) if err != nil { diff --git a/bmc/redfish_local.go b/bmc/redfish_local.go index 4b9b3357..85411a2f 100644 --- a/bmc/redfish_local.go +++ b/bmc/redfish_local.go @@ -8,6 +8,7 @@ import ( "fmt" "time" + "github.com/ironcore-dev/metal-operator/bmc/common" "github.com/stmcginnis/gofish/redfish" ctrl "sigs.k8s.io/controller-runtime" ) @@ -243,3 +244,136 @@ func (r *RedfishLocalBMC) GetBiosUpgradeTask( } return &UnitTestMockUps.BIOSUpgradeTaskStatus[UnitTestMockUps.BIOSUpgradeTaskIndex], nil } + +func (r *RedfishLocalBMC) ResetManager(ctx context.Context, UUID string, resetType redfish.ResetType) error { + + // mock the bmc update here with timed delay + go func() { + if len(UnitTestMockUps.PendingBMCSetting) > 0 { + time.Sleep(150 * time.Millisecond) + for key, data := range UnitTestMockUps.PendingBMCSetting { + if _, ok := UnitTestMockUps.BMCSettingAttr[key]; ok { + UnitTestMockUps.BMCSettingAttr[key] = data + } + } + UnitTestMockUps.ResetPendingBMCSetting() + } + }() + + return nil +} + +// mock SetBiosAttributesOnReset sets given bios attributes for unit testing. +func (r *RedfishLocalBMC) SetBMCAttributesImediately( + ctx context.Context, + UUID string, + attributes redfish.SettingsAttributes, +) (err error) { + attrs := make(map[string]interface{}, len(attributes)) + for name, value := range attributes { + attrs[name] = value + } + + for key, attrData := range attributes { + if AttributesData, ok := UnitTestMockUps.BMCSettingAttr[key]; ok { + if reboot, ok := AttributesData["reboot"]; ok && !reboot.(bool) { + // if reboot not needed, set the attribute immediately. + AttributesData["value"] = attrData + } else { + // if reboot needed, set the attribute at next power on. + UnitTestMockUps.PendingBMCSetting[key] = map[string]any{ + "type": AttributesData["type"], + "reboot": AttributesData["reboot"], + "value": attrData, + } + } + } + } + return nil +} + +func (r *RedfishLocalBMC) GetBMCAttributeValues( + ctx context.Context, + UUID string, + attributes []string, +) ( + result redfish.SettingsAttributes, + err error, +) { + if len(attributes) == 0 { + return + } + filteredAttr, err := r.getFilteredBMCRegistryAttributes(false, false) + if err != nil { + return + } + result = make(redfish.SettingsAttributes, len(attributes)) + for _, name := range attributes { + if _, ok := filteredAttr[name]; ok { + if AttributesData, ok := UnitTestMockUps.BMCSettingAttr[name]; ok { + result[name] = AttributesData["value"] + } + } + } + return result, nil +} + +func (r *RedfishLocalBMC) GetBMCPendingAttributeValues( + ctx context.Context, + systemUUID string, +) ( + redfish.SettingsAttributes, + error, +) { + if len(UnitTestMockUps.PendingBMCSetting) == 0 { + return redfish.SettingsAttributes{}, nil + } + + result := make(redfish.SettingsAttributes, len(UnitTestMockUps.PendingBMCSetting)) + + for key, data := range UnitTestMockUps.PendingBMCSetting { + result[key] = data["value"] + } + + return result, nil +} + +func (r *RedfishLocalBMC) getFilteredBMCRegistryAttributes( + readOnly bool, + immutable bool, +) ( + filtered map[string]redfish.Attribute, + err error, +) { + filtered = make(map[string]redfish.Attribute) + if len(UnitTestMockUps.BMCSettingAttr) == 0 { + return filtered, fmt.Errorf("no bmc setting attributes found") + } + for name, AttributesData := range UnitTestMockUps.BMCSettingAttr { + data := redfish.Attribute{} + data.AttributeName = name + data.Immutable = immutable + data.ReadOnly = readOnly + data.Type = AttributesData["type"].(redfish.AttributeType) + data.ResetRequired = AttributesData["reboot"].(bool) + filtered[name] = data + } + + return filtered, err +} + +// check if the arrtibutes need to reboot when changed, and are correct type. +// supported attrType, bmc and bios +func (r *RedfishLocalBMC) CheckBMCAttributes(UUID string, attrs redfish.SettingsAttributes) (reset bool, err error) { + reset = false + filtered, err := r.getFilteredBMCRegistryAttributes(false, false) + + if err != nil { + return reset, err + } + + if len(filtered) == 0 { + return reset, err + } + return common.CheckAttribues(attrs, filtered) +} diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 8fea63a6..5bd240e3 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -410,6 +410,30 @@ func main() { // nolint: gocyclo os.Exit(1) } } + if err = (&controller.BMCSettingsReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + ManagerNamespace: managerNamespace, + ResyncInterval: serverResyncInterval, + Insecure: insecure, + BMCOptions: bmc.Options{ + BasicAuth: true, + PowerPollingInterval: powerPollingInterval, + PowerPollingTimeout: powerPollingTimeout, + ResourcePollingInterval: resourcePollingInterval, + ResourcePollingTimeout: resourcePollingTimeout, + }, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "BMCSettings") + os.Exit(1) + } + // nolint:goconst + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = webhookmetalv1alpha1.SetupBMCSettingsWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "BMCSettings") + os.Exit(1) + } + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/config/crd/bases/metal.ironcore.dev_bmcs.yaml b/config/crd/bases/metal.ironcore.dev_bmcs.yaml index e9ecf8af..da30ee85 100644 --- a/config/crd/bases/metal.ironcore.dev_bmcs.yaml +++ b/config/crd/bases/metal.ironcore.dev_bmcs.yaml @@ -103,6 +103,27 @@ spec: type: string type: object x-kubernetes-map-type: atomic + bmcSettingsRef: + description: |- + BMCSettingRef is a reference to a BMCSettings object that specifies + the BMC configuration for this BMC. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + bmcUUID: + description: |- + bmcUUID is the unique identifier for the BMC as defined in REDFISH API. + This field is optional and can be omitted, controller will choose the first avaialbe Manager + type: string consoleProtocol: description: |- ConsoleProtocol specifies the protocol to be used for console access to the BMC. diff --git a/config/crd/bases/metal.ironcore.dev_bmcsettings.yaml b/config/crd/bases/metal.ironcore.dev_bmcsettings.yaml new file mode 100644 index 00000000..eda789fc --- /dev/null +++ b/config/crd/bases/metal.ironcore.dev_bmcsettings.yaml @@ -0,0 +1,155 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: bmcsettings.metal.ironcore.dev +spec: + group: metal.ironcore.dev + names: + kind: BMCSettings + listKind: BMCSettingsList + plural: bmcsettings + singular: bmcsettings + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.bmcSettings.version + name: BMCVersion + type: string + - jsonPath: .status.state + name: State + type: string + - jsonPath: .spec.BMCRef.name + name: BMCRef + type: string + - jsonPath: .spec.serverRef.name + name: ServerRef + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: BMCSettings is the Schema for the BMCSettings API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: BMCSettingsSpec defines the desired state of BMCSettings. + properties: + BMCRef: + description: BMCRef is a reference to a specific BMC to apply setting + to. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: serverRef is immutable + rule: self == oldSelf + serverMaintenancePolicy: + description: "ServerMaintenancePolicy is maintenance policy to be + enforced on the server when applying setting.\nServerMaintenancePolicyOwnerApproval + is asking for User approval for changing BMC settings\n\tnote: User + approval is only enforced for server's which are reserved state\nServerMaintenancePolicyEnforced + will will bypass user approval and apply setting directly" + type: string + serverMaintenanceRefs: + description: ServerMaintenanceRefs are references to a ServerMaintenance + objects that Controller has requested for the each of the related + server. + items: + properties: + serverMaintenanceRef: + description: ObjectReference contains enough information to + let you inspect or modify the referred object. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + type: object + type: array + settings: + additionalProperties: + type: string + description: SettingsMap contains bmc settings as map + type: object + version: + description: Version contains BMC version this settings applies to + type: string + required: + - version + type: object + status: + description: BMCSettingsStatus defines the observed state of BMCSettings. + properties: + state: + description: State represents the current state of the BMC configuration + task. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 1f3a4587..eeb48e04 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -11,6 +11,7 @@ resources: - bases/metal.ironcore.dev_servermaintenances.yaml - bases/metal.ironcore.dev_biossettings.yaml - bases/metal.ironcore.dev_biosversions.yaml +- bases/metal.ironcore.dev_bmcsettings.yaml #+kubebuilder:scaffold:crdkustomizeresource patches: diff --git a/config/rbac/bmcsettings_admin_role.yaml b/config/rbac/bmcsettings_admin_role.yaml new file mode 100644 index 00000000..6ab5aead --- /dev/null +++ b/config/rbac/bmcsettings_admin_role.yaml @@ -0,0 +1,27 @@ +# This rule is not used by the project metal-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants full permissions ('*') over metal.ironcore.dev. +# This role is intended for users authorized to modify roles and bindings within the cluster, +# enabling them to delegate specific permissions to other users or groups as needed. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: metal-operator + app.kubernetes.io/managed-by: kustomize + name: bmcsettings-admin-role +rules: +- apiGroups: + - metal.ironcore.dev + resources: + - bmcsettings + verbs: + - '*' +- apiGroups: + - metal.ironcore.dev + resources: + - bmcsettings/status + verbs: + - get diff --git a/config/rbac/bmcsettings_editor_role.yaml b/config/rbac/bmcsettings_editor_role.yaml new file mode 100644 index 00000000..35b8896d --- /dev/null +++ b/config/rbac/bmcsettings_editor_role.yaml @@ -0,0 +1,33 @@ +# This rule is not used by the project metal-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants permissions to create, update, and delete resources within the metal.ironcore.dev. +# This role is intended for users who need to manage these resources +# but should not control RBAC or manage permissions for others. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: metal-operator + app.kubernetes.io/managed-by: kustomize + name: bmcsettings-editor-role +rules: +- apiGroups: + - metal.ironcore.dev + resources: + - bmcsettings + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - metal.ironcore.dev + resources: + - bmcsettings/status + verbs: + - get diff --git a/config/rbac/bmcsettings_viewer_role.yaml b/config/rbac/bmcsettings_viewer_role.yaml new file mode 100644 index 00000000..4e835f3a --- /dev/null +++ b/config/rbac/bmcsettings_viewer_role.yaml @@ -0,0 +1,29 @@ +# This rule is not used by the project metal-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants read-only access to metal.ironcore.dev resources. +# This role is intended for users who need visibility into these resources +# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: metal-operator + app.kubernetes.io/managed-by: kustomize + name: bmcsettings-viewer-role +rules: +- apiGroups: + - metal.ironcore.dev + resources: + - bmcsettings + verbs: + - get + - list + - watch +- apiGroups: + - metal.ironcore.dev + resources: + - bmcsettings/status + verbs: + - get diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index 240dda73..13b6b24c 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -25,6 +25,9 @@ resources: - biosversion_admin_role.yaml - biosversion_editor_role.yaml - biosversion_viewer_role.yaml +- bmcsettings_admin_role.yaml +- bmcsettings_editor_role.yaml +- bmcsettings_viewer_role.yaml - servermaintenance_admin_role.yaml - servermaintenance_editor_role.yaml - servermaintenance_viewer_role.yaml diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 36046666..f7afda99 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -35,6 +35,7 @@ rules: - biosversions - bmcs - bmcsecrets + - bmcsettings - endpoints - serverbootconfigurations - serverclaims @@ -56,6 +57,7 @@ rules: - biosversions/finalizers - bmcs/finalizers - bmcsecrets/finalizers + - bmcsettings/finalizers - endpoints/finalizers - serverbootconfigurations/finalizers - serverclaims/finalizers @@ -70,6 +72,7 @@ rules: - biosversions/status - bmcs/status - bmcsecrets/status + - bmcsettings/status - endpoints/status - serverbootconfigurations/status - serverclaims/status diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index ec73d22f..8e454e03 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -9,4 +9,5 @@ resources: - metal_v1alpha1_servermaintenance.yaml - metal_v1alpha1_biossettings.yaml - metal_v1alpha1_biosversion.yaml +- metal_v1alpha1_bmcsettings.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/metal_v1alpha1_bmcsettings.yaml b/config/samples/metal_v1alpha1_bmcsettings.yaml new file mode 100644 index 00000000..3f8a491e --- /dev/null +++ b/config/samples/metal_v1alpha1_bmcsettings.yaml @@ -0,0 +1,15 @@ +apiVersion: metal.ironcore.dev/v1alpha1 +kind: BMCSettings +metadata: + labels: + app.kubernetes.io/name: metal-operator + app.kubernetes.io/managed-by: kustomize + name: bmcsettings-sample +spec: + serverRef: + name: endpoint-sample-system-0 + bmcSettingsSpec: + version: 7.00.00.171 + settings: + ThermalSettings.1.FanSpeedOffset: "3" + serverMaintenancePolicyType: Enforced diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index dded248b..ea4e54df 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -21,6 +21,7 @@ webhooks: operations: - CREATE - UPDATE + - DELETE resources: - biossettings sideEffects: None @@ -45,6 +46,27 @@ webhooks: resources: - biosversions sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-metal-ironcore-dev-v1alpha1-bmcsettings + failurePolicy: Fail + name: vbmcsettings-v1alpha1.kb.io + rules: + - apiGroups: + - metal.ironcore.dev + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - bmcsettings + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/dist/chart/templates/crd/metal.ironcore.dev_bmcs.yaml b/dist/chart/templates/crd/metal.ironcore.dev_bmcs.yaml index f9050158..b014fb59 100755 --- a/dist/chart/templates/crd/metal.ironcore.dev_bmcs.yaml +++ b/dist/chart/templates/crd/metal.ironcore.dev_bmcs.yaml @@ -124,6 +124,27 @@ spec: type: string type: object x-kubernetes-map-type: atomic + bmcSettingsRef: + description: |- + BMCSettingRef is a reference to a BMCSettings object that specifies + the BMC configuration for this BMC. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + bmcUUID: + description: |- + bmcUUID is the unique identifier for the BMC as defined in REDFISH API. + This field is optional and can be omitted, controller will choose the first avaialbe Manager + type: string consoleProtocol: description: |- ConsoleProtocol specifies the protocol to be used for console access to the BMC. diff --git a/dist/chart/templates/crd/metal.ironcore.dev_bmcsettings.yaml b/dist/chart/templates/crd/metal.ironcore.dev_bmcsettings.yaml new file mode 100755 index 00000000..e37e0aa4 --- /dev/null +++ b/dist/chart/templates/crd/metal.ironcore.dev_bmcsettings.yaml @@ -0,0 +1,162 @@ +{{- if .Values.crd.enable }} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + labels: + {{- include "chart.labels" . | nindent 4 }} + annotations: + {{- if .Values.crd.keep }} + "helm.sh/resource-policy": keep + {{- end }} + controller-gen.kubebuilder.io/version: v0.18.0 + name: bmcsettings.metal.ironcore.dev +spec: + group: metal.ironcore.dev + names: + kind: BMCSettings + listKind: BMCSettingsList + plural: bmcsettings + singular: bmcsettings + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.bmcSettings.version + name: BMCVersion + type: string + - jsonPath: .status.state + name: State + type: string + - jsonPath: .spec.BMCRef.name + name: BMCRef + type: string + - jsonPath: .spec.serverRef.name + name: ServerRef + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: BMCSettings is the Schema for the BMCSettings API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: BMCSettingsSpec defines the desired state of BMCSettings. + properties: + BMCRef: + description: BMCRef is a reference to a specific BMC to apply setting + to. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: serverRef is immutable + rule: self == oldSelf + serverMaintenancePolicy: + description: "ServerMaintenancePolicy is maintenance policy to be + enforced on the server when applying setting.\nServerMaintenancePolicyOwnerApproval + is asking for User approval for changing BMC settings\n\tnote: User + approval is only enforced for server's which are reserved state\nServerMaintenancePolicyEnforced + will will bypass user approval and apply setting directly" + type: string + serverMaintenanceRefs: + description: ServerMaintenanceRefs are references to a ServerMaintenance + objects that Controller has requested for the each of the related + server. + items: + properties: + serverMaintenanceRef: + description: ObjectReference contains enough information to + let you inspect or modify the referred object. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + type: object + type: array + settings: + additionalProperties: + type: string + description: SettingsMap contains bmc settings as map + type: object + version: + description: Version contains BMC version this settings applies to + type: string + required: + - version + type: object + status: + description: BMCSettingsStatus defines the observed state of BMCSettings. + properties: + state: + description: State represents the current state of the BMC configuration + task. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +{{- end -}} diff --git a/dist/chart/templates/rbac/bmcsettings_admin_role.yaml b/dist/chart/templates/rbac/bmcsettings_admin_role.yaml new file mode 100755 index 00000000..61fc4931 --- /dev/null +++ b/dist/chart/templates/rbac/bmcsettings_admin_role.yaml @@ -0,0 +1,28 @@ +{{- if .Values.rbac.enable }} +# This rule is not used by the project metal-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants full permissions ('*') over metal.ironcore.dev. +# This role is intended for users authorized to modify roles and bindings within the cluster, +# enabling them to delegate specific permissions to other users or groups as needed. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + {{- include "chart.labels" . | nindent 4 }} + name: bmcsettings-admin-role +rules: +- apiGroups: + - metal.ironcore.dev + resources: + - bmcsettings + verbs: + - '*' +- apiGroups: + - metal.ironcore.dev + resources: + - bmcsettings/status + verbs: + - get +{{- end -}} diff --git a/dist/chart/templates/rbac/bmcsettings_editor_role.yaml b/dist/chart/templates/rbac/bmcsettings_editor_role.yaml new file mode 100755 index 00000000..c71b7214 --- /dev/null +++ b/dist/chart/templates/rbac/bmcsettings_editor_role.yaml @@ -0,0 +1,34 @@ +{{- if .Values.rbac.enable }} +# This rule is not used by the project metal-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants permissions to create, update, and delete resources within the metal.ironcore.dev. +# This role is intended for users who need to manage these resources +# but should not control RBAC or manage permissions for others. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + {{- include "chart.labels" . | nindent 4 }} + name: bmcsettings-editor-role +rules: +- apiGroups: + - metal.ironcore.dev + resources: + - bmcsettings + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - metal.ironcore.dev + resources: + - bmcsettings/status + verbs: + - get +{{- end -}} diff --git a/dist/chart/templates/rbac/bmcsettings_viewer_role.yaml b/dist/chart/templates/rbac/bmcsettings_viewer_role.yaml new file mode 100755 index 00000000..1011dd1b --- /dev/null +++ b/dist/chart/templates/rbac/bmcsettings_viewer_role.yaml @@ -0,0 +1,30 @@ +{{- if .Values.rbac.enable }} +# This rule is not used by the project metal-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants read-only access to metal.ironcore.dev resources. +# This role is intended for users who need visibility into these resources +# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + {{- include "chart.labels" . | nindent 4 }} + name: bmcsettings-viewer-role +rules: +- apiGroups: + - metal.ironcore.dev + resources: + - bmcsettings + verbs: + - get + - list + - watch +- apiGroups: + - metal.ironcore.dev + resources: + - bmcsettings/status + verbs: + - get +{{- end -}} diff --git a/dist/chart/templates/rbac/role.yaml b/dist/chart/templates/rbac/role.yaml index 65aa8e3f..24f432c9 100755 --- a/dist/chart/templates/rbac/role.yaml +++ b/dist/chart/templates/rbac/role.yaml @@ -38,6 +38,7 @@ rules: - biosversions - bmcs - bmcsecrets + - bmcsettings - endpoints - serverbootconfigurations - serverclaims @@ -59,6 +60,7 @@ rules: - biosversions/finalizers - bmcs/finalizers - bmcsecrets/finalizers + - bmcsettings/finalizers - endpoints/finalizers - serverbootconfigurations/finalizers - serverclaims/finalizers @@ -73,6 +75,7 @@ rules: - biosversions/status - bmcs/status - bmcsecrets/status + - bmcsettings/status - endpoints/status - serverbootconfigurations/status - serverclaims/status diff --git a/dist/chart/templates/webhook/webhooks.yaml b/dist/chart/templates/webhook/webhooks.yaml index 49434018..de75b98e 100644 --- a/dist/chart/templates/webhook/webhooks.yaml +++ b/dist/chart/templates/webhook/webhooks.yaml @@ -25,6 +25,7 @@ webhooks: - operations: - CREATE - UPDATE + - DELETE apiGroups: - metal.ironcore.dev apiVersions: @@ -52,6 +53,27 @@ webhooks: - v1alpha1 resources: - biosversions + - name: vbmcsettings-v1alpha1.kb.io + clientConfig: + service: + name: metal-operator-webhook-service + namespace: {{ .Release.Namespace }} + path: /validate-metal-ironcore-dev-v1alpha1-bmcsettings + failurePolicy: Fail + sideEffects: None + admissionReviewVersions: + - v1 + rules: + - operations: + - CREATE + - UPDATE + - DELETE + apiGroups: + - metal.ironcore.dev + apiVersions: + - v1alpha1 + resources: + - bmcsettings - name: vendpoint-v1alpha1.kb.io clientConfig: service: diff --git a/docs/api-reference/api.md b/docs/api-reference/api.md index 2dea8676..722eace8 100644 --- a/docs/api-reference/api.md +++ b/docs/api-reference/api.md @@ -665,6 +665,19 @@ BMCSpec
+bmcUUID+ +string + + |
+
+(Optional)
+ bmcUUID is the unique identifier for the BMC as defined in REDFISH API. +This field is optional and can be omitted, controller will choose the first avaialbe Manager + |
+
endpointRef@@ -736,6 +749,20 @@ ConsoleProtocol This field is optional and can be omitted if console access is not required. |
|
+bmcSettingsRef+ + +Kubernetes core/v1.LocalObjectReference + + + |
+
+ BMCSettingRef is a reference to a BMCSettings object that specifies +the BMC configuration for this BMC. + |
+
BMCSettings is the Schema for the BMCSettings API.
+| Field | +Description | +||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
+metadata+ + +Kubernetes meta/v1.ObjectMeta + + + |
+
+Refer to the Kubernetes API documentation for the fields of the
+metadata field.
+ |
+||||||||||
+spec+ + +BMCSettingsSpec + + + |
+
+ + +
|
+||||||||||
+status+ + +BMCSettingsStatus + + + |
++ | +
+(Appears on:BMCSettings) +
+BMCSettingsSpec defines the desired state of BMCSettings.
+| Field | +Description | +
|---|---|
+version+ +string + + |
+
+ Version contains BMC version this settings applies to + |
+
+settings+ +map[string]string + + |
+
+(Optional)
+ SettingsMap contains bmc settings as map + |
+
+BMCRef+ + +Kubernetes core/v1.LocalObjectReference + + + |
+
+ BMCRef is a reference to a specific BMC to apply setting to. + |
+
+serverMaintenancePolicy+ + +ServerMaintenancePolicy + + + |
+
+ ServerMaintenancePolicy is maintenance policy to be enforced on the server when applying setting. +ServerMaintenancePolicyOwnerApproval is asking for User approval for changing BMC settings +note: User approval is only enforced for server’s which are reserved state +ServerMaintenancePolicyEnforced will will bypass user approval and apply setting directly + |
+
+serverMaintenanceRefs+ + +[]ServerMaintenanceRefItem + + + |
+
+ ServerMaintenanceRefs are references to a ServerMaintenance objects that Controller has requested for the each of the related server. + |
+
string alias)+(Appears on:BMCSettingsStatus) +
+ServerMaintenanceState specifies the current state of the server maintenance.
+| Value | +Description | +
|---|---|
"Applied" |
+BMCSettingsStateApplied specifies that the BMC maintenance has been completed. + |
+
"Failed" |
+BMCSettingsStateFailed specifies that the BMC maintenance has failed. + |
+
"InProgress" |
+BMCSettingsStateInProgress specifies that the BMC setting changes are in progress + |
+
"Pending" |
+BMCSettingsStatePending specifies that the BMC maintenance is waiting + |
+
+(Appears on:BMCSettings) +
+BMCSettingsStatus defines the observed state of BMCSettings.
+| Field | +Description | +
|---|---|
+state+ + +BMCSettingsState + + + |
+
+ State represents the current state of the BMC configuration task. + |
+
@@ -954,6 +1246,19 @@ More info: @@ -1025,6 +1330,20 @@ ConsoleProtocol This field is optional and can be omitted if console access is not required.
+bmcSettingsRefBMCSettingRef is a reference to a BMCSettings object that specifies +the BMC configuration for this BMC.
+string alias)-(Appears on:BIOSSettingsSpec, BIOSVersionSpec, ServerMaintenanceSpec) +(Appears on:BIOSSettingsSpec, BIOSVersionSpec, BMCSettingsSpec, ServerMaintenanceSpec)
ServerMaintenancePolicy specifies the maintenance policy to be enforced on the server.
@@ -2828,6 +3147,35 @@ ServerMaintenanceStatus ++(Appears on:BMCSettingsSpec) +
+| Field | +Description | +
|---|---|
+serverMaintenanceRef+ + +Kubernetes core/v1.ObjectReference + + + |
++ | +
diff --git a/docs/concepts/biossettings.md b/docs/concepts/biossettings.md index abd6eba3..281304ae 100644 --- a/docs/concepts/biossettings.md +++ b/docs/concepts/biossettings.md @@ -17,9 +17,9 @@ 2. Provided settings are checked against the current BIOS setting. 3. If settings are same as on the server, the state is moved to `Applied` (even if the version does not match) 4. If the settings needs update, `BIOSSettings` check the version of BIOS and if required version does not match, it waits for the bios version to reach the spec version. -5. `BIOSSettings` checks if the required setting update needs physical server reboot. -6. If reboot is needed and `ServerMaintenance` is not provided already. it requests for one and waits for the `server` to enter `Maintenance` state. - - `policy` used by `ServerMaintenance` is to be provided through Spec `ServerMaintenancePolicyType` in `BIOSSettings` +5. If `ServerMaintenance` is not provided already. it requests for one and waits for the `server` to enter `Maintenance` state. + - `policy` used by `ServerMaintenance` is to be provided through Spec `ServerMaintenancePolicy` in `BIOSSettings` +6. `BIOSSettings` checks if the required setting update needs physical server reboot. 7. Setting update process is started and the server is rebooted if required. 8. `BIOSSettings` verfiy the setting has been applied and trasistions the state to `Applied`. removes the `ServerMaintenance` resource if created by self. 9. Any further update to the `BIOSSettings` Spec will restart the process. diff --git a/docs/concepts/bmcsettings.md b/docs/concepts/bmcsettings.md index f958357d..5a0e9f53 100644 --- a/docs/concepts/bmcsettings.md +++ b/docs/concepts/bmcsettings.md @@ -8,7 +8,7 @@ - Only one `BMCSettings` can be active per `BMC` at a time. - `BMCSettings` related changes are applied once the BMC version matches with the physical server's BMC version. - `BMCSettings` handles reboots of BMC -- `BMCSettings` requests for `Maintenance` if `ServerMaintenancePolicy` is set to "OwnerApproval". +- `BMCSettings` requests for `Maintenance`, `ServerMaintenancePolicy` used for maintenance type - Once`BMCSettings` moves to `Failed` state, It stays in this state unless Manually moved out of this state. ## Workflow @@ -17,7 +17,7 @@ 2. Provided settings are checked against the current BMC setting. 3. If settings are same as on the server, the state is moved to `Applied` (even if the version does not match) 4. If the settings needs update, `BMCSettings` check the version of BMC and if required version does not match, it waits for the BMC version to reach the spec version. -5. If "OwnerApproval" `ServerMaintenancePolicy` type is requested and `ServerMaintenance` is not provided already. it requests one per `server` managed by `BMC` and waits for all the `server` to enter `Maintenance` state. +5. If `ServerMaintenance` is not provided already. it requests one per `server` managed by `BMC` and waits for all the `server` to enter `Maintenance` state. 6. Setting update process is started and the physical server's BMC is rebooted if required. 7. `BMCSettings` verfiy the setting has been applied and trasistions the state to `Applied`. removes all the `ServerMaintenance` resource if created by self. 8. Any further update to the `BMCSettings` Spec will restart the process. diff --git a/internal/controller/biossettings_controller.go b/internal/controller/biossettings_controller.go index 386b246e..266bb852 100644 --- a/internal/controller/biossettings_controller.go +++ b/internal/controller/biossettings_controller.go @@ -284,7 +284,7 @@ func (r *BiosSettingsReconciler) handleSettingInProgressState( return ctrl.Result{}, nil } - if req, err := r.checkAndRequestMaintenance(ctx, log, bmcClient, biosSettings, server, settingsDiff); err != nil || req { + if req, err := r.requestMaintenanceOnServer(ctx, log, biosSettings, server); err != nil || req { return ctrl.Result{}, err } @@ -297,30 +297,6 @@ func (r *BiosSettingsReconciler) handleSettingInProgressState( return r.applySettingUpdateStateTransition(ctx, log, bmcClient, biosSettings, server, settingsDiff) } -func (r *BiosSettingsReconciler) checkAndRequestMaintenance( - ctx context.Context, - log logr.Logger, - bmcClient bmc.BMC, - biosSettings *metalv1alpha1.BIOSSettings, - server *metalv1alpha1.Server, - settingsDiff redfish.SettingsAttributes, -) (bool, error) { - // check if we need to request maintenance if we dont have it already - // note: having this check will reduce the call made to BMC. - if biosSettings.Spec.ServerMaintenanceRef == nil { - resetReq, err := bmcClient.CheckBiosAttributes(settingsDiff) - if resetReq { - // request maintenance if needed, even if err was reported. - requeue, errMainReq := r.requestMaintenanceOnServer(ctx, log, biosSettings, server) - return requeue, errors.Join(err, errMainReq) - } - if err != nil { - return false, fmt.Errorf("failed to check BMC settings provided: %w", err) - } - } - return false, nil -} - func (r *BiosSettingsReconciler) applySettingUpdateStateTransition( ctx context.Context, log logr.Logger, @@ -385,12 +361,17 @@ func (r *BiosSettingsReconciler) applySettingUpdateStateTransition( return ctrl.Result{}, err } - // if we dont need (have not requested maintenance) reboot. skip reboot steps. + resetReq, err := bmcClient.CheckBiosAttributes(settingsDiff) + if err != nil { + log.V(1).Error(err, "could not determine if reboot needed") + return ctrl.Result{}, err + } + + // if we dont need reboot. skip reboot steps. nextState := metalv1alpha1.BIOSSettingUpdateWaitOnServerRebootPowerOff - if biosSettings.Spec.ServerMaintenanceRef == nil { + if !resetReq { nextState = metalv1alpha1.BIOSSettingUpdateStateVerification } - err = r.updateBIOSSettingUpdateStatus(ctx, log, biosSettings, nextState) log.V(1).Info("Reconciled biosSettings at update Settings state") return ctrl.Result{}, err diff --git a/internal/controller/biossettings_controller_test.go b/internal/controller/biossettings_controller_test.go index b2150690..bd96774d 100644 --- a/internal/controller/biossettings_controller_test.go +++ b/internal/controller/biossettings_controller_test.go @@ -181,67 +181,6 @@ var _ = Describe("BIOSSettings Controller", func() { ) }) - It("should update the setting without maintenance if setting requested needs no server reboot", func(ctx SpecContext) { - BIOSSetting := make(map[string]string) - // settings which does not reboot. mocked at - // metal-operator/bmc/redfish_local.go defaultMockedBIOSSetting - BIOSSetting["abc"] = "bar-changed-no-reboot" - - // mock BIOSSettings to not request maintenance by powering on the system (mock no need of power change on system) - // note: cant be in Available state as it will power off automatically. - serverClaim := BuildServerClaim(ctx, k8sClient, *server, ns.Name, nil, metalv1alpha1.PowerOn, "foo:bar") - TransistionServerToReserveredState(ctx, k8sClient, serverClaim, server, ns.Name) - - By("Creating a BIOS settings") - biosSettings := &metalv1alpha1.BIOSSettings{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: ns.Name, - GenerateName: "test-bios-change-noreboot-", - }, - Spec: metalv1alpha1.BIOSSettingsSpec{ - Version: defaultMockUpServerBiosVersion, - SettingsMap: BIOSSetting, - ServerRef: &v1.LocalObjectReference{Name: server.Name}, - ServerMaintenancePolicy: metalv1alpha1.ServerMaintenancePolicyEnforced, - }, - } - Expect(k8sClient.Create(ctx, biosSettings)).To(Succeed()) - - // due to how the mocked setting is updated, the state transition are super fast - By("Ensuring that the BIOS setting has reached next state: inProgress") - Eventually(Object(biosSettings)).Should(SatisfyAny( - HaveField("Status.State", metalv1alpha1.BIOSSettingsStateInProgress), - HaveField("Status.State", metalv1alpha1.BIOSSettingsStateApplied), - )) - - By("Ensuring that the Server has correct state") - Eventually(Object(server)).Should(SatisfyAll( - HaveField("Spec.BIOSSettingsRef", &v1.LocalObjectReference{Name: biosSettings.Name}), - HaveField("Spec.Power", metalv1alpha1.PowerOn), - HaveField("Status.PowerState", metalv1alpha1.ServerOnPowerState), - )) - - By("Ensuring that the Maintenance resource has not been created") - var serverMaintenanceList metalv1alpha1.ServerMaintenanceList - Consistently(ObjectList(&serverMaintenanceList)).Should(HaveField("Items", BeEmpty())) - Consistently(Object(biosSettings)).Should( - HaveField("Spec.ServerMaintenanceRef", BeNil()), - ) - - By("Ensuring that the BIOS setting has reached next state: stateSynced") - Eventually(Object(biosSettings)).Should( - HaveField("Status.State", metalv1alpha1.BIOSSettingsStateApplied), - ) - - By("Deleting the BIOSSettings") - Expect(k8sClient.Delete(ctx, biosSettings)).To(Succeed()) - - By("Ensuring that the Server BIOSSettings ref is empty") - Eventually(Object(server)).Should( - HaveField("Spec.BIOSSettingsRef", BeNil()), - ) - }) - It("should request maintenance when changing power status of server, even if bios settings update does not need it", func(ctx SpecContext) { BIOSSetting := make(map[string]string) // settings which does not reboot. mocked at @@ -315,8 +254,7 @@ var _ = Describe("BIOSSettings Controller", func() { metautils.SetAnnotation(serverClaim, metalv1alpha1.ServerMaintenanceApprovalKey, "true") })).Should(Succeed()) - // because of how we mock the setting update, we can not determine the next state, Hence check for multiple - By("Ensuring that the BIOS setting has reached next state") + By("Ensuring that the biosSettings resource has started bios setting update") Eventually(Object(biosSettings)).Should(SatisfyAll( HaveField("Status.State", metalv1alpha1.BIOSSettingsStateInProgress), HaveField("Status.UpdateSettingState", metalv1alpha1.BIOSSettingUpdateState("")), @@ -425,6 +363,12 @@ var _ = Describe("BIOSSettings Controller", func() { metautils.SetAnnotation(serverClaim, metalv1alpha1.ServerMaintenanceApprovalKey, "true") })).Should(Succeed()) + By("Ensuring that the biosSettings resource has started bios setting update") + Eventually(Object(biosSettings)).Should(SatisfyAll( + HaveField("Status.State", metalv1alpha1.BIOSSettingsStateInProgress), + HaveField("Status.UpdateSettingState", metalv1alpha1.BIOSSettingUpdateState("")), + )) + By("Ensuring that the Server is in Maintenance") Eventually(Object(server)).Should( HaveField("Status.State", metalv1alpha1.ServerStateMaintenance), @@ -454,7 +398,7 @@ var _ = Describe("BIOSSettings Controller", func() { Consistently(Get(serverMaintenance)).Should(Satisfy(apierrors.IsNotFound)) }) - It("should update setting if server is in availalbe state", func(ctx SpecContext) { + It("should update setting if server is in available state", func(ctx SpecContext) { // settings which does not reboot. mocked at // metal-operator/bmc/redfish_local.go defaultMockedBIOSSetting BIOSSetting := make(map[string]string) diff --git a/internal/controller/biosversion_controller_test.go b/internal/controller/biosversion_controller_test.go index 403349ff..566ff11e 100644 --- a/internal/controller/biosversion_controller_test.go +++ b/internal/controller/biosversion_controller_test.go @@ -21,7 +21,6 @@ import ( var _ = Describe("BIOSVersion Controller", func() { ns := SetupTest() - ns.Name = "default" var ( server *metalv1alpha1.Server diff --git a/internal/controller/bmc_controller.go b/internal/controller/bmc_controller.go index d8061d90..1d6cba64 100644 --- a/internal/controller/bmc_controller.go +++ b/internal/controller/bmc_controller.go @@ -131,18 +131,19 @@ func (r *BMCReconciler) updateBMCStatusDetails(ctx context.Context, log logr.Log // TODO: Secret rotation/User management - manager, err := bmcClient.GetManager() + manager, err := bmcClient.GetManager(bmcObj.Spec.BMCUUID) if err != nil { return fmt.Errorf("failed to get manager details for BMC %s: %w", bmcObj.Name, err) } + if manager != nil { bmcBase := bmcObj.DeepCopy() bmcObj.Status.Manufacturer = manager.Manufacturer - bmcObj.Status.State = metalv1alpha1.BMCState(manager.State) - bmcObj.Status.PowerState = metalv1alpha1.BMCPowerState(manager.PowerState) + bmcObj.Status.State = metalv1alpha1.BMCState(string(manager.Status.State)) + bmcObj.Status.PowerState = metalv1alpha1.BMCPowerState(string(manager.PowerState)) bmcObj.Status.FirmwareVersion = manager.FirmwareVersion bmcObj.Status.SerialNumber = manager.SerialNumber - bmcObj.Status.SKU = manager.SKU + bmcObj.Status.SKU = manager.PartNumber bmcObj.Status.Model = manager.Model if err := r.Status().Patch(ctx, bmcObj, client.MergeFrom(bmcBase)); err != nil { return fmt.Errorf("failed to patch manager details for BMC %s: %w", bmcObj.Name, err) diff --git a/internal/controller/bmcsettings_controller.go b/internal/controller/bmcsettings_controller.go new file mode 100644 index 00000000..7d69774b --- /dev/null +++ b/internal/controller/bmcsettings_controller.go @@ -0,0 +1,855 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package controller + +import ( + "context" + "errors" + "fmt" + "maps" + "slices" + "strconv" + "time" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" + + "github.com/go-logr/logr" + "github.com/ironcore-dev/controller-utils/clientutils" + metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1" + "github.com/ironcore-dev/metal-operator/bmc" + "github.com/ironcore-dev/metal-operator/internal/bmcutils" + "github.com/stmcginnis/gofish/redfish" +) + +// BMCSettingsReconciler reconciles a BMCSettings object +type BMCSettingsReconciler struct { + client.Client + ManagerNamespace string + ResyncInterval time.Duration + Insecure bool + Scheme *runtime.Scheme + BMCOptions bmc.Options +} + +const BMCSettingFinalizer = "firmware.ironcore.dev/out-of-band-management" + +// +kubebuilder:rbac:groups=metal.ironcore.dev,resources=bmcsettings,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=metal.ironcore.dev,resources=bmcsettings/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=metal.ironcore.dev,resources=bmcsettings/finalizers,verbs=update +//+kubebuilder:rbac:groups=metal.ironcore.dev,resources=servers,verbs=get;list;watch;update +//+kubebuilder:rbac:groups=metal.ironcore.dev,resources=servermaintenances,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=metal.ironcore.dev,resources=servermaintenances/status,verbs=get;update;patch +//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups="batch",resources=jobs,verbs=get;list;watch;create;update;patch;delete + +func (r *BMCSettingsReconciler) Reconcile( + ctx context.Context, + req ctrl.Request, +) (ctrl.Result, error) { + log := ctrl.LoggerFrom(ctx) + bmcSetting := &metalv1alpha1.BMCSettings{} + if err := r.Get(ctx, req.NamespacedName, bmcSetting); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + log.V(1).Info("Reconciling BMC Settings") + + return r.reconcileExists(ctx, log, bmcSetting) +} + +// Determine whether reconciliation is required. It's not required if: +// - object is being deleted; +// - object does not contain reference to server; +// - object contains reference to server, but server references to another object with lower version; +func (r *BMCSettingsReconciler) reconcileExists( + ctx context.Context, + log logr.Logger, + bmcSetting *metalv1alpha1.BMCSettings, +) (ctrl.Result, error) { + // if object is being deleted - reconcile deletion + if !bmcSetting.DeletionTimestamp.IsZero() { + log.V(1).Info("object is being deleted") + return r.delete(ctx, log, bmcSetting) + } + + return r.reconcile(ctx, log, bmcSetting) +} + +func (r *BMCSettingsReconciler) delete( + ctx context.Context, + log logr.Logger, + bmcSetting *metalv1alpha1.BMCSettings, +) (ctrl.Result, error) { + if !controllerutil.ContainsFinalizer(bmcSetting, BMCSettingFinalizer) { + return ctrl.Result{}, nil + } + if err := r.cleanupReferences(ctx, log, bmcSetting); err != nil { + log.Error(err, "failed to cleanup references") + return ctrl.Result{}, err + } + log.V(1).Info("ensured references were cleaned up") + + log.V(1).Info("Ensuring that the finalizer is removed") + if modified, err := clientutils.PatchEnsureNoFinalizer(ctx, r.Client, bmcSetting, BMCSettingFinalizer); err != nil || modified { + return ctrl.Result{}, err + } + + log.V(1).Info("bmcSetting is deleted") + return ctrl.Result{}, nil +} + +func (r *BMCSettingsReconciler) cleanupServerMaintenanceReferences( + ctx context.Context, + log logr.Logger, + bmcSettings *metalv1alpha1.BMCSettings, +) error { + if bmcSettings.Spec.ServerMaintenanceRefs == nil { + return nil + } + // try to get the serverMaintenances created + serverMaintenances, errs := r.getReferredServerMaintenances(ctx, log, bmcSettings.Spec.ServerMaintenanceRefs) + + var finalErr []error + var missingServerMaintenanceRef []error + + if len(errs) > 0 { + for _, err := range errs { + if apierrors.IsNotFound(err) { + missingServerMaintenanceRef = append(missingServerMaintenanceRef, err) + } else { + finalErr = append(finalErr, err) + } + } + } + + if len(missingServerMaintenanceRef) != len(bmcSettings.Spec.ServerMaintenanceRefs) { + // delete the serverMaintenance if not marked for deletion already + for _, serverMaintenance := range serverMaintenances { + if serverMaintenance.DeletionTimestamp.IsZero() && metav1.IsControlledBy(serverMaintenance, bmcSettings) { + log.V(1).Info("Deleting server maintenance", "serverMaintenance Name", serverMaintenance.Name, "state", serverMaintenance.Status.State) + if err := r.Delete(ctx, serverMaintenance); err != nil { + log.V(1).Info("Failed to delete server maintenance", "serverMaintenance Name", serverMaintenance.Name) + finalErr = append(finalErr, err) + } + } else { + log.V(1).Info( + "server maintenance not deleted", + "serverMaintenance Name", serverMaintenance.Name, + "state", serverMaintenance.Status.State, + "owner", serverMaintenance.OwnerReferences, + ) + } + } + } + + if len(finalErr) == 0 { + // all serverMaintenance are deleted + err := r.patchMaintenanceRequestRefOnBMCSettings(ctx, log, bmcSettings, nil) + if err != nil { + return fmt.Errorf("failed to clean up serverMaintenance ref in bmcSetting status: %w", err) + } + log.V(1).Info("server maintenance ref all cleaned up") + } + return errors.Join(finalErr...) +} + +func (r *BMCSettingsReconciler) cleanupReferences( + ctx context.Context, + log logr.Logger, + bmcSetting *metalv1alpha1.BMCSettings, +) (err error) { + if bmcSetting.Spec.BMCRef != nil { + BMC, err := r.getBMC(ctx, log, bmcSetting) + if err != nil && !apierrors.IsNotFound(err) { + return err + } + // if we can not find the server, nothing else to clean up + if apierrors.IsNotFound(err) { + return nil + } + // if we have found the server, check if ref is this bmcSetting and remove it + if err == nil { + if BMC.Spec.BMCSettingRef != nil { + if BMC.Spec.BMCSettingRef.Name != bmcSetting.Name { + return nil + } + return r.patchBMCSettingsRefOnBMC(ctx, log, BMC, nil) + } else { + // nothing else to clean up + return nil + } + } + } + + return err +} + +func (r *BMCSettingsReconciler) reconcile( + ctx context.Context, + log logr.Logger, + bmcSetting *metalv1alpha1.BMCSettings, +) (ctrl.Result, error) { + if shouldIgnoreReconciliation(bmcSetting) { + log.V(1).Info("Skipped BMCSettings reconciliation") + return ctrl.Result{}, nil + } + + // if object does not refer to BMC object - stop reconciliation + // todo length + if bmcSetting.Spec.BMCRef == nil { + log.V(1).Info("object does not refer to BMC object") + return ctrl.Result{}, nil + } + + // if referred BMC contains reference to different BMCSettings object - stop reconciliation + BMC, err := r.getBMC(ctx, log, bmcSetting) + if err != nil { + log.V(1).Info("referred server object could not be fetched") + return ctrl.Result{}, err + } + // patch BMC with BMCSettings reference + if BMC.Spec.BMCSettingRef == nil { + if err := r.patchBMCSettingsRefOnBMC(ctx, log, BMC, &corev1.LocalObjectReference{Name: bmcSetting.Name}); err != nil { + return ctrl.Result{}, err + } + } else if BMC.Spec.BMCSettingRef.Name != bmcSetting.Name { + referredBMCSettings, err := r.getReferredBMCSettings(ctx, log, BMC.Spec.BMCSettingRef) + if err != nil { + log.V(1).Info("referred server contains reference to different BMCSettings object, unable to fetch the referenced BMCSettings") + return ctrl.Result{}, err + } + // check if the current BMCSettings version is newer and update reference if it is newer + // todo : handle version checks correctly + if referredBMCSettings.Spec.Version < bmcSetting.Spec.Version { + log.V(1).Info("Updating BMCSettings reference to the latest BMC version") + if err := r.patchBMCSettingsRefOnBMC(ctx, log, BMC, &corev1.LocalObjectReference{Name: bmcSetting.Name}); err != nil { + return ctrl.Result{}, err + } + } + } + + if modified, err := clientutils.PatchEnsureFinalizer(ctx, r.Client, bmcSetting, BMCSettingFinalizer); err != nil || modified { + return ctrl.Result{}, err + } + + return r.ensureBMCSettingsMaintenanceStateTransition(ctx, log, bmcSetting, BMC) +} + +func (r *BMCSettingsReconciler) ensureBMCSettingsMaintenanceStateTransition( + ctx context.Context, + log logr.Logger, + bmcSetting *metalv1alpha1.BMCSettings, + BMC *metalv1alpha1.BMC, +) (ctrl.Result, error) { + bmcClient, err := bmcutils.GetBMCClientFromBMC(ctx, r.Client, BMC, r.Insecure, r.BMCOptions) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to create BMC client: %w", err) + } + defer bmcClient.Logout() + switch bmcSetting.Status.State { + case "", metalv1alpha1.BMCSettingsStatePending: + //todo: check that in initial state there is no pending BMCSettings maintenance left behind, + + err := r.updateBMCSettingsStatus(ctx, log, bmcSetting, metalv1alpha1.BMCSettingsStateInProgress) + return ctrl.Result{}, err + case metalv1alpha1.BMCSettingsStateInProgress: + return r.handleSettingInProgressState(ctx, log, bmcSetting, BMC, bmcClient) + case metalv1alpha1.BMCSettingsStateApplied: + return ctrl.Result{}, r.handleSettingAppliedState(ctx, log, bmcSetting, BMC, bmcClient) + case metalv1alpha1.BMCSettingsStateFailed: + r.handleFailedState(ctx, log, bmcSetting, BMC) + return ctrl.Result{}, nil + } + log.V(1).Info("Unknown State found", "BMCSettings state", bmcSetting.Status.State) + return ctrl.Result{}, nil +} + +func (r *BMCSettingsReconciler) handleSettingInProgressState( + ctx context.Context, + log logr.Logger, + bmcSetting *metalv1alpha1.BMCSettings, + BMC *metalv1alpha1.BMC, + bmcClient bmc.BMC, +) (ctrl.Result, error) { + currentBMCVersion, settingsDiff, err := r.getBMCVersionAndSettingsDifference(ctx, log, bmcSetting, BMC, bmcClient) + + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to get BMC settings: %w", err) + } + // if setting is not different, complete the BMCSettings tasks + if len(settingsDiff) == 0 { + // move status to completed + err := r.updateBMCSettingsStatus(ctx, log, bmcSetting, metalv1alpha1.BMCSettingsStateApplied) + return ctrl.Result{}, err + } + + // todo:wait on the result from the resource which does upgrade to requeue. + if currentBMCVersion != bmcSetting.Spec.Version { + log.V(1).Info("Pending BMC version upgrade.", "current bmc Version", currentBMCVersion, "required version", bmcSetting.Spec.Version) + return ctrl.Result{}, nil + } + + if req, err := r.requestMaintenanceOnServers(ctx, log, bmcSetting, bmcClient); err != nil || req { + return ctrl.Result{}, err + } + + // check if the maintenance is granted + if ok := r.checkIfMaintenanceGranted(ctx, log, bmcSetting, bmcClient); !ok { + log.V(1).Info("Waiting for maintenance to be granted before continuing with updating settings", "reason", err) + return ctrl.Result{}, err + } + + return r.updateSettingsAndVerify(ctx, log, bmcSetting, BMC, settingsDiff, bmcClient) +} + +func (r *BMCSettingsReconciler) updateSettingsAndVerify( + ctx context.Context, + log logr.Logger, + bmcSetting *metalv1alpha1.BMCSettings, + BMC *metalv1alpha1.BMC, + settingsDiff redfish.SettingsAttributes, + bmcClient bmc.BMC, +) (ctrl.Result, error) { + + if BMC.Status.PowerState != metalv1alpha1.OnPowerState { + log.V(1).Info("BMC is not turned On. Can not proceed") + err := r.updateBMCSettingsStatus(ctx, log, bmcSetting, metalv1alpha1.BMCSettingsStateFailed) + return ctrl.Result{}, err + } + + pendingAttr, err := bmcClient.GetBMCPendingAttributeValues(ctx, BMC.Spec.BMCUUID) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to check pending BMC settings: %w", err) + } + + if len(pendingAttr) == 0 { + resetBMCReq, err := bmcClient.CheckBMCAttributes(BMC.Spec.BMCUUID, settingsDiff) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to check BMC settings provided: %w", err) + } + + err = bmcClient.SetBMCAttributesImediately(ctx, BMC.Spec.BMCUUID, settingsDiff) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to set BMC settings: %w", err) + } + + if resetBMCReq { + err = bmcClient.ResetManager(ctx, BMC.Spec.BMCUUID, redfish.GracefulRestartResetType) + if err != nil { + log.V(1).Error(err, "failed to reset BMC") + return ctrl.Result{}, err + } + } + } + + // verify setting already applied + _, settingsDiff, err = r.getBMCVersionAndSettingsDifference(ctx, log, bmcSetting, BMC, bmcClient) + + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to get BMC settings: %w", err) + } + // if setting is not different, complete the BMC settings tasks + if len(settingsDiff) == 0 { + // move bmcSetting state to completed, and revert the settingUpdate state to initial + err := r.updateBMCSettingsStatus(ctx, log, bmcSetting, metalv1alpha1.BMCSettingsStateApplied) + return ctrl.Result{}, err + } + return ctrl.Result{RequeueAfter: r.ResyncInterval}, nil + +} + +func (r *BMCSettingsReconciler) handleSettingAppliedState( + ctx context.Context, + log logr.Logger, + bmcSetting *metalv1alpha1.BMCSettings, + BMC *metalv1alpha1.BMC, + bmcClient bmc.BMC, +) error { + // clean up maintenance crd and references. + if err := r.cleanupServerMaintenanceReferences(ctx, log, bmcSetting); err != nil { + return err + } + + _, settingsDiff, err := r.getBMCVersionAndSettingsDifference(ctx, log, bmcSetting, BMC, bmcClient) + + if err != nil { + log.V(1).Error(err, "unable to fetch and check BMCSettings") + return err + } + if len(settingsDiff) > 0 { + err := r.updateBMCSettingsStatus(ctx, log, bmcSetting, "") + return err + } + + log.V(1).Info("Done with BMC setting update", "ctx", ctx, "bmcSetting", bmcSetting, "bmc", BMC) + return nil +} + +func (r *BMCSettingsReconciler) handleFailedState( + ctx context.Context, + log logr.Logger, + bmcSetting *metalv1alpha1.BMCSettings, + BMC *metalv1alpha1.BMC, +) { + log.V(1).Info("Handle failed setting update with no maintenance reference") + // todo: revisit this logic to either create maintenance if not present, put server in Error state on failed bmc settings maintenance + log.V(1).Info("Failed to update BMC setting", "ctx", ctx, "bmcSetting", bmcSetting, "BMC", BMC) +} + +func (r *BMCSettingsReconciler) getBMCVersionAndSettingsDifference( + ctx context.Context, + log logr.Logger, + bmcSetting *metalv1alpha1.BMCSettings, + BMC *metalv1alpha1.BMC, + bmcClient bmc.BMC, +) (currentBMCVersion string, diff redfish.SettingsAttributes, err error) { + keys := slices.Collect(maps.Keys(bmcSetting.Spec.SettingsMap)) + + currentSettings, err := bmcClient.GetBMCAttributeValues(ctx, BMC.Spec.BMCUUID, keys) + if err != nil { + log.V(1).Info("Failed to get with BMC setting", "error", err) + return currentBMCVersion, diff, fmt.Errorf("failed to get BMC settings: %w", err) + } + + diff = redfish.SettingsAttributes{} + var errs []error + for key, value := range bmcSetting.Spec.SettingsMap { + res, ok := currentSettings[key] + if ok { + switch data := res.(type) { + case int: + intvalue, err := strconv.Atoi(value) + if err != nil { + log.V(1).Info("Failed to check type for", "Setting name", key, "setting value", value, "error", err) + errs = append(errs, fmt.Errorf("failed to check type for name %v; value %v; error: %v", key, value, err)) + continue + } + if data != intvalue { + diff[key] = intvalue + } + case string: + if data != value { + diff[key] = value + } + case float64: + floatvalue, err := strconv.ParseFloat(value, 64) + if err != nil { + log.V(1).Info("Failed to check type for", "Setting name", key, "setting value", value, "error", err) + errs = append(errs, fmt.Errorf("failed to check type for name %v; value %v; error: %v", key, value, err)) + } + if data != floatvalue { + diff[key] = floatvalue + } + } + } else { + diff[key] = value + } + } + + if len(errs) > 0 { + return currentBMCVersion, diff, fmt.Errorf("failed to find diff for some BMC settings: %v", errs) + } + + // fetch the current BMC version from the server bmc + currentBMCVersion, err = bmcClient.GetBMCVersion(ctx, BMC.Spec.BMCUUID) + if err != nil { + return currentBMCVersion, diff, fmt.Errorf("failed to load BMC version: %w for BMC %v", err, BMC.Name) + } + + return currentBMCVersion, diff, nil +} + +func (r *BMCSettingsReconciler) checkIfMaintenanceGranted( + ctx context.Context, + log logr.Logger, + bmcSetting *metalv1alpha1.BMCSettings, + bmcClient bmc.BMC, +) bool { + if bmcSetting.Spec.ServerMaintenanceRefs == nil { + return false + } + + servers, err := r.getServers(ctx, log, bmcSetting, bmcClient) + if err != nil { + log.V(1).Error(err, "Failed to get ref. servers to determine maintenance state ") + return false + } + + if len(bmcSetting.Spec.ServerMaintenanceRefs) != len(servers) { + log.V(1).Info("Not all servers have Maintenance", "ServerMaintenanceRefs", bmcSetting.Spec.ServerMaintenanceRefs, "servers", servers) + return false + } + + notInMaintenanceState := make([]string, 0, len(servers)) + for _, server := range servers { + if server.Status.State == metalv1alpha1.ServerStateMaintenance { + serverMaintenanceRef := r.getServerMaintenanceRefForServer(bmcSetting.Spec.ServerMaintenanceRefs, server.Spec.ServerMaintenanceRef.UID) + if server.Spec.ServerMaintenanceRef == nil || serverMaintenanceRef == nil { + // server in maintenance for other tasks. or + // server maintenance ref is wrong in either server or bmcSetting + // wait for update on the server obj + log.V(1).Info("Server is already in maintenance for other tasks", + "Server", server.Name, + "serverMaintenanceRef", server.Spec.ServerMaintenanceRef, + "bmcSettingMaintenaceRef", serverMaintenanceRef, + ) + notInMaintenanceState = append(notInMaintenanceState, server.Name) + } + } else { + // we still need to wait for server to enter maintenance + // wait for update on the server obj + log.V(1).Info("Server not yet in maintenance", "Server", server.Name, "State", server.Status.State, "MaintenanceRef", server.Spec.ServerMaintenanceRef) + notInMaintenanceState = append(notInMaintenanceState, server.Name) + } + } + + if len(notInMaintenanceState) > 0 { + log.V(1).Info("some servers not yet in maintenance", + "req maintenances on servers", bmcSetting.Spec.ServerMaintenanceRefs, + "servers not in maintence", notInMaintenanceState) + return false + } + + return true +} + +func (r *BMCSettingsReconciler) requestMaintenanceOnServers( + ctx context.Context, + log logr.Logger, + bmcSetting *metalv1alpha1.BMCSettings, + bmcClient bmc.BMC, +) (bool, error) { + + servers, err := r.getServers(ctx, log, bmcSetting, bmcClient) + if err != nil { + log.V(1).Error(err, "Failed to get ref. servers to request maintenance on servers") + return false, err + } + + // if Server maintenance ref is already given. no further action required. + if bmcSetting.Spec.ServerMaintenanceRefs != nil && len(bmcSetting.Spec.ServerMaintenanceRefs) == len(servers) { + return false, nil + } + + var errs []error + ServerMaintenanceRefs := make([]metalv1alpha1.ServerMaintenanceRefItem, 0, len(servers)) + for _, server := range servers { + serverMaintenance := &metalv1alpha1.ServerMaintenance{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: r.ManagerNamespace, + Name: fmt.Sprintf("%s-%s", bmcSetting.Name, server.Name), + }} + + opResult, err := controllerutil.CreateOrPatch(ctx, r.Client, serverMaintenance, func() error { + serverMaintenance.Spec.Policy = bmcSetting.Spec.ServerMaintenancePolicy + serverMaintenance.Spec.ServerPower = metalv1alpha1.PowerOn + serverMaintenance.Spec.ServerRef = &corev1.LocalObjectReference{Name: server.Name} + if serverMaintenance.Status.State != metalv1alpha1.ServerMaintenanceStateInMaintenance && serverMaintenance.Status.State != "" { + serverMaintenance.Status.State = "" + } + return controllerutil.SetControllerReference(bmcSetting, serverMaintenance, r.Client.Scheme()) + }) + if err != nil { + log.V(1).Info("failed to create or patch serverMaintenance for server %v: \nError: %w", server.Name, err) + errs = append(errs, err) + continue + } + log.V(1).Info("Created serverMaintenance", "serverMaintenance", serverMaintenance.Name, "serverMaintenance label", serverMaintenance.Labels, "Operation", opResult) + + ServerMaintenanceRefs = append( + ServerMaintenanceRefs, + metalv1alpha1.ServerMaintenanceRefItem{ + ServerMaintenanceRef: &corev1.ObjectReference{ + APIVersion: serverMaintenance.GroupVersionKind().GroupVersion().String(), + Kind: "ServerMaintenance", + Namespace: serverMaintenance.Namespace, + Name: serverMaintenance.Name, + UID: serverMaintenance.UID, + }}) + } + + if len(errs) > 0 { + return false, errors.Join(errs...) + } + + err = r.patchMaintenanceRequestRefOnBMCSettings(ctx, log, bmcSetting, ServerMaintenanceRefs) + if err != nil { + return false, fmt.Errorf("failed to patch serverMaintenance ref in bmcSetting status: %w", err) + } + + log.V(1).Info("Patched serverMaintenanceMap on bmcSetting") + + return true, nil +} + +func (r *BMCSettingsReconciler) getBMC( + ctx context.Context, + log logr.Logger, + bmcSetting *metalv1alpha1.BMCSettings, +) (*metalv1alpha1.BMC, error) { + + var refName string + if bmcSetting.Spec.BMCRef == nil { + return nil, fmt.Errorf("bmc ref not provided") + } else { + refName = bmcSetting.Spec.BMCRef.Name + } + + key := client.ObjectKey{Name: refName} + BMC := &metalv1alpha1.BMC{} + if err := r.Get(ctx, key, BMC); err != nil { + log.V(1).Error(err, "failed to get referred server's Manager") + return BMC, err + } + + return BMC, nil +} + +func (r *BMCSettingsReconciler) getServers( + ctx context.Context, + log logr.Logger, + bmcSetting *metalv1alpha1.BMCSettings, + bmcClient bmc.BMC, +) ([]*metalv1alpha1.Server, error) { + if bmcSetting.Spec.BMCRef == nil { + return nil, fmt.Errorf("BMC reference not found") + } + BMC, err := r.getBMC(ctx, log, bmcSetting) + + if err != nil { + log.V(1).Error(err, "failed to get referred BMC") + return nil, err + } + bmcServers, err := bmcClient.GetSystems(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get servers from BMC %s: %w", BMC.Name, err) + } + serversRefList := make([]*corev1.LocalObjectReference, len(bmcServers)) + for i := range bmcServers { + serversRefList[i] = &corev1.LocalObjectReference{Name: bmcutils.GetServerNameFromBMCandIndex(i, BMC)} + } + servers, err := r.getReferredServers(ctx, log, serversRefList) + if err != nil { + return servers, fmt.Errorf("errors occurred during fetching servers from BMC: %v", err) + } + return servers, nil +} + +func (r *BMCSettingsReconciler) getReferredServers( + ctx context.Context, + log logr.Logger, + serverRefList []*corev1.LocalObjectReference, +) ([]*metalv1alpha1.Server, error) { + var errs []error + servers := make([]*metalv1alpha1.Server, len(serverRefList)) + for idx, serverRef := range serverRefList { + key := client.ObjectKey{Name: serverRef.Name} + server := &metalv1alpha1.Server{} + if err := r.Get(ctx, key, server); err != nil { + log.V(1).Error(err, "failed to get referred server", "reference", serverRef.Name) + errs = append(errs, err) + continue + } + servers[idx] = server + } + + return servers, errors.Join(errs...) +} + +func (r *BMCSettingsReconciler) getReferredServerMaintenances( + ctx context.Context, + log logr.Logger, + ServerMaintenanceRefs []metalv1alpha1.ServerMaintenanceRefItem, +) ([]*metalv1alpha1.ServerMaintenance, []error) { + + serverMaintenances := make([]*metalv1alpha1.ServerMaintenance, 0, len(ServerMaintenanceRefs)) + var errs []error + cnt := 0 + for _, serverMaintenanceRef := range ServerMaintenanceRefs { + key := client.ObjectKey{Name: serverMaintenanceRef.ServerMaintenanceRef.Name, Namespace: r.ManagerNamespace} + serverMaintenance := &metalv1alpha1.ServerMaintenance{} + if err := r.Get(ctx, key, serverMaintenance); err != nil { + log.V(1).Error(err, "failed to get referred serverMaintenance obj", serverMaintenanceRef.ServerMaintenanceRef.Name) + errs = append(errs, err) + continue + } + serverMaintenances = append(serverMaintenances, serverMaintenance) + cnt = cnt + 1 + } + + if len(errs) > 0 { + return serverMaintenances, errs + } + + return serverMaintenances, nil +} + +func (r *BMCSettingsReconciler) getReferredBMCSettings( + ctx context.Context, + log logr.Logger, + referredBMCSettingsRef *corev1.LocalObjectReference, +) (*metalv1alpha1.BMCSettings, error) { + key := client.ObjectKey{Name: referredBMCSettingsRef.Name, Namespace: metav1.NamespaceNone} + bmcSetting := &metalv1alpha1.BMCSettings{} + if err := r.Get(ctx, key, bmcSetting); err != nil { + log.V(1).Error(err, "failed to get referred bmcSetting") + return bmcSetting, err + } + return bmcSetting, nil +} + +func (r *BMCSettingsReconciler) getServerMaintenanceRefForServer( + ServerMaintenanceRefs []metalv1alpha1.ServerMaintenanceRefItem, + serverMaintenanceUID types.UID, +) *corev1.ObjectReference { + for _, serverMaintenanceRef := range ServerMaintenanceRefs { + if serverMaintenanceRef.ServerMaintenanceRef.UID == serverMaintenanceUID { + return serverMaintenanceRef.ServerMaintenanceRef + } + } + return nil +} + +func (r *BMCSettingsReconciler) patchBMCSettingsRefOnBMC( + ctx context.Context, + log logr.Logger, + BMC *metalv1alpha1.BMC, + BMCSettingsReference *corev1.LocalObjectReference, +) error { + if BMC.Spec.BMCSettingRef == BMCSettingsReference { + return nil + } + + var err error + BMCBase := BMC.DeepCopy() + BMC.Spec.BMCSettingRef = BMCSettingsReference + if err = r.Patch(ctx, BMC, client.MergeFrom(BMCBase)); err != nil { + log.V(1).Error(err, "failed to patch BMC settings ref") + return err + } + return err +} + +func (r *BMCSettingsReconciler) patchMaintenanceRequestRefOnBMCSettings( + ctx context.Context, + log logr.Logger, + bmcSetting *metalv1alpha1.BMCSettings, + ServerMaintenanceRefs []metalv1alpha1.ServerMaintenanceRefItem, +) error { + BMCSettingsBase := bmcSetting.DeepCopy() + + if ServerMaintenanceRefs == nil { + bmcSetting.Spec.ServerMaintenanceRefs = nil + } else { + bmcSetting.Spec.ServerMaintenanceRefs = ServerMaintenanceRefs + } + + if err := r.Patch(ctx, bmcSetting, client.MergeFrom(BMCSettingsBase)); err != nil { + log.V(1).Error(err, "failed to patch BMCSettings ref") + return err + } + + return nil +} + +func (r *BMCSettingsReconciler) updateBMCSettingsStatus( + ctx context.Context, + log logr.Logger, + bmcSetting *metalv1alpha1.BMCSettings, + state metalv1alpha1.BMCSettingsState, +) error { + + if bmcSetting.Status.State == state { + return nil + } + + BMCSettingsBase := bmcSetting.DeepCopy() + bmcSetting.Status.State = state + + if err := r.Status().Patch(ctx, bmcSetting, client.MergeFrom(BMCSettingsBase)); err != nil { + return fmt.Errorf("failed to patch bmcSetting status: %w", err) + } + + log.V(1).Info("Updated bmcSetting state ", "new state", state) + + return nil +} + +func (r *BMCSettingsReconciler) enqueueBMCSettingsByServerRefs( + ctx context.Context, + obj client.Object, +) []ctrl.Request { + log := ctrl.LoggerFrom(ctx) + host := obj.(*metalv1alpha1.Server) + + // return early if hosts are not required states + if host.Status.State != metalv1alpha1.ServerStateMaintenance { + return nil + } + + bmcSettingsList := &metalv1alpha1.BMCSettingsList{} + if err := r.List(ctx, bmcSettingsList); err != nil { + log.Error(err, "failed to list BMCSettings") + return nil + } + var req []ctrl.Request + + for _, bmcSetting := range bmcSettingsList.Items { + // if we dont have maintenance request on this bmcsetting we do not want to queue changes from servers. + if bmcSetting.Spec.ServerMaintenanceRefs == nil { + continue + } + if bmcSetting.Status.State == metalv1alpha1.BMCSettingsStateApplied || bmcSetting.Status.State == metalv1alpha1.BMCSettingsStateFailed { + continue + } + serverMaintenanceRef := r.getServerMaintenanceRefForServer(bmcSetting.Spec.ServerMaintenanceRefs, host.Spec.ServerMaintenanceRef.UID) + if serverMaintenanceRef != nil { + req = append(req, ctrl.Request{ + NamespacedName: types.NamespacedName{Namespace: bmcSetting.Namespace, Name: bmcSetting.Name}, + }) + } + } + return req +} + +func (r *BMCSettingsReconciler) enqueueBMCSettingsByBMCRefs( + ctx context.Context, + obj client.Object, +) []ctrl.Request { + + log := ctrl.LoggerFrom(ctx) + BMC := obj.(*metalv1alpha1.BMC) + bmcSettingsList := &metalv1alpha1.BMCSettingsList{} + if err := r.List(ctx, bmcSettingsList); err != nil { + log.Error(err, "failed to list BMCSettingsList") + return nil + } + + for _, bmcSetting := range bmcSettingsList.Items { + if bmcSetting.Spec.BMCRef != nil && bmcSetting.Spec.BMCRef.Name == BMC.Name { + if bmcSetting.Status.State == metalv1alpha1.BMCSettingsStateApplied || bmcSetting.Status.State == metalv1alpha1.BMCSettingsStateFailed { + return nil + } + return []ctrl.Request{{NamespacedName: types.NamespacedName{Namespace: bmcSetting.Namespace, Name: bmcSetting.Name}}} + } + } + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *BMCSettingsReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&metalv1alpha1.BMCSettings{}). + Owns(&metalv1alpha1.ServerMaintenance{}). + Watches(&metalv1alpha1.Server{}, handler.EnqueueRequestsFromMapFunc(r.enqueueBMCSettingsByServerRefs)). + Watches(&metalv1alpha1.BMC{}, handler.EnqueueRequestsFromMapFunc(r.enqueueBMCSettingsByBMCRefs)). + Complete(r) +} diff --git a/internal/controller/bmcsettings_controller_test.go b/internal/controller/bmcsettings_controller_test.go new file mode 100644 index 00000000..73ca2af6 --- /dev/null +++ b/internal/controller/bmcsettings_controller_test.go @@ -0,0 +1,387 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package controller + +import ( + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + . "sigs.k8s.io/controller-runtime/pkg/envtest/komega" + + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/ironcore-dev/controller-utils/metautils" + metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1" + "github.com/ironcore-dev/metal-operator/internal/bmcutils" + + bmcPkg "github.com/ironcore-dev/metal-operator/bmc" +) + +var _ = Describe("BMCSettings Controller", func() { + ns := SetupTest() + + var server *metalv1alpha1.Server + var bmc *metalv1alpha1.BMC + + BeforeEach(func(ctx SpecContext) { + By("Creating a BMCSecret") + bmcSecret := &metalv1alpha1.BMCSecret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + GenerateName: "test-bmc-secret-", + }, + Data: map[string][]byte{ + metalv1alpha1.BMCSecretUsernameKeyName: []byte("foo"), + metalv1alpha1.BMCSecretPasswordKeyName: []byte("bar"), + }, + } + Expect(k8sClient.Create(ctx, bmcSecret)).To(Succeed()) + + By("Creating a BMC resource") + bmc = &metalv1alpha1.BMC{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-bmc-", + Namespace: ns.Name, + }, + Spec: metalv1alpha1.BMCSpec{ + Endpoint: &metalv1alpha1.InlineEndpoint{ + IP: metalv1alpha1.MustParseIP("127.0.0.1"), + MACAddress: "23:11:8A:33:CF:EA", + }, + Protocol: metalv1alpha1.Protocol{ + Name: metalv1alpha1.ProtocolRedfishLocal, + Port: 8000, + }, + BMCSecretRef: v1.LocalObjectReference{ + Name: bmcSecret.Name, + }, + }, + } + Expect(k8sClient.Create(ctx, bmc)).To(Succeed()) + + By("Ensuring that the Server resource will be created") + server = &metalv1alpha1.Server{ + ObjectMeta: metav1.ObjectMeta{ + Name: bmcutils.GetServerNameFromBMCandIndex(0, bmc), + }, + } + Eventually(Get(server)).Should(Succeed()) + + By("Ensuring that the BMC has right state: enabled") + Eventually(Object(bmc)).Should(SatisfyAll( + HaveField("Status.State", metalv1alpha1.BMCStateEnabled), + )) + }) + + AfterEach(func(ctx SpecContext) { + DeleteAllMetalResources(ctx, ns.Name) + bmcPkg.UnitTestMockUps.ResetBMCSettings() + }) + + It("should successfully patch BMCSettings reference to referred BMC", func(ctx SpecContext) { + bmcSetting := make(map[string]string) + + By("Creating a bmcSetting") + bmcSettings := &metalv1alpha1.BMCSettings{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + GenerateName: "test-bmc-", + }, + Spec: metalv1alpha1.BMCSettingsSpec{ + Version: "1.45.455b66-rev4", + SettingsMap: bmcSetting, + BMCRef: &v1.LocalObjectReference{Name: bmc.Name}, + ServerMaintenancePolicy: metalv1alpha1.ServerMaintenancePolicyEnforced, + }, + } + Expect(k8sClient.Create(ctx, bmcSettings)).To(Succeed()) + + By("Ensuring that the BMC has the BMCSettings ref") + Eventually(Object(bmc)).Should(SatisfyAll( + HaveField("Spec.BMCSettingRef", &v1.LocalObjectReference{Name: bmcSettings.Name}), + )) + + Eventually(Object(bmcSettings)).Should(SatisfyAny( + HaveField("Status.State", metalv1alpha1.BMCSettingsStateApplied), + )) + }) + + It("should move to completed if no BMCSettings changes to referred BMC", func(ctx SpecContext) { + bmcSetting := make(map[string]string) + + By("Creating a bmcSetting") + bmcSettings := &metalv1alpha1.BMCSettings{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + GenerateName: "test-bmc-nochange", + }, + Spec: metalv1alpha1.BMCSettingsSpec{ + Version: "1.45.455b66-rev4", + SettingsMap: bmcSetting, + BMCRef: &v1.LocalObjectReference{Name: bmc.Name}, + ServerMaintenancePolicy: metalv1alpha1.ServerMaintenancePolicyEnforced, + }, + } + Expect(k8sClient.Create(ctx, bmcSettings)).To(Succeed()) + + By("Ensuring that the BMC has the BMCSettings ref") + Eventually(Object(bmc)).Should(SatisfyAll( + HaveField("Spec.BMCSettingRef", &v1.LocalObjectReference{Name: bmcSettings.Name}), + )) + + Eventually(Object(bmcSettings)).Should(SatisfyAll( + HaveField("Status.State", metalv1alpha1.BMCSettingsStateApplied), + )) + + By("Deleting the BMCSettings") + Expect(k8sClient.Delete(ctx, bmcSettings)).To(Succeed()) + + By("Ensuring that the BMCSettings ref is empty on BMC") + Eventually(Object(bmc)).Should(SatisfyAll( + HaveField("Spec.BMCSettingRef", BeNil()), + )) + }) + + It("should update the setting if BMCSettings changes requested in Available State", func(ctx SpecContext) { + bmcSetting := make(map[string]string) + bmcSetting["abc"] = "changed-bmc-setting" + + By("update the server state to Available state") + Eventually(UpdateStatus(server, func() { + server.Status.State = metalv1alpha1.ServerStateAvailable + server.Status.PowerState = metalv1alpha1.ServerOffPowerState + })).Should(Succeed()) + + By("Creating a BMCSetting") + bmcSettings := &metalv1alpha1.BMCSettings{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + GenerateName: "test-bmc-change", + }, + Spec: metalv1alpha1.BMCSettingsSpec{ + Version: "1.45.455b66-rev4", + SettingsMap: bmcSetting, + BMCRef: &v1.LocalObjectReference{Name: bmc.Name}, + ServerMaintenancePolicy: metalv1alpha1.ServerMaintenancePolicyEnforced, + }, + } + Expect(k8sClient.Create(ctx, bmcSettings)).To(Succeed()) + + By("Ensuring that the BMC has the BMCSettings ref") + Eventually(Object(bmc)).Should(SatisfyAll( + HaveField("Spec.BMCSettingRef", &v1.LocalObjectReference{Name: bmcSettings.Name}), + )) + + By("Ensuring that the BMCSettings has reached next state") + Eventually(Object(bmcSettings)).Should(SatisfyAny( + HaveField("Status.State", metalv1alpha1.BMCSettingsStateInProgress), + HaveField("Status.State", metalv1alpha1.BMCSettingsStateApplied), + )) + Eventually(Object(bmcSettings)).Should(SatisfyAll( + HaveField("Status.State", metalv1alpha1.BMCSettingsStateApplied), + )) + + By("Deleting the BMCSettings") + Expect(k8sClient.Delete(ctx, bmcSettings)).To(Succeed()) + + By("Ensuring that the BMCSettings ref is empty on BMC") + Eventually(Object(bmc)).Should(SatisfyAll( + HaveField("Spec.BMCSettingRef", BeNil()), + )) + }) + + It("should create maintenance and wait for its approval before applying settings", func(ctx SpecContext) { + + bmcSetting := make(map[string]string) + bmcSetting["abc"] = "changed-to-req-server-maintenance-through-Ownerapproved" + + // put server in reserved state. and create a bmc setting in Ownerapproved which needs reboot. + // this is needed to check the states traversed. + serverClaim := BuildServerClaim(ctx, k8sClient, *server, ns.Name, nil, metalv1alpha1.PowerOff, "foo:bar") + TransistionServerToReserveredState(ctx, k8sClient, serverClaim, server, ns.Name) + + By("Creating a BMCSetting") + bmcSettings := &metalv1alpha1.BMCSettings{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + GenerateName: "test-bmc-change", + }, + Spec: metalv1alpha1.BMCSettingsSpec{ + Version: "1.45.455b66-rev4", + SettingsMap: bmcSetting, + BMCRef: &v1.LocalObjectReference{Name: bmc.Name}, + ServerMaintenancePolicy: metalv1alpha1.ServerMaintenancePolicyOwnerApproval, + }, + } + Expect(k8sClient.Create(ctx, bmcSettings)).To(Succeed()) + Eventually(Object(bmcSettings)).Should(SatisfyAny( + HaveField("Status.State", metalv1alpha1.BMCSettingsStateInProgress), + )) + + By("Ensuring that the Maintenance resource has been created") + var serverMaintenanceList metalv1alpha1.ServerMaintenanceList + Eventually(ObjectList(&serverMaintenanceList)).Should(HaveField("Items", Not(BeEmpty()))) + + serverMaintenance := &metalv1alpha1.ServerMaintenance{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: fmt.Sprintf("%s-%s", bmcSettings.Name, server.Name), + }, + } + Eventually(Get(serverMaintenance)).Should(Succeed()) + + By("Ensuring that the Maintenance resource has been referenced by BMCSettings resource") + Eventually(Object(bmcSettings)).Should(SatisfyAny( + HaveField("Spec.ServerMaintenanceRefs", + []metalv1alpha1.ServerMaintenanceRefItem{{ + ServerMaintenanceRef: &v1.ObjectReference{ + Kind: "ServerMaintenance", + Name: serverMaintenance.Name, + Namespace: serverMaintenance.Namespace, + UID: serverMaintenance.UID, + APIVersion: serverMaintenance.GroupVersionKind().GroupVersion().String(), + }}}), + HaveField("Spec.ServerMaintenanceRefs", + []metalv1alpha1.ServerMaintenanceRefItem{{ + ServerMaintenanceRef: &v1.ObjectReference{ + Kind: "ServerMaintenance", + Name: serverMaintenance.Name, + Namespace: serverMaintenance.Namespace, + UID: serverMaintenance.UID, + APIVersion: "metal.ironcore.dev/v1alpha1", + }}}), + )) + + By("Ensuring that the BMC has the BMCSettings ref") + Eventually(Object(bmc)).Should(SatisfyAll( + HaveField("Spec.BMCSettingRef", &v1.LocalObjectReference{Name: bmcSettings.Name}), + )) + + Eventually(Object(bmcSettings)).Should(SatisfyAny( + HaveField("Status.State", metalv1alpha1.BMCSettingsStateInProgress), + )) + + By("Approving the maintenance") + Eventually(Update(serverClaim, func() { + metautils.SetAnnotation(serverClaim, metalv1alpha1.ServerMaintenanceApprovalKey, "true") + })).Should(Succeed()) + + Eventually(Object(bmcSettings)).Should(SatisfyAny( + HaveField("Status.State", metalv1alpha1.BMCSettingsStateInProgress), + HaveField("Status.State", metalv1alpha1.BMCSettingsStateApplied), + )) + + Eventually(Object(bmcSettings)).Should(SatisfyAll( + HaveField("Status.State", metalv1alpha1.BMCSettingsStateApplied), + )) + + By("Ensuring that the Maintenance resource has been deleted") + Eventually(ObjectList(&serverMaintenanceList)).Should(HaveField("Items", BeEmpty())) + Consistently(ObjectList(&serverMaintenanceList)).Should(HaveField("Items", BeEmpty())) + Consistently(Object(bmcSettings)).Should(SatisfyAll( + HaveField("Spec.ServerMaintenanceRefs", BeNil()), + )) + + By("Deleting the BMCSettings") + Expect(k8sClient.Delete(ctx, bmcSettings)).To(Succeed()) + + By("Ensuring that the BMCSettings ref is empty on BMC") + Eventually(Object(bmc)).Should(SatisfyAll( + HaveField("Spec.BMCSettingRef", BeNil()), + )) + }) + + It("should wait for upgrade and reconcile BMCSettings version is correct", func(ctx SpecContext) { + bmcSetting := make(map[string]string) + bmcSetting["fooreboot"] = "145" + + By("update the server state to Available state") + Eventually(UpdateStatus(server, func() { + server.Status.State = metalv1alpha1.ServerStateAvailable + server.Status.PowerState = metalv1alpha1.ServerOffPowerState + })).Should(Succeed()) + + By("Creating a BMCSetting") + BMCSettings := &metalv1alpha1.BMCSettings{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + GenerateName: "test-bmc-upgrade", + }, + Spec: metalv1alpha1.BMCSettingsSpec{ + Version: "2.45.455b66-rev4", + SettingsMap: bmcSetting, + BMCRef: &v1.LocalObjectReference{Name: bmc.Name}, + ServerMaintenancePolicy: metalv1alpha1.ServerMaintenancePolicyEnforced, + }, + } + Expect(k8sClient.Create(ctx, BMCSettings)).To(Succeed()) + + By("Ensuring that the BMC has the correct BMC settings ref") + Eventually(Object(bmc)).Should(SatisfyAll( + HaveField("Spec.BMCSettingRef", Not(BeNil())), + HaveField("Spec.BMCSettingRef.Name", BMCSettings.Name), + )) + + By("Ensuring that the BMCSettings resource state is correct State inVersionUpgrade") + Eventually(Object(BMCSettings)).Should(SatisfyAny( + HaveField("Status.State", metalv1alpha1.BMCSettingsStateInProgress), + )) + + By("Ensuring that the serverMaintenance not ref. while waiting for upgrade") + Consistently(Object(BMCSettings)).Should(SatisfyAll( + HaveField("Spec.ServerMaintenanceRefs", BeNil()), + )) + + By("Simulate the server BMCSettings version update by matching the spec version") + Eventually(Update(BMCSettings, func() { + BMCSettings.Spec.Version = "1.45.455b66-rev4" + })).Should(Succeed()) + + By("Ensuring that the BMCSettings resource has completed Upgrade and setting update, and moved the state") + Eventually(Object(BMCSettings)).Should(SatisfyAny( + HaveField("Status.State", metalv1alpha1.BMCSettingsStateInProgress), + )) + + serverMaintenance := &metalv1alpha1.ServerMaintenance{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: fmt.Sprintf("%s-%s", BMCSettings.Name, server.Name), + }, + } + Eventually(Get(serverMaintenance)).Should(Succeed()) + + By("Ensuring that the BMCSettings resource hasmoved to next state") + Eventually(Object(BMCSettings)).Should(SatisfyAny( + HaveField("Status.State", metalv1alpha1.BMCSettingsStateInProgress), + HaveField("Status.State", metalv1alpha1.BMCSettingsStateApplied), + )) + Eventually(Object(BMCSettings)).Should(SatisfyAll( + HaveField("Status.State", metalv1alpha1.BMCSettingsStateApplied), + )) + + By("Ensuring that the Maintenance resource has been deleted") + var serverMaintenanceList metalv1alpha1.ServerMaintenanceList + Eventually(ObjectList(&serverMaintenanceList)).Should(HaveField("Items", BeEmpty())) + Consistently(ObjectList(&serverMaintenanceList)).Should(HaveField("Items", BeEmpty())) + Consistently(Object(BMCSettings)).Should(SatisfyAll( + HaveField("Spec.ServerMaintenanceRefs", BeNil()), + )) + + By("Deleting the BMCSetting resource") + Expect(k8sClient.Delete(ctx, BMCSettings)).To(Succeed()) + + By("Ensuring that the BMCSettings resource is removed") + Eventually(Get(BMCSettings)).Should(Satisfy(apierrors.IsNotFound)) + Consistently(Get(BMCSettings)).Should(Satisfy(apierrors.IsNotFound)) + + By("Ensuring that the Server BMCSettings ref is empty on BMC") + Eventually(Object(bmc)).Should(SatisfyAll( + HaveField("Spec.BMCSettingRef", BeNil()), + )) + }) +}) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 620d4a3f..7bf5e396 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -76,13 +76,18 @@ func DeleteAllMetalResources(ctx context.Context, namespace string) { Eventually(deleteAndList(ctx, &metalv1alpha1.Server{}, &metalv1alpha1.ServerList{})).Should( HaveField("Items", BeEmpty())) - Eventually(deleteAndList(ctx, &metalv1alpha1.BMCSecret{}, &metalv1alpha1.BMCSecretList{})).Should( - HaveField("Items", BeEmpty())) + var server metalv1alpha1.Server + Expect(k8sClient.DeleteAllOf(ctx, &server)).To(Succeed()) + var serverList metalv1alpha1.ServerList + Eventually(ObjectList(&serverList)).Should(HaveField("Items", BeEmpty())) Eventually(deleteAndList(ctx, &metalv1alpha1.BIOSSettings{}, &metalv1alpha1.BIOSSettingsList{})).Should( HaveField("Items", BeEmpty())) - Eventually(deleteAndList(ctx, &metalv1alpha1.BIOSVersion{}, &metalv1alpha1.BIOSSettingsList{})).Should( + Eventually(deleteAndList(ctx, &metalv1alpha1.BIOSVersion{}, &metalv1alpha1.BIOSVersionList{})).Should( + HaveField("Items", BeEmpty())) + + Eventually(deleteAndList(ctx, &metalv1alpha1.BMCSettings{}, &metalv1alpha1.BMCSettingsList{})).Should( HaveField("Items", BeEmpty())) } @@ -271,6 +276,19 @@ func SetupTest() *corev1.Namespace { }, }).SetupWithManager(k8sManager)).To(Succeed()) + Expect((&BMCSettingsReconciler{ + Client: k8sManager.GetClient(), + ManagerNamespace: ns.Name, + Insecure: true, + Scheme: k8sManager.GetScheme(), + ResyncInterval: 10 * time.Millisecond, + BMCOptions: bmc.Options{ + PowerPollingInterval: 50 * time.Millisecond, + PowerPollingTimeout: 200 * time.Millisecond, + BasicAuth: true, + }, + }).SetupWithManager(k8sManager)).To(Succeed()) + go func() { defer GinkgoRecover() Expect(k8sManager.Start(mgrCtx)).To(Succeed(), "failed to start manager") diff --git a/internal/webhook/v1alpha1/biossettings_webhook.go b/internal/webhook/v1alpha1/biossettings_webhook.go index d6529ff7..529ea93c 100644 --- a/internal/webhook/v1alpha1/biossettings_webhook.go +++ b/internal/webhook/v1alpha1/biossettings_webhook.go @@ -33,7 +33,7 @@ func SetupBIOSSettingsWebhookWithManager(mgr ctrl.Manager) error { // NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here. // Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. -// +kubebuilder:webhook:path=/validate-metal-ironcore-dev-v1alpha1-biossettings,mutating=false,failurePolicy=fail,sideEffects=None,groups=metal.ironcore.dev,resources=biossettings,verbs=create;update,versions=v1alpha1,name=vbiossettings-v1alpha1.kb.io,admissionReviewVersions=v1 +// +kubebuilder:webhook:path=/validate-metal-ironcore-dev-v1alpha1-biossettings,mutating=false,failurePolicy=fail,sideEffects=None,groups=metal.ironcore.dev,resources=biossettings,verbs=create;update;delete,versions=v1alpha1,name=vbiossettings-v1alpha1.kb.io,admissionReviewVersions=v1 // BIOSSettingsCustomValidator struct is responsible for validating the BIOSSettings resource // when it is created, updated, or deleted. @@ -59,15 +59,7 @@ func (v *BIOSSettingsCustomValidator) ValidateCreate(ctx context.Context, obj ru return nil, fmt.Errorf("failed to list BIOSSettingsList: %w", err) } - for _, bs := range biosSettingsList.Items { - if biossettings.Spec.ServerRef.Name == bs.Spec.ServerRef.Name { - return nil, apierrors.NewInvalid( - schema.GroupKind{Group: biossettings.GroupVersionKind().Group, Kind: biossettings.Kind}, - biossettings.GetName(), field.ErrorList{field.Duplicate(field.NewPath("spec").Child("ServerRef").Child("Name"), bs.Spec.ServerRef.Name)}) - } - } - - return nil, nil + return checkForDuplicateBiosSettingsRefToServer(biosSettingsList, biossettings) } // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type BIOSSettings. @@ -83,14 +75,7 @@ func (v *BIOSSettingsCustomValidator) ValidateUpdate(ctx context.Context, oldObj return nil, fmt.Errorf("failed to list BIOSSettingsList: %w", err) } - for _, bs := range biosSettingsList.Items { - if biossettings.Spec.ServerRef.Name == bs.Spec.ServerRef.Name && biossettings.Name != bs.Name { - return nil, apierrors.NewInvalid( - schema.GroupKind{Group: biossettings.GroupVersionKind().Group, Kind: biossettings.Kind}, - biossettings.GetName(), field.ErrorList{field.Duplicate(field.NewPath("spec").Child("ServerRef").Child("Name"), bs.Spec.ServerRef.Name)}) - } - } - return nil, nil + return checkForDuplicateBiosSettingsRefToServer(biosSettingsList, biossettings) } // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type BIOSSettings. @@ -101,7 +86,32 @@ func (v *BIOSSettingsCustomValidator) ValidateDelete(ctx context.Context, obj ru } biossettingslog.Info("Validation for BIOSSettings upon deletion", "name", biossettings.GetName()) - // TODO(user): fill in your validation logic upon object deletion. + if biossettings.Status.State == metalv1alpha1.BIOSSettingsStateInProgress { + return nil, apierrors.NewBadRequest("The bios settings in progress, unable to delete") + } + + return nil, nil +} + +func checkForDuplicateBiosSettingsRefToServer( + biosSettingsList *metalv1alpha1.BIOSSettingsList, + biosSettings *metalv1alpha1.BIOSSettings, +) (admission.Warnings, error) { + for _, bs := range biosSettingsList.Items { + if biosSettings.Name == bs.Name { + continue + } + if biosSettings.Spec.ServerRef.Name == bs.Spec.ServerRef.Name { + err := fmt.Errorf("BMC (%v) referred in %v is duplicate of BMC (%v) referred in %v", + biosSettings.Spec.ServerRef.Name, + biosSettings.Name, + bs.Spec.ServerRef.Name, + bs.Name) + return nil, apierrors.NewInvalid( + schema.GroupKind{Group: biosSettings.GroupVersionKind().Group, Kind: biosSettings.Kind}, + biosSettings.GetName(), field.ErrorList{field.Duplicate(field.NewPath("spec", "ServerRef"), err)}) + } + } return nil, nil } diff --git a/internal/webhook/v1alpha1/biossettings_webhook_test.go b/internal/webhook/v1alpha1/biossettings_webhook_test.go index 45eed796..3dfd1312 100644 --- a/internal/webhook/v1alpha1/biossettings_webhook_test.go +++ b/internal/webhook/v1alpha1/biossettings_webhook_test.go @@ -10,48 +10,38 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1" - // TODO (user): Add any additional imports if needed + . "sigs.k8s.io/controller-runtime/pkg/envtest/komega" ) var _ = Describe("BIOSSettings Webhook", func() { var ( - obj *metalv1alpha1.BIOSSettings - oldObj *metalv1alpha1.BIOSSettings - validator BIOSSettingsCustomValidator + biosSettingsV1 *metalv1alpha1.BIOSSettings + validator BIOSSettingsCustomValidator ) BeforeEach(func() { - obj = &metalv1alpha1.BIOSSettings{} - oldObj = &metalv1alpha1.BIOSSettings{} validator = BIOSSettingsCustomValidator{Client: k8sClient} - Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") - Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") - Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") - }) - - AfterEach(func() { - // TODO (user): Add any teardown logic common to all tests + SetClient(k8sClient) + By("Creating an BIOSSetttings") + biosSettingsV1 = &metalv1alpha1.BIOSSettings{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns.Name", + GenerateName: "test-", + }, + Spec: metalv1alpha1.BIOSSettingsSpec{ + Version: "P70 v1.45 (12/06/2017)", + SettingsMap: map[string]string{}, + ServerRef: &v1.LocalObjectReference{Name: "foo"}, + ServerMaintenancePolicy: metalv1alpha1.ServerMaintenancePolicyEnforced, + }, + } + Expect(k8sClient.Create(ctx, biosSettingsV1)).To(Succeed()) + DeferCleanup(k8sClient.Delete, biosSettingsV1) }) Context("When creating or updating BIOSSettings under Validating Webhook", func() { It("Should deny creation if a Spec.ServerRef field is duplicate", func(ctx SpecContext) { - By("Creating an BIOSSetttings") - biosSettingsV1 := &metalv1alpha1.BIOSSettings{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns.Name", - GenerateName: "test-", - }, - Spec: metalv1alpha1.BIOSSettingsSpec{ - Version: "P70 v1.45 (12/06/2017)", - SettingsMap: map[string]string{}, - ServerRef: &v1.LocalObjectReference{Name: "foo"}, - ServerMaintenancePolicy: metalv1alpha1.ServerMaintenancePolicyEnforced, - }, - } - Expect(k8sClient.Create(ctx, biosSettingsV1)).To(Succeed()) - DeferCleanup(k8sClient.Delete, biosSettingsV1) - By("Creating another BIOSSettings with existing ServerRef") biosSettingsV2 := &metalv1alpha1.BIOSSettings{ ObjectMeta: metav1.ObjectMeta{ @@ -69,22 +59,6 @@ var _ = Describe("BIOSSettings Webhook", func() { }) It("Should create if a Spec.ServerRef field is NOT duplicate", func() { - By("Creating an BIOSSetttings") - biosSettingsV1 := &metalv1alpha1.BIOSSettings{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns.Name", - GenerateName: "test-", - }, - Spec: metalv1alpha1.BIOSSettingsSpec{ - Version: "P70 v1.45 (12/06/2017)", - SettingsMap: map[string]string{}, - ServerRef: &v1.LocalObjectReference{Name: "foo"}, - ServerMaintenancePolicy: metalv1alpha1.ServerMaintenancePolicyEnforced, - }, - } - Expect(k8sClient.Create(ctx, biosSettingsV1)).To(Succeed()) - DeferCleanup(k8sClient.Delete, biosSettingsV1) - By("Creating another BIOSSetting with different ServerRef") biosSettingsV2 := &metalv1alpha1.BIOSSettings{ ObjectMeta: metav1.ObjectMeta{ @@ -103,22 +77,6 @@ var _ = Describe("BIOSSettings Webhook", func() { }) It("Should deny update if a Spec.ServerRef field is duplicate", func() { - By("Creating an BIOSSetttings") - biosSettingsV1 := &metalv1alpha1.BIOSSettings{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns.Name", - GenerateName: "test-", - }, - Spec: metalv1alpha1.BIOSSettingsSpec{ - Version: "P70 v1.45 (12/06/2017)", - SettingsMap: map[string]string{}, - ServerRef: &v1.LocalObjectReference{Name: "foo"}, - ServerMaintenancePolicy: metalv1alpha1.ServerMaintenancePolicyEnforced, - }, - } - Expect(k8sClient.Create(ctx, biosSettingsV1)).To(Succeed()) - DeferCleanup(k8sClient.Delete, biosSettingsV1) - By("Creating an BIOSSetting with different ServerRef") biosSettingsV2 := &metalv1alpha1.BIOSSettings{ ObjectMeta: metav1.ObjectMeta{ @@ -142,22 +100,6 @@ var _ = Describe("BIOSSettings Webhook", func() { }) It("Should allow update if a different field is duplicate", func() { - By("Creating an BIOSSetttings") - biosSettingsV1 := &metalv1alpha1.BIOSSettings{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns.Name", - GenerateName: "test-", - }, - Spec: metalv1alpha1.BIOSSettingsSpec{ - Version: "P70 v1.45 (12/06/2017)", - SettingsMap: map[string]string{}, - ServerRef: &v1.LocalObjectReference{Name: "foo"}, - ServerMaintenancePolicy: metalv1alpha1.ServerMaintenancePolicyEnforced, - }, - } - Expect(k8sClient.Create(ctx, biosSettingsV1)).To(Succeed()) - DeferCleanup(k8sClient.Delete, biosSettingsV1) - By("Creating an BIOSSetting with different ServerRef") biosSettingsV2 := &metalv1alpha1.BIOSSettings{ ObjectMeta: metav1.ObjectMeta{ @@ -181,22 +123,6 @@ var _ = Describe("BIOSSettings Webhook", func() { }) It("Should allow update if a ServerRef field is NOT duplicate", func() { - By("Creating an BIOSSetttings") - biosSettingsV1 := &metalv1alpha1.BIOSSettings{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns.Name", - GenerateName: "test-", - }, - Spec: metalv1alpha1.BIOSSettingsSpec{ - Version: "P70 v1.45 (12/06/2017)", - SettingsMap: map[string]string{}, - ServerRef: &v1.LocalObjectReference{Name: "foo"}, - ServerMaintenancePolicy: metalv1alpha1.ServerMaintenancePolicyEnforced, - }, - } - Expect(k8sClient.Create(ctx, biosSettingsV1)).To(Succeed()) - DeferCleanup(k8sClient.Delete, biosSettingsV1) - By("Creating an BIOSSetting with different ServerRef") biosSettingsV2 := &metalv1alpha1.BIOSSettings{ ObjectMeta: metav1.ObjectMeta{ @@ -218,6 +144,22 @@ var _ = Describe("BIOSSettings Webhook", func() { biosSettingsV2Updated.Spec.ServerRef = &v1.LocalObjectReference{Name: "foobar"} Expect(validator.ValidateUpdate(ctx, biosSettingsV2, biosSettingsV2Updated)).Error().ToNot(HaveOccurred()) }) + + It("Should refuse to delete if InProgress", func() { + By("Patching the biosSettingsV1 to Inprogress state") + Eventually(UpdateStatus(biosSettingsV1, func() { + biosSettingsV1.Status.State = metalv1alpha1.BIOSSettingsStateInProgress + })).Should(Succeed()) + + By("Deleting the BIOSSettings should fail") + Expect(k8sClient.Delete(ctx, biosSettingsV1)).To(Not(Succeed())) + + Eventually(UpdateStatus(biosSettingsV1, func() { + biosSettingsV1.Status.State = metalv1alpha1.BIOSSettingsStateApplied + })).Should(Succeed()) + + By("Deleting the BIOSSettings should pass: by DeferCleanup") + }) }) }) diff --git a/internal/webhook/v1alpha1/biosversion_webhook_test.go b/internal/webhook/v1alpha1/biosversion_webhook_test.go index 065310f8..7dcaa751 100644 --- a/internal/webhook/v1alpha1/biosversion_webhook_test.go +++ b/internal/webhook/v1alpha1/biosversion_webhook_test.go @@ -150,7 +150,7 @@ var _ = Describe("BIOSVersion Webhook", func() { }) It("Should refuse to delete if InProgress", func() { - By("Patching the boot configuration to a Ready state") + By("Patching the biosVersionV1 to InProgress state") Eventually(UpdateStatus(biosVersionV1, func() { biosVersionV1.Status.State = metalv1alpha1.BIOSVersionStateInProgress })).Should(Succeed()) diff --git a/internal/webhook/v1alpha1/bmcsettings_webhook.go b/internal/webhook/v1alpha1/bmcsettings_webhook.go new file mode 100644 index 00000000..2d61888f --- /dev/null +++ b/internal/webhook/v1alpha1/bmcsettings_webhook.go @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1" +) + +// nolint:unused +// log is for logging in this package. +var bmcsettingslog = logf.Log.WithName("bmcsettings-resource") + +// SetupBMCSettingsWebhookWithManager registers the webhook for BMCSettings in the manager. +func SetupBMCSettingsWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&metalv1alpha1.BMCSettings{}). + WithValidator(&BMCSettingsCustomValidator{Client: mgr.GetClient()}). + Complete() +} + +// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here. +// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. +// +kubebuilder:webhook:path=/validate-metal-ironcore-dev-v1alpha1-bmcsettings,mutating=false,failurePolicy=fail,sideEffects=None,groups=metal.ironcore.dev,resources=bmcsettings,verbs=create;update;delete,versions=v1alpha1,name=vbmcsettings-v1alpha1.kb.io,admissionReviewVersions=v1 + +// BMCSettingsCustomValidator struct is responsible for validating the BMCSettings resource +// when it is created, updated, or deleted. +// +// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, +// as this struct is used only for temporary operations and does not need to be deeply copied. +type BMCSettingsCustomValidator struct { + Client client.Client +} + +var _ webhook.CustomValidator = &BMCSettingsCustomValidator{} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type BMCSettings. +func (v *BMCSettingsCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + bmcSettings, ok := obj.(*metalv1alpha1.BMCSettings) + if !ok { + return nil, fmt.Errorf("expected a BMCSettings object but got %T", obj) + } + bmcsettingslog.Info("Validation for BMCSettings upon creation", "name", bmcSettings.GetName()) + + bmcSettingsList := &metalv1alpha1.BMCSettingsList{} + if err := v.Client.List(ctx, bmcSettingsList); err != nil { + return nil, fmt.Errorf("failed to list bmcSettingsList: %w", err) + } + return checkForDuplicateBMCSettingsRefToBMC(bmcSettingsList, bmcSettings) +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type BMCSettings. +func (v *BMCSettingsCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + bmcSettings, ok := newObj.(*metalv1alpha1.BMCSettings) + if !ok { + return nil, fmt.Errorf("expected a BMCSettings object for the newObj but got %T", newObj) + } + bmcsettingslog.Info("Validation for BMCSettings upon update", "name", bmcSettings.GetName()) + + bmcSettingsList := &metalv1alpha1.BMCSettingsList{} + if err := v.Client.List(ctx, bmcSettingsList); err != nil { + return nil, fmt.Errorf("failed to list bmcSettingsList: %w", err) + } + return checkForDuplicateBMCSettingsRefToBMC(bmcSettingsList, bmcSettings) +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type BMCSettings. +func (v *BMCSettingsCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + bmcsettings, ok := obj.(*metalv1alpha1.BMCSettings) + if !ok { + return nil, fmt.Errorf("expected a BMCSettings object but got %T", obj) + } + bmcsettingslog.Info("Validation for BMCSettings upon deletion", "name", bmcsettings.GetName()) + + if bmcsettings.Status.State == metalv1alpha1.BMCSettingsStateInProgress { + return nil, apierrors.NewBadRequest("The BMC settings in progress, unable to delete") + } + + return nil, nil +} + +func checkForDuplicateBMCSettingsRefToBMC( + bmcSettingsList *metalv1alpha1.BMCSettingsList, + bmcSettings *metalv1alpha1.BMCSettings, +) (admission.Warnings, error) { + for _, bs := range bmcSettingsList.Items { + if bmcSettings.Name == bs.Name { + continue + } + if bs.Spec.BMCRef.Name == bmcSettings.Spec.BMCRef.Name { + err := fmt.Errorf("BMC (%v) referred in %v is duplicate of BMC (%v) referred in %v", + bmcSettings.Spec.BMCRef.Name, + bmcSettings.Name, + bs.Spec.BMCRef.Name, + bs.Name) + return nil, apierrors.NewInvalid( + schema.GroupKind{Group: bmcSettings.GroupVersionKind().Group, Kind: bmcSettings.Kind}, + bmcSettings.GetName(), field.ErrorList{field.Duplicate(field.NewPath("spec", "BMCRef"), err)}) + } + } + return nil, nil +} diff --git a/internal/webhook/v1alpha1/bmcsettings_webhook_test.go b/internal/webhook/v1alpha1/bmcsettings_webhook_test.go new file mode 100644 index 00000000..0da4fbc3 --- /dev/null +++ b/internal/webhook/v1alpha1/bmcsettings_webhook_test.go @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1" + . "sigs.k8s.io/controller-runtime/pkg/envtest/komega" +) + +var _ = Describe("BMCSettings Webhook", func() { + var ( + BMCSettingsV1 *metalv1alpha1.BMCSettings + validator BMCSettingsCustomValidator + ) + + BeforeEach(func() { + BMCSettingsV1 = &metalv1alpha1.BMCSettings{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns.Name", + GenerateName: "test-", + }, + Spec: metalv1alpha1.BMCSettingsSpec{ + Version: "P70 v1.45 (12/06/2017)", + SettingsMap: map[string]string{}, + BMCRef: &v1.LocalObjectReference{Name: "foo"}, + ServerMaintenancePolicy: metalv1alpha1.ServerMaintenancePolicyEnforced, + }, + } + By("Creating an BMCSettings") + Expect(k8sClient.Create(ctx, BMCSettingsV1)).To(Succeed()) + DeferCleanup(k8sClient.Delete, BMCSettingsV1) + validator = BMCSettingsCustomValidator{Client: k8sClient} + SetClient(k8sClient) + + }) + + Context("When creating or updating BMCSettings under Validating Webhook", func() { + + It("Should deny creation if a BMC referred is already referred by another", func(ctx SpecContext) { + By("Creating another BMCSettings with reference to existing referred BMC") + BMCSettingsV2 := &metalv1alpha1.BMCSettings{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns.Name", + GenerateName: "test-bmc-", + }, + Spec: metalv1alpha1.BMCSettingsSpec{ + Version: "1.45.455b66-rev4", + SettingsMap: map[string]string{}, + BMCRef: &v1.LocalObjectReference{Name: "foo"}, + ServerMaintenancePolicy: metalv1alpha1.ServerMaintenancePolicyEnforced, + }, + } + Expect(validator.ValidateCreate(ctx, BMCSettingsV2)).Error().To(HaveOccurred()) + }) + + It("Should create if a referenced BMC is NOT duplicate", func() { + By("Creating another BMCSetting for different BMCRef") + BMCSettingsV2 := &metalv1alpha1.BMCSettings{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns.Name", + GenerateName: "test-", + }, + Spec: metalv1alpha1.BMCSettingsSpec{ + Version: "P70 v1.45 (12/06/2017)", + SettingsMap: map[string]string{}, + BMCRef: &v1.LocalObjectReference{Name: "bar"}, + ServerMaintenancePolicy: metalv1alpha1.ServerMaintenancePolicyEnforced, + }, + } + Expect(k8sClient.Create(ctx, BMCSettingsV2)).To(Succeed()) + DeferCleanup(k8sClient.Delete, BMCSettingsV2) + }) + + It("Should deny Update if a BMC referred is already referred by another", func() { + By("Creating another BMCSetting with different BMCRef") + BMCSettingsV2 := &metalv1alpha1.BMCSettings{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns.Name", + GenerateName: "test-", + }, + Spec: metalv1alpha1.BMCSettingsSpec{ + Version: "P70 v1.45 (12/06/2017)", + SettingsMap: map[string]string{}, + BMCRef: &v1.LocalObjectReference{Name: "bar"}, + ServerMaintenancePolicy: metalv1alpha1.ServerMaintenancePolicyEnforced, + }, + } + Expect(k8sClient.Create(ctx, BMCSettingsV2)).To(Succeed()) + DeferCleanup(k8sClient.Delete, BMCSettingsV2) + + By("Updating an BMCSettingsV2 to refer to existing BMC") + BMCSettingsV2Updated := BMCSettingsV2.DeepCopy() + BMCSettingsV2Updated.Spec.BMCRef = BMCSettingsV1.Spec.BMCRef + Expect(validator.ValidateUpdate(ctx, BMCSettingsV2, BMCSettingsV2Updated)).Error().To(HaveOccurred()) + }) + + It("Should Update if a BMC referred is NOT referred by another", func() { + By("Creating another BMCSetting with different BMCref") + BMCSettingsV2 := &metalv1alpha1.BMCSettings{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns.Name", + GenerateName: "test-", + }, + Spec: metalv1alpha1.BMCSettingsSpec{ + Version: "P70 v1.45 (12/06/2017)", + SettingsMap: map[string]string{}, + BMCRef: &v1.LocalObjectReference{Name: "bar"}, + ServerMaintenancePolicy: metalv1alpha1.ServerMaintenancePolicyEnforced, + }, + } + Expect(k8sClient.Create(ctx, BMCSettingsV2)).To(Succeed()) + DeferCleanup(k8sClient.Delete, BMCSettingsV2) + + By("Updating an BMCSettingsV2 to refer to new BMC") + BMCSettingsV2Updated := BMCSettingsV2.DeepCopy() + BMCSettingsV2Updated.Spec.BMCRef = &v1.LocalObjectReference{Name: "new-bmc2"} + Expect(validator.ValidateUpdate(ctx, BMCSettingsV2, BMCSettingsV2Updated)).Error().NotTo(HaveOccurred()) + }) + + It("Should refuse to delete if InProgress", func() { + By("Patching the BMCSettingsV1 to a InProgress state") + Eventually(UpdateStatus(BMCSettingsV1, func() { + BMCSettingsV1.Status.State = metalv1alpha1.BMCSettingsStateInProgress + })).Should(Succeed()) + + By("Deleting the BMCSettings should fail") + Expect(k8sClient.Delete(ctx, BMCSettingsV1)).To(Not(Succeed()), fmt.Sprintf("BMCSettings state %v", BMCSettingsV1.Status.State)) + + Eventually(UpdateStatus(BMCSettingsV1, func() { + BMCSettingsV1.Status.State = metalv1alpha1.BMCSettingsStateApplied + })).Should(Succeed()) + + By("Deleting the BMCSettings should pass: by DeferCleanup") + }) + }) +}) diff --git a/internal/webhook/v1alpha1/webhook_suite_test.go b/internal/webhook/v1alpha1/webhook_suite_test.go index 8532cdbc..59a6eecd 100644 --- a/internal/webhook/v1alpha1/webhook_suite_test.go +++ b/internal/webhook/v1alpha1/webhook_suite_test.go @@ -125,6 +125,9 @@ var _ = BeforeSuite(func() { err = SetupBIOSVersionWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) + err = SetupBMCSettingsWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + // +kubebuilder:scaffold:webhook go func() {