diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 637c879d..3e14194f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,8 +18,8 @@ jobs: - name: Run golangci-lint uses: golangci/golangci-lint-action@v3 with: - args: -v --config .golangci.yml --timeout=5m - version: latest + args: -v --config .golangci.yml --timeout=5m --out-format=colored-line-number + version: v1.55.2 - name: make all-checks run: make all-checks test: diff --git a/internal/redfishwrapper/firmware.go b/internal/redfishwrapper/firmware.go index 81c3f1a5..9910a37d 100644 --- a/internal/redfishwrapper/firmware.go +++ b/internal/redfishwrapper/firmware.go @@ -19,6 +19,7 @@ import ( "github.com/bmc-toolbox/bmclib/v2/constants" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" + redfish "github.com/stmcginnis/gofish/redfish" ) type installMethod string @@ -105,6 +106,14 @@ func (c *Client) FirmwareUpload(ctx context.Context, updateFile *os.File, params ) } + // For X13 the full Task structure is returned in the body. If we can Unmarshall then we can safely assume + // that redfishTask.ID contains the ID. + redfishTask := &redfish.Task{} + err = json.Unmarshal(response, redfishTask) + if err == nil { + return redfishTask.ID, nil + } + // The response contains a location header pointing to the task URI // Location: /redfish/v1/TaskService/Tasks/JID_467696020275 var location = resp.Header.Get("Location") diff --git a/providers/supermicro/firmware.go b/providers/supermicro/firmware.go index a7140c81..b235f916 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") @@ -46,7 +47,7 @@ func (c *Client) FirmwareUpload(ctx context.Context, component string, file *os. return "", err } - // // expect atleast 5 minutes left in the deadline to proceed with the upload + // expect atleast 5 minutes left in the deadline to proceed with the upload d, _ := ctx.Deadline() if time.Until(d) < 5*time.Minute { return "", errors.New("remaining context deadline insufficient to perform update: " + time.Until(d).String()) diff --git a/providers/supermicro/supermicro.go b/providers/supermicro/supermicro.go index e7d224f0..189f4855 100644 --- a/providers/supermicro/supermicro.go +++ b/providers/supermicro/supermicro.go @@ -175,10 +175,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 { @@ -197,6 +201,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())) } @@ -281,17 +286,36 @@ 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) + x11bmc := newX11Client(c.serviceClient, c.log) + x12bmc := newX12Client(c.serviceClient, c.log) + x13bmc := newX13Client(c.serviceClient, c.log) var queryor bmcQueryor - for _, bmc := range []bmcQueryor{x11, x12} { + expected := func(deviceModel string, bmc bmcQueryor) bool { + deviceModel = strings.ToLower(deviceModel) + switch bmc.(type) { + case *x11: + if strings.HasPrefix(deviceModel, "x11") { + return true + } + case *x12: + if strings.HasPrefix(deviceModel, "x12") { + return true + } + case *x13: + if strings.HasPrefix(deviceModel, "x13") { + return true + } + } + + return false + } + + for _, bmc := range []bmcQueryor{x11bmc, x12bmc, x13bmc} { var err error - // Note to maintainers: x12 lacks support for the ipmi.cgi endpoint, - // which will lead to our graceful handling of ErrXMLAPIUnsupported below. - _, err = bmc.queryDeviceModel(ctx) + deviceModel, err := bmc.queryDeviceModel(ctx) if err != nil { if errors.Is(err, ErrXMLAPIUnsupported) { continue @@ -300,6 +324,11 @@ func (c *Client) bmcQueryor(ctx context.Context) (bmcQueryor, error) { return nil, errors.Wrap(ErrModelUnknown, err.Error()) } + // ensure the device model matches the expected queryor + if !expected(deviceModel, bmc) { + continue + } + queryor = bmc break } @@ -309,8 +338,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/supermicro_test.go b/providers/supermicro/supermicro_test.go index f503813a..989dc910 100644 --- a/providers/supermicro/supermicro_test.go +++ b/providers/supermicro/supermicro_test.go @@ -143,7 +143,7 @@ func TestOpen(t *testing.T) { diff --git a/providers/supermicro/x11.go b/providers/supermicro/x11.go index db24f028..e31fabca 100644 --- a/providers/supermicro/x11.go +++ b/providers/supermicro/x11.go @@ -39,7 +39,7 @@ 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") { + if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "") { return "", ErrXMLAPIUnsupported } diff --git a/providers/supermicro/x12.go b/providers/supermicro/x12.go index 52893a99..83a8cf61 100644 --- a/providers/supermicro/x12.go +++ b/providers/supermicro/x12.go @@ -142,6 +142,10 @@ func (c *x12) firmwareTaskActive(ctx context.Context, component string) error { // noTasksRunning returns an error if a firmware related task was found active func noTasksRunning(component string, t *redfish.Task) error { + if t.TaskState == "Killed" { + return nil + } + errTaskActive := errors.New("A firmware task was found active for component: " + component) const ( @@ -222,7 +226,7 @@ func (c *x12) biosFwInstallParams() (map[string]bool, error) { }, 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) + return nil, errors.New("unsupported model for x12 BIOS fw install: " + c.model) } } diff --git a/providers/supermicro/x13.go b/providers/supermicro/x13.go new file mode 100644 index 00000000..1bf3673c --- /dev/null +++ b/providers/supermicro/x13.go @@ -0,0 +1,266 @@ +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.FirmwareInstallStepUploadInitiateInstall, + // constants.FirmwareInstallStepInstallStatus, + // }, nil + return []constants.FirmwareInstallStep{ + constants.FirmwareInstallStepUploadInitiateInstall, + 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 +} + +// redfish OEM fw install parameters +func (c *x13) biosFwInstallParams() (map[string]bool, error) { + switch c.model { + case "x13dem": + 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 x13 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) +} + +func (c *x13) getBootProgress() (*redfish.BootProgress, error) { + bps, err := c.redfish.GetBootProgress() + if err != nil { + return nil, err + } + return bps[0], nil +} + +// this is some syntactic sugar to avoid having to code potentially provider- or model-specific knowledge into a caller +func (c *x13) bootComplete() (bool, error) { + bp, err := c.getBootProgress() + if err != nil { + return false, err + } + // we determined this by experiment on X12STH-SYS with redfish 1.14.0 + return bp.LastState == redfish.SystemHardwareInitializationCompleteBootProgressTypes, nil +}