From c57b61d004066b9cdee4736cf1db86579c716aa9 Mon Sep 17 00:00:00 2001 From: Olivier FAURAX Date: Wed, 9 Aug 2023 11:36:41 +0200 Subject: [PATCH] Current state of work --- providers/redfish/firmware.go | 94 +++++++++++++++++++++++++----- providers/redfish/firmware_test.go | 46 ++++++++++++++- providers/redfish/main_test.go | 1 + providers/redfish/tasks.go | 30 ++++++++++ providers/redfish/tasks_test.go | 28 +++++++++ 5 files changed, 184 insertions(+), 15 deletions(-) diff --git a/providers/redfish/firmware.go b/providers/redfish/firmware.go index 94112521..92b6528f 100644 --- a/providers/redfish/firmware.go +++ b/providers/redfish/firmware.go @@ -106,7 +106,23 @@ func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt string, f "UpdateFile": reader, } - resp, err := c.runRequestWithMultipartPayload(http.MethodPost, "/redfish/v1/UpdateService/MultipartUpload", payload) + updateService, err := c.redfishwrapper.UpdateService() + + if err != nil { + return "", errors.Wrap(bmclibErrs.ErrFirmwareUpload, err.Error()) + } + + var resp *http.Response + if updateService.MultipartHTTPPushURI != "" { + // TODO: should use updateService.MultipartHTTPPushURI rather than hardcoded path + // but should be tested when modified + resp, err = c.runRequestWithMultipartPayload(http.MethodPost, "/redfish/v1/UpdateService/MultipartUpload", payload) + } else if updateService.HTTPPushURI != "" { + resp, err = c.runRequestWithPayload(http.MethodPost, updateService.HTTPPushURI, payload["UpdateFile"]) + } else { + return "", errors.Wrap(bmclibErrs.ErrFirmwareUpload, "No URI available for push updates") + } + if err != nil { return "", errors.Wrap(bmclibErrs.ErrFirmwareUpload, err.Error()) } @@ -118,10 +134,21 @@ 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] + return location2TaskID(resp.Header.Get("Location")) +} + +func location2TaskID(location string) (taskID string, err error) { + if strings.Contains(location, "JID_") { + // The response contains a location header pointing to the task URI + // Location: /redfish/v1/TaskService/Tasks/JID_467696020275 + taskID = strings.Split(location, "JID_")[1] + } else if strings.Contains(location, "/Monitor") { + // OpenBMC returns a monitor URL in Location + // Location: /redfish/v1/TaskService/Tasks/12/Monitor + splits := strings.Split(location, "/") + taskID = splits[5] + } else { + return "", bmclibErrs.ErrTaskNotFound } return taskID, nil @@ -134,10 +161,41 @@ func (c *Conn) FirmwareInstallStatus(ctx context.Context, installVersion, compon return state, errors.Wrap(err, "unable to determine device vendor, model attributes") } + // component is not used, we hack it for tests + if component == "testOpenbmc" { + vendor = constants.Packet + } + var task *gofishrf.Task switch { case strings.Contains(vendor, constants.Dell): task, err = c.dellJobAsRedfishTask(taskID) + if task == nil { + return state, errors.New("failed to lookup task status for task ID: " + taskID) + } + + state = strings.ToLower(string(task.TaskState)) + + case strings.Contains(vendor, constants.Packet): + resp, _ := c.redfishwrapper.Get("/redfish/v1/TaskService/Tasks/" + taskID) + if resp.StatusCode != 200 { + err = errors.Wrap( + bmclibErrs.ErrFirmwareInstall, + "HTTP Error: "+fmt.Sprint(resp.StatusCode), + ) + + state = "failed" + break + } + + //task, err := gofishrf.GetTask(c.redfishwrapper, "/redfish/v1/TaskService/Tasks/" + taskID) + //fmt.Printf("task:", task); + + data, _ := io.ReadAll(resp.Body) + resp.Body.Close() + + state, err = c.openbmcGetStatus(data) + default: err = errors.Wrap( bmclibErrs.ErrNotImplemented, @@ -149,12 +207,6 @@ func (c *Conn) FirmwareInstallStatus(ctx context.Context, installVersion, compon 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": @@ -189,9 +241,10 @@ func (c *Conn) firmwareUpdateCompatible(ctx context.Context) (err error) { 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") + // for now we expect multipart HTTP push update support, + // or at least the unstructured HTTP push update support + if updateService.MultipartHTTPPushURI == "" && updateService.HTTPPushURI == "" { + return errors.Wrap(bmclibErrs.ErrRedfishUpdateService, "No HTTP push updates supported (multipart or unstructured)") } return nil @@ -248,6 +301,17 @@ func (c *Conn) runRequestWithMultipartPayload(method, url string, payload map[st return c.redfishwrapper.RunRawRequestWithHeaders(method, url, bytes.NewReader(payloadBuffer.Bytes()), payloadWriter.FormDataContentType(), nil) } +// Updates using an unstrctured HTTP updates +func (c *Conn) runRequestWithPayload(method, url string, payload io.Reader) (*http.Response, error) { + if url == "" { + return nil, fmt.Errorf("unable to execute request, no target provided") + } + + b, _ := io.ReadAll(payload) + payloadReadSeeker := bytes.NewReader(b) + return c.redfishwrapper.RunRawRequestWithHeaders(method, url, payloadReadSeeker, "application/octet-stream", nil) +} + // 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 @@ -276,6 +340,8 @@ func (c *Conn) GetFirmwareInstallTaskQueued(ctx context.Context, component strin switch { case strings.Contains(vendor, constants.Dell): task, err = c.getDellFirmwareInstallTaskScheduled(component) + case strings.Contains(vendor, constants.Packet): + //task, err = c.getDellFirmwareInstallTaskScheduled(component) default: err = errors.Wrap( bmclibErrs.ErrNotImplemented, diff --git a/providers/redfish/firmware_test.go b/providers/redfish/firmware_test.go index 8e098e4d..ea8bdcde 100644 --- a/providers/redfish/firmware_test.go +++ b/providers/redfish/firmware_test.go @@ -19,7 +19,7 @@ import ( "github.com/bmc-toolbox/common" ) -// handler registered in mock_test.go +// handler registered in main_test.go func multipartUpload(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { w.WriteHeader(http.StatusNotFound) @@ -157,3 +157,47 @@ func Test_firmwareUpdateCompatible(t *testing.T) { t.Fatal(err) } } + +func Test_runRequestWithPayload(t *testing.T) { + var reader io.Reader + resp, err := mockClient.runRequestWithPayload(http.MethodPost, "", reader) + if resp != nil { + t.Fatal(err) + } +} + +// referenced in main_test.go +func openbmcStatus(w http.ResponseWriter, r *http.Request) { + 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) + } +} diff --git a/providers/redfish/main_test.go b/providers/redfish/main_test.go index 924dfe8a..d68dc399 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/15", openbmcStatus) return httptest.NewTLSServer(handler) }() diff --git a/providers/redfish/tasks.go b/providers/redfish/tasks.go index bc6aaeb1..668de8f1 100644 --- a/providers/redfish/tasks.go +++ b/providers/redfish/tasks.go @@ -2,6 +2,7 @@ package redfish import ( "encoding/json" + "fmt" "io" "strconv" "strings" @@ -171,3 +172,32 @@ func (c *Conn) dellJobs(state string) ([]*gofishrf.Task, error) { return tasks, nil } + +func (c *Conn) openbmcGetStatus(jsonstr []byte) (state string, err error) { + type TaskMsg struct { + Message string + } + + type TaskStatus struct { + TaskState string + TaskStatus string + Messages []TaskMsg + } + + var status TaskStatus + + err = json.Unmarshal(jsonstr, &status) + if err != nil { + fmt.Println(err) + } else { + state = strings.ToLower(status.TaskState) + if state != "running" { + // Display all messages when not running (failed or completed) + fmt.Println(status.TaskState, status.TaskStatus) + for _, m := range status.Messages { + fmt.Println(m.Message) + } + } + } + return state, err +} diff --git a/providers/redfish/tasks_test.go b/providers/redfish/tasks_test.go index bf1a376b..830fbaca 100644 --- a/providers/redfish/tasks_test.go +++ b/providers/redfish/tasks_test.go @@ -38,3 +38,31 @@ func Test_dellPurgeScheduledFirmwareInstallJob(t *testing.T) { t.Fatal(err) } } + +func Test_openbmcGetStatus(t *testing.T) { + var err error + var state string + + // empty (invalid json) + _, err = mockClient.openbmcGetStatus([]byte("")) + if err == nil { + t.Fatal("no error with empty invalid json") + } + + // empty valid json + _, err = mockClient.openbmcGetStatus([]byte("{}")) + if err != nil { + t.Fatal(err) + } + + // empty valid json + state, err = mockClient.openbmcGetStatus([]byte( + "{\"Id\":\"15\", \"TaskState\": \"TestState\", \"TaskStatus\": \"TestStatus\"}", + )) + if err != nil { + t.Fatal(err) + } + if state != "teststate" { + t.Fatal("Wrong test state:", state) + } +}