From 7b2304e97c79f5373ada03e2581b8d969fcea759 Mon Sep 17 00:00:00 2001 From: Olivier FAURAX Date: Wed, 9 Aug 2023 14:25:53 +0200 Subject: [PATCH 1/3] Update x/net to v0.7.0 for security --- go.mod | 4 ++-- go.sum | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index ff0b55c8..e651873c 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,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 ) @@ -28,6 +28,6 @@ require ( github.com/hashicorp/errwrap v1.1.0 // 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.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ba4b45b7..7ec79c19 100644 --- a/go.sum +++ b/go.sum @@ -55,13 +55,13 @@ 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.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= From 7448c7170fc6085f74bb1af3485f8a2133f3ffae Mon Sep 17 00:00:00 2001 From: Olivier FAURAX Date: Wed, 9 Aug 2023 11:12:44 +0200 Subject: [PATCH 2/3] Add Equinix/Packet constants --- constants/constants.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/constants/constants.go b/constants/constants.go index b4e7ce22..70eca32f 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -22,8 +22,11 @@ const ( Cloudline = "Cloudline" // Quanta is the contant to identify Quanta hardware Quanta = "Quanta" - // Quanta is the contant to identify Intel hardware + // Intel is the contant to identify Intel hardware Intel = "Intel" + // Equinix, formerly Packet + Equinix = "Equinix" + Packet = "Packet" // Redfish firmware apply at constants // FirmwareApplyImmediate sets the firmware to be installed immediately after upload From 9365e6cb471b77d8d798966e11cbc6bd671f73a6 Mon Sep 17 00:00:00 2001 From: Olivier FAURAX Date: Wed, 9 Aug 2023 11:36:41 +0200 Subject: [PATCH 3/3] redfish: Add support for unstructured updates --- 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) + } +}