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
+}