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:
+(Appears on:ServerSpec, ServerStatus) +
+BIOSSettings represents the BIOS settings for a server.
+Field | +Description | +
---|---|
+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. + |
+
+(Appears on:ServerSpec) +
+BootOrder represents the boot order of the server.
+Field | +Description | +
---|---|
+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. + |
+
@@ -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 specifies the boot order of the server.
+BIOS
BIOS specifies the BIOS settings for the server.
+bootOrder
BootOrder specifies the boot order of the server.
+BIOS
BIOS specifies the BIOS settings for the server.
+BIOS
conditions