diff --git a/api/v1alpha1/server_types.go b/api/v1alpha1/server_types.go index f2ecd82..bcff418 100644 --- a/api/v1alpha1/server_types.go +++ b/api/v1alpha1/server_types.go @@ -55,6 +55,24 @@ type BMCAccess struct { BMCSecretRef v1.LocalObjectReference `json:"bmcSecretRef"` } +// BootOrder represents the boot order of the server. +type BootOrder struct { + // Name is the name of the boot device. + Name string `json:"name"` + // Priority is the priority of the boot device. + Priority int `json:"priority"` + // Device is the device to boot from. + Device string `json:"device"` +} + +// BIOSSettings represents the BIOS settings for a server. +type BIOSSettings struct { + // Version specifies the version of the server BIOS for which the settings are defined. + Version string `json:"version"` + // Settings is a map of key-value pairs representing the BIOS settings. + Settings map[string]string `json:"settings,omitempty"` +} + // ServerSpec defines the desired state of a Server. type ServerSpec struct { // UUID is the unique identifier for the server. @@ -82,6 +100,11 @@ type ServerSpec struct { // the boot configuration for this server. This field is optional and can be omitted // if no boot configuration is specified. BootConfigurationRef *v1.ObjectReference `json:"bootConfigurationRef,omitempty"` + + // BootOrder specifies the boot order of the server. + BootOrder []BootOrder `json:"bootOrder,omitempty"` + // BIOS specifies the BIOS settings for the server. + BIOS []BIOSSettings `json:"BIOS,omitempty"` } // ServerState defines the possible states of a server. @@ -139,6 +162,8 @@ type ServerStatus struct { // NetworkInterfaces is a list of network interfaces associated with the server. NetworkInterfaces []NetworkInterface `json:"networkInterfaces,omitempty"` + BIOS BIOSSettings `json:"BIOS,omitempty"` + // Conditions represents the latest available observations of the server's current state. // +patchStrategy=merge // +patchMergeKey=type diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index d13b642..8000b7f 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -13,6 +13,28 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BIOSSettings) DeepCopyInto(out *BIOSSettings) { + *out = *in + if in.Settings != nil { + in, out := &in.Settings, &out.Settings + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BIOSSettings. +func (in *BIOSSettings) DeepCopy() *BIOSSettings { + if in == nil { + return nil + } + out := new(BIOSSettings) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BMC) DeepCopyInto(out *BMC) { *out = *in @@ -220,6 +242,21 @@ func (in *BMCStatus) DeepCopy() *BMCStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BootOrder) DeepCopyInto(out *BootOrder) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BootOrder. +func (in *BootOrder) DeepCopy() *BootOrder { + if in == nil { + return nil + } + out := new(BootOrder) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConsoleProtocol) DeepCopyInto(out *ConsoleProtocol) { *out = *in @@ -637,6 +674,18 @@ func (in *ServerSpec) DeepCopyInto(out *ServerSpec) { *out = new(corev1.ObjectReference) **out = **in } + if in.BootOrder != nil { + in, out := &in.BootOrder, &out.BootOrder + *out = make([]BootOrder, len(*in)) + copy(*out, *in) + } + if in.BIOS != nil { + in, out := &in.BIOS, &out.BIOS + *out = make([]BIOSSettings, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerSpec. @@ -659,6 +708,7 @@ func (in *ServerStatus) DeepCopyInto(out *ServerStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + in.BIOS.DeepCopyInto(&out.BIOS) if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]v1.Condition, len(*in)) diff --git a/bmc/bmc.go b/bmc/bmc.go index 6f0705d..e9592a9 100644 --- a/bmc/bmc.go +++ b/bmc/bmc.go @@ -20,10 +20,10 @@ type BMC interface { Reset() error // SetPXEBootOnce sets the boot device for the next system boot. - SetPXEBootOnce(systemID string) error + SetPXEBootOnce(systemUUID string) error // GetSystemInfo retrieves information about the system. - GetSystemInfo(systemID string) (SystemInfo, error) + GetSystemInfo(systemUUID string) (SystemInfo, error) // Logout closes the BMC client connection by logging out Logout() @@ -33,6 +33,59 @@ type BMC interface { // GetManager returns the manager GetManager() (*Manager, error) + + GetBootOrder(systemUUID string) ([]string, error) + + GetBiosAttributeValues(systemUUID string, attributes []string) (map[string]string, error) + + SetBiosAttributes(systemUUID string, attributes map[string]string) (reset bool, err error) + + GetBiosVersion(systemUUID string) (string, error) + + SetBootOrder(systemUUID string, order []string) error +} + +type Bios struct { + Version string + Attributes map[string]string +} + +type RegistryEntryAttributes struct { + AttributeName string + CurrentValue interface{} + DisplayName string + DisplayOrder int + HelpText string + Hidden bool + Immutable bool + MaxLength int + MenuPath string + MinLength int + ReadOnly bool + ResetRequired bool + Type string + WriteOnly bool +} + +type RegistryEntry struct { + Attributes []RegistryEntryAttributes +} + +// BiosRegistry describes the Message Registry file locator Resource. +type BiosRegistry struct { + common.Entity + // ODataContext is the odata context. + ODataContext string `json:"@odata.context"` + // ODataType is the odata type. + ODataType string `json:"@odata.type"` + // Description provides a description of this resource. + Description string + // Languages is the RFC5646-conformant language codes for the + // available Message Registries. + Languages []string + // Registry shall contain the Message Registry name and it major and + // minor versions, as defined by the Redfish Specification. + RegistryEntries RegistryEntry } type NetworkInterface struct { diff --git a/bmc/redfish.go b/bmc/redfish.go index b9c170b..96268c2 100644 --- a/bmc/redfish.go +++ b/bmc/redfish.go @@ -5,7 +5,10 @@ package bmc import ( "context" + "errors" "fmt" + "strconv" + "strings" "github.com/stmcginnis/gofish" "github.com/stmcginnis/gofish/redfish" @@ -46,45 +49,30 @@ func (r *RedfishBMC) Logout() { } // PowerOn powers on the system using Redfish. -func (r *RedfishBMC) PowerOn(systemID string) error { - service := r.client.GetService() - systems, err := service.Systems() +func (r *RedfishBMC) PowerOn(systemUUID string) error { + system, err := r.getSystemByUUID(systemUUID) if err != nil { return fmt.Errorf("failed to get systems: %w", err) } - for _, system := range systems { - if system.UUID == systemID { - powerState := system.PowerState - if powerState != redfish.OnPowerState { - if err := system.Reset(redfish.OnResetType); err != nil { - return fmt.Errorf("failed to reset system to power on state: %w", err) - } - } - break + powerState := system.PowerState + if powerState != redfish.OnPowerState { + if err := system.Reset(redfish.OnResetType); err != nil { + return fmt.Errorf("failed to reset system to power on state: %w", err) } } - return nil } // PowerOff powers off the system using Redfish. -func (r *RedfishBMC) PowerOff(systemID string) error { - service := r.client.GetService() - systems, err := service.Systems() +func (r *RedfishBMC) PowerOff(systemUUID string) error { + system, err := r.getSystemByUUID(systemUUID) if err != nil { return fmt.Errorf("failed to get systems: %w", err) } - - for _, system := range systems { - if system.UUID == systemID { - if err := system.Reset(redfish.GracefulShutdownResetType); err != nil { - return fmt.Errorf("failed to reset system to power on state: %w", err) - } - break - } + if err := system.Reset(redfish.GracefulShutdownResetType); err != nil { + return fmt.Errorf("failed to reset system to power on state: %w", err) } - return nil } @@ -116,25 +104,17 @@ func (r *RedfishBMC) GetSystems() ([]Server, error) { // SetPXEBootOnce sets the boot device for the next system boot using Redfish. func (r *RedfishBMC) SetPXEBootOnce(systemUUID string) error { - service := r.client.GetService() - - systems, err := service.Systems() + system, err := r.getSystemByUUID(systemUUID) if err != nil { return fmt.Errorf("failed to get systems: %w", err) } - - for _, system := range systems { - if system.UUID == systemUUID { - if err := system.SetBoot(redfish.Boot{ - BootSourceOverrideEnabled: redfish.OnceBootSourceOverrideEnabled, - BootSourceOverrideMode: redfish.UEFIBootSourceOverrideMode, - BootSourceOverrideTarget: redfish.PxeBootSourceOverrideTarget, - }); err != nil { - return fmt.Errorf("failed to set the boot order: %w", err) - } - } + if err := system.SetBoot(redfish.Boot{ + BootSourceOverrideEnabled: redfish.OnceBootSourceOverrideEnabled, + BootSourceOverrideMode: redfish.UEFIBootSourceOverrideMode, + BootSourceOverrideTarget: redfish.PxeBootSourceOverrideTarget, + }); err != nil { + return fmt.Errorf("failed to set the boot order: %w", err) } - return nil } @@ -146,7 +126,6 @@ func (r *RedfishBMC) GetManager() (*Manager, error) { if err != nil { return nil, fmt.Errorf("failed to get managers: %w", err) } - for _, m := range managers { // TODO: always take the first for now. return &Manager{ @@ -166,27 +145,182 @@ func (r *RedfishBMC) GetManager() (*Manager, error) { // GetSystemInfo retrieves information about the system using Redfish. func (r *RedfishBMC) GetSystemInfo(systemUUID string) (SystemInfo, error) { - service := r.client.GetService() - - systems, err := service.Systems() + system, err := r.getSystemByUUID(systemUUID) if err != nil { return SystemInfo{}, fmt.Errorf("failed to get systems: %w", err) } + return SystemInfo{ + SystemUUID: system.UUID, + Manufacturer: system.Manufacturer, + Model: system.Model, + Status: system.Status, + PowerState: system.PowerState, + SerialNumber: system.SerialNumber, + SKU: system.SKU, + IndicatorLED: string(system.IndicatorLED), + }, nil +} + +func (r *RedfishBMC) GetBootOrder(systemUUID string) ([]string, error) { + system, err := r.getSystemByUUID(systemUUID) + if err != nil { + return []string{}, err + } + return system.Boot.BootOrder, nil +} + +func (r *RedfishBMC) GetBiosVersion(systemUUID string) (string, error) { + system, err := r.getSystemByUUID(systemUUID) + if err != nil { + return "", err + } + return system.BIOSVersion, nil +} + +func (r *RedfishBMC) GetBiosAttributeValues( + systemUUID string, + attributes []string, +) ( + result map[string]string, + err error, +) { + if len(attributes) == 0 { + return + } + system, err := r.getSystemByUUID(systemUUID) + if err != nil { + return + } + bios, err := system.Bios() + if err != nil { + return + } + filteredAttr, err := r.getFilteredBiosRegistryAttributes(false, false) + if err != nil { + return + } + result = make(map[string]string, len(attributes)) + for _, name := range attributes { + if _, ok := filteredAttr[name]; ok { + result[name] = bios.Attributes.String(name) + } + } + return +} + +// SetBiosAttributes sets given bios attributes. Returns true if bios reset is required +func (r *RedfishBMC) SetBiosAttributes( + systemUUID string, + attributes map[string]string, +) ( + reset bool, + err error, +) { + reset = false + system, err := r.getSystemByUUID(systemUUID) + if err != nil { + return + } + bios, err := system.Bios() + if err != nil { + return + } + reset, err = r.checkBiosAttributes(attributes) + if err != nil { + return + } + attrs := make(map[string]interface{}, len(attributes)) + for name, value := range attributes { + attrs[name] = value + } + return reset, bios.UpdateBiosAttributes(attrs) +} + +// SetBootOrder sets bios boot order +func (r *RedfishBMC) SetBootOrder(systemUUID string, bootOrder []string) error { + system, err := r.getSystemByUUID(systemUUID) + if err != nil { + return err + } + return system.SetBoot( + redfish.Boot{ + BootSourceOverrideEnabled: redfish.ContinuousBootSourceOverrideEnabled, + BootSourceOverrideTarget: redfish.NoneBootSourceOverrideTarget, + BootOrder: bootOrder, + }, + ) +} + +func (r *RedfishBMC) getFilteredBiosRegistryAttributes( + readOnly bool, + immutable bool, +) ( + filtered map[string]RegistryEntryAttributes, + err error, +) { + registries, err := r.client.Service.Registries() + biosRegistry := &BiosRegistry{} + for _, registry := range registries { + if strings.Contains(registry.ID, "BiosAttributeRegistry") { + err = registry.Get(r.client, registry.Location[0].URI, biosRegistry) + if err != nil { + return + } + } + } + // filter out immutable, readonly and hidden attributes + filtered = make(map[string]RegistryEntryAttributes) + for _, entry := range biosRegistry.RegistryEntries.Attributes { + if entry.Immutable == immutable && entry.ReadOnly == readOnly && !entry.Hidden { + filtered[entry.AttributeName] = entry + } + } + return +} + +func (r *RedfishBMC) checkBiosAttributes(attrs map[string]string) (reset bool, err error) { + reset = false + // filter out immutable, readonly and hidden attributes + filtered, err := r.getFilteredBiosRegistryAttributes(false, false) + if err != nil { + return + } + //TODO: add more types like maps and Enumerations + for name, value := range attrs { + entryAttribute, ok := filtered[name] + if !ok { + err = errors.Join(err, fmt.Errorf("attribute %s not found or immutable/hidden", name)) + continue + } + if entryAttribute.ResetRequired { + reset = true + } + switch strings.ToLower(entryAttribute.Type) { + case "integer": + _, Aerr := strconv.Atoi(value) + if Aerr != nil { + err = errors.Join(err, fmt.Errorf("attribute %s value has wrong type", name)) + } + case "string": + continue + default: + err = errors.Join(err, fmt.Errorf("attribute %s value has wrong type", name)) + } + } + return +} + +func (r *RedfishBMC) getSystemByUUID(systemUUID string) (*redfish.ComputerSystem, error) { + service := r.client.GetService() + systems, err := service.Systems() + if err != nil { + return nil, err + } for _, system := range systems { if system.UUID == systemUUID { - return SystemInfo{ - SystemUUID: system.UUID, - Manufacturer: system.Manufacturer, - Model: system.Model, - Status: system.Status, - PowerState: system.PowerState, - SerialNumber: system.SerialNumber, - SKU: system.SKU, - IndicatorLED: string(system.IndicatorLED), - }, nil + return system, nil } } - - return SystemInfo{}, nil + return nil, errors.New("no system found") } diff --git a/bmc/redfish_local.go b/bmc/redfish_local.go index 048c75f..2ac3af9 100644 --- a/bmc/redfish_local.go +++ b/bmc/redfish_local.go @@ -7,7 +7,6 @@ import ( "context" "fmt" - "github.com/stmcginnis/gofish" "github.com/stmcginnis/gofish/redfish" ) @@ -15,7 +14,7 @@ var _ BMC = (*RedfishLocalBMC)(nil) // RedfishLocalBMC is an implementation of the BMC interface for Redfish. type RedfishLocalBMC struct { - client *gofish.APIClient + *RedfishBMC } // NewRedfishLocalBMCClient creates a new RedfishLocalBMC with the given connection details. @@ -23,19 +22,12 @@ func NewRedfishLocalBMCClient( ctx context.Context, endpoint, username, password string, basicAuth bool, -) (*RedfishLocalBMC, error) { - clientConfig := gofish.ClientConfig{ - Endpoint: endpoint, - Username: username, - Password: password, - Insecure: true, - BasicAuth: basicAuth, - } - client, err := gofish.ConnectContext(ctx, clientConfig) +) (BMC, error) { + bmc, err := NewRedfishBMCClient(ctx, endpoint, username, password, basicAuth) if err != nil { - return nil, fmt.Errorf("failed to connect to redfish endpoint: %w", err) + return nil, err } - return &RedfishLocalBMC{client: client}, nil + return &RedfishLocalBMC{RedfishBMC: bmc}, nil } func (r RedfishLocalBMC) PowerOn(systemUUID string) error { @@ -77,108 +69,3 @@ func (r RedfishLocalBMC) PowerOff(systemUUID string) error { } return nil } - -func (r RedfishLocalBMC) Reset() error { - //TODO implement me - panic("implement me") -} - -func (r RedfishLocalBMC) SetPXEBootOnce(systemUUID string) error { - service := r.client.GetService() - - systems, err := service.Systems() - if err != nil { - return fmt.Errorf("failed to get systems: %w", err) - } - - for _, system := range systems { - if system.UUID == systemUUID { - if err := system.SetBoot(redfish.Boot{ - BootSourceOverrideEnabled: redfish.OnceBootSourceOverrideEnabled, - BootSourceOverrideMode: redfish.UEFIBootSourceOverrideMode, - BootSourceOverrideTarget: redfish.PxeBootSourceOverrideTarget, - }); err != nil { - return fmt.Errorf("failed to set the boot order: %w", err) - } - } - } - - return nil -} - -func (r RedfishLocalBMC) GetSystemInfo(systemUUID string) (SystemInfo, error) { - service := r.client.GetService() - - systems, err := service.Systems() - if err != nil { - return SystemInfo{}, fmt.Errorf("failed to get systems: %w", err) - } - - for _, system := range systems { - if system.UUID == systemUUID { - return SystemInfo{ - SystemUUID: system.UUID, - Manufacturer: system.Manufacturer, - Model: system.Model, - Status: system.Status, - PowerState: system.PowerState, - SerialNumber: system.SerialNumber, - SKU: system.SKU, - IndicatorLED: string(system.IndicatorLED), - }, nil - } - } - - return SystemInfo{}, nil -} - -func (r RedfishLocalBMC) Logout() { - if r.client != nil { - r.client.Logout() - } -} - -func (r RedfishLocalBMC) GetSystems() ([]Server, error) { - service := r.client.GetService() - systems, err := service.Systems() - if err != nil { - return nil, fmt.Errorf("failed to get systems: %w", err) - } - servers := make([]Server, 0, len(systems)) - for _, s := range systems { - servers = append(servers, Server{ - UUID: s.UUID, - Model: s.Model, - Manufacturer: s.Manufacturer, - PowerState: PowerState(s.PowerState), - SerialNumber: s.SerialNumber, - }) - } - return servers, nil -} - -func (r RedfishLocalBMC) GetManager() (*Manager, error) { - if r.client == nil { - return nil, fmt.Errorf("no client found") - } - managers, err := r.client.Service.Managers() - if err != nil { - return nil, fmt.Errorf("failed to get managers: %w", err) - } - - 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 - } - - return nil, err -} diff --git a/config/crd/bases/metal.ironcore.dev_servers.yaml b/config/crd/bases/metal.ironcore.dev_servers.yaml index 2a1d23e..1664dfb 100644 --- a/config/crd/bases/metal.ironcore.dev_servers.yaml +++ b/config/crd/bases/metal.ironcore.dev_servers.yaml @@ -67,6 +67,25 @@ spec: spec: description: ServerSpec defines the desired state of a Server. properties: + BIOS: + description: BIOS specifies the BIOS settings for the server. + items: + description: BIOSSettings represents the BIOS settings for a server. + properties: + settings: + additionalProperties: + type: string + description: Settings is a map of key-value pairs representing + the BIOS settings. + type: object + version: + description: Version specifies the version of the server BIOS + for which the settings are defined. + type: string + required: + - version + type: object + type: array bmc: description: |- BMC contains the access details for the BMC. @@ -182,6 +201,26 @@ spec: type: string type: object x-kubernetes-map-type: atomic + bootOrder: + description: BootOrder specifies the boot order of the server. + items: + description: BootOrder represents the boot order of the server. + properties: + device: + description: Device is the device to boot from. + type: string + name: + description: Name is the name of the boot device. + type: string + priority: + description: Priority is the priority of the boot device. + type: integer + required: + - device + - name + - priority + type: object + type: array indicatorLED: description: IndicatorLED specifies the desired state of the server's indicator LED. @@ -244,6 +283,22 @@ spec: status: description: ServerStatus defines the observed state of Server. properties: + BIOS: + description: BIOSSettings represents the BIOS settings for a server. + properties: + settings: + additionalProperties: + type: string + description: Settings is a map of key-value pairs representing + the BIOS settings. + type: object + version: + description: Version specifies the version of the server BIOS + for which the settings are defined. + type: string + required: + - version + type: object conditions: description: Conditions represents the latest available observations of the server's current state. diff --git a/docs/api-reference/api.md b/docs/api-reference/api.md index d49a894..a9d7422 100644 --- a/docs/api-reference/api.md +++ b/docs/api-reference/api.md @@ -10,6 +10,46 @@ Resource Types: +

