diff --git a/bmc/boot_device.go b/bmc/boot_device.go index ceabb991..27c236cf 100644 --- a/bmc/boot_device.go +++ b/bmc/boot_device.go @@ -9,25 +9,56 @@ import ( "github.com/pkg/errors" ) +type BootDeviceType string + +const ( + BootDeviceTypeBIOS BootDeviceType = "bios" + BootDeviceTypeCDROM BootDeviceType = "cdrom" + BootDeviceTypeDiag BootDeviceType = "diag" + BootDeviceTypeFloppy BootDeviceType = "floppy" + BootDeviceTypeDisk BootDeviceType = "disk" + BootDeviceTypeNone BootDeviceType = "none" + BootDeviceTypePXE BootDeviceType = "pxe" + BootDeviceTypeRemoteDrive BootDeviceType = "remote_drive" + BootDeviceTypeSDCard BootDeviceType = "sd_card" + BootDeviceTypeUSB BootDeviceType = "usb" + BootDeviceTypeUtil BootDeviceType = "utilities" +) + // BootDeviceSetter sets the next boot device for a machine 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 + bootOverrideGetter BootDeviceOverrideGetter +} + +type BootDeviceOverride struct { + IsPersistent bool + IsEFIBoot bool + Device BootDeviceType +} + // setBootDevice sets the next boot device. // // setPersistent persists the next boot device. // efiBoot sets up the device to boot off UEFI instead of legacy. func setBootDevice(ctx context.Context, timeout time.Duration, bootDevice string, setPersistent, efiBoot bool, b []bootDeviceProviders) (ok bool, metadata Metadata, err error) { - metadataLocal := Metadata{ - FailedProviderDetail: make(map[string]string), - } + metadataLocal := newMetadata() for _, elem := range b { if elem.bootDeviceSetter == nil { @@ -78,3 +109,63 @@ 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, ok bool, err error) { + select { + case <-ctx.Done(): + err = multierror.Append(err, ctx.Err()) + return override, ok, err + default: + metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, provider.name) + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + override, err = provider.bootOverrideGetter.BootDeviceOverrideGet(ctx) + if err != nil { + metadata.FailedProviderDetail[provider.name] = err.Error() + return override, ok, nil + } + + metadata.SuccessfulProvider = provider.name + return override, true, 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{}, +) (override BootDeviceOverride, metadata Metadata, err error) { + metadata = newMetadata() + + for _, elem := range providers { + switch p := elem.(type) { + case BootDeviceOverrideGetter: + provider := &bootOverrideProvider{name: getProviderName(elem), bootOverrideGetter: p} + override, ok, getErr := getBootDeviceOverride(ctx, timeout, provider, &metadata) + if getErr != nil || ok { + 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 override, metadata, err +} diff --git a/bmc/boot_device_test.go b/bmc/boot_device_test.go index 103c904b..17e10549 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,129 @@ 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) (BootDeviceOverride, error) { + return m.overrideReturn, m.errReturn +} + +func TestBootDeviceOverrideGet(t *testing.T) { + successOverride := BootDeviceOverride{ + IsPersistent: false, + IsEFIBoot: true, + Device: BootDeviceTypeDisk, + } + + 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 + hasCanceledContext bool + 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}, + }, + { + name: "with canceled context", + hasCanceledContext: true, + expectedMetadata: emptyMetadata, + expectedErrorMsg: "context canceled", + getters: []interface{}{ + &mockBootDeviceOverrideGetter{}, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if testCase.hasCanceledContext { + cancel() + } + + override, metadata, err := GetBootDeviceOverrideFromInterface(ctx, 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 f5a13f91..fe9b3fd7 100644 --- a/client.go +++ b/client.go @@ -455,6 +455,17 @@ func (c *Client) ReadUsers(ctx context.Context) (users []map[string]string, err return users, err } +// GetBootDeviceOverride pass through to library function +func (c *Client) GetBootDeviceOverride(ctx context.Context) (override bmc.BootDeviceOverride, err error) { + ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "GetBootDeviceOverride") + defer span.End() + + 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) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "SetBootDevice") diff --git a/errors/errors.go b/errors/errors.go index 80aea51b..c1fe94b4 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") + + // ErrRedfishNoSystems is returned when the API of the device provides and empty array of systems. + ErrRedfishNoSystems = errors.New("redfish: 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..5a674294 100644 --- a/internal/redfishwrapper/boot_device.go +++ b/internal/redfishwrapper/boot_device.go @@ -3,13 +3,88 @@ 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) { +type bootDeviceMapping struct { + BootDeviceType bmc.BootDeviceType + RedFishTarget rf.BootSourceOverrideTarget +} + +var bootDeviceTypeMappings = []bootDeviceMapping{ + { + BootDeviceType: bmc.BootDeviceTypeBIOS, + RedFishTarget: rf.BiosSetupBootSourceOverrideTarget, + }, + { + BootDeviceType: bmc.BootDeviceTypeCDROM, + RedFishTarget: rf.CdBootSourceOverrideTarget, + }, + { + BootDeviceType: bmc.BootDeviceTypeDiag, + RedFishTarget: rf.DiagsBootSourceOverrideTarget, + }, + { + BootDeviceType: bmc.BootDeviceTypeFloppy, + RedFishTarget: rf.FloppyBootSourceOverrideTarget, + }, + { + BootDeviceType: bmc.BootDeviceTypeDisk, + RedFishTarget: rf.HddBootSourceOverrideTarget, + }, + { + BootDeviceType: bmc.BootDeviceTypeNone, + RedFishTarget: rf.NoneBootSourceOverrideTarget, + }, + { + BootDeviceType: bmc.BootDeviceTypePXE, + RedFishTarget: rf.PxeBootSourceOverrideTarget, + }, + { + BootDeviceType: bmc.BootDeviceTypeRemoteDrive, + RedFishTarget: rf.RemoteDriveBootSourceOverrideTarget, + }, + { + BootDeviceType: bmc.BootDeviceTypeSDCard, + RedFishTarget: rf.SDCardBootSourceOverrideTarget, + }, + { + BootDeviceType: bmc.BootDeviceTypeUSB, + RedFishTarget: rf.UsbBootSourceOverrideTarget, + }, + { + BootDeviceType: bmc.BootDeviceTypeUtil, + RedFishTarget: rf.UtilitiesBootSourceOverrideTarget, + }, +} + +// bootDeviceStringToTarget gets the RedFish BootSourceOverrideTarget that corresponds to the given device string, +// or an error if the device is not a RedFish BootSourceOverrideTarget. +func bootDeviceStringToTarget(device string) (rf.BootSourceOverrideTarget, error) { + for _, bootDevice := range bootDeviceTypeMappings { + if string(bootDevice.BootDeviceType) == device { + return bootDevice.RedFishTarget, nil + } + } + return "", errors.New("invalid boot device") +} + +// bootTargetToBootDeviceType converts the redfish boot target to a bmc.BootDeviceType. +// if the target is unknown or unsupported, then an error is returned. +func bootTargetToBootDeviceType(target rf.BootSourceOverrideTarget) (bmc.BootDeviceType, error) { + for _, bootDevice := range bootDeviceTypeMappings { + if bootDevice.RedFishTarget == target { + return bootDevice.BootDeviceType, nil + } + } + return "", errors.New("invalid boot device") +} + +// 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()) } @@ -22,31 +97,9 @@ func (c *Client) SystemBootDeviceSet(ctx context.Context, bootDevice string, set for _, system := range systems { boot := system.Boot - switch bootDevice { - case "bios": - boot.BootSourceOverrideTarget = rf.BiosSetupBootSourceOverrideTarget - case "cdrom": - boot.BootSourceOverrideTarget = rf.CdBootSourceOverrideTarget - case "diag": - boot.BootSourceOverrideTarget = rf.DiagsBootSourceOverrideTarget - case "floppy": - boot.BootSourceOverrideTarget = rf.FloppyBootSourceOverrideTarget - case "disk": - boot.BootSourceOverrideTarget = rf.HddBootSourceOverrideTarget - case "none": - boot.BootSourceOverrideTarget = rf.NoneBootSourceOverrideTarget - case "pxe": - boot.BootSourceOverrideTarget = rf.PxeBootSourceOverrideTarget - case "remote_drive": - boot.BootSourceOverrideTarget = rf.RemoteDriveBootSourceOverrideTarget - case "sd_card": - boot.BootSourceOverrideTarget = rf.SDCardBootSourceOverrideTarget - case "usb": - boot.BootSourceOverrideTarget = rf.UsbBootSourceOverrideTarget - case "utilities": - boot.BootSourceOverrideTarget = rf.UtilitiesBootSourceOverrideTarget - default: - return false, errors.New("invalid boot device") + boot.BootSourceOverrideTarget, err = bootDeviceStringToTarget(bootDevice) + if err != nil { + return false, err } if setPersistent { @@ -76,3 +129,37 @@ func (c *Client) SystemBootDeviceSet(ctx context.Context, bootDevice string, set return true, nil } + +// GetBootDeviceOverride returns the current boot override settings +func (c *Client) GetBootDeviceOverride(_ context.Context) (override bmc.BootDeviceOverride, err error) { + if err := c.SessionActive(); err != nil { + return override, errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) + } + + systems, err := c.client.Service.Systems() + if err != nil { + return override, err + } + + for _, system := range systems { + if system == nil { + continue + } + + boot := system.Boot + bootDevice, err := bootTargetToBootDeviceType(boot.BootSourceOverrideTarget) + if err != nil { + return override, err + } + + override = bmc.BootDeviceOverride{ + IsPersistent: boot.BootSourceOverrideEnabled == rf.ContinuousBootSourceOverrideEnabled, + IsEFIBoot: boot.BootSourceOverrideMode == rf.UEFIBootSourceOverrideMode, + Device: bootDevice, + } + + return override, nil + } + + return override, bmclibErrs.ErrRedfishNoSystems +} diff --git a/providers/redfish/redfish.go b/providers/redfish/redfish.go index 0a8d2907..458f1522 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,11 @@ func (c *Conn) BootDeviceSet(ctx context.Context, bootDevice string, setPersiste return c.redfishwrapper.SystemBootDeviceSet(ctx, bootDevice, setPersistent, efiBoot) } +// BootDeviceOverrideGet gets the boot override device information +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)