From fe8af477905332d6799eec7e023c41c4ee3c7ee2 Mon Sep 17 00:00:00 2001 From: "James W. Brinkerhoff" Date: Tue, 20 Aug 2024 08:25:30 -0400 Subject: [PATCH] Supermicro X13 inventory/firmware support FS-1671 --- providers/supermicro/firmware.go | 1 + providers/supermicro/supermicro.go | 7 +- providers/supermicro/x13.go | 316 +++++++++++++++++++++++++++++ 3 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 providers/supermicro/x13.go diff --git a/providers/supermicro/firmware.go b/providers/supermicro/firmware.go index a7140c81..fdaba142 100644 --- a/providers/supermicro/firmware.go +++ b/providers/supermicro/firmware.go @@ -27,6 +27,7 @@ var ( "X11SSE-F", "X12STH-SYS", "X12SPO-NTF", + "X13DEM", } errUploadTaskIDExpected = errors.New("expected an firmware upload taskID") diff --git a/providers/supermicro/supermicro.go b/providers/supermicro/supermicro.go index e40f4a66..3748de6d 100644 --- a/providers/supermicro/supermicro.go +++ b/providers/supermicro/supermicro.go @@ -279,10 +279,11 @@ func (c *Client) ResetBiosConfiguration(ctx context.Context) (err error) { func (c *Client) bmcQueryor(ctx context.Context) (bmcQueryor, error) { x11 := newX11Client(c.serviceClient, c.log) x12 := newX12Client(c.serviceClient, c.log) + x13 := newX13Client(c.serviceClient, c.log) var queryor bmcQueryor - for _, bmc := range []bmcQueryor{x11, x12} { + for _, bmc := range []bmcQueryor{x11, x12, x13} { var err error _, err = bmc.queryDeviceModel(ctx) @@ -303,8 +304,8 @@ func (c *Client) bmcQueryor(ctx context.Context) (bmcQueryor, error) { } model := strings.ToLower(queryor.deviceModel()) - if !strings.HasPrefix(model, "x12") && !strings.HasPrefix(model, "x11") { - return nil, errors.Wrap(ErrModelUnsupported, "expected one of X11* or X12*, got:"+model) + if !strings.HasPrefix(model, "x13") && !strings.HasPrefix(model, "x12") && !strings.HasPrefix(model, "x11") { + return nil, errors.Wrap(ErrModelUnsupported, "expected one of X11*, X12* or X13*, got:"+model) } return queryor, nil diff --git a/providers/supermicro/x13.go b/providers/supermicro/x13.go new file mode 100644 index 00000000..0cee7d13 --- /dev/null +++ b/providers/supermicro/x13.go @@ -0,0 +1,316 @@ +package supermicro + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/bmc-toolbox/bmclib/v2/constants" + brrs "github.com/bmc-toolbox/bmclib/v2/errors" + rfw "github.com/bmc-toolbox/bmclib/v2/internal/redfishwrapper" + "github.com/bmc-toolbox/common" + "github.com/go-logr/logr" + "github.com/pkg/errors" + "github.com/stmcginnis/gofish/redfish" + "golang.org/x/exp/slices" +) + +type x13 struct { + *serviceClient + model string + log logr.Logger +} + +func newX13Client(client *serviceClient, logger logr.Logger) bmcQueryor { + return &x13{ + serviceClient: client, + log: logger, + } +} + +func (c *x13) deviceModel() string { + return c.model +} + +func (c *x13) queryDeviceModel(ctx context.Context) (string, error) { + if err := c.redfishSession(ctx); err != nil { + return "", err + } + + _, model, err := c.redfish.DeviceVendorModel(ctx) + if err != nil { + return "", err + } + + if model == "" { + return "", errors.Wrap(ErrModelUnknown, "empty value") + } + + c.model = common.FormatProductName(model) + + return c.model, nil +} + +// var ( +// errUploadTaskIDEmpty = errors.New("firmware upload request returned empty firmware upload verify TaskID") +// ) + +func (c *x13) supportsInstall(component string) error { + errComponentNotSupported := fmt.Errorf("component %s on device %s not supported", component, c.model) + + supported := []string{common.SlugBIOS, common.SlugBMC} + if !slices.Contains(supported, strings.ToUpper(component)) { + return errComponentNotSupported + } + + return nil +} + +func (c *x13) firmwareInstallSteps(component string) ([]constants.FirmwareInstallStep, error) { + if err := c.supportsInstall(component); err != nil { + return nil, err + } + + return []constants.FirmwareInstallStep{ + constants.FirmwareInstallStepUpload, + constants.FirmwareInstallStepUploadStatus, + constants.FirmwareInstallStepInstallUploaded, + constants.FirmwareInstallStepInstallStatus, + }, nil +} + +// upload firmware +func (c *x13) firmwareUpload(ctx context.Context, component string, file *os.File) (taskID string, err error) { + if err = c.supportsInstall(component); err != nil { + return "", err + } + + err = c.firmwareTaskActive(ctx, component) + if err != nil { + return "", err + } + + targetID, err := c.redfishOdataID(ctx, component) + if err != nil { + return "", err + } + + params, err := c.redfishParameters(component, targetID) + if err != nil { + return "", err + } + + taskID, err = c.redfish.FirmwareUpload(ctx, file, params) + if err != nil { + if strings.Contains(err.Error(), "OemFirmwareAlreadyInUpdateMode") { + return "", errors.Wrap(brrs.ErrBMCColdResetRequired, "BMC currently in update mode, either continue the update OR if no update is currently running - reset the BMC") + } + + return "", errors.Wrap(err, "error in firmware upload") + } + + if taskID == "" { + return "", errUploadTaskIDEmpty + } + + return taskID, nil +} + +// returns an error when a bmc firmware install is active +func (c *x13) firmwareTaskActive(ctx context.Context, component string) error { + tasks, err := c.redfish.Tasks(ctx) + if err != nil { + return errors.Wrap(err, "error querying redfish tasks") + } + + for _, t := range tasks { + t := t + + if stateFinalized(t.TaskState) { + continue + } + + if err := noTasksRunning(component, t); err != nil { + return err + } + } + + return nil +} + +// // noTasksRunning returns an error if a firmware related task was found active +// func noTasksRunning(component string, t *redfish.Task) error { +// errTaskActive := errors.New("A firmware task was found active for component: " + component) + +// const ( +// // The redfish task name when the BMC is verifies the uploaded BMC firmware. +// verifyBMCFirmware = "BMC Verify" +// // The redfish task name when the BMC is installing the uploaded BMC firmware. +// updateBMCFirmware = "BMC Update" +// // The redfish task name when the BMC is verifies the uploaded BIOS firmware. +// verifyBIOSFirmware = "BIOS Verify" +// // The redfish task name when the BMC is installing the uploaded BIOS firmware. +// updateBIOSFirmware = "BIOS Update" +// ) + +// var verifyTaskName, updateTaskName string + +// switch strings.ToUpper(component) { +// case common.SlugBMC: +// verifyTaskName = verifyBMCFirmware +// updateTaskName = updateBMCFirmware +// case common.SlugBIOS: +// verifyTaskName = verifyBIOSFirmware +// updateTaskName = updateBIOSFirmware +// } + +// taskInfo := fmt.Sprintf("id: %s, state: %s, status: %s", t.ID, t.TaskState, t.TaskStatus) + +// switch t.Name { +// case verifyTaskName: +// return errors.Wrap(errTaskActive, taskInfo) +// case updateTaskName: +// return errors.Wrap(errTaskActive, taskInfo) +// default: +// return nil +// } +// } + +// func stateFinalized(s redfish.TaskState) bool { +// finalized := []redfish.TaskState{ +// redfish.CompletedTaskState, +// redfish.CancelledTaskState, +// redfish.InterruptedTaskState, +// redfish.ExceptionTaskState, +// } + +// return slices.Contains(finalized, s) +// } + +// type Supermicro struct { +// BIOS map[string]bool `json:"BIOS,omitempty"` +// BMC map[string]bool `json:"BMC,omitempty"` +// } + +// type OEM struct { +// Supermicro `json:"Supermicro"` +// } + +// redfish OEM fw install parameters +func (c *x13) biosFwInstallParams() (map[string]bool, error) { + switch c.model { + case "x13spo-ntf": + return map[string]bool{ + "PreserveME": false, + "PreserveNVRAM": false, + "PreserveSMBIOS": true, + "BackupBIOS": false, + "PreserveBOOTCONF": true, + }, nil + case "x13sth-sys": + return map[string]bool{ + "PreserveME": false, + "PreserveNVRAM": false, + "PreserveSMBIOS": true, + "PreserveOA": true, + "PreserveSETUPCONF": true, + "PreserveSETUPPWD": true, + "PreserveSECBOOTKEY": true, + "PreserveBOOTCONF": true, + }, nil + default: + // ideally we never get in this position, since theres model number validation in parent callers. + return nil, errors.New("unsupported model for BIOS fw install: " + c.model) + } +} + +// redfish OEM fw install parameters +func (c *x13) bmcFwInstallParams() map[string]bool { + return map[string]bool{ + "PreserveCfg": true, + "PreserveSdr": true, + "PreserveSsl": true, + } +} + +func (c *x13) redfishParameters(component, targetODataID string) (*rfw.RedfishUpdateServiceParameters, error) { + errUnsupported := errors.New("redfish parameters for x13 hardware component not supported: " + component) + + oem := OEM{} + + biosInstallParams, err := c.biosFwInstallParams() + if err != nil { + return nil, err + } + + switch strings.ToUpper(component) { + case common.SlugBIOS: + oem.Supermicro.BIOS = biosInstallParams + case common.SlugBMC: + oem.Supermicro.BMC = c.bmcFwInstallParams() + default: + return nil, errUnsupported + } + + b, err := json.Marshal(oem) + if err != nil { + return nil, errors.Wrap(err, "error preparing redfish parameters") + } + + return &rfw.RedfishUpdateServiceParameters{ + // NOTE: + // X13s support the OnReset Apply time for BIOS updates if we want to implement that in the future. + OperationApplyTime: constants.OnStartUpdateRequest, + Targets: []string{targetODataID}, + Oem: b, + }, nil +} + +func (c *x13) redfishOdataID(ctx context.Context, component string) (string, error) { + errUnsupported := errors.New("unable to return redfish OData ID for unsupported component: " + component) + + switch strings.ToUpper(component) { + case common.SlugBMC: + return c.redfish.ManagerOdataID(ctx) + case common.SlugBIOS: + // hardcoded since SMCs without the DCMS license will throw license errors + return "/redfish/v1/Systems/1/Bios", nil + //return c.redfish.SystemsBIOSOdataID(ctx) + } + + return "", errUnsupported +} + +func (c *x13) firmwareInstallUploaded(ctx context.Context, component, uploadTaskID string) (installTaskID string, err error) { + if err = c.supportsInstall(component); err != nil { + return "", err + } + + task, err := c.redfish.Task(ctx, uploadTaskID) + if err != nil { + e := fmt.Sprintf("error querying redfish tasks for firmware upload taskID: %s, err: %s", uploadTaskID, err.Error()) + return "", errors.Wrap(brrs.ErrFirmwareVerifyTask, e) + } + + taskInfo := fmt.Sprintf("id: %s, state: %s, status: %s", task.ID, task.TaskState, task.TaskStatus) + + if task.TaskState != redfish.CompletedTaskState { + return "", errors.Wrap(brrs.ErrFirmwareVerifyTask, taskInfo) + } + + if task.TaskStatus != "OK" { + return "", errors.Wrap(brrs.ErrFirmwareVerifyTask, taskInfo) + } + + return c.redfish.StartUpdateForUploadedFirmware(ctx) +} + +func (c *x13) firmwareTaskStatus(ctx context.Context, component, taskID string) (state constants.TaskState, status string, err error) { + if err = c.supportsInstall(component); err != nil { + return "", "", errors.Wrap(brrs.ErrFirmwareTaskStatus, err.Error()) + } + + return c.redfish.TaskStatus(ctx, taskID) +}