Skip to content

Commit

Permalink
Add RedfishLocal BMC protocol type (#17)
Browse files Browse the repository at this point in the history
In order to properly test the end to end flow against a local BMC
emulator a new BMC protocol type `RedFishLocal` has been introduced. The
main reason for that is that the `PowerOn` and `PowerOff` behaviour is
different for varios BMC implementations.
  • Loading branch information
afritzler authored Apr 23, 2024
1 parent be6af6c commit 513d418
Show file tree
Hide file tree
Showing 9 changed files with 282 additions and 17 deletions.
4 changes: 2 additions & 2 deletions bmc/bmc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 36 additions & 5 deletions bmc/redfish.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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()
Expand All @@ -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,
Expand Down
197 changes: 197 additions & 0 deletions bmc/redfish_local.go
Original file line number Diff line number Diff line change
@@ -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
}
10 changes: 10 additions & 0 deletions internal/controller/bmcutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
26 changes: 23 additions & 3 deletions internal/controller/endpoint_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion internal/controller/endpoint_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
13 changes: 10 additions & 3 deletions internal/controller/server_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
Expand Down
Loading

0 comments on commit 513d418

Please sign in to comment.