Skip to content

Commit

Permalink
providers/dell: Implements FirmwareInstallSteps(), FirmwareInstallUpl…
Browse files Browse the repository at this point in the history
…oadedAndInitiate(), FirmwareInstallStatus() methods
  • Loading branch information
joelrebel committed Nov 27, 2023
1 parent dc27a6e commit 48209f4
Show file tree
Hide file tree
Showing 3 changed files with 335 additions and 0 deletions.
235 changes: 235 additions & 0 deletions providers/dell/firmware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package dell

import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"strings"
"time"

"github.com/bmc-toolbox/bmclib/v2/constants"
bmcliberrs "github.com/bmc-toolbox/bmclib/v2/errors"
rfw "github.com/bmc-toolbox/bmclib/v2/internal/redfishwrapper"
"github.com/bmc-toolbox/common"
"github.com/pkg/errors"
"github.com/stmcginnis/gofish/redfish"
)

var (
ErrUnsupportedHardware = errors.New("hardware not supported")
)

// bmc client interface implementations methods
func (c *Conn) FirmwareInstallSteps(ctx context.Context, component string) ([]constants.FirmwareInstallStep, error) {
if err := c.deviceSupported(ctx); err != nil {
return nil, errors.Wrap(ErrUnsupportedHardware, err.Error())
}

return []constants.FirmwareInstallStep{
constants.FirmwareInstallStepUploadInitiateInstall,
constants.FirmwareInstallStepInstallStatus,
}, nil
}

func (c *Conn) FirmwareInstallUploadAndInitiate(ctx context.Context, component string, file *os.File) (taskID string, err error) {
if err := c.deviceSupported(ctx); err != nil {
return "", errors.Wrap(ErrUnsupportedHardware, err.Error())
}

// // expect atleast 5 minutes left in the deadline to proceed with the upload
d, _ := ctx.Deadline()
if time.Until(d) < 10*time.Minute {
return "", errors.New("remaining context deadline insufficient to perform update: " + time.Until(d).String())
}

// list current tasks on BMC
tasks, err := c.redfishwrapper.Tasks(ctx)
if err != nil {
return "", errors.Wrap(err, "error listing bmc redfish tasks")
}

// validate a new firmware install task can be queued
if err := c.checkQueueability(component, tasks); err != nil {
return "", errors.Wrap(bmcliberrs.ErrFirmwareInstall, err.Error())
}

params := &rfw.RedfishUpdateServiceParameters{
Targets: []string{},
OperationApplyTime: constants.OnReset,
Oem: []byte(`{}`),
}

return c.redfishwrapper.FirmwareUpload(ctx, file, params)
}

// checkQueueability returns an error if an existing firmware task is in progress for the given component
func (c *Conn) checkQueueability(component string, tasks []*redfish.Task) error {
errTaskActive := errors.New("A firmware job was found active for component: " + component)

// Redfish on the Idrac names firmware install tasks in this manner.
taskNameMap := map[string]string{
common.SlugBIOS: "Firmware Update: BIOS",
common.SlugBMC: "Firmware Update: iDRAC with Lifecycle Controller",
common.SlugNIC: "Firmware Update: Network",
common.SlugDrive: "Firmware Update: Serial ATA",
common.SlugStorageController: "Firmware Update: SAS RAID",
}

for _, t := range tasks {
if t.Name == taskNameMap[strings.ToUpper(component)] {
// taskInfo returned in error if any.
taskInfo := fmt.Sprintf("id: %s, state: %s, status: %s", t.ID, t.TaskState, t.TaskStatus)

// convert redfish task state to bmclib state
convstate := c.redfishwrapper.ConvertTaskState(string(t.TaskState))
// check if task is active based on converted state
active, err := c.redfishwrapper.TaskStateActive(convstate)
if err != nil {
return errors.Wrap(err, taskInfo)
}

if active {
return errors.Wrap(errTaskActive, taskInfo)
}
}
}

return nil
}

// FirmwareTaskStatus returns the status of a firmware related task queued on the BMC.
func (c *Conn) FirmwareTaskStatus(ctx context.Context, kind constants.FirmwareInstallStep, component, taskID, installVersion string) (state constants.TaskState, status string, err error) {
if err := c.deviceSupported(ctx); err != nil {
return "", "", errors.Wrap(ErrUnsupportedHardware, err.Error())
}

// Dell jobs are turned into Redfish tasks on the idrac
// once the Redfish task completes successfully, the Redfish task is purged,
// and the dell Job stays around.
task, err := c.redfishwrapper.Task(ctx, taskID)
if err != nil {
if errors.Is(err, bmcliberrs.ErrTaskNotFound) {
return c.statusFromJob(taskID)
}

return "", "", err
}

return c.statusFromTaskOem(taskID, task.Oem)

Check failure on line 120 in providers/dell/firmware.go

View workflow job for this annotation

GitHub Actions / lint

task.Oem undefined (type *redfish.Task has no field or method Oem) (typecheck)

Check failure on line 120 in providers/dell/firmware.go

View workflow job for this annotation

GitHub Actions / test

task.Oem undefined (type *redfish.Task has no field or method Oem)
}

func (c *Conn) statusFromJob(taskID string) (constants.TaskState, string, error) {
job, err := c.job(taskID)
if err != nil {
return "", "", err
}

s := strings.ToLower(job.JobState)
state := c.redfishwrapper.ConvertTaskState(s)

status := fmt.Sprintf(
"id: %s, state: %s, status: %s, progress: %d%%",
taskID,
job.JobState,
job.Message,
job.PercentComplete,
)

return state, status, nil
}

