-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
providers/dell: Implements FirmwareInstallSteps(), FirmwareInstallUpl…
…oadedAndInitiate(), FirmwareInstallStatus() methods
- Loading branch information
Showing
3 changed files
with
335 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
|
||
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters