Skip to content

Commit

Permalink
Supermicro X13 inventory/firmware support FS-1671
Browse files Browse the repository at this point in the history
  • Loading branch information
splaspood committed Aug 27, 2024
1 parent c88b955 commit b826034
Show file tree
Hide file tree
Showing 4 changed files with 326 additions and 6 deletions.
1 change: 1 addition & 0 deletions providers/supermicro/firmware.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var (
"X11SSE-F",
"X12STH-SYS",
"X12SPO-NTF",
"X13DEM",
}

errUploadTaskIDExpected = errors.New("expected an firmware upload taskID")
Expand Down
16 changes: 11 additions & 5 deletions providers/supermicro/supermicro.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -193,6 +197,7 @@ func (c *Client) Open(ctx context.Context) (err error) {
c.serviceClient.setCsrfToken(csrfToken)

c.bmc, err = c.bmcQueryor(ctx)
\

Check failure on line 200 in providers/supermicro/supermicro.go

View workflow job for this annotation

GitHub Actions / lint

illegal character U+005C '\' (typecheck)

Check failure on line 200 in providers/supermicro/supermicro.go

View workflow job for this annotation

GitHub Actions / test

invalid character U+005C '\'

Check failure on line 200 in providers/supermicro/supermicro.go

View workflow job for this annotation

GitHub Actions / test

invalid character U+005C '\'

Check failure on line 200 in providers/supermicro/supermicro.go

View workflow job for this annotation

GitHub Actions / lint

illegal character U+005C '\' (typecheck)
if err != nil {
return closeWithError(ctx, errors.Wrap(bmclibErrs.ErrLoginFailed, err.Error()))
}
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion providers/supermicro/x11.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(), "<html>") {
return "", ErrXMLAPIUnsupported
}

Expand Down
312 changes: 312 additions & 0 deletions providers/supermicro/x13.go
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit b826034

Please sign in to comment.