func (c *Conn) statusFromTaskOem(taskID string, oem json.RawMessage) (constants.TaskState, string, error) {
data, err := convFirmwareTaskOem(oem)
if err != nil {
return "", "", err
}

s := strings.ToLower(data.Dell.JobState)
state := c.redfishwrapper.ConvertTaskState(s)

status := fmt.Sprintf(
"id: %s, state: %s, status: %s, progress: %d%%",
taskID,
data.Dell.JobState,
data.Dell.Message,
data.Dell.PercentComplete,
)

return state, status, nil
}

func (c *Conn) job(jobID string) (*Dell, error) {
errLookup := errors.New("error querying dell job: " + jobID)

endpoint := "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/" + jobID
resp, err := c.redfishwrapper.Get(endpoint)
if err != nil {
return nil, errors.Wrap(errLookup, err.Error())
}

if resp.StatusCode != 200 {
return nil, errors.Wrap(errLookup, "unexpected status code: "+resp.Status)
}

defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(errLookup, err.Error())
}

dell := &Dell{}
err = json.Unmarshal(body, &dell)
if err != nil {
return nil, errors.Wrap(errLookup, err.Error())
}

return dell, nil
}

type oem struct {
Dell `json:"Dell"`
}

type Dell struct {
OdataType string `json:"@odata.type"`
CompletionTime interface{} `json:"CompletionTime"`
Description string `json:"Description"`
EndTime string `json:"EndTime"`
ID string `json:"Id"`
JobState string `json:"JobState"`
JobType string `json:"JobType"`
Message string `json:"Message"`
MessageArgs []interface{} `json:"MessageArgs"`
MessageID string `json:"MessageId"`
Name string `json:"Name"`
PercentComplete int `json:"PercentComplete"`
StartTime string `json:"StartTime"`
TargetSettingsURI interface{} `json:"TargetSettingsURI"`
}

func convFirmwareTaskOem(oemdata json.RawMessage) (oem, error) {
oem := oem{}

errTaskOem := errors.New("error in Task Oem data: " + string(oemdata))

if len(oemdata) == 0 || string(oemdata) == `{}` {
return oem, errors.Wrap(errTaskOem, "empty oem data")
}

if err := json.Unmarshal(oemdata, &oem); err != nil {
return oem, errors.Wrap(errTaskOem, "failed to unmarshal: "+err.Error())
}

if oem.Dell.Description == "" || oem.Dell.JobState == "" {
return oem, errors.Wrap(errTaskOem, "invalid oem data")
}

if oem.Dell.JobType != "FirmwareUpdate" {
return oem, errors.Wrap(errTaskOem, "unexpected job type: "+oem.Dell.JobType)
}

return oem, nil
}
94 changes: 94 additions & 0 deletions providers/dell/firmware_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package dell

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestConvFirmwareTaskOem(t *testing.T) {
testCases := []struct {
name string
oemdata []byte
expectedJob oem
expectedErr string
}{
{
name: "Valid OEM data",
oemdata: []byte(`{
"Dell": {
"@odata.type": "#DellJob.v1_4_0.DellJob",
"CompletionTime": null,
"Description": "Job Instance",
"EndTime": "TIME_NA",
"Id": "JID_005950769310",
"JobState": "Scheduled",
"JobType": "FirmwareUpdate",
"Message": "Task successfully scheduled.",
"MessageArgs": [],
"MessageId": "IDRAC.2.8.JCP001",
"Name": "Firmware Update: BIOS",
"PercentComplete": 0,
"StartTime": "TIME_NOW",
"TargetSettingsURI": null
}
}`),
expectedJob: oem{
Dell{
OdataType: "#DellJob.v1_4_0.DellJob",
CompletionTime: nil,
Description: "Job Instance",
EndTime: "TIME_NA",
ID: "JID_005950769310",
JobState: "Scheduled",
JobType: "FirmwareUpdate",
Message: "Task successfully scheduled.",
MessageArgs: []interface{}{},
MessageID: "IDRAC.2.8.JCP001",
Name: "Firmware Update: BIOS",
PercentComplete: 0,
StartTime: "TIME_NOW",
TargetSettingsURI: nil,
},
},
expectedErr: "",
},
{
name: "Empty OEM data",
oemdata: []byte(`{}`),
expectedJob: oem{},
expectedErr: "empty oem data",
},
{
name: "Invalid OEM data",
oemdata: []byte(`{"InvalidKey": "InvalidValue"}`),
expectedJob: oem{},
expectedErr: "invalid oem data",
},
{
name: "Unexpected job type",
oemdata: []byte(`{
"Dell": {
"JobType": "InvalidJobType",
"Description": "Job Instance",
"JobState": "Scheduled"
}
}`),
expectedJob: oem{},
expectedErr: "unexpected job type",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
job, err := convFirmwareTaskOem(tc.oemdata)
if tc.expectedErr == "" {
assert.NoError(t, err)
assert.Equal(t, tc.expectedJob, job)
} else {
assert.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedErr)
}
})
}
}
6 changes: 6 additions & 0 deletions providers/dell/idrac.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ var (
// Features implemented by dell redfish
Features = registrar.Features{
providers.FeatureScreenshot,
providers.FeaturePowerState,
providers.FeaturePowerSet,
providers.FeatureFirmwareInstallSteps,
providers.FeatureFirmwareUploadInitiateInstall,
providers.FeatureFirmwareTaskStatus,
providers.FeatureInventoryRead,
}
)

Expand Down

0 comments on commit 48209f4

Please sign in to comment.