From 5519890db65e05796e3193408cb847c4ff831a94 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 --- go.sum | 2 - providers/supermicro/firmware.go | 1 + providers/supermicro/supermicro.go | 16 +- providers/supermicro/x11.go | 3 +- providers/supermicro/x13.go | 312 +++++++++++++++++++++++++++++ 5 files changed, 326 insertions(+), 8 deletions(-) create mode 100644 providers/supermicro/x13.go diff --git a/go.sum b/go.sum index c6c8301e..a6e80eb2 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,6 @@ github.com/VictorLowther/simplexml v0.0.0-20180716164440-0bff93621230 h1:t95Grn2 github.com/VictorLowther/simplexml v0.0.0-20180716164440-0bff93621230/go.mod h1:t2EzW1qybnPDQ3LR/GgeF0GOzHUXT5IVMLP2gkW1cmc= github.com/VictorLowther/soap v0.0.0-20150314151524-8e36fca84b22 h1:a0MBqYm44o0NcthLKCljZHe1mxlN6oahCQHHThnSwB4= github.com/VictorLowther/soap v0.0.0-20150314151524-8e36fca84b22/go.mod h1:/B7V22rcz4860iDqstGvia/2+IYWXf3/JdQCVd/1D2A= -github.com/bmc-toolbox/common v0.0.0-20240805193945-ce25765471a7 h1:+NcnInwZxn25daBCb3d1y3x9QF23uob1ghdiimj2Dwo= -github.com/bmc-toolbox/common v0.0.0-20240805193945-ce25765471a7/go.mod h1:Cdnkm+edb6C0pVkyCrwh3JTXAe0iUF9diDG/DztPI9I= github.com/bmc-toolbox/common v0.0.0-20240806132831-ba8adc6a35e3 h1:/BjZSX/sphptIdxpYo4wxAQkgMLyMMgfdl48J9DKNeE= github.com/bmc-toolbox/common v0.0.0-20240806132831-ba8adc6a35e3/go.mod h1:Cdnkm+edb6C0pVkyCrwh3JTXAe0iUF9diDG/DztPI9I= github.com/bombsimon/logrusr/v2 v2.0.1 h1:1VgxVNQMCvjirZIYaT9JYn6sAVGVEcNtRE0y4mvaOAM= 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..2de6bdf2 100644 --- a/providers/supermicro/supermicro.go +++ b/providers/supermicro/supermicro.go @@ -171,10 +171,14 @@ func (c *Client) Open(ctx context.Context) (err error) { return err } - if !bytes.Contains(body, []byte(`url_redirect.cgi?url_name=mainmenu`)) && - !bytes.Contains(body, []byte(`url_redirect.cgi?url_name=topmenu`)) { + // X13 appears to have dropped the initial 'mainmenu' redirect + if !bytes.Contains(body, []byte(`url_redirect.cgi?url_name=topmenu`)) { return closeWithError(ctx, errors.Wrap(bmclibErrs.ErrLoginFailed, "unexpected response contents")) } + // if !bytes.Contains(body, []byte(`url_redirect.cgi?url_name=mainmenu`)) && + // !bytes.Contains(body, []byte(`url_redirect.cgi?url_name=topmenu`)) { + // return closeWithError(ctx, errors.Wrap(bmclibErrs.ErrLoginFailed, "unexpected response contents")) + // } contentsTopMenu, status, err := c.serviceClient.query(ctx, "cgi/url_redirect.cgi?url_name=topmenu", http.MethodGet, nil, nil, 0) if err != nil { @@ -193,6 +197,7 @@ func (c *Client) Open(ctx context.Context) (err error) { c.serviceClient.setCsrfToken(csrfToken) c.bmc, err = c.bmcQueryor(ctx) + if err != nil { return closeWithError(ctx, errors.Wrap(bmclibErrs.ErrLoginFailed, err.Error())) } @@ -279,10 +284,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 +309,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/x11.go b/providers/supermicro/x11.go index 08525e7a..f84f9846 100644 --- a/providers/supermicro/x11.go +++ b/providers/supermicro/x11.go @@ -38,7 +38,8 @@ func (c *x11) queryDeviceModel(ctx context.Context) (string, error) { errBoardPartNumUnknown := errors.New("baseboard part number unknown") data, err := c.fruInfo(ctx) if err != nil { - if strings.Contains(err.Error(), "404") { + c.log.Error(err, "fruInfo error") + if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "") { return "", ErrXMLAPIUnsupported } diff --git a/providers/supermicro/x13.go b/providers/supermicro/x13.go new file mode 100644 index 00000000..f9dabc60 --- /dev/null +++ b/providers/supermicro/x13.go @@ -0,0 +1,312 @@ +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 +} + +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) +}