From 70021989e0f49e3e32fc153df63b18155983a67e Mon Sep 17 00:00:00 2001 From: John Mears Date: Fri, 17 Nov 2023 19:18:49 -0700 Subject: [PATCH] Add GetBootDeviceOverride support for redfish --- bmc/boot_device.go | 81 ++++++++++++++++++ bmc/boot_device_test.go | 111 +++++++++++++++++++++++++ client.go | 6 ++ errors/errors.go | 3 + internal/redfishwrapper/boot_device.go | 67 ++++++++++++++- providers/redfish/redfish.go | 7 +- 6 files changed, 271 insertions(+), 4 deletions(-) diff --git a/bmc/boot_device.go b/bmc/boot_device.go index ceabb991..133b9493 100644 --- a/bmc/boot_device.go +++ b/bmc/boot_device.go @@ -14,12 +14,29 @@ type BootDeviceSetter interface { BootDeviceSet(ctx context.Context, bootDevice string, setPersistent, efiBoot bool) (ok bool, err error) } +// BootDeviceOverrideGetter gets boot override settings for a machine +type BootDeviceOverrideGetter interface { + BootDeviceOverrideGet(ctx context.Context) (override *BootDeviceOverride, err error) +} + // bootDeviceProviders is an internal struct to correlate an implementation/provider and its name type bootDeviceProviders struct { name string bootDeviceSetter BootDeviceSetter } +// bootOverrideProvider is an internal struct to correlate an implementation/provider and its name +type bootOverrideProvider struct { + name string + bootOverrider BootDeviceOverrideGetter +} + +type BootDeviceOverride struct { + IsPersistent bool + IsEFIBoot bool + Device string +} + // setBootDevice sets the next boot device. // // setPersistent persists the next boot device. @@ -78,3 +95,67 @@ func SetBootDeviceFromInterfaces(ctx context.Context, timeout time.Duration, boo } return setBootDevice(ctx, timeout, bootDevice, setPersistent, efiBoot, bdSetters) } + +// getBootDeviceOverride gets the boot device override settings for the given provider, +// and updates the given metadata with provider attempts and errors. +func getBootDeviceOverride( + ctx context.Context, + timeout time.Duration, + provider *bootOverrideProvider, + metadata *Metadata, +) (override *BootDeviceOverride, err error) { + select { + case <-ctx.Done(): + err = multierror.Append(err, ctx.Err()) + return nil, err + default: + metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, provider.name) + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + override, err = provider.bootOverrider.BootDeviceOverrideGet(ctx) + if err != nil { + metadata.FailedProviderDetail[provider.name] = err.Error() + return nil, nil + } + + metadata.SuccessfulProvider = provider.name + return override, nil + } +} + +// GetBootDeviceOverrideFromInterface will get boot device override settings from the first successful +// call to a BootDeviceOverrideGetter in the array of providers. +func GetBootDeviceOverrideFromInterface( + ctx context.Context, + timeout time.Duration, + providers []interface{}, +) (*BootDeviceOverride, Metadata, error) { + var err error + metadata := Metadata{ + FailedProviderDetail: make(map[string]string), + } + + for _, elem := range providers { + provider := &bootOverrideProvider{name: getProviderName(elem)} + switch p := elem.(type) { + case BootDeviceOverrideGetter: + provider.bootOverrider = p + override, getErr := getBootDeviceOverride(ctx, timeout, provider, &metadata) + if getErr != nil || override != nil { + return override, metadata, getErr + } + default: + e := fmt.Errorf("not a BootDeviceOverrideGetter implementation: %T", p) + err = multierror.Append(err, e) + } + } + + if len(metadata.ProvidersAttempted) == 0 { + err = multierror.Append(err, errors.New("no BootDeviceOverrideGetter implementations found")) + } else { + err = multierror.Append(err, errors.New("failed to get boot device override settings")) + } + + return nil, metadata, err +} diff --git a/bmc/boot_device_test.go b/bmc/boot_device_test.go index 103c904b..2a65395a 100644 --- a/bmc/boot_device_test.go +++ b/bmc/boot_device_test.go @@ -6,8 +6,10 @@ import ( "testing" "time" + "fmt" "github.com/google/go-cmp/cmp" "github.com/hashicorp/go-multierror" + "github.com/stretchr/testify/assert" ) type bootDeviceTester struct { @@ -117,3 +119,112 @@ func TestSetBootDeviceFromInterfaces(t *testing.T) { }) } } + +type mockBootDeviceOverrideGetter struct { + overrideReturn *BootDeviceOverride + errReturn error +} + +func (m *mockBootDeviceOverrideGetter) Name() string { + return "Mock" +} + +func (m *mockBootDeviceOverrideGetter) BootDeviceOverrideGet(_ context.Context) (override *BootDeviceOverride, err error) { + return m.overrideReturn, m.errReturn +} + +func TestBootDeviceOverrideGet(t *testing.T) { + successOverride := &BootDeviceOverride{ + IsPersistent: false, + IsEFIBoot: true, + Device: "disk", + } + + successMetadata := &Metadata{ + SuccessfulProvider: "Mock", + ProvidersAttempted: []string{"Mock"}, + SuccessfulOpenConns: nil, + SuccessfulCloseConns: []string(nil), + FailedProviderDetail: map[string]string{}, + } + + mixedMetadata := &Metadata{ + SuccessfulProvider: "Mock", + ProvidersAttempted: []string{"Mock", "Mock"}, + SuccessfulOpenConns: nil, + SuccessfulCloseConns: []string(nil), + FailedProviderDetail: map[string]string{"Mock": "foo-failure"}, + } + + failMetadata := &Metadata{ + SuccessfulProvider: "", + ProvidersAttempted: []string{"Mock"}, + SuccessfulOpenConns: nil, + SuccessfulCloseConns: []string(nil), + FailedProviderDetail: map[string]string{"Mock": "foo-failure"}, + } + + emptyMetadata := &Metadata{ + FailedProviderDetail: make(map[string]string), + } + + testCases := []struct { + name string + expectedErrorMsg string + expectedMetadata *Metadata + expectedOverride *BootDeviceOverride + getters []interface{} + }{ + { + name: "success", + expectedMetadata: successMetadata, + expectedOverride: successOverride, + getters: []interface{}{ + &mockBootDeviceOverrideGetter{overrideReturn: successOverride}, + }, + }, + { + name: "multiple getters", + expectedMetadata: mixedMetadata, + expectedOverride: successOverride, + getters: []interface{}{ + "not a getter", + &mockBootDeviceOverrideGetter{errReturn: fmt.Errorf("foo-failure")}, + &mockBootDeviceOverrideGetter{overrideReturn: successOverride}, + }, + }, + { + name: "error", + expectedMetadata: failMetadata, + expectedErrorMsg: "failed to get boot device override settings", + getters: []interface{}{ + &mockBootDeviceOverrideGetter{errReturn: fmt.Errorf("foo-failure")}, + }, + }, + { + name: "nil BootDeviceOverrideGetters", + expectedMetadata: emptyMetadata, + expectedErrorMsg: "no BootDeviceOverrideGetter implementations found", + }, + { + name: "nil BootDeviceOverrideGetter", + expectedMetadata: emptyMetadata, + expectedErrorMsg: "no BootDeviceOverrideGetter implementations found", + getters: []interface{}{nil}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + override, metadata, err := GetBootDeviceOverrideFromInterface(context.Background(), 0, testCase.getters) + + if testCase.expectedErrorMsg != "" { + assert.ErrorContains(t, err, testCase.expectedErrorMsg) + } else { + assert.Nil(t, err) + } + assert.Equal(t, testCase.expectedOverride, override) + assert.Equal(t, testCase.expectedMetadata, &metadata) + }) + } +} diff --git a/client.go b/client.go index a1917543..9ff2801b 100644 --- a/client.go +++ b/client.go @@ -383,6 +383,12 @@ func (c *Client) ReadUsers(ctx context.Context) (users []map[string]string, err return users, err } +func (c *Client) GetBootDeviceOverride(ctx context.Context) (override *bmc.BootDeviceOverride, err error) { + override, metadata, err := bmc.GetBootDeviceOverrideFromInterface(ctx, c.perProviderTimeout(ctx), c.registry().GetDriverInterfaces()) + c.setMetadata(metadata) + return override, err +} + // SetBootDevice pass through to library function func (c *Client) SetBootDevice(ctx context.Context, bootDevice string, setPersistent, efiBoot bool) (ok bool, err error) { ok, metadata, err := bmc.SetBootDeviceFromInterfaces(ctx, c.perProviderTimeout(ctx), bootDevice, setPersistent, efiBoot, c.registry().GetDriverInterfaces()) diff --git a/errors/errors.go b/errors/errors.go index 80aea51b..537597df 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -117,6 +117,9 @@ var ( // ErrSystemVendorModel is returned when the system vendor, model attributes could not be identified. ErrSystemVendorModel = errors.New("error identifying system vendor, model attributes") + + // ErrNoSystemsAvailable is returned when the API of the device provides and empty array of systems. + ErrNoSystemsAvailable = errors.New("no systems were found on the device") ) type ErrUnsupportedHardware struct { diff --git a/internal/redfishwrapper/boot_device.go b/internal/redfishwrapper/boot_device.go index 630d55ad..a1f4cad4 100644 --- a/internal/redfishwrapper/boot_device.go +++ b/internal/redfishwrapper/boot_device.go @@ -3,13 +3,14 @@ package redfishwrapper import ( "context" + "github.com/bmc-toolbox/bmclib/v2/bmc" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/pkg/errors" rf "github.com/stmcginnis/gofish/redfish" ) -// Set the boot device for the system. -func (c *Client) SystemBootDeviceSet(ctx context.Context, bootDevice string, setPersistent, efiBoot bool) (ok bool, err error) { +// SystemBootDeviceSet set the boot device for the system. +func (c *Client) SystemBootDeviceSet(_ context.Context, bootDevice string, setPersistent, efiBoot bool) (ok bool, err error) { if err := c.SessionActive(); err != nil { return false, errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) } @@ -76,3 +77,65 @@ func (c *Client) SystemBootDeviceSet(ctx context.Context, bootDevice string, set return true, nil } + +// bootTargetToDevice tries to convert the redfish boot target to a bmclib supported device string. +// if the target is unknown or unsupported, then a string of the target is returned. +func bootTargetToDevice(target rf.BootSourceOverrideTarget) (device string) { + switch target { + case rf.BiosSetupBootSourceOverrideTarget: + device = "bios" + case rf.CdBootSourceOverrideTarget: + device = "cdrom" + case rf.DiagsBootSourceOverrideTarget: + device = "diag" + case rf.FloppyBootSourceOverrideTarget: + device = "floppy" + case rf.HddBootSourceOverrideTarget: + device = "disk" + case rf.NoneBootSourceOverrideTarget: + device = "none" + case rf.PxeBootSourceOverrideTarget: + device = "pxe" + case rf.RemoteDriveBootSourceOverrideTarget: + device = "remote_drive" + case rf.SDCardBootSourceOverrideTarget: + device = "sd_card" + case rf.UsbBootSourceOverrideTarget: + device = "usb" + case rf.UtilitiesBootSourceOverrideTarget: + device = "utilities" + default: + device = string(target) + } + + return device +} + +// GetBootDeviceOverride returns the current boot override settings +func (c *Client) GetBootDeviceOverride(_ context.Context) (*bmc.BootDeviceOverride, error) { + if err := c.SessionActive(); err != nil { + return nil, errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) + } + + systems, err := c.client.Service.Systems() + if err != nil { + return nil, err + } + + for _, system := range systems { + if system == nil { + continue + } + + boot := system.Boot + override := &bmc.BootDeviceOverride{ + IsPersistent: boot.BootSourceOverrideEnabled == rf.ContinuousBootSourceOverrideEnabled, + IsEFIBoot: boot.BootSourceOverrideMode == rf.UEFIBootSourceOverrideMode, + Device: bootTargetToDevice(boot.BootSourceOverrideTarget), + } + + return override, nil + } + + return nil, bmclibErrs.ErrNoSystemsAvailable +} diff --git a/providers/redfish/redfish.go b/providers/redfish/redfish.go index 0a8d2907..25ac1e98 100644 --- a/providers/redfish/redfish.go +++ b/providers/redfish/redfish.go @@ -13,6 +13,7 @@ import ( "github.com/jacobweinstock/registrar" "github.com/pkg/errors" + "github.com/bmc-toolbox/bmclib/v2/bmc" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" ) @@ -180,8 +181,6 @@ func (c *Conn) Compatible(ctx context.Context) bool { return err == nil } - - // BmcReset power cycles the BMC func (c *Conn) BmcReset(ctx context.Context, resetType string) (ok bool, err error) { return c.redfishwrapper.BMCReset(ctx, resetType) @@ -215,6 +214,10 @@ func (c *Conn) BootDeviceSet(ctx context.Context, bootDevice string, setPersiste return c.redfishwrapper.SystemBootDeviceSet(ctx, bootDevice, setPersistent, efiBoot) } +func (c *Conn) BootDeviceOverrideGet(ctx context.Context) (*bmc.BootDeviceOverride, error) { + return c.redfishwrapper.GetBootDeviceOverride(ctx) +} + // SetVirtualMedia sets the virtual media func (c *Conn) SetVirtualMedia(ctx context.Context, kind string, mediaURL string) (ok bool, err error) { return c.redfishwrapper.SetVirtualMedia(ctx, kind, mediaURL)