diff --git a/go.mod b/go.mod index d73adff9..4ef38640 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( go.uber.org/goleak v1.2.1 golang.org/x/crypto v0.1.0 golang.org/x/exp v0.0.0-20230127130021-4ca2cb1a16b7 - golang.org/x/net v0.1.0 + golang.org/x/net v0.7.0 gopkg.in/go-playground/assert.v1 v1.2.1 ) @@ -35,7 +35,7 @@ require ( github.com/mattn/go-isatty v0.0.14 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/satori/go.uuid v1.2.0 // indirect - golang.org/x/sys v0.1.0 // indirect + golang.org/x/sys v0.5.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index cdb371e3..c73a5614 100644 --- a/go.sum +++ b/go.sum @@ -72,15 +72,15 @@ golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/exp v0.0.0-20230127130021-4ca2cb1a16b7 h1:o7Ps2IYdzLRolS9/nadqeMSHpa9k8pu8u+VKBFUG7cQ= golang.org/x/exp v0.0.0-20230127130021-4ca2cb1a16b7/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210608053332-aa57babbf139/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/redfishwrapper/client.go b/internal/redfishwrapper/client.go index f600631c..6d9c01ee 100644 --- a/internal/redfishwrapper/client.go +++ b/internal/redfishwrapper/client.go @@ -13,6 +13,7 @@ import ( "github.com/bmc-toolbox/bmclib/v2/internal/httpclient" "github.com/pkg/errors" "github.com/stmcginnis/gofish" + "github.com/stmcginnis/gofish/redfish" "golang.org/x/exp/slices" ) @@ -208,3 +209,7 @@ func (c *Client) PostWithHeaders(ctx context.Context, url string, payload interf func (c *Client) PatchWithHeaders(ctx context.Context, url string, payload interface{}, headers map[string]string) (*http.Response, error) { return c.client.PatchWithHeaders(url, payload, headers) } + +func (c *Client) Tasks(ctx context.Context) ([]*redfish.Task, error) { + return c.client.Service.Tasks() +} diff --git a/providers/redfish/firmware.go b/providers/redfish/firmware.go index ca6231f4..9db2f436 100644 --- a/providers/redfish/firmware.go +++ b/providers/redfish/firmware.go @@ -28,6 +28,13 @@ var ( errMultiPartPayload = errors.New("error preparing multipart payload") ) +type installMethod string + +const ( + unstructuredHttpPush installMethod = "unstructuredHttpPush" + multipartHttpUpload installMethod = "multipartUpload" +) + // SupportedFirmwareApplyAtValues returns the supported redfish firmware applyAt values func SupportedFirmwareApplyAtValues() []string { return []string{ @@ -44,8 +51,7 @@ func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt string, f return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, "method expects an *os.File object") } - // validate firmware update mechanism is supported - err = c.firmwareUpdateCompatible(ctx) + installMethod, installURI, err := c.firmwareInstallMethodURI(ctx) if err != nil { return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, err.Error()) } @@ -83,24 +89,10 @@ func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt string, f } } - updateParameters, err := json.Marshal(struct { - Targets []string `json:"Targets"` - RedfishOpApplyTime string `json:"@Redfish.OperationApplyTime"` - Oem struct{} `json:"Oem"` - }{ - []string{}, - applyAt, - struct{}{}, - }) - - if err != nil { - return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, err.Error()) - } - // 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 + // record the http client timeout to be restored when this method returns httpClientTimeout := c.redfishwrapper.HttpClientTimeout() defer func() { c.redfishwrapper.SetHttpClientTimeout(httpClientTimeout) @@ -108,14 +100,25 @@ func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt string, f c.redfishwrapper.SetHttpClientTimeout(time.Until(ctxDeadline)) - payload := &multipartPayload{ - updateParameters: updateParameters, - updateFile: updateFile, - } + var resp *http.Response - resp, err := c.runRequestWithMultipartPayload(http.MethodPost, "/redfish/v1/UpdateService/MultipartUpload", payload) - if err != nil { - return "", errors.Wrap(bmclibErrs.ErrFirmwareUpload, err.Error()) + switch installMethod { + case multipartHttpUpload: + var uploadErr error + resp, uploadErr = c.multipartHTTPUpload(ctx, installURI, applyAt, updateFile) + if uploadErr != nil { + return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, uploadErr.Error()) + } + + case unstructuredHttpPush: + var uploadErr error + resp, uploadErr = c.unstructuredHttpUpload(ctx, installURI, applyAt, 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 { @@ -127,8 +130,30 @@ func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt string, f // The response contains a location header pointing to the task URI // Location: /redfish/v1/TaskService/Tasks/JID_467696020275 - if strings.Contains(resp.Header.Get("Location"), "JID_") { - taskID = strings.Split(resp.Header.Get("Location"), "JID_")[1] + 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 @@ -139,74 +164,67 @@ type multipartPayload struct { updateFile *os.File } -// 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.DeviceVendorModel(ctx) - if err != nil { - return state, errors.Wrap(err, "unable to determine device vendor, model attributes") +func (c *Conn) multipartHTTPUpload(ctx context.Context, url, applyAt string, update *os.File) (*http.Response, error) { + if url == "" { + return nil, fmt.Errorf("unable to execute request, no target provided") } - var task *gofishrf.Task - switch { - case strings.Contains(vendor, constants.Dell): - task, err = c.dellJobAsRedfishTask(taskID) - default: - err = errors.Wrap( - bmclibErrs.ErrNotImplemented, - "FirmwareInstallStatus() for vendor: "+vendor, - ) - } + parameters, err := json.Marshal(struct { + Targets []string `json:"Targets"` + RedfishOpApplyTime string `json:"@Redfish.OperationApplyTime"` + Oem struct{} `json:"Oem"` + }{ + []string{}, + applyAt, + struct{}{}, + }) if err != nil { - return state, err + return nil, errors.Wrap(err, "error preparing multipart UpdateParameters payload") } - if task == nil { - return state, errors.New("failed to lookup task status for task ID: " + taskID) + // payload ordered in the format it ends up in the multipart form + payload := &multipartPayload{ + updateParameters: []byte(parameters), + updateFile: update, } - state = strings.ToLower(string(task.TaskState)) + return c.runRequestWithMultipartPayload(url, payload) +} - // 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.FirmwareInstallPowerCyleHost, nil - case "interrupted", "killed", "exception", "cancelled", "suspended", "failed": - return constants.FirmwareInstallFailed, nil - case "completed": - return constants.FirmwareInstallComplete, nil - default: - return constants.FirmwareInstallUnknown + ": " + state, nil +func (c *Conn) unstructuredHttpUpload(ctx context.Context, url, applyAt 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) + } -// firmwareUpdateCompatible retuns an error if the firmware update process for the BMC is not supported -func (c *Conn) firmwareUpdateCompatible(ctx context.Context) (err error) { +// 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 err + return "", "", errors.Wrap(bmclibErrs.ErrRedfishUpdateService, err.Error()) } - // TODO: check for redfish version - // update service disabled if !updateService.ServiceEnabled { - return errors.Wrap(bmclibErrs.ErrRedfishUpdateService, "service disabled") + return "", "", errors.Wrap(bmclibErrs.ErrRedfishUpdateService, "service disabled") } - // for now we expect multipart HTTP push update support - if updateService.MultipartHTTPPushURI == "" { - return errors.Wrap(bmclibErrs.ErrRedfishUpdateService, "Multipart HTTP push updates not supported") + switch { + case updateService.MultipartHTTPPushURI != "": + return multipartHttpUpload, updateService.MultipartHTTPPushURI, nil + case updateService.HTTPPushURI != "": + return unstructuredHttpPush, updateService.HTTPPushURI, nil } - return nil + return "", "", errors.Wrap(bmclibErrs.ErrRedfishUpdateService, "unsupported update method") } // pipeReaderFakeSeeker wraps the io.PipeReader and implements the io.Seeker interface @@ -283,7 +301,7 @@ func multipartPayloadSize(payload *multipartPayload) (int64, *bytes.Buffer, erro // hey. // --------------------------1771f60800cb2801-- -func (c *Conn) runRequestWithMultipartPayload(method, url string, payload *multipartPayload) (*http.Response, error) { +func (c *Conn) runRequestWithMultipartPayload(url string, payload *multipartPayload) (*http.Response, error) { if url == "" { return nil, fmt.Errorf("unable to execute request, no target provided") } @@ -357,7 +375,7 @@ func (c *Conn) runRequestWithMultipartPayload(method, url string, payload *multi // pipeReader wrapped as a io.ReadSeeker to satisfy the gofish method signature reader := pipeReaderFakeSeeker{pipeReader} - return c.redfishwrapper.RunRawRequestWithHeaders(method, url, reader, form.FormDataContentType(), headers) + return c.redfishwrapper.RunRawRequestWithHeaders(http.MethodPost, url, reader, form.FormDataContentType(), headers) } // sets up the UpdateParameters MIMEHeader for the multipart form @@ -375,50 +393,52 @@ func updateParametersFormField(fieldName string, writer *multipart.Writer) (io.W return writer.CreatePart(h) } -// GetFirmwareInstallTaskQueued returns the redfish task object for a queued update task -func (c *Conn) GetFirmwareInstallTaskQueued(ctx context.Context, component string) (*gofishrf.Task, error) { +// 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.DeviceVendorModel(ctx) if err != nil { - return nil, errors.Wrap(err, "unable to determine device vendor, model attributes") + return state, errors.Wrap(err, "unable to determine device vendor, model attributes") } - var task *gofishrf.Task + // component is not used, we hack it for tests, easier than mocking + if component == "testOpenbmc" { + vendor = "defaultVendor" + } - // check an update task for the component is currently scheduled + var task *gofishrf.Task switch { case strings.Contains(vendor, constants.Dell): - task, err = c.getDellFirmwareInstallTaskScheduled(component) + task, err = c.dellJobAsRedfishTask(taskID) default: - err = errors.Wrap( - bmclibErrs.ErrNotImplemented, - "GetFirmwareInstallTask() for vendor: "+vendor, - ) + task, err = c.GetTask(taskID) } if err != nil { - return nil, err + return state, err } - return task, nil -} - -// purgeQueuedFirmwareInstallTask removes any existing queued firmware install task for the given component slug -func (c *Conn) purgeQueuedFirmwareInstallTask(ctx context.Context, component string) error { - vendor, _, err := c.DeviceVendorModel(ctx) - if err != nil { - return errors.Wrap(err, "unable to determine device vendor, model attributes") + if task == nil { + return state, errors.New("failed to lookup task status for task ID: " + taskID) } - // check an update task for the component is currently scheduled - switch { - case strings.Contains(vendor, constants.Dell): - err = c.dellPurgeScheduledFirmwareInstallJob(component) + 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.FirmwareInstallPowerCyleHost, nil + case "interrupted", "killed", "exception", "cancelled", "suspended", "failed": + return constants.FirmwareInstallFailed, nil + case "completed": + return constants.FirmwareInstallComplete, nil default: - err = errors.Wrap( - bmclibErrs.ErrNotImplemented, - "purgeFirmwareInstallTask() for vendor: "+vendor, - ) + return constants.FirmwareInstallUnknown + ": " + state, nil } - return err } diff --git a/providers/redfish/firmware_test.go b/providers/redfish/firmware_test.go index b0fbedec..16b224cc 100644 --- a/providers/redfish/firmware_test.go +++ b/providers/redfish/firmware_test.go @@ -227,9 +227,65 @@ func TestMultipartPayloadSize(t *testing.T) { } } -func TestFirmwareUpdateCompatible(t *testing.T) { - err := mockClient.firmwareUpdateCompatible(context.TODO()) +// 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 924dfe8a..379292ba 100644 --- a/providers/redfish/main_test.go +++ b/providers/redfish/main_test.go @@ -59,6 +59,7 @@ func TestMain(m *testing.M) { handler.HandleFunc("/redfish/v1/SessionService/Sessions", sessionService) 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) return httptest.NewTLSServer(handler) }() diff --git a/providers/redfish/tasks.go b/providers/redfish/tasks.go index bc6aaeb1..49bd1713 100644 --- a/providers/redfish/tasks.go +++ b/providers/redfish/tasks.go @@ -1,173 +1,161 @@ package redfish import ( + "context" "encoding/json" + "fmt" "io" - "strconv" + "regexp" "strings" - bmcliberrs "github.com/bmc-toolbox/bmclib/v2/errors" - "github.com/bmc-toolbox/common" + "github.com/bmc-toolbox/bmclib/v2/constants" + bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/pkg/errors" - gofishcommon "github.com/stmcginnis/gofish/common" gofishrf "github.com/stmcginnis/gofish/redfish" ) -// Dell specific redfish methods +func (c *Conn) activeTask(ctx context.Context) (*gofishrf.Task, error) { + resp, err := c.redfishwrapper.Get("/redfish/v1/TaskService/Tasks") + if err != nil { + fmt.Println("err1", err) + return nil, err + } + if resp.StatusCode != 200 { + err = errors.Wrap( + bmclibErrs.ErrFirmwareInstallStatus, + "HTTP Error: "+fmt.Sprint(resp.StatusCode), + ) -var ( - componentSlugDellJobName = 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", + return nil, err } -) -type dellJob struct { - PercentComplete int - OdataID string `json:"@odata.id"` - StartTime string - CompletionTime string - ID string - Message string - Name string - JobState string - JobType string -} + data, _ := io.ReadAll(resp.Body) + resp.Body.Close() -type dellJobResponseData struct { - Members []*dellJob -} + type TaskId struct { + OdataID string `json:"@odata.id"` + TaskState string + TaskStatus string + } -// dellJobID formats and returns taskID as a Dell Job ID -func dellJobID(id string) string { - if !strings.HasPrefix(id, "JID") { - return "JID_" + id + type Tasks struct { + Members []TaskId } - return id -} + var status Tasks -func (c *Conn) getDellFirmwareInstallTaskScheduled(slug string) (*gofishrf.Task, error) { - // get tasks by state - tasks, err := c.dellJobs("scheduled") + err = json.Unmarshal(data, &status) if err != nil { - return nil, err + fmt.Println(err) } - // filter to match the task Name based on the component slug - for _, task := range tasks { - if task.Name == componentSlugDellJobName[strings.ToUpper(slug)] { - return task, nil + // For each task, check if it's running + // It's usually the latest that is running, so it would be faster to + // start by the end, but an easy way to do this is only available in go 1.21 + // for _, t := range slices.Reverse(status.Members) { // when go 1.21 + for _, t := range status.Members { + re := regexp.MustCompile("/redfish/v1/TaskService/Tasks/([0-9]+)") + taskmatch := re.FindSubmatch([]byte(t.OdataID)) + if len(taskmatch) < 1 { + continue } - } - return nil, nil -} + tasknum := string(taskmatch[1]) -func (c *Conn) dellPurgeScheduledFirmwareInstallJob(slug string) error { - // get tasks by state - tasks, err := c.dellJobs("scheduled") - if err != nil { - return err - } + task, err := c.GetTask(tasknum) + if err != nil { + continue + } - // filter to match the task Name based on the component slug - for _, task := range tasks { - if task.Name == componentSlugDellJobName[strings.ToUpper(slug)] { - err = c.dellPurgeJob(task.ID) - if err != nil { - return err - } + if task.TaskState == "Running" { + return task, nil } } - return nil + return nil, nil } -func (c *Conn) dellPurgeJob(id string) error { - id = dellJobID(id) +// GetFirmwareInstallTaskQueued returns the redfish task object for a queued update task +func (c *Conn) GetFirmwareInstallTaskQueued(ctx context.Context, component string) (*gofishrf.Task, error) { + vendor, _, err := c.DeviceVendorModel(ctx) + if err != nil { + return nil, errors.Wrap(err, "unable to determine device vendor, model attributes") + } - endpoint := "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/" + id + var task *gofishrf.Task - resp, err := c.redfishwrapper.Delete(endpoint) - if err != nil { - return errors.Wrap(bmcliberrs.ErrTaskPurge, err.Error()) + // check an update task for the component is currently scheduled + switch { + case strings.Contains(vendor, constants.Dell): + task, err = c.getDellFirmwareInstallTaskScheduled(component) + default: + task, err = c.activeTask(ctx) } - if resp.StatusCode != 200 { - return errors.Wrap(bmcliberrs.ErrTaskPurge, "response code: "+resp.Status) + if err != nil { + return nil, err } - return nil + return task, nil } -// dellFirmwareUpdateTaskStatus looks up the Dell Job and returns it as a redfish task object -func (c *Conn) dellJobAsRedfishTask(jobID string) (*gofishrf.Task, error) { - jobID = dellJobID(jobID) - - tasks, err := c.dellJobs("") +// purgeQueuedFirmwareInstallTask removes any existing queued firmware install task for the given component slug +func (c *Conn) purgeQueuedFirmwareInstallTask(ctx context.Context, component string) error { + vendor, _, err := c.DeviceVendorModel(ctx) if err != nil { - return nil, err + return errors.Wrap(err, "unable to determine device vendor, model attributes") } - for _, task := range tasks { - if task.ID == jobID { - return task, nil - } + // check an update task for the component is currently scheduled + switch { + case strings.Contains(vendor, constants.Dell): + err = c.dellPurgeScheduledFirmwareInstallJob(component) + default: + err = errors.Wrap( + bmclibErrs.ErrFirmwareInstall, + "Update is already running", + ) } - return nil, errors.Wrap(bmcliberrs.ErrTaskNotFound, "task with ID not found: "+jobID) + return err } -// dellJobs returns all dell jobs as redfish task objects -// state: optional -func (c *Conn) dellJobs(state string) ([]*gofishrf.Task, error) { - endpoint := "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs?$expand=*($levels=1)" +// GetTask returns the current Task fir the given TaskID +func (c *Conn) GetTask(taskID string) (task *gofishrf.Task, err error) { - resp, err := c.redfishwrapper.Get(endpoint) + resp, err := c.redfishwrapper.Get("/redfish/v1/TaskService/Tasks/" + taskID) if err != nil { return nil, err } - if resp.StatusCode != 200 { - return nil, errors.New("dell jobs endpoint returned unexpected status code: " + strconv.Itoa(resp.StatusCode)) - } + err = errors.Wrap( + bmclibErrs.ErrFirmwareInstallStatus, + "HTTP Error: "+fmt.Sprint(resp.StatusCode), + ) - body, err := io.ReadAll(resp.Body) - if err != nil { return nil, err } - data := dellJobResponseData{} - err = json.Unmarshal(body, &data) - if err != nil { - return nil, err + data, _ := io.ReadAll(resp.Body) + resp.Body.Close() + + type TaskStatus struct { + TaskState string + TaskStatus string } - tasks := []*gofishrf.Task{} - for _, job := range data.Members { - if state != "" && !strings.EqualFold(job.JobState, state) { - continue - } + var status TaskStatus - tasks = append(tasks, &gofishrf.Task{ - Entity: gofishcommon.Entity{ - ID: job.ID, - ODataID: job.OdataID, - Name: job.Name, - }, - Description: job.Name, - PercentComplete: job.PercentComplete, - StartTime: job.StartTime, - EndTime: job.CompletionTime, - TaskState: gofishrf.TaskState(job.JobState), - TaskStatus: gofishcommon.Health(job.Message), // abuse the TaskStatus to include any status message - }) + err = json.Unmarshal(data, &status) + if err != nil { + fmt.Println(err) + } else { + task = &gofishrf.Task{ + TaskState: gofishrf.TaskState(status.TaskState), + TaskStatus: gofishcommon.Health(status.TaskStatus), + } } - return tasks, nil + return task, err } diff --git a/providers/redfish/tasks_dell.go b/providers/redfish/tasks_dell.go new file mode 100644 index 00000000..0ecd92de --- /dev/null +++ b/providers/redfish/tasks_dell.go @@ -0,0 +1,175 @@ +package redfish + +import ( + "encoding/json" + "io" + "strconv" + "strings" + + bmcliberrs "github.com/bmc-toolbox/bmclib/v2/errors" + "github.com/bmc-toolbox/common" + "github.com/pkg/errors" + + gofishcommon "github.com/stmcginnis/gofish/common" + gofishrf "github.com/stmcginnis/gofish/redfish" +) + +// TODO: figure how this can be moved into the dell provider +// +// Dell specific redfish methods + +var ( + componentSlugDellJobName = 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", + } +) + +type dellJob struct { + PercentComplete int + OdataID string `json:"@odata.id"` + StartTime string + CompletionTime string + ID string + Message string + Name string + JobState string + JobType string +} + +type dellJobResponseData struct { + Members []*dellJob +} + +// dellJobID formats and returns taskID as a Dell Job ID +func dellJobID(id string) string { + if !strings.HasPrefix(id, "JID") { + return "JID_" + id + } + + return id +} + +func (c *Conn) getDellFirmwareInstallTaskScheduled(slug string) (*gofishrf.Task, error) { + // get tasks by state + tasks, err := c.dellJobs("scheduled") + if err != nil { + return nil, err + } + + // filter to match the task Name based on the component slug + for _, task := range tasks { + if task.Name == componentSlugDellJobName[strings.ToUpper(slug)] { + return task, nil + } + } + + return nil, nil +} + +func (c *Conn) dellPurgeScheduledFirmwareInstallJob(slug string) error { + // get tasks by state + tasks, err := c.dellJobs("scheduled") + if err != nil { + return err + } + + // filter to match the task Name based on the component slug + for _, task := range tasks { + if task.Name == componentSlugDellJobName[strings.ToUpper(slug)] { + err = c.dellPurgeJob(task.ID) + if err != nil { + return err + } + } + } + + return nil +} + +func (c *Conn) dellPurgeJob(id string) error { + id = dellJobID(id) + + endpoint := "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/" + id + + resp, err := c.redfishwrapper.Delete(endpoint) + if err != nil { + return errors.Wrap(bmcliberrs.ErrTaskPurge, err.Error()) + } + + if resp.StatusCode != 200 { + return errors.Wrap(bmcliberrs.ErrTaskPurge, "response code: "+resp.Status) + } + + return nil +} + +// dellFirmwareUpdateTaskStatus looks up the Dell Job and returns it as a redfish task object +func (c *Conn) dellJobAsRedfishTask(jobID string) (*gofishrf.Task, error) { + jobID = dellJobID(jobID) + + tasks, err := c.dellJobs("") + if err != nil { + return nil, err + } + + for _, task := range tasks { + if task.ID == jobID { + return task, nil + } + } + + return nil, errors.Wrap(bmcliberrs.ErrTaskNotFound, "task with ID not found: "+jobID) +} + +// dellJobs returns all dell jobs as redfish task objects +// state: optional +func (c *Conn) dellJobs(state string) ([]*gofishrf.Task, error) { + endpoint := "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs?$expand=*($levels=1)" + + resp, err := c.redfishwrapper.Get(endpoint) + if err != nil { + return nil, err + } + + if resp.StatusCode != 200 { + return nil, errors.New("dell jobs endpoint returned unexpected status code: " + strconv.Itoa(resp.StatusCode)) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + data := dellJobResponseData{} + err = json.Unmarshal(body, &data) + if err != nil { + return nil, err + } + + tasks := []*gofishrf.Task{} + for _, job := range data.Members { + if state != "" && !strings.EqualFold(job.JobState, state) { + continue + } + + tasks = append(tasks, &gofishrf.Task{ + Entity: gofishcommon.Entity{ + ID: job.ID, + ODataID: job.OdataID, + Name: job.Name, + }, + Description: job.Name, + PercentComplete: job.PercentComplete, + StartTime: job.StartTime, + EndTime: job.CompletionTime, + TaskState: gofishrf.TaskState(job.JobState), + TaskStatus: gofishcommon.Health(job.Message), // abuse the TaskStatus to include any status message + }) + } + + return tasks, nil +} diff --git a/providers/redfish/tasks_dell_test.go b/providers/redfish/tasks_dell_test.go new file mode 100644 index 00000000..bf1a376b --- /dev/null +++ b/providers/redfish/tasks_dell_test.go @@ -0,0 +1,40 @@ +package redfish + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +// handler registered in redfish_test.go +func dellJobs(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + w.WriteHeader(http.StatusNotFound) + } + + _, _ = w.Write(jsonResponse(r.RequestURI)) +} + +func Test_dellFirmwareUpdateTask(t *testing.T) { + // see fixtures/v1/dell/jobs.json for the job IDs + // completed job + status, err := mockClient.dellJobAsRedfishTask("467767920358") + if err != nil { + t.Fatal(err) + } + + assert.NotNil(t, status) + assert.Equal(t, "2022-03-08T16:02:33", status.EndTime) + assert.Equal(t, "2022-03-08T15:59:52", status.StartTime) + assert.Equal(t, 100, status.PercentComplete) + assert.Equal(t, "Completed", string(status.TaskState)) + assert.Equal(t, "Job completed successfully.", string(status.TaskStatus)) +} + +func Test_dellPurgeScheduledFirmwareInstallJob(t *testing.T) { + err := mockClient.dellPurgeScheduledFirmwareInstallJob("bios") + if err != nil { + t.Fatal(err) + } +} diff --git a/providers/redfish/tasks_test.go b/providers/redfish/tasks_test.go index bf1a376b..f763b1bd 100644 --- a/providers/redfish/tasks_test.go +++ b/providers/redfish/tasks_test.go @@ -1,40 +1,27 @@ package redfish import ( - "net/http" "testing" - - "github.com/stretchr/testify/assert" ) -// handler registered in redfish_test.go -func dellJobs(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { - w.WriteHeader(http.StatusNotFound) - } - - _, _ = w.Write(jsonResponse(r.RequestURI)) -} +func Test_GetTask(t *testing.T) { + var err error -func Test_dellFirmwareUpdateTask(t *testing.T) { - // see fixtures/v1/dell/jobs.json for the job IDs - // completed job - status, err := mockClient.dellJobAsRedfishTask("467767920358") + task, err := mockClient.GetTask("15") if err != nil { t.Fatal(err) } + if task.TaskState != "TestState" { + t.Fatal("Wrong test state:", task.TaskState) + } - assert.NotNil(t, status) - assert.Equal(t, "2022-03-08T16:02:33", status.EndTime) - assert.Equal(t, "2022-03-08T15:59:52", status.StartTime) - assert.Equal(t, 100, status.PercentComplete) - assert.Equal(t, "Completed", string(status.TaskState)) - assert.Equal(t, "Job completed successfully.", string(status.TaskStatus)) -} - -func Test_dellPurgeScheduledFirmwareInstallJob(t *testing.T) { - err := mockClient.dellPurgeScheduledFirmwareInstallJob("bios") - if err != nil { - t.Fatal(err) + // inexistent + task, err = mockClient.GetTask("151515") + if task != nil { + t.Fatal("Task should be nil, but got:", task) } + if err == nil { + t.Fatal("err shouldn't be nil:", err) + } + }