diff --git a/bmc/bmc.go b/bmc/bmc.go index 71aedc9..73e23cd 100644 --- a/bmc/bmc.go +++ b/bmc/bmc.go @@ -24,10 +24,10 @@ import ( // BMC defines an interface for interacting with a Baseboard Management Controller. type BMC interface { // PowerOn powers on the system. - PowerOn() error + PowerOn(systemUUID string) error // PowerOff powers off the system. - PowerOff() error + PowerOff(systemUUID string) error // Reset performs a reset on the system. Reset() error diff --git a/bmc/redfish.go b/bmc/redfish.go index fe1e519..146eca8 100644 --- a/bmc/redfish.go +++ b/bmc/redfish.go @@ -31,7 +31,7 @@ type RedfishBMC struct { client *gofish.APIClient } -// NewRedfishBMCClient creates a new RedfishLocalBMC with the given connection details. +// NewRedfishBMCClient creates a new RedfishBMC with the given connection details. func NewRedfishBMCClient( ctx context.Context, endpoint, username, password string, @@ -59,12 +59,43 @@ func (r *RedfishBMC) Logout() { } // PowerOn powers on the system using Redfish. -func (r *RedfishBMC) PowerOn() error { +func (r *RedfishBMC) PowerOn(systemID 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 == systemID { + if err := system.Reset(redfish.OnResetType); err != nil { + return fmt.Errorf("failed to reset system to power on state: %w", err) + } + break + } + } + return nil } // PowerOff powers off the system using Redfish. -func (r *RedfishBMC) PowerOff() error { +func (r *RedfishBMC) PowerOff(systemID 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 == systemID { + if err := system.Reset(redfish.GracefulShutdownResetType); err != nil { + return fmt.Errorf("failed to reset system to power on state: %w", err) + } + break + } + } + return nil } @@ -95,7 +126,7 @@ func (r *RedfishBMC) GetSystems() ([]Server, error) { } // SetPXEBootOnce sets the boot device for the next system boot using Redfish. -func (r *RedfishBMC) SetPXEBootOnce(systemID string) error { +func (r *RedfishBMC) SetPXEBootOnce(systemUUID string) error { service := r.client.GetService() systems, err := service.Systems() @@ -104,7 +135,7 @@ func (r *RedfishBMC) SetPXEBootOnce(systemID string) error { } for _, system := range systems { - if system.ID == systemID { + if system.UUID == systemUUID { if err := system.SetBoot(redfish.Boot{ BootSourceOverrideEnabled: redfish.OnceBootSourceOverrideEnabled, BootSourceOverrideMode: redfish.UEFIBootSourceOverrideMode, diff --git a/bmc/redfish_local.go b/bmc/redfish_local.go new file mode 100644 index 0000000..e55d9c3 --- /dev/null +++ b/bmc/redfish_local.go @@ -0,0 +1,197 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package bmc + +import ( + "context" + "fmt" + + "github.com/stmcginnis/gofish" + "github.com/stmcginnis/gofish/redfish" +) + +var _ BMC = (*RedfishLocalBMC)(nil) + +// RedfishLocalBMC is an implementation of the BMC interface for Redfish. +type RedfishLocalBMC struct { + client *gofish.APIClient +} + +// NewRedfishLocalBMCClient creates a new RedfishLocalBMC with the given connection details. +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) + if err != nil { + return nil, fmt.Errorf("failed to connect to redfish endpoint: %w", err) + } + return &RedfishLocalBMC{client: client}, nil +} + +func (r RedfishLocalBMC) PowerOn(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 { + system.PowerState = redfish.OnPowerState + systemURI := fmt.Sprintf("/redfish/v1/Systems/%s", system.ID) + if err := system.Patch(systemURI, system); err != nil { + return fmt.Errorf("failed to set power state %s for system %s: %w", redfish.OnPowerState, systemUUID, err) + } + break + } + } + return nil +} + +func (r RedfishLocalBMC) PowerOff(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 { + system.PowerState = redfish.OffPowerState + systemURI := fmt.Sprintf("/redfish/v1/Systems/%s", system.ID) + if err := system.Patch(systemURI, system); err != nil { + return fmt.Errorf("failed to set power state %s for system %s: %w", redfish.OffPowerState, systemUUID, err) + } + break + } + } + 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/internal/controller/bmcutils.go b/internal/controller/bmcutils.go index 00ad92d..73c4fac 100644 --- a/internal/controller/bmcutils.go +++ b/internal/controller/bmcutils.go @@ -67,6 +67,16 @@ func GetBMCClientFromBMC(ctx context.Context, c client.Client, bmcObj *metalv1al if err != nil { return nil, fmt.Errorf("failed to create Redfish client: %w", err) } + case ProtocolRedfishLocal: + bmcAddress := fmt.Sprintf("%s://%s:%d", protocol, endpoint.Spec.IP, bmcObj.Spec.Protocol.Port) + username, password, err := GetBMCCredentialsFromSecret(bmcSecret) + if err != nil { + return nil, fmt.Errorf("failed to get credentials from BMC secret: %w", err) + } + bmcClient, err = bmc.NewRedfishLocalBMCClient(ctx, bmcAddress, username, password, true) + if err != nil { + return nil, fmt.Errorf("failed to create Redfish client: %w", err) + } default: return nil, fmt.Errorf("unsupported BMC protocol %s", bmcObj.Spec.Protocol.Name) } diff --git a/internal/controller/endpoint_controller.go b/internal/controller/endpoint_controller.go index ba4d5b2..f6ea3b5 100644 --- a/internal/controller/endpoint_controller.go +++ b/internal/controller/endpoint_controller.go @@ -36,9 +36,10 @@ import ( ) const ( - BMCType = "bmc" - ProtocolRedfish = "Redfish" - EndpointFinalizer = "metal.ironcore.dev/endpoint" + BMCType = "bmc" + ProtocolRedfish = "Redfish" + ProtocolRedfishLocal = "RedfishLocal" + EndpointFinalizer = "metal.ironcore.dev/endpoint" ) // EndpointReconciler reconciles a Endpoints object @@ -118,6 +119,25 @@ func (r *EndpointReconciler) reconcile(ctx context.Context, log logr.Logger, end return ctrl.Result{}, fmt.Errorf("failed to apply BMC object: %w", err) } log.V(1).Info("Applied BMC object for endpoint") + case ProtocolRedfishLocal: + log.V(1).Info("Creating client for a local test BMC") + bmcAddress := fmt.Sprintf("%s://%s:%d", r.getProtocol(), endpoint.Spec.IP, m.Port) + bmcClient, err := bmc.NewRedfishLocalBMCClient(ctx, bmcAddress, m.DefaultCredentials[0].Username, m.DefaultCredentials[0].Password, true) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to create BMC client: %w", err) + } + defer bmcClient.Logout() + + var bmcSecret *metalv1alpha1.BMCSecret + if bmcSecret, err = r.applyBMCSecret(ctx, endpoint, m); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to apply BMCSecret: %w", err) + } + log.V(1).Info("Applied local test BMC secret for endpoint") + + if err := r.applyBMC(ctx, endpoint, bmcSecret, m); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to apply BMC object: %w", err) + } + log.V(1).Info("Applied local test BMC object for endpoint") } // TODO: other types like Switches can be handled here later } diff --git a/internal/controller/endpoint_controller_test.go b/internal/controller/endpoint_controller_test.go index 58df225..9f2809e 100644 --- a/internal/controller/endpoint_controller_test.go +++ b/internal/controller/endpoint_controller_test.go @@ -83,7 +83,7 @@ var _ = Describe("Endpoints Controller", func() { HaveField("Spec.EndpointRef.Name", Equal(endpoint.Name)), HaveField("Spec.BMCSecretRef.Name", Equal(bmc.Name)), HaveField("Spec.Protocol", metalv1alpha1.Protocol{ - Name: ProtocolRedfish, + Name: ProtocolRedfishLocal, Port: 8000, }), HaveField("Spec.ConsoleProtocol", &metalv1alpha1.ConsoleProtocol{ diff --git a/internal/controller/server_controller.go b/internal/controller/server_controller.go index 9e14702..a9e6ffe 100644 --- a/internal/controller/server_controller.go +++ b/internal/controller/server_controller.go @@ -184,6 +184,13 @@ func (r *ServerReconciler) reconcile(ctx context.Context, log logr.Logger, serve } log.V(1).Info("Extracted Server details") + // TODO: fix that by providing the power state to the ensure method + server.Spec.Power = metalv1alpha1.PowerOff + if err := r.ensureServerPowerState(ctx, log, server); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to shutdown server: %w", err) + } + log.V(1).Info("Server state set to shutdown") + if err := r.patchServerState(ctx, server, metalv1alpha1.ServerStateAvailable); err != nil { return ctrl.Result{}, err } @@ -346,7 +353,7 @@ func (r *ServerReconciler) pxeBootServer(ctx context.Context, server *metalv1alp return fmt.Errorf("failed to set PXE boot one for server: %w", err) } - if err := bmcClient.PowerOn(); err != nil { + if err := bmcClient.PowerOn(server.Spec.UUID); err != nil { return fmt.Errorf("failed to power on server: %w", err) } return nil @@ -421,12 +428,12 @@ func (r *ServerReconciler) ensureServerPowerState(ctx context.Context, log logr. } if powerOp == powerOpOn { - if err := bmcClient.PowerOn(); err != nil { + if err := bmcClient.PowerOn(server.Spec.UUID); err != nil { return fmt.Errorf("failed to power on server: %w", err) } } if powerOp == powerOpOff { - if err := bmcClient.PowerOff(); err != nil { + if err := bmcClient.PowerOff(server.Spec.UUID); err != nil { return fmt.Errorf("failed to power off server: %w", err) } } diff --git a/internal/controller/server_controller_test.go b/internal/controller/server_controller_test.go index 2777152..9eaba80 100644 --- a/internal/controller/server_controller_test.go +++ b/internal/controller/server_controller_test.go @@ -126,7 +126,6 @@ var _ = Describe("Server Controller", func() { HaveField("Status.Manufacturer", "Contoso"), HaveField("Status.SKU", "8675309"), HaveField("Status.SerialNumber", "437XR1138R2"), - HaveField("Status.PowerState", metalv1alpha1.ServerOnPowerState), HaveField("Status.IndicatorLED", metalv1alpha1.OffIndicatorLED), HaveField("Status.State", metalv1alpha1.ServerStateInitial), )) @@ -143,9 +142,10 @@ var _ = Describe("Server Controller", func() { bootConfig.Status.State = metalv1alpha1.ServerBootConfigurationStateReady })).Should(Succeed()) - By("Ensuring that the server is set to available") + By("Ensuring that the server is set to available and powered off") Eventually(Object(server)).Should(SatisfyAll( HaveField("Status.State", metalv1alpha1.ServerStateAvailable), + HaveField("Status.PowerState", metalv1alpha1.ServerOffPowerState), HaveField("Status.NetworkInterfaces", Not(BeEmpty())), )) }) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index fae5ae8..17eefba 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -143,7 +143,7 @@ func SetupTest() *corev1.Namespace { { MacPrefix: "23", Manufacturer: "Foo", - Protocol: "Redfish", + Protocol: "RedfishLocal", Port: 8000, Type: "bmc", DefaultCredentials: []macdb.Credential{