Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RedfishLocal BMC protocol type #17

Merged
merged 1 commit into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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