From a0867842921560cf5fe1abf96186df3e28efd9ac Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Wed, 22 Nov 2023 17:52:18 +0100 Subject: [PATCH] WIP --- bmc/firmware.go | 111 +++- bmc/firmware_test.go | 93 +++ client.go | 11 + constants/constants.go | 6 + examples/install-firmware/main.go | 5 + internal/redfishwrapper/firmware.go | 9 +- internal/redfishwrapper/task.go | 7 +- providers/dell/firmware.go | 235 ++++++++ providers/dell/firmware_test.go | 94 +++ providers/dell/idrac.go | 25 +- providers/providers.go | 5 +- providers/redfish/firmware.go | 854 ++++++++++++++-------------- providers/redfish/firmware_test.go | 558 +++++++++--------- providers/redfish/main_test.go | 4 +- providers/redfish/redfish.go | 6 +- 15 files changed, 1281 insertions(+), 742 deletions(-) create mode 100644 providers/dell/firmware.go create mode 100644 providers/dell/firmware_test.go diff --git a/bmc/firmware.go b/bmc/firmware.go index c6fb44ec..309447b2 100644 --- a/bmc/firmware.go +++ b/bmc/firmware.go @@ -53,7 +53,7 @@ func firmwareInstall(ctx context.Context, component, operationApplyTime string, taskID, vErr := elem.FirmwareInstall(ctx, component, operationApplyTime, forceInstall, reader) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) - err = multierror.Append(err, vErr) + metadata.FailedProviderDetail[elem.name] = err.Error() continue } @@ -134,7 +134,7 @@ func firmwareInstallStatus(ctx context.Context, installVersion, component, taskI status, vErr := elem.FirmwareInstallStatus(ctx, installVersion, component, taskID) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) - err = multierror.Append(err, vErr) + metadata.FailedProviderDetail[elem.name] = err.Error() continue } @@ -175,7 +175,82 @@ func FirmwareInstallStatusFromInterfaces(ctx context.Context, installVersion, co return firmwareInstallStatus(ctx, installVersion, component, taskID, implementations) } -// FirmwareInstallerWithOpts defines an interface to install firmware that was previously uploaded with FirmwareUpload +// FirmwareInstallProvider defines an interface to upload and initiate a firmware install in the same implementation method +// +// Its intended to deprecate the FirmwareInstall interface +type FirmwareInstallProvider interface { + // FirmwareInstallUploadAndInitiate uploads _and_ initiates the firmware install process. + // + // return values: + // taskID - A taskID is returned if the update process on the BMC returns an identifier for the update process. + FirmwareInstallUploadAndInitiate(ctx context.Context, component string, file *os.File) (taskID string, err error) +} + +// firmwareInstallProvider is an internal struct to correlate an implementation/provider and its name +type firmwareInstallProvider struct { + name string + FirmwareInstallProvider +} + +// firmwareInstall uploads and initiates firmware update for the component +func firmwareInstallUploadAndInitiate(ctx context.Context, component string, file *os.File, generic []firmwareInstallProvider) (taskID string, metadata Metadata, err error) { + var metadataLocal Metadata + + for _, elem := range generic { + if elem.FirmwareInstallProvider == nil { + continue + } + select { + case <-ctx.Done(): + err = multierror.Append(err, ctx.Err()) + + return taskID, metadata, err + default: + metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) + taskID, vErr := elem.FirmwareInstallUploadAndInitiate(ctx, component, file) + if vErr != nil { + err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) + metadata.FailedProviderDetail[elem.name] = err.Error() + continue + } + metadataLocal.SuccessfulProvider = elem.name + return taskID, metadataLocal, nil + } + } + + return taskID, metadataLocal, multierror.Append(err, errors.New("failure in FirmwareInstallUploadAndInitiate")) +} + +// FirmwareInstallUploadAndInitiateFromInterfaces identifies implementations of the FirmwareInstallProvider interface and passes the found implementations to the firmwareInstallUploadAndInitiate() wrapper +func FirmwareInstallUploadAndInitiateFromInterfaces(ctx context.Context, component string, file *os.File, generic []interface{}) (taskID string, metadata Metadata, err error) { + metadata = newMetadata() + + implementations := make([]firmwareInstallProvider, 0) + for _, elem := range generic { + temp := firmwareInstallProvider{name: getProviderName(elem)} + switch p := elem.(type) { + case FirmwareInstallProvider: + temp.FirmwareInstallProvider = p + implementations = append(implementations, temp) + default: + e := fmt.Sprintf("not a FirmwareInstallProvider implementation: %T", p) + err = multierror.Append(err, errors.New(e)) + } + } + if len(implementations) == 0 { + return taskID, metadata, multierror.Append( + err, + errors.Wrap( + bmclibErrs.ErrProviderImplementation, + ("no FirmwareInstallProvider implementations found"), + ), + ) + } + + return firmwareInstallUploadAndInitiate(ctx, component, file, implementations) +} + +// FirmwareInstallerUploaded defines an interface to install firmware that was previously uploaded with FirmwareUpload type FirmwareInstallerUploaded interface { // FirmwareInstallUploaded uploads firmware update payload to the BMC returning the firmware install task ID // @@ -213,7 +288,7 @@ func firmwareInstallUploaded(ctx context.Context, component, uploadTaskID string installTaskID, vErr = elem.FirmwareInstallUploaded(ctx, component, uploadTaskID) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) - err = multierror.Append(err, vErr) + metadata.FailedProviderDetail[elem.name] = err.Error() continue } @@ -310,7 +385,7 @@ func firmwareInstallSteps(ctx context.Context, component string, generic []firmw steps, vErr := elem.FirmwareInstallSteps(ctx, component) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) - err = multierror.Append(err, vErr) + metadata.FailedProviderDetail[elem.name] = err.Error() continue } @@ -362,7 +437,7 @@ func FirmwareUploadFromInterfaces(ctx context.Context, component string, file *o } func firmwareUpload(ctx context.Context, component string, file *os.File, generic []firmwareUploaderProvider) (taskID string, metadata Metadata, err error) { - var metadataLocal Metadata + metadata = newMetadata() for _, elem := range generic { if elem.FirmwareUploader == nil { @@ -374,20 +449,20 @@ func firmwareUpload(ctx context.Context, component string, file *os.File, generi return taskID, metadata, err default: - metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) + metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, elem.name) taskID, vErr := elem.FirmwareUpload(ctx, component, file) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) - err = multierror.Append(err, vErr) + metadata.FailedProviderDetail[elem.name] = err.Error() continue } - metadataLocal.SuccessfulProvider = elem.name - return taskID, metadataLocal, nil + metadata.SuccessfulProvider = elem.name + return taskID, metadata, nil } } - return taskID, metadataLocal, multierror.Append(err, errors.New("failure in FirmwareUpload")) + return taskID, metadata, multierror.Append(err, errors.New("failure in FirmwareUpload")) } // FirmwareTaskVerifier defines an interface to check the status for firmware related tasks queued on the BMC. @@ -417,7 +492,7 @@ type firmwareTaskVerifierProvider struct { // firmwareTaskStatus returns the status of the firmware upload process. func firmwareTaskStatus(ctx context.Context, kind bconsts.FirmwareInstallStep, component, taskID, installVersion string, generic []firmwareTaskVerifierProvider) (state constants.TaskState, status string, metadata Metadata, err error) { - var metadataLocal Metadata + metadata = newMetadata() for _, elem := range generic { if elem.FirmwareTaskVerifier == nil { @@ -429,20 +504,20 @@ func firmwareTaskStatus(ctx context.Context, kind bconsts.FirmwareInstallStep, c return state, status, metadata, err default: - metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) + metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, elem.name) state, status, vErr := elem.FirmwareTaskStatus(ctx, kind, component, taskID, installVersion) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) - err = multierror.Append(err, vErr) + metadata.FailedProviderDetail[elem.name] = err.Error() continue - } - metadataLocal.SuccessfulProvider = elem.name - return state, status, metadataLocal, nil + + metadata.SuccessfulProvider = elem.name + return state, status, metadata, nil } } - return state, status, metadataLocal, multierror.Append(err, errors.New("failure in FirmwareTaskStatus")) + return state, status, metadata, multierror.Append(err, errors.New("failure in FirmwareTaskStatus")) } // FirmwareTaskStatusFromInterfaces identifies implementations of the FirmwareTaskVerifier interface and passes the found implementations to the firmwareTaskStatus() wrapper. diff --git a/bmc/firmware_test.go b/bmc/firmware_test.go index 0756bcd2..9d3401fb 100644 --- a/bmc/firmware_test.go +++ b/bmc/firmware_test.go @@ -204,6 +204,99 @@ func TestFirmwareInstallStatusFromInterfaces(t *testing.T) { } } +type firmwareInstallUploadAndInitiateTester struct { + returnTaskID string + returnError error +} + +func (f *firmwareInstallUploadAndInitiateTester) FirmwareInstallUploadAndInitiate(ctx context.Context, component string, file *os.File) (taskID string, err error) { + return f.returnTaskID, f.returnError +} + +func (r *firmwareInstallUploadAndInitiateTester) Name() string { + return "foo" +} + +func TestFirmwareInstallUploadAndInitiate(t *testing.T) { + testCases := []struct { + testName string + component string + file *os.File + returnTaskID string + returnError error + ctxTimeout time.Duration + providerName string + providersAttempted int + }{ + {"success with metadata", "componentA", &os.File{}, "1234", nil, 5 * time.Second, "foo", 1}, + {"failure with metadata", "componentB", &os.File{}, "1234", errors.New("failed to upload and initiate"), 5 * time.Second, "foo", 1}, + {"failure with context timeout", "componentC", &os.File{}, "", context.DeadlineExceeded, 1 * time.Nanosecond, "foo", 1}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + testImplementation := &firmwareInstallUploadAndInitiateTester{returnTaskID: tc.returnTaskID, returnError: tc.returnError} + if tc.ctxTimeout == 0 { + tc.ctxTimeout = time.Second * 3 + } + ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) + defer cancel() + taskID, metadata, err := firmwareInstallUploadAndInitiate(ctx, tc.component, tc.file, []firmwareInstallProvider{{tc.providerName, testImplementation}}) + if tc.returnError != nil { + assert.ErrorIs(t, err, tc.returnError) + return + } + + if err != nil { + t.Fatal(err) + } + assert.Equal(t, tc.returnTaskID, taskID) + assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) + assert.Equal(t, tc.providersAttempted, len(metadata.ProvidersAttempted)) + }) + } +} + +func TestFirmwareInstallUploadAndInitiateFromInterfaces(t *testing.T) { + testCases := []struct { + testName string + component string + file *os.File + returnTaskID string + returnError error + providerName string + badImplementation bool + }{ + {"success with metadata", "componentA", &os.File{}, "1234", nil, "foo", false}, + {"failure with bad implementation", "componentB", &os.File{}, "1234", bmclibErrs.ErrProviderImplementation, "foo", true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + var generic []interface{} + if tc.badImplementation { + badImplementation := struct{}{} + generic = []interface{}{&badImplementation} + } else { + testImplementation := &firmwareInstallUploadAndInitiateTester{returnTaskID: tc.returnTaskID, returnError: tc.returnError} + generic = []interface{}{testImplementation} + } + taskID, metadata, err := FirmwareInstallUploadAndInitiateFromInterfaces(context.Background(), tc.component, tc.file, generic) + if tc.returnError != nil { + assert.ErrorIs(t, err, tc.returnError) + return + } + + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, tc.returnTaskID, taskID) + assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) + }) + } +} + type firmwareInstallUploadTester struct { TaskID string Err error diff --git a/client.go b/client.go index f5a13f91..717f56aa 100644 --- a/client.go +++ b/client.go @@ -645,3 +645,14 @@ func (c *Client) FirmwareInstallUploaded(ctx context.Context, component, uploadV return installTaskID, err } + +func (c *Client) FirmwareInstallUploadAndInitiate(ctx context.Context, component string, file *os.File) (taskID string, err error) { + ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "FirmwareInstallUploadAndInitiate") + defer span.End() + + taskID, metadata, err := bmc.FirmwareInstallUploadAndInitiateFromInterfaces(ctx, component, file, c.registry().GetDriverInterfaces()) + c.setMetadata(metadata) + metadata.RegisterSpanAttributes(c.Auth.Host, span) + + return taskID, err +} diff --git a/constants/constants.go b/constants/constants.go index d2a6bd10..eaf8e3fa 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -114,3 +114,9 @@ const ( func ListSupportedVendors() []string { return []string{HP, Dell, Supermicro} } + +type FirmwareInstallProperties struct { + InstallOrder []FirmwareInstallStep + PreInstallHostPowerOff bool + AcceptsOperationApplyTimes []OperationApplyTime +} diff --git a/examples/install-firmware/main.go b/examples/install-firmware/main.go index 098a0dbf..dedc8616 100644 --- a/examples/install-firmware/main.go +++ b/examples/install-firmware/main.go @@ -70,6 +70,11 @@ func main() { l.Fatal(err, "bmc login failed") } + steps, err := cl.FirmwareInstallSteps(ctx, *component) + if err != nil { + l.Fatal(err, "FirmwareInstallSteps returned error") + } + defer cl.Close(ctx) // open file handle diff --git a/internal/redfishwrapper/firmware.go b/internal/redfishwrapper/firmware.go index 00c6031f..32c83d64 100644 --- a/internal/redfishwrapper/firmware.go +++ b/internal/redfishwrapper/firmware.go @@ -106,6 +106,9 @@ func (c *Client) FirmwareUpload(ctx context.Context, updateFile *os.File, params return taskIDFromLocationHeader(location) } + fmt.Println(location) + fmt.Println(string(response)) + return taskIDFromResponseBody(response) } @@ -193,9 +196,9 @@ func taskIDFromLocationHeader(uri string) (taskID string, err error) { switch { // idracs return /redfish/v1/TaskService/Tasks/JID_467696020275 - case strings.Contains(uri, "JID_"): - taskID = strings.Split(uri, "JID_")[1] - return taskID, nil + //case strings.Contains(uri, "JID_"): + // taskID = strings.Split(uri, "JID_")[1] + // return taskID, nil // OpenBMC returns /redfish/v1/TaskService/Tasks/12/Monitor case strings.Contains(uri, "/Tasks/") && strings.HasSuffix(uri, "/Monitor"): diff --git a/internal/redfishwrapper/task.go b/internal/redfishwrapper/task.go index 592712f2..0d5227be 100644 --- a/internal/redfishwrapper/task.go +++ b/internal/redfishwrapper/task.go @@ -37,14 +37,15 @@ func (c *Client) TaskStatus(ctx context.Context, taskID string) (constants.TaskS if err != nil { return "", "", errors.Wrap(err, "error querying redfish for taskID: "+taskID) } + taskInfo := fmt.Sprintf("id: %s, state: %s, status: %s", task.ID, task.TaskState, task.TaskStatus) - state := strings.ToLower(string(task.TaskState)) - return c.ConvertTaskState(state), taskInfo, nil + s := c.ConvertTaskState(string(task.TaskState)) + return s, taskInfo, nil } func (c *Client) ConvertTaskState(state string) constants.TaskState { - switch state { + switch strings.ToLower(state) { case "starting", "downloading", "downloaded": return constants.Initializing case "running", "stopping", "cancelling", "scheduling": diff --git a/providers/dell/firmware.go b/providers/dell/firmware.go new file mode 100644 index 00000000..f311ea73 --- /dev/null +++ b/providers/dell/firmware.go @@ -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 +} diff --git a/providers/dell/firmware_test.go b/providers/dell/firmware_test.go new file mode 100644 index 00000000..77f099e2 --- /dev/null +++ b/providers/dell/firmware_test.go @@ -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) + } + }) + } +} diff --git a/providers/dell/idrac.go b/providers/dell/idrac.go index f1282f5e..b0be39d1 100644 --- a/providers/dell/idrac.go +++ b/providers/dell/idrac.go @@ -36,6 +36,10 @@ var ( // Features implemented by dell redfish Features = registrar.Features{ providers.FeatureScreenshot, + providers.FeaturePowerState, + providers.FeatureFirmwareInstallSteps, + providers.FeatureFirmwareUploadInitiateInstall, + providers.FeatureFirmwareTaskStatus, } ) @@ -126,19 +130,26 @@ func (c *Conn) Open(ctx context.Context) (err error) { // because this uses the redfish interface and the redfish interface // is available across various BMC vendors, we verify the device we're connected to is dell. - manufacturer, err := c.deviceManufacturer(ctx) - if err != nil { + if err := c.deviceSupported(ctx); err != nil { if er := c.redfishwrapper.Close(ctx); er != nil { return fmt.Errorf("%v: %w", err, er) } + return err } - if !strings.Contains(strings.ToLower(manufacturer), common.VendorDell) { - if er := c.redfishwrapper.Close(ctx); er != nil { - return fmt.Errorf("%v: %w", err, er) - } - return bmclibErrs.ErrIncompatibleProvider + return nil +} + +func (c *Conn) deviceSupported(ctx context.Context) error { + manufacturer, err := c.deviceManufacturer(ctx) + if err != nil { + return err + } + + m := strings.ToLower(manufacturer) + if !strings.Contains(m, common.VendorDell) { + return errors.Wrap(bmclibErrs.ErrIncompatibleProvider, m) } return nil diff --git a/providers/providers.go b/providers/providers.go index b7eb0518..41b90f1a 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -44,7 +44,7 @@ const ( FeatureClearSystemEventLog registrar.Feature = "clearsystemeventlog" // FeatureFirmwareInstallSteps means an implementation returns the steps part of the firmware update process. - FeatureFirmwareInstallSteps registrar.Feature = "firmwareinstallactions" + FeatureFirmwareInstallSteps registrar.Feature = "firmwareinstallsteps" // FeatureFirmwareUpload means an implementation that uploads firmware for installing. FeatureFirmwareUpload registrar.Feature = "firmwareupload" @@ -54,4 +54,7 @@ const ( // FeatureFirmwareTaskStatus identifies an implementaton that can return the status of a firmware upload/install task. FeatureFirmwareTaskStatus registrar.Feature = "firmwaretaskstatus" + + // FeatureFirmwareUploadInitiateInstall identifies an implementation that uploads firmware _and_ initiates the install process. + FeatureFirmwareUploadInitiateInstall registrar.Feature = "uploadandinitiateinstall" ) diff --git a/providers/redfish/firmware.go b/providers/redfish/firmware.go index 8606bfdd..6b121bfd 100644 --- a/providers/redfish/firmware.go +++ b/providers/redfish/firmware.go @@ -1,430 +1,432 @@ package redfish -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/http" - "net/textproto" - "os" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/pkg/errors" - gofishrf "github.com/stmcginnis/gofish/redfish" - - "github.com/bmc-toolbox/bmclib/v2/constants" - bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" -) - -var ( - errInsufficientCtxTimeout = errors.New("remaining context timeout insufficient to install firmware") - errMultiPartPayload = errors.New("error preparing multipart payload") -) - -type installMethod string - -const ( - unstructuredHttpPush installMethod = "unstructuredHttpPush" - multipartHttpUpload installMethod = "multipartUpload" -) - -// FirmwareInstall uploads and initiates the firmware install process -func (c *Conn) FirmwareInstall(ctx context.Context, component string, operationApplyTime string, forceInstall bool, reader io.Reader) (taskID string, err error) { - // limit to *os.File until theres a need for other types of readers - updateFile, ok := reader.(*os.File) - if !ok { - return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, "method expects an *os.File object") - } - - installMethod, installURI, err := c.firmwareInstallMethodURI(ctx) - if err != nil { - return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, err.Error()) - } - - // expect atleast 10 minutes left in the deadline to proceed with the update - // - // this gives the BMC enough time to have the firmware uploaded and return a response to the client. - ctxDeadline, _ := ctx.Deadline() - if time.Until(ctxDeadline) < 10*time.Minute { - return "", errors.Wrap(errInsufficientCtxTimeout, " "+time.Until(ctxDeadline).String()) - } - - // list redfish firmware install task if theres one present - task, err := c.GetFirmwareInstallTaskQueued(ctx, component) - if err != nil { - return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, err.Error()) - } - - if task != nil { - msg := fmt.Sprintf("task for %s firmware install present: %s", component, task.ID) - c.Log.V(2).Info("warn", msg) - - if forceInstall { - err = c.purgeQueuedFirmwareInstallTask(ctx, component) - if err != nil { - return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, err.Error()) - } - } else { - return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, msg) - } - } - - // override the gofish HTTP client timeout, - // since the context timeout is set at Open() and is at a lower value than required for this operation. - // - // record the http client timeout to be restored when this method returns - httpClientTimeout := c.redfishwrapper.HttpClientTimeout() - defer func() { - c.redfishwrapper.SetHttpClientTimeout(httpClientTimeout) - }() - - c.redfishwrapper.SetHttpClientTimeout(time.Until(ctxDeadline)) - - var resp *http.Response - - switch installMethod { - case multipartHttpUpload: - var uploadErr error - resp, uploadErr = c.multipartHTTPUpload(ctx, installURI, operationApplyTime, updateFile) - if uploadErr != nil { - return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, uploadErr.Error()) - } - - case unstructuredHttpPush: - var uploadErr error - resp, uploadErr = c.unstructuredHttpUpload(ctx, installURI, operationApplyTime, reader) - if uploadErr != nil { - return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, uploadErr.Error()) - } - - default: - return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, "unsupported install method: "+string(installMethod)) - } - - if resp.StatusCode != http.StatusAccepted { - return "", errors.Wrap( - bmclibErrs.ErrFirmwareUpload, - "non 202 status code returned: "+strconv.Itoa(resp.StatusCode), - ) - } - - // The response contains a location header pointing to the task URI - // Location: /redfish/v1/TaskService/Tasks/JID_467696020275 - var location = resp.Header.Get("Location") - - taskID, err = TaskIDFromLocationURI(location) - - return taskID, err -} - -func TaskIDFromLocationURI(uri string) (taskID string, err error) { - - if strings.Contains(uri, "JID_") { - taskID = strings.Split(uri, "JID_")[1] - } else if strings.Contains(uri, "/Monitor") { - // OpenBMC returns a monitor URL in Location - // Location: /redfish/v1/TaskService/Tasks/12/Monitor - splits := strings.Split(uri, "/") - if len(splits) >= 6 { - taskID = splits[5] - } else { - taskID = "" - } - } - - if taskID == "" { - return "", bmclibErrs.ErrTaskNotFound - } - - return taskID, nil -} - -type multipartPayload struct { - updateParameters []byte - updateFile *os.File -} - -func (c *Conn) multipartHTTPUpload(ctx context.Context, url string, operationApplyTime string, update *os.File) (*http.Response, error) { - if url == "" { - return nil, fmt.Errorf("unable to execute request, no target provided") - } - - parameters, err := json.Marshal(struct { - Targets []string `json:"Targets"` - RedfishOpApplyTime string `json:"@Redfish.OperationApplyTime"` - Oem struct{} `json:"Oem"` - }{ - []string{}, - operationApplyTime, - struct{}{}, - }) - - if err != nil { - return nil, errors.Wrap(err, "error preparing multipart UpdateParameters payload") - } - - // payload ordered in the format it ends up in the multipart form - payload := &multipartPayload{ - updateParameters: []byte(parameters), - updateFile: update, - } - - return c.runRequestWithMultipartPayload(url, payload) -} - -func (c *Conn) unstructuredHttpUpload(ctx context.Context, url string, operationApplyTime string, update io.Reader) (*http.Response, error) { - if url == "" { - return nil, fmt.Errorf("unable to execute request, no target provided") - } - - // TODO: transform this to read the update so that we don't hold the data in memory - b, _ := io.ReadAll(update) - payloadReadSeeker := bytes.NewReader(b) - - return c.redfishwrapper.RunRawRequestWithHeaders(http.MethodPost, url, payloadReadSeeker, "application/octet-stream", nil) - -} - -// firmwareUpdateMethodURI returns the updateMethod and URI -func (c *Conn) firmwareInstallMethodURI(ctx context.Context) (method installMethod, updateURI string, err error) { - updateService, err := c.redfishwrapper.UpdateService() - if err != nil { - return "", "", errors.Wrap(bmclibErrs.ErrRedfishUpdateService, err.Error()) - } - - // update service disabled - if !updateService.ServiceEnabled { - return "", "", errors.Wrap(bmclibErrs.ErrRedfishUpdateService, "service disabled") - } - - switch { - case updateService.MultipartHTTPPushURI != "": - return multipartHttpUpload, updateService.MultipartHTTPPushURI, nil - case updateService.HTTPPushURI != "": - return unstructuredHttpPush, updateService.HTTPPushURI, nil - } - - return "", "", errors.Wrap(bmclibErrs.ErrRedfishUpdateService, "unsupported update method") -} - -// pipeReaderFakeSeeker wraps the io.PipeReader and implements the io.Seeker interface -// to meet the API requirements for the Gofish client https://github.com/stmcginnis/gofish/blob/46b1b33645ed1802727dc4df28f5d3c3da722b15/client.go#L434 -// -// The Gofish method linked does not currently perform seeks and so a PR will be suggested -// to change the method signature to accept an io.Reader instead. -type pipeReaderFakeSeeker struct { - *io.PipeReader -} - -// Seek impelements the io.Seeker interface only to panic if called -func (p pipeReaderFakeSeeker) Seek(offset int64, whence int) (int64, error) { - return 0, errors.New("Seek() not implemented for fake pipe reader seeker.") -} - -// multipartPayloadSize prepares a temporary multipart form to determine the form size // -// It creates a temporary form without reading in the update file payload and returns -// sizeOf(form) + sizeOf(update file) -func multipartPayloadSize(payload *multipartPayload) (int64, *bytes.Buffer, error) { - body := &bytes.Buffer{} - form := multipart.NewWriter(body) - - // Add UpdateParameters field part - part, err := updateParametersFormField("UpdateParameters", form) - if err != nil { - return 0, body, err - } - - if _, err = io.Copy(part, bytes.NewReader(payload.updateParameters)); err != nil { - return 0, body, err - } - - // Add updateFile form - _, err = form.CreateFormFile("UpdateFile", filepath.Base(payload.updateFile.Name())) - if err != nil { - return 0, body, err - } - - // determine update file size - finfo, err := payload.updateFile.Stat() - if err != nil { - return 0, body, err - } - - // add terminating boundary to multipart form - err = form.Close() - if err != nil { - return 0, body, err - } - - return int64(body.Len()) + finfo.Size(), body, nil -} - -// runRequestWithMultipartPayload is a copy of https://github.com/stmcginnis/gofish/blob/main/client.go#L349 -// with a change to add the UpdateParameters multipart form field with a json content type header -// the resulting form ends up in this format +//import ( +// "bytes" +// "context" +// "encoding/json" +// "fmt" +// "io" +// "mime/multipart" +// "net/http" +// "net/textproto" +// "os" +// "path/filepath" +// "strconv" +// "strings" +// "time" +// +// "github.com/pkg/errors" +// gofishrf "github.com/stmcginnis/gofish/redfish" +// +// "github.com/bmc-toolbox/bmclib/v2/constants" +// bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" +//) +// +//var ( +// errInsufficientCtxTimeout = errors.New("remaining context timeout insufficient to install firmware") +// errMultiPartPayload = errors.New("error preparing multipart payload") +//) +// +//type installMethod string +// +//const ( +// unstructuredHttpPush installMethod = "unstructuredHttpPush" +// multipartHttpUpload installMethod = "multipartUpload" +//) +// +//// FirmwareInstall uploads and initiates the firmware install process +//func (c *Conn) FirmwareInstall(ctx context.Context, component string, operationApplyTime string, forceInstall bool, reader io.Reader) (taskID string, err error) { +// // limit to *os.File until theres a need for other types of readers +// updateFile, ok := reader.(*os.File) +// if !ok { +// return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, "method expects an *os.File object") +// } +// +// installMethod, installURI, err := c.firmwareInstallMethodURI(ctx) +// if err != nil { +// return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, err.Error()) +// } +// +// // expect atleast 10 minutes left in the deadline to proceed with the update +// // +// // this gives the BMC enough time to have the firmware uploaded and return a response to the client. +// ctxDeadline, _ := ctx.Deadline() +// if time.Until(ctxDeadline) < 10*time.Minute { +// return "", errors.Wrap(errInsufficientCtxTimeout, " "+time.Until(ctxDeadline).String()) +// } +// +// // list redfish firmware install task if theres one present +// task, err := c.GetFirmwareInstallTaskQueued(ctx, component) +// if err != nil { +// return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, err.Error()) +// } +// +// if task != nil { +// msg := fmt.Sprintf("task for %s firmware install present: %s", component, task.ID) +// c.Log.V(2).Info("warn", msg) +// +// if forceInstall { +// err = c.purgeQueuedFirmwareInstallTask(ctx, component) +// if err != nil { +// return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, err.Error()) +// } +// } else { +// return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, msg) +// } +// } +// +// // override the gofish HTTP client timeout, +// // since the context timeout is set at Open() and is at a lower value than required for this operation. +// // +// // record the http client timeout to be restored when this method returns +// httpClientTimeout := c.redfishwrapper.HttpClientTimeout() +// defer func() { +// c.redfishwrapper.SetHttpClientTimeout(httpClientTimeout) +// }() +// +// c.redfishwrapper.SetHttpClientTimeout(time.Until(ctxDeadline)) +// +// var resp *http.Response +// +// switch installMethod { +// case multipartHttpUpload: +// var uploadErr error +// resp, uploadErr = c.multipartHTTPUpload(ctx, installURI, operationApplyTime, updateFile) +// if uploadErr != nil { +// return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, uploadErr.Error()) +// } +// +// case unstructuredHttpPush: +// var uploadErr error +// resp, uploadErr = c.unstructuredHttpUpload(ctx, installURI, operationApplyTime, reader) +// if uploadErr != nil { +// return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, uploadErr.Error()) +// } +// +// default: +// return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, "unsupported install method: "+string(installMethod)) +// } +// +// if resp.StatusCode != http.StatusAccepted { +// return "", errors.Wrap( +// bmclibErrs.ErrFirmwareUpload, +// "non 202 status code returned: "+strconv.Itoa(resp.StatusCode), +// ) +// } +// +// // The response contains a location header pointing to the task URI +// // Location: /redfish/v1/TaskService/Tasks/JID_467696020275 +// var location = resp.Header.Get("Location") +// +// taskID, err = TaskIDFromLocationURI(location) +// +// return taskID, err +//} +// +//func TaskIDFromLocationURI(uri string) (taskID string, err error) { +// +// if strings.Contains(uri, "JID_") { +// taskID = strings.Split(uri, "JID_")[1] +// } else if strings.Contains(uri, "/Monitor") { +// // OpenBMC returns a monitor URL in Location +// // Location: /redfish/v1/TaskService/Tasks/12/Monitor +// splits := strings.Split(uri, "/") +// if len(splits) >= 6 { +// taskID = splits[5] +// } else { +// taskID = "" +// } +// } +// +// if taskID == "" { +// return "", bmclibErrs.ErrTaskNotFound +// } +// +// return taskID, nil +//} +// +//type multipartPayload struct { +// updateParameters []byte +// updateFile *os.File +//} +// +//func (c *Conn) multipartHTTPUpload(ctx context.Context, url string, operationApplyTime string, update *os.File) (*http.Response, error) { +// if url == "" { +// return nil, fmt.Errorf("unable to execute request, no target provided") +// } +// +// parameters, err := json.Marshal(struct { +// Targets []string `json:"Targets"` +// RedfishOpApplyTime string `json:"@Redfish.OperationApplyTime"` +// Oem struct{} `json:"Oem"` +// }{ +// []string{}, +// operationApplyTime, +// struct{}{}, +// }) +// +// if err != nil { +// return nil, errors.Wrap(err, "error preparing multipart UpdateParameters payload") +// } +// +// // payload ordered in the format it ends up in the multipart form +// payload := &multipartPayload{ +// updateParameters: []byte(parameters), +// updateFile: update, +// } +// +// return c.runRequestWithMultipartPayload(url, payload) +//} +// +//func (c *Conn) unstructuredHttpUpload(ctx context.Context, url string, operationApplyTime string, update io.Reader) (*http.Response, error) { +// if url == "" { +// return nil, fmt.Errorf("unable to execute request, no target provided") +// } +// +// // TODO: transform this to read the update so that we don't hold the data in memory +// b, _ := io.ReadAll(update) +// payloadReadSeeker := bytes.NewReader(b) +// +// return c.redfishwrapper.RunRawRequestWithHeaders(http.MethodPost, url, payloadReadSeeker, "application/octet-stream", nil) +// +//} +// +//// firmwareUpdateMethodURI returns the updateMethod and URI +//func (c *Conn) firmwareInstallMethodURI(ctx context.Context) (method installMethod, updateURI string, err error) { +// updateService, err := c.redfishwrapper.UpdateService() +// if err != nil { +// return "", "", errors.Wrap(bmclibErrs.ErrRedfishUpdateService, err.Error()) +// } +// +// // update service disabled +// if !updateService.ServiceEnabled { +// return "", "", errors.Wrap(bmclibErrs.ErrRedfishUpdateService, "service disabled") +// } +// +// switch { +// case updateService.MultipartHTTPPushURI != "": +// return multipartHttpUpload, updateService.MultipartHTTPPushURI, nil +// case updateService.HTTPPushURI != "": +// return unstructuredHttpPush, updateService.HTTPPushURI, nil +// } +// +// return "", "", errors.Wrap(bmclibErrs.ErrRedfishUpdateService, "unsupported update method") +//} +// +//// pipeReaderFakeSeeker wraps the io.PipeReader and implements the io.Seeker interface +//// to meet the API requirements for the Gofish client https://github.com/stmcginnis/gofish/blob/46b1b33645ed1802727dc4df28f5d3c3da722b15/client.go#L434 +//// +//// The Gofish method linked does not currently perform seeks and so a PR will be suggested +//// to change the method signature to accept an io.Reader instead. +//type pipeReaderFakeSeeker struct { +// *io.PipeReader +//} +// +//// Seek impelements the io.Seeker interface only to panic if called +//func (p pipeReaderFakeSeeker) Seek(offset int64, whence int) (int64, error) { +// return 0, errors.New("Seek() not implemented for fake pipe reader seeker.") +//} +// +//// multipartPayloadSize prepares a temporary multipart form to determine the form size +//// +//// It creates a temporary form without reading in the update file payload and returns +//// sizeOf(form) + sizeOf(update file) +//func multipartPayloadSize(payload *multipartPayload) (int64, *bytes.Buffer, error) { +// body := &bytes.Buffer{} +// form := multipart.NewWriter(body) +// +// // Add UpdateParameters field part +// part, err := updateParametersFormField("UpdateParameters", form) +// if err != nil { +// return 0, body, err +// } +// +// if _, err = io.Copy(part, bytes.NewReader(payload.updateParameters)); err != nil { +// return 0, body, err +// } +// +// // Add updateFile form +// _, err = form.CreateFormFile("UpdateFile", filepath.Base(payload.updateFile.Name())) +// if err != nil { +// return 0, body, err +// } +// +// // determine update file size +// finfo, err := payload.updateFile.Stat() +// if err != nil { +// return 0, body, err +// } +// +// // add terminating boundary to multipart form +// err = form.Close() +// if err != nil { +// return 0, body, err +// } +// +// return int64(body.Len()) + finfo.Size(), body, nil +//} +// +//// runRequestWithMultipartPayload is a copy of https://github.com/stmcginnis/gofish/blob/main/client.go#L349 +//// with a change to add the UpdateParameters multipart form field with a json content type header +//// the resulting form ends up in this format +//// +//// Content-Length: 416 +//// Content-Type: multipart/form-data; boundary=-------------------- +//// ----1771f60800cb2801 +// +//// --------------------------1771f60800cb2801 +//// Content-Disposition: form-data; name="UpdateParameters" +//// Content-Type: application/json +// +//// {"Targets": [], "@Redfish.OperationApplyTime": "OnReset", "Oem": +//// {}} +//// --------------------------1771f60800cb2801 +//// Content-Disposition: form-data; name="UpdateFile"; filename="dum +//// myfile" +//// Content-Type: application/octet-stream +// +//// hey. +//// --------------------------1771f60800cb2801-- +//func (c *Conn) runRequestWithMultipartPayload(url string, payload *multipartPayload) (*http.Response, error) { +// if url == "" { +// return nil, fmt.Errorf("unable to execute request, no target provided") +// } +// +// // A content-length header is passed in to indicate the payload size +// // +// // The Content-length is set explicitly since the payload is an io.Reader, +// // https://github.com/golang/go/blob/ddad9b618cce0ed91d66f0470ddb3e12cfd7eeac/src/net/http/request.go#L861 +// // +// // Without the content-length header the http client will set the Transfer-Encoding to 'chunked' +// // and that does not work for some BMCs (iDracs). +// contentLength, _, err := multipartPayloadSize(payload) +// if err != nil { +// return nil, errors.Wrap(err, "error determining multipart payload size") +// } +// +// headers := map[string]string{ +// "Content-Length": strconv.FormatInt(contentLength, 10), +// } +// +// // setup pipe +// pipeReader, pipeWriter := io.Pipe() +// defer pipeReader.Close() +// +// // initiate a mulitpart writer +// form := multipart.NewWriter(pipeWriter) +// +// // go routine blocks on the io.Copy until the http request is made +// go func() { +// var err error +// defer func() { +// if err != nil { +// c.Log.Error(err, "multipart upload error occurred") +// } +// }() +// +// defer pipeWriter.Close() +// +// // Add UpdateParameters part +// parametersPart, err := updateParametersFormField("UpdateParameters", form) +// if err != nil { +// c.Log.Error(errMultiPartPayload, err.Error()+": UpdateParameters part copy error") +// +// return +// } +// +// if _, err = io.Copy(parametersPart, bytes.NewReader(payload.updateParameters)); err != nil { +// c.Log.Error(errMultiPartPayload, err.Error()+": UpdateParameters part copy error") +// +// return +// } +// +// // Add UpdateFile part +// updateFilePart, err := form.CreateFormFile("UpdateFile", filepath.Base(payload.updateFile.Name())) +// if err != nil { +// c.Log.Error(errMultiPartPayload, err.Error()+": UpdateFile part create error") +// +// return +// } +// +// if _, err = io.Copy(updateFilePart, payload.updateFile); err != nil { +// c.Log.Error(errMultiPartPayload, err.Error()+": UpdateFile part copy error") +// +// return +// } +// +// // add terminating boundary to multipart form +// form.Close() +// }() +// +// // pipeReader wrapped as a io.ReadSeeker to satisfy the gofish method signature +// reader := pipeReaderFakeSeeker{pipeReader} +// +// return c.redfishwrapper.RunRawRequestWithHeaders(http.MethodPost, url, reader, form.FormDataContentType(), headers) +//} +// +//// sets up the UpdateParameters MIMEHeader for the multipart form +//// the Go multipart writer CreateFormField does not currently let us set Content-Type on a MIME Header +//// https://cs.opensource.google/go/go/+/refs/tags/go1.17.8:src/mime/multipart/writer.go;l=151 +//func updateParametersFormField(fieldName string, writer *multipart.Writer) (io.Writer, error) { +// if fieldName != "UpdateParameters" { +// return nil, errors.New("") +// } +// +// h := make(textproto.MIMEHeader) +// h.Set("Content-Disposition", `form-data; name="UpdateParameters"`) +// h.Set("Content-Type", "application/json") +// +// return writer.CreatePart(h) +//} +// +//// FirmwareInstallStatus returns the status of the firmware install task queued +//func (c *Conn) FirmwareInstallStatus(ctx context.Context, installVersion, component, taskID string) (state string, err error) { +// vendor, _, err := c.redfishwrapper.DeviceVendorModel(ctx) +// if err != nil { +// return state, errors.Wrap(err, "unable to determine device vendor, model attributes") +// } +// +// // component is not used, we hack it for tests, easier than mocking +// if component == "testOpenbmc" { +// vendor = "defaultVendor" +// } +// +// var task *gofishrf.Task +// switch { +// case strings.Contains(vendor, constants.Dell): +// task, err = c.dellJobAsRedfishTask(taskID) +// default: +// task, err = c.GetTask(taskID) +// } +// +// if err != nil { +// return state, err +// } +// +// if task == nil { +// return state, errors.New("failed to lookup task status for task ID: " + taskID) +// } +// +// state = strings.ToLower(string(task.TaskState)) +// +// // so much for standards... +// switch state { +// case "starting", "downloading", "downloaded": +// return constants.FirmwareInstallInitializing, nil +// case "running", "stopping", "cancelling", "scheduling": +// return constants.FirmwareInstallRunning, nil +// case "pending", "new": +// return constants.FirmwareInstallQueued, nil +// case "scheduled": +// return constants.FirmwareInstallPowerCycleHost, nil +// case "interrupted", "killed", "exception", "cancelled", "suspended", "failed": +// return constants.FirmwareInstallFailed, nil +// case "completed": +// return constants.FirmwareInstallComplete, nil +// default: +// return constants.FirmwareInstallUnknown + ": " + state, nil +// } +// +//} // -// Content-Length: 416 -// Content-Type: multipart/form-data; boundary=-------------------- -// ----1771f60800cb2801 - -// --------------------------1771f60800cb2801 -// Content-Disposition: form-data; name="UpdateParameters" -// Content-Type: application/json - -// {"Targets": [], "@Redfish.OperationApplyTime": "OnReset", "Oem": -// {}} -// --------------------------1771f60800cb2801 -// Content-Disposition: form-data; name="UpdateFile"; filename="dum -// myfile" -// Content-Type: application/octet-stream - -// hey. -// --------------------------1771f60800cb2801-- -func (c *Conn) runRequestWithMultipartPayload(url string, payload *multipartPayload) (*http.Response, error) { - if url == "" { - return nil, fmt.Errorf("unable to execute request, no target provided") - } - - // A content-length header is passed in to indicate the payload size - // - // The Content-length is set explicitly since the payload is an io.Reader, - // https://github.com/golang/go/blob/ddad9b618cce0ed91d66f0470ddb3e12cfd7eeac/src/net/http/request.go#L861 - // - // Without the content-length header the http client will set the Transfer-Encoding to 'chunked' - // and that does not work for some BMCs (iDracs). - contentLength, _, err := multipartPayloadSize(payload) - if err != nil { - return nil, errors.Wrap(err, "error determining multipart payload size") - } - - headers := map[string]string{ - "Content-Length": strconv.FormatInt(contentLength, 10), - } - - // setup pipe - pipeReader, pipeWriter := io.Pipe() - defer pipeReader.Close() - - // initiate a mulitpart writer - form := multipart.NewWriter(pipeWriter) - - // go routine blocks on the io.Copy until the http request is made - go func() { - var err error - defer func() { - if err != nil { - c.Log.Error(err, "multipart upload error occurred") - } - }() - - defer pipeWriter.Close() - - // Add UpdateParameters part - parametersPart, err := updateParametersFormField("UpdateParameters", form) - if err != nil { - c.Log.Error(errMultiPartPayload, err.Error()+": UpdateParameters part copy error") - - return - } - - if _, err = io.Copy(parametersPart, bytes.NewReader(payload.updateParameters)); err != nil { - c.Log.Error(errMultiPartPayload, err.Error()+": UpdateParameters part copy error") - - return - } - - // Add UpdateFile part - updateFilePart, err := form.CreateFormFile("UpdateFile", filepath.Base(payload.updateFile.Name())) - if err != nil { - c.Log.Error(errMultiPartPayload, err.Error()+": UpdateFile part create error") - - return - } - - if _, err = io.Copy(updateFilePart, payload.updateFile); err != nil { - c.Log.Error(errMultiPartPayload, err.Error()+": UpdateFile part copy error") - - return - } - - // add terminating boundary to multipart form - form.Close() - }() - - // pipeReader wrapped as a io.ReadSeeker to satisfy the gofish method signature - reader := pipeReaderFakeSeeker{pipeReader} - - return c.redfishwrapper.RunRawRequestWithHeaders(http.MethodPost, url, reader, form.FormDataContentType(), headers) -} - -// sets up the UpdateParameters MIMEHeader for the multipart form -// the Go multipart writer CreateFormField does not currently let us set Content-Type on a MIME Header -// https://cs.opensource.google/go/go/+/refs/tags/go1.17.8:src/mime/multipart/writer.go;l=151 -func updateParametersFormField(fieldName string, writer *multipart.Writer) (io.Writer, error) { - if fieldName != "UpdateParameters" { - return nil, errors.New("") - } - - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", `form-data; name="UpdateParameters"`) - h.Set("Content-Type", "application/json") - - return writer.CreatePart(h) -} - -// FirmwareInstallStatus returns the status of the firmware install task queued -func (c *Conn) FirmwareInstallStatus(ctx context.Context, installVersion, component, taskID string) (state string, err error) { - vendor, _, err := c.redfishwrapper.DeviceVendorModel(ctx) - if err != nil { - return state, errors.Wrap(err, "unable to determine device vendor, model attributes") - } - - // component is not used, we hack it for tests, easier than mocking - if component == "testOpenbmc" { - vendor = "defaultVendor" - } - - var task *gofishrf.Task - switch { - case strings.Contains(vendor, constants.Dell): - task, err = c.dellJobAsRedfishTask(taskID) - default: - task, err = c.GetTask(taskID) - } - - if err != nil { - return state, err - } - - if task == nil { - return state, errors.New("failed to lookup task status for task ID: " + taskID) - } - - state = strings.ToLower(string(task.TaskState)) - - // so much for standards... - switch state { - case "starting", "downloading", "downloaded": - return constants.FirmwareInstallInitializing, nil - case "running", "stopping", "cancelling", "scheduling": - return constants.FirmwareInstallRunning, nil - case "pending", "new": - return constants.FirmwareInstallQueued, nil - case "scheduled": - return constants.FirmwareInstallPowerCycleHost, nil - case "interrupted", "killed", "exception", "cancelled", "suspended", "failed": - return constants.FirmwareInstallFailed, nil - case "completed": - return constants.FirmwareInstallComplete, nil - default: - return constants.FirmwareInstallUnknown + ": " + state, nil - } - -} diff --git a/providers/redfish/firmware_test.go b/providers/redfish/firmware_test.go index 9af54a34..5c855e4e 100644 --- a/providers/redfish/firmware_test.go +++ b/providers/redfish/firmware_test.go @@ -1,280 +1,282 @@ package redfish -import ( - "context" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/bmc-toolbox/bmclib/v2/constants" - bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" - "github.com/bmc-toolbox/common" -) - -// handler registered in mock_test.go -func multipartUpload(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { - w.WriteHeader(http.StatusNotFound) - } - - body, err := io.ReadAll(r.Body) - if err != nil { - log.Fatal(err) - } - - // payload size - expectedContentLength := "476" - - expected := []string{ - `Content-Disposition: form-data; name="UpdateParameters"`, - `Content-Type: application/json`, - `{"Targets":[],"@Redfish.OperationApplyTime":"OnReset","Oem":{}}`, - `Content-Disposition: form-data; name="UpdateFile"; filename="test.bin"`, - `Content-Type: application/octet-stream`, - `HELLOWORLD`, - } - - for _, want := range expected { - if !strings.Contains(string(body), want) { - fmt.Println(string(body)) - log.Fatal("expected value not in multipartUpload payload: " + string(want)) - } - } - - if r.Header.Get("Content-Length") != expectedContentLength { - log.Fatal("Header Content-Length does not match expected") - } - - w.Header().Add("Location", "/redfish/v1/TaskService/Tasks/JID_467696020275") - w.WriteHeader(http.StatusAccepted) -} - -func TestFirmwareInstall(t *testing.T) { - // curl -Lv -s -k -u root:calvin \ - // -F 'UpdateParameters={"Targets": [], "@Redfish.OperationApplyTime": "OnReset", "Oem": {}};type=application/json' \ - // -F'foo.bin=@/tmp/dummyfile;application/octet-stream' - // https://192.168.1.1/redfish/v1/UpdateService/MultipartUpload --trace-ascii /dev/stdout - - tmpdir := t.TempDir() - binPath := filepath.Join(tmpdir, "test.bin") - err := os.WriteFile(binPath, []byte(`HELLOWORLD`), 0600) - if err != nil { - t.Fatal(err) - } - - fh, err := os.Open(binPath) - if err != nil { - t.Fatalf("%s -> %s", err.Error(), binPath) - } - - defer os.Remove(binPath) - - tests := []struct { - component string - applyAt constants.OperationApplyTime - forceInstall bool - setRequiredTimeout bool - reader io.Reader - expectTaskID string - expectErr error - expectErrSubStr string - testName string - }{ - { - common.SlugBIOS, - constants.OnReset, - false, - false, - nil, - "", - bmclibErrs.ErrFirmwareInstall, - "method expects an *os.File object", - "expect *os.File object", - }, - { - common.SlugBIOS, - constants.OnReset, - false, - false, - &os.File{}, - "", - errInsufficientCtxTimeout, - "", - "remaining context deadline", - }, - { - common.SlugBIOS, - constants.OnReset, - false, - true, - fh, - "467696020275", - bmclibErrs.ErrFirmwareInstall, - "task for BIOS firmware install present", - "task ID exists", - }, - { - common.SlugBIOS, - constants.OnReset, - true, - true, - fh, - "467696020275", - nil, - "task for BIOS firmware install present", - "task created (previous task purged with force)", - }, - } - - for _, tc := range tests { - t.Run(tc.testName, func(t *testing.T) { - ctx, cancel := context.WithTimeout(context.TODO(), 1*time.Second) - if tc.setRequiredTimeout { - ctx, cancel = context.WithTimeout(context.TODO(), 20*time.Minute) - } - - taskID, err := mockClient.FirmwareInstall(ctx, tc.component, string(tc.applyAt), tc.forceInstall, tc.reader) - if tc.expectErr != nil { - assert.ErrorIs(t, err, tc.expectErr) - if tc.expectErrSubStr != "" { - assert.ErrorContains(t, err, tc.expectErrSubStr) - } - } else { - assert.Nil(t, err) - assert.Equal(t, tc.expectTaskID, taskID) - } - - defer cancel() - }) - } - -} - -func TestMultipartPayloadSize(t *testing.T) { - updateParameters, err := json.Marshal(struct { - Targets []string `json:"Targets"` - RedfishOpApplyTime string `json:"@Redfish.OperationApplyTime"` - Oem struct{} `json:"Oem"` - }{ - []string{}, - "foobar", - struct{}{}, - }) - - if err != nil { - t.Fatal(err) - } - - tmpdir := t.TempDir() - binPath := filepath.Join(tmpdir, "test.bin") - err = os.WriteFile(binPath, []byte(`HELLOWORLD`), 0600) - if err != nil { - t.Fatal(err) - } - - testfileFH, err := os.Open(binPath) - if err != nil { - t.Fatalf("%s -> %s", err.Error(), binPath) - } - - testCases := []struct { - testName string - payload *multipartPayload - expectedSize int64 - errorMsg string - }{ - { - "content length as expected", - &multipartPayload{ - updateParameters: updateParameters, - updateFile: testfileFH, - }, - 475, - "", - }, - } - - for _, tc := range testCases { - t.Run(tc.testName, func(t *testing.T) { - gotSize, _, err := multipartPayloadSize(tc.payload) - if tc.errorMsg != "" { - assert.Contains(t, err.Error(), tc.errorMsg) - } - - assert.Nil(t, err) - assert.Equal(t, tc.expectedSize, gotSize) - }) - } -} - -// referenced in main_test.go -func openbmcStatus(w http.ResponseWriter, r *http.Request) { - - if r.URL.Path != "/redfish/v1/TaskService/Tasks/15" { - // return an HTTP error, don't care to return correct data after - http.Error(w, "404 page not found:"+r.URL.Path, http.StatusNotFound) - } - - mytask := `{ - "@odata.id": "/redfish/v1/TaskService/Tasks/15", - "@odata.type": "#Task.v1_4_3.Task", - "Id": "15", - "Messages": [ - { - "@odata.type": "#Message.v1_1_1.Message", - "Message": "The task with Id '15' has started.", - "MessageArgs": [ - "15" - ], - "MessageId": "TaskEvent.1.0.3.TaskStarted", - "MessageSeverity": "OK", - "Resolution": "None." - } - ], - "Name": "Task 15", - "TaskState": "TestState", - "TaskStatus": "TestStatus" -} -` - _, _ = w.Write([]byte(mytask)) - -} - -func Test_FirmwareInstall2(t *testing.T) { - state, err := mockClient.FirmwareInstallStatus(context.TODO(), "", "testOpenbmc", "15") - if err != nil { - t.Fatal(err) - } - if state != "unknown: teststate" { - t.Fatal("Wrong test state:", state) - } -} - -func Test_TaskIDFromLocationURI(t *testing.T) { - var task string - var err error - - task, err = TaskIDFromLocationURI("/redfish/v1/TaskService/Tasks/JID_467696020275") - if err != nil || task != "467696020275" { - t.Fatal("Wrong task ID 467696020275. task,err=", task, err) - } - - task, err = TaskIDFromLocationURI("/redfish/v1/TaskService/Tasks/12/Monitor") - if err != nil || task != "12" { - t.Fatal("Wrong task ID 12. task,err=", task, err) - } - - task, err = TaskIDFromLocationURI("/redfish/v1/TaskService/Tasks/NO-TASK-ID") - if err == nil { - t.Fatal("Should return an error. task,err=", task, err) - } -} +// +//import ( +// "context" +// "encoding/json" +// "fmt" +// "io" +// "log" +// "net/http" +// "os" +// "path/filepath" +// "strings" +// "testing" +// "time" +// +// "github.com/stretchr/testify/assert" +// +// "github.com/bmc-toolbox/bmclib/v2/constants" +// bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" +// "github.com/bmc-toolbox/common" +//) +// +//// handler registered in mock_test.go +//func multipartUpload(w http.ResponseWriter, r *http.Request) { +// if r.Method != "POST" { +// w.WriteHeader(http.StatusNotFound) +// } +// +// body, err := io.ReadAll(r.Body) +// if err != nil { +// log.Fatal(err) +// } +// +// // payload size +// expectedContentLength := "476" +// +// expected := []string{ +// `Content-Disposition: form-data; name="UpdateParameters"`, +// `Content-Type: application/json`, +// `{"Targets":[],"@Redfish.OperationApplyTime":"OnReset","Oem":{}}`, +// `Content-Disposition: form-data; name="UpdateFile"; filename="test.bin"`, +// `Content-Type: application/octet-stream`, +// `HELLOWORLD`, +// } +// +// for _, want := range expected { +// if !strings.Contains(string(body), want) { +// fmt.Println(string(body)) +// log.Fatal("expected value not in multipartUpload payload: " + string(want)) +// } +// } +// +// if r.Header.Get("Content-Length") != expectedContentLength { +// log.Fatal("Header Content-Length does not match expected") +// } +// +// w.Header().Add("Location", "/redfish/v1/TaskService/Tasks/JID_467696020275") +// w.WriteHeader(http.StatusAccepted) +//} +// +//func TestFirmwareInstall(t *testing.T) { +// // curl -Lv -s -k -u root:calvin \ +// // -F 'UpdateParameters={"Targets": [], "@Redfish.OperationApplyTime": "OnReset", "Oem": {}};type=application/json' \ +// // -F'foo.bin=@/tmp/dummyfile;application/octet-stream' +// // https://192.168.1.1/redfish/v1/UpdateService/MultipartUpload --trace-ascii /dev/stdout +// +// tmpdir := t.TempDir() +// binPath := filepath.Join(tmpdir, "test.bin") +// err := os.WriteFile(binPath, []byte(`HELLOWORLD`), 0600) +// if err != nil { +// t.Fatal(err) +// } +// +// fh, err := os.Open(binPath) +// if err != nil { +// t.Fatalf("%s -> %s", err.Error(), binPath) +// } +// +// defer os.Remove(binPath) +// +// tests := []struct { +// component string +// applyAt constants.OperationApplyTime +// forceInstall bool +// setRequiredTimeout bool +// reader io.Reader +// expectTaskID string +// expectErr error +// expectErrSubStr string +// testName string +// }{ +// { +// common.SlugBIOS, +// constants.OnReset, +// false, +// false, +// nil, +// "", +// bmclibErrs.ErrFirmwareInstall, +// "method expects an *os.File object", +// "expect *os.File object", +// }, +// { +// common.SlugBIOS, +// constants.OnReset, +// false, +// false, +// &os.File{}, +// "", +// errInsufficientCtxTimeout, +// "", +// "remaining context deadline", +// }, +// { +// common.SlugBIOS, +// constants.OnReset, +// false, +// true, +// fh, +// "467696020275", +// bmclibErrs.ErrFirmwareInstall, +// "task for BIOS firmware install present", +// "task ID exists", +// }, +// { +// common.SlugBIOS, +// constants.OnReset, +// true, +// true, +// fh, +// "467696020275", +// nil, +// "task for BIOS firmware install present", +// "task created (previous task purged with force)", +// }, +// } +// +// for _, tc := range tests { +// t.Run(tc.testName, func(t *testing.T) { +// ctx, cancel := context.WithTimeout(context.TODO(), 1*time.Second) +// if tc.setRequiredTimeout { +// ctx, cancel = context.WithTimeout(context.TODO(), 20*time.Minute) +// } +// +// taskID, err := mockClient.FirmwareInstall(ctx, tc.component, string(tc.applyAt), tc.forceInstall, tc.reader) +// if tc.expectErr != nil { +// assert.ErrorIs(t, err, tc.expectErr) +// if tc.expectErrSubStr != "" { +// assert.ErrorContains(t, err, tc.expectErrSubStr) +// } +// } else { +// assert.Nil(t, err) +// assert.Equal(t, tc.expectTaskID, taskID) +// } +// +// defer cancel() +// }) +// } +// +//} +// +//func TestMultipartPayloadSize(t *testing.T) { +// updateParameters, err := json.Marshal(struct { +// Targets []string `json:"Targets"` +// RedfishOpApplyTime string `json:"@Redfish.OperationApplyTime"` +// Oem struct{} `json:"Oem"` +// }{ +// []string{}, +// "foobar", +// struct{}{}, +// }) +// +// if err != nil { +// t.Fatal(err) +// } +// +// tmpdir := t.TempDir() +// binPath := filepath.Join(tmpdir, "test.bin") +// err = os.WriteFile(binPath, []byte(`HELLOWORLD`), 0600) +// if err != nil { +// t.Fatal(err) +// } +// +// testfileFH, err := os.Open(binPath) +// if err != nil { +// t.Fatalf("%s -> %s", err.Error(), binPath) +// } +// +// testCases := []struct { +// testName string +// payload *multipartPayload +// expectedSize int64 +// errorMsg string +// }{ +// { +// "content length as expected", +// &multipartPayload{ +// updateParameters: updateParameters, +// updateFile: testfileFH, +// }, +// 475, +// "", +// }, +// } +// +// for _, tc := range testCases { +// t.Run(tc.testName, func(t *testing.T) { +// gotSize, _, err := multipartPayloadSize(tc.payload) +// if tc.errorMsg != "" { +// assert.Contains(t, err.Error(), tc.errorMsg) +// } +// +// assert.Nil(t, err) +// assert.Equal(t, tc.expectedSize, gotSize) +// }) +// } +//} +// +//// referenced in main_test.go +//func openbmcStatus(w http.ResponseWriter, r *http.Request) { +// +// if r.URL.Path != "/redfish/v1/TaskService/Tasks/15" { +// // return an HTTP error, don't care to return correct data after +// http.Error(w, "404 page not found:"+r.URL.Path, http.StatusNotFound) +// } +// +// mytask := `{ +// "@odata.id": "/redfish/v1/TaskService/Tasks/15", +// "@odata.type": "#Task.v1_4_3.Task", +// "Id": "15", +// "Messages": [ +// { +// "@odata.type": "#Message.v1_1_1.Message", +// "Message": "The task with Id '15' has started.", +// "MessageArgs": [ +// "15" +// ], +// "MessageId": "TaskEvent.1.0.3.TaskStarted", +// "MessageSeverity": "OK", +// "Resolution": "None." +// } +// ], +// "Name": "Task 15", +// "TaskState": "TestState", +// "TaskStatus": "TestStatus" +//} +//` +// _, _ = w.Write([]byte(mytask)) +// +//} +// +//func Test_FirmwareInstall2(t *testing.T) { +// state, err := mockClient.FirmwareInstallStatus(context.TODO(), "", "testOpenbmc", "15") +// if err != nil { +// t.Fatal(err) +// } +// if state != "unknown: teststate" { +// t.Fatal("Wrong test state:", state) +// } +//} +// +//func Test_TaskIDFromLocationURI(t *testing.T) { +// var task string +// var err error +// +// task, err = TaskIDFromLocationURI("/redfish/v1/TaskService/Tasks/JID_467696020275") +// if err != nil || task != "467696020275" { +// t.Fatal("Wrong task ID 467696020275. task,err=", task, err) +// } +// +// task, err = TaskIDFromLocationURI("/redfish/v1/TaskService/Tasks/12/Monitor") +// if err != nil || task != "12" { +// t.Fatal("Wrong task ID 12. task,err=", task, err) +// } +// +// task, err = TaskIDFromLocationURI("/redfish/v1/TaskService/Tasks/NO-TASK-ID") +// if err == nil { +// t.Fatal("Should return an error. task,err=", task, err) +// } +//} +// diff --git a/providers/redfish/main_test.go b/providers/redfish/main_test.go index 379292ba..f16ce93a 100644 --- a/providers/redfish/main_test.go +++ b/providers/redfish/main_test.go @@ -57,9 +57,9 @@ func TestMain(m *testing.M) { handler := http.NewServeMux() handler.HandleFunc("/redfish/v1/", serviceRoot) handler.HandleFunc("/redfish/v1/SessionService/Sessions", sessionService) - handler.HandleFunc("/redfish/v1/UpdateService/MultipartUpload", multipartUpload) + // handler.HandleFunc("/redfish/v1/UpdateService/MultipartUpload", multipartUpload) handler.HandleFunc("/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs?$expand=*($levels=1)", dellJobs) - handler.HandleFunc("/redfish/v1/TaskService/Tasks/", openbmcStatus) + // handler.HandleFunc("/redfish/v1/TaskService/Tasks/", openbmcStatus) return httptest.NewTLSServer(handler) }() diff --git a/providers/redfish/redfish.go b/providers/redfish/redfish.go index 0a8d2907..09e8aa64 100644 --- a/providers/redfish/redfish.go +++ b/providers/redfish/redfish.go @@ -34,8 +34,8 @@ var ( providers.FeatureBootDeviceSet, providers.FeatureVirtualMedia, providers.FeatureInventoryRead, - providers.FeatureFirmwareInstall, - providers.FeatureFirmwareInstallStatus, + //providers.FeatureFirmwareInstall, + //providers.FeatureFirmwareInstallStatus, providers.FeatureBmcReset, providers.FeatureClearSystemEventLog, } @@ -180,8 +180,6 @@ func (c *Conn) Compatible(ctx context.Context) bool { return err == nil } - - // BmcReset power cycles the BMC func (c *Conn) BmcReset(ctx context.Context, resetType string) (ok bool, err error) { return c.redfishwrapper.BMCReset(ctx, resetType)