BIOSSettings +

+

+(Appears on:ServerSpec, ServerStatus) +

+
+

BIOSSettings represents the BIOS settings for a server.

+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+version
+ +string + +
+

Version specifies the version of the server BIOS for which the settings are defined.

+
+settings
+ +map[string]string + +
+

Settings is a map of key-value pairs representing the BIOS settings.

+

BMC

@@ -544,6 +584,57 @@ BMCPowerState +

BootOrder +

+

+(Appears on:ServerSpec) +

+
+

BootOrder represents the boot order of the server.

+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Name is the name of the boot device.

+
+priority
+ +int + +
+

Priority is the priority of the boot device.

+
+device
+ +string + +
+

Device is the device to boot from.

+

ConsoleProtocol

@@ -1136,6 +1227,32 @@ the boot configuration for this server. This field is optional and can be omitte if no boot configuration is specified.

+ + +bootOrder
+ + +[]BootOrder + + + + +

BootOrder specifies the boot order of the server.

+ + + + +BIOS
+ + +[]BIOSSettings + + + + +

BIOS specifies the BIOS settings for the server.

+ + @@ -1746,6 +1863,32 @@ the boot configuration for this server. This field is optional and can be omitte if no boot configuration is specified.

+ + +bootOrder
+ + +[]BootOrder + + + + +

BootOrder specifies the boot order of the server.

+ + + + +BIOS
+ + +[]BIOSSettings + + + + +

BIOS specifies the BIOS settings for the server.

+ +

ServerState @@ -1880,6 +2023,18 @@ ServerState +BIOS
+ + +BIOSSettings + + + + + + + + conditions
diff --git a/internal/controller/server_controller.go b/internal/controller/server_controller.go index fa509be..09cb3fd 100644 --- a/internal/controller/server_controller.go +++ b/internal/controller/server_controller.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "net/http" + "sort" "time" "k8s.io/apimachinery/pkg/types" @@ -23,6 +24,7 @@ import ( metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1" "github.com/ironcore-dev/metal-operator/internal/ignition" v1 "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" @@ -145,6 +147,16 @@ func (r *ServerReconciler) reconcile(ctx context.Context, log logr.Logger, serve } log.V(1).Info("Updated Server status") + if err := r.applyBiosSettings(ctx, log, server); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to update server bios settings: %w", err) + } + log.V(1).Info("Updated Server bios settings") + + if err := r.applyBootOrder(ctx, log, server); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to update server bios boot order: %w", err) + } + log.V(1).Info("Updated Server bios boot order") + requeue, err := r.ensureServerStateTransition(ctx, log, server) if requeue && err == nil { return ctrl.Result{Requeue: requeue, RequeueAfter: r.RequeueInterval}, nil @@ -309,6 +321,27 @@ func (r *ServerReconciler) updateServerStatus(ctx context.Context, log logr.Logg server.Status.Manufacturer = systemInfo.Manufacturer server.Status.IndicatorLED = metalv1alpha1.IndicatorLED(systemInfo.IndicatorLED) + currentBiosVersion, err := bmcClient.GetBiosVersion(server.Spec.UUID) + if err != nil { + return fmt.Errorf("failed to load bios version: %w", err) + } + + for _, bios := range server.Spec.BIOS { + if bios.Version == currentBiosVersion { + // with go 1.23: switch to maps.Keys(bios.Settings) + keys := make([]string, 0, len(bios.Settings)) + for k := range bios.Settings { + keys = append(keys, k) + } + attributes, err := bmcClient.GetBiosAttributeValues(server.Spec.UUID, keys) + if err != nil { + return fmt.Errorf("failed load bios settings: %w", err) + } + server.Status.BIOS.Version = currentBiosVersion + server.Status.BIOS.Settings = attributes + } + } + if err := r.Status().Patch(ctx, server, client.MergeFrom(serverBase)); err != nil { return fmt.Errorf("failed to patch Server status: %w", err) } @@ -613,3 +646,88 @@ func (r *ServerReconciler) enqueueServerByServerBootConfiguration() handler.Even } }) } + +func (r *ServerReconciler) applyBootOrder(ctx context.Context, log logr.Logger, server *metalv1alpha1.Server) error { + if server.Spec.BMCRef == nil && server.Spec.BMC == nil { + log.V(1).Info("Server has no BMC connection configured") + return nil + } + bmcClient, err := GetBMCClientForServer(ctx, r.Client, server, r.Insecure) + if err != nil { + return fmt.Errorf("failed to create BMC client: %w", err) + } + defer bmcClient.Logout() + + order, err := bmcClient.GetBootOrder(server.Spec.UUID) + if err != nil { + return fmt.Errorf("failed to create BMC client: %w", err) + } + + sort.Slice(server.Spec.BootOrder, func(i, j int) bool { + return server.Spec.BootOrder[i].Priority < server.Spec.BootOrder[j].Priority + }) + newOrder := []string{} + change := false + for i, boot := range server.Spec.BootOrder { + newOrder = append(newOrder, boot.Device) + if order[i] != boot.Device { + change = true + } + } + if change { + return bmcClient.SetBootOrder(server.Spec.UUID, newOrder) + } + return nil +} + +func (r *ServerReconciler) applyBiosSettings(ctx context.Context, log logr.Logger, server *metalv1alpha1.Server) error { + serverBase := server.DeepCopy() + if server.Spec.BMCRef == nil && server.Spec.BMC == nil { + log.V(1).Info("Server has no BMC connection configured") + return nil + } + bmcClient, err := GetBMCClientForServer(ctx, r.Client, server, r.Insecure) + if err != nil { + return fmt.Errorf("failed to create BMC client: %w", err) + } + defer bmcClient.Logout() + + version, err := bmcClient.GetBiosVersion(server.Spec.UUID) + if err != nil { + return fmt.Errorf("failed to create BMC client: %w", err) + } + + versionMatch := false + diff := map[string]string{} + for _, bios := range server.Spec.BIOS { + if bios.Version == version { + versionMatch = true + for key, value := range bios.Settings { + if res, ok := server.Status.BIOS.Settings[key]; !ok { + if !ok || res != value { + diff[key] = value + } + } + } + reset, err := bmcClient.SetBiosAttributes(server.Spec.UUID, diff) + if err != nil { + return err + } + if reset { + if changed := meta.SetStatusCondition(&server.Status.Conditions, metav1.Condition{ + Type: "Reboot needed", + }); changed { + if err := r.Status().Patch(ctx, server, client.MergeFrom(serverBase)); err != nil { + return fmt.Errorf("failed to patch Server status: %w", err) + } + } + } + break + } + } + if !versionMatch { + log.V(1).Info("none of the Bios versions match") + return nil + } + return nil +}