From bf091a6dde435785b73a27c72ab863b3c1618486 Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Wed, 16 Aug 2023 07:28:53 +0200 Subject: [PATCH 1/5] redfish/firmware: implement Redfish streaming firmware upload These changes were already part of bmclib v1 but missed out from being included in v2, https://github.com/bmc-toolbox/bmclib/pull/279 --- providers/redfish/firmware.go | 157 ++++++++++++++++++++++++----- providers/redfish/firmware_test.go | 70 ++++++++++++- 2 files changed, 201 insertions(+), 26 deletions(-) diff --git a/providers/redfish/firmware.go b/providers/redfish/firmware.go index 94112521..2f3a17e2 100644 --- a/providers/redfish/firmware.go +++ b/providers/redfish/firmware.go @@ -93,7 +93,7 @@ func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt string, f // 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 time to be restored + // record the http client timeout to be restored httpClientTimeout := c.redfishwrapper.HttpClientTimeout() defer func() { c.redfishwrapper.SetHttpClientTimeout(httpClientTimeout) @@ -101,9 +101,9 @@ func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt string, f c.redfishwrapper.SetHttpClientTimeout(time.Until(ctxDeadline)) - payload := map[string]io.Reader{ - "UpdateParameters": bytes.NewReader(updateParameters), - "UpdateFile": reader, + payload := []map[string]io.Reader{ + {"UpdateParameters": bytes.NewReader(updateParameters)}, + {"UpdateFile": reader}, } resp, err := c.runRequestWithMultipartPayload(http.MethodPost, "/redfish/v1/UpdateService/MultipartUpload", payload) @@ -197,6 +197,71 @@ func (c *Conn) firmwareUpdateCompatible(ctx context.Context) (err error) { return nil } +// 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) { + panic("Seek() not implemented for fake pipe reader seeker.") +} + +// multipartPayloadSize prepares a temporary multipart form to determine the form size +func multipartPayloadSize(payload []map[string]io.Reader) (int64, *bytes.Buffer, error) { + body := &bytes.Buffer{} + form := multipart.NewWriter(body) + + var size int64 + var err error + for idx, elem := range payload { + for key, reader := range elem { + var part io.Writer + if file, ok := reader.(*os.File); ok { + // Add update file fields + if _, err = form.CreateFormFile(key, filepath.Base(file.Name())); err != nil { + return 0, body, err + } + + // determine file size + finfo, err := file.Stat() + if err != nil { + return 0, body, err + } + + size = finfo.Size() + + } else { + // Add other fields + if part, err = updateParametersFormField(key, form); err != nil { + return 0, body, err + } + + buf := bytes.Buffer{} + teeReader := io.TeeReader(reader, &buf) + + if _, err = io.Copy(part, teeReader); err != nil { + return 0, body, err + } + + // place it back so its available to be read again + payload[idx][key] = bytes.NewReader(buf.Bytes()) + } + } + } + + err = form.Close() + if err != nil { + return 0, body, err + } + + return int64(body.Len()) + 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 @@ -218,34 +283,78 @@ func (c *Conn) firmwareUpdateCompatible(ctx context.Context) (err error) { // hey. // --------------------------1771f60800cb2801-- -func (c *Conn) runRequestWithMultipartPayload(method, url string, payload map[string]io.Reader) (*http.Response, error) { +func (c *Conn) runRequestWithMultipartPayload(method, url string, payload []map[string]io.Reader) (*http.Response, error) { if url == "" { return nil, fmt.Errorf("unable to execute request, no target provided") } - var payloadBuffer bytes.Buffer - var err error - payloadWriter := multipart.NewWriter(&payloadBuffer) - for key, reader := range payload { - var partWriter io.Writer - if file, ok := reader.(*os.File); ok { - // Add a file stream - if partWriter, err = payloadWriter.CreateFormFile(key, filepath.Base(file.Name())); err != nil { - return nil, err + // A content-length header is passed in to indicate the payload size + // + // The content-length is required since the + 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 func() { + var err error + defer func() { + if err != nil { + c.Log.Error(err, "multipart upload error occurred") } - } else { - // Add other fields - if partWriter, err = updateParametersFormField(key, payloadWriter); err != nil { - return nil, err + }() + + defer pipeWriter.Close() + + for _, elem := range payload { + for key, reader := range elem { + var part io.Writer + // add update file multipart form header + // + // Content-Disposition: form-data; name="UpdateFile"; filename="dum + // myfile" + // Content-Type: application/octet-stream + if file, ok := reader.(*os.File); ok { + // Add a file stream + if part, err = form.CreateFormFile(key, filepath.Base(file.Name())); err != nil { + return + } + } else { + // add update parameters multipart form header + // + // Content-Disposition: form-data; name="UpdateParameters" + // Content-Type: application/json + if part, err = updateParametersFormField(key, form); err != nil { + return + } + } + + // copy from source into form part writer + if _, err = io.Copy(part, reader); err != nil { + return + } } } - if _, err = io.Copy(partWriter, reader); err != nil { - return nil, err - } - } - payloadWriter.Close() - return c.redfishwrapper.RunRawRequestWithHeaders(method, url, bytes.NewReader(payloadBuffer.Bytes()), payloadWriter.FormDataContentType(), nil) + // 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(method, url, reader, form.FormDataContentType(), headers) } // sets up the UpdateParameters MIMEHeader for the multipart form diff --git a/providers/redfish/firmware_test.go b/providers/redfish/firmware_test.go index 8e098e4d..26c9131e 100644 --- a/providers/redfish/firmware_test.go +++ b/providers/redfish/firmware_test.go @@ -1,7 +1,9 @@ package redfish import ( + "bytes" "context" + "encoding/json" "fmt" "io" "log" @@ -30,6 +32,9 @@ func multipartUpload(w http.ResponseWriter, r *http.Request) { log.Fatal(err) } + // payload size + expectedContentLength := "476" + expected := []string{ `Content-Disposition: form-data; name="UpdateParameters"`, `Content-Type: application/json`, @@ -46,11 +51,15 @@ func multipartUpload(w http.ResponseWriter, r *http.Request) { } } + 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 Test_FirmwareInstall(t *testing.T) { +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' @@ -151,7 +160,64 @@ func Test_FirmwareInstall(t *testing.T) { } -func Test_firmwareUpdateCompatible(t *testing.T) { +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 []map[string]io.Reader + expectedSize int64 + errorMsg string + }{ + { + "content length as expected", + []map[string]io.Reader{ + {"UpdateParameters": bytes.NewReader(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) + }) + } +} + +func TestFirmwareUpdateCompatible(t *testing.T) { err := mockClient.firmwareUpdateCompatible(context.TODO()) if err != nil { t.Fatal(err) From 77f524b85bf41473315e1eff442e1c83c4090b5c Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Wed, 16 Aug 2023 08:00:16 +0200 Subject: [PATCH 2/5] examples/install-firmware: include initializing state --- examples/install-firmware/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/install-firmware/main.go b/examples/install-firmware/main.go index d1820e0a..fa7d757b 100644 --- a/examples/install-firmware/main.go +++ b/examples/install-firmware/main.go @@ -114,7 +114,7 @@ func main() { } switch state { - case constants.FirmwareInstallRunning: + case constants.FirmwareInstallRunning, constants.FirmwareInstallInitializing: l.WithFields(logrus.Fields{"state": state, "component": *component}).Info("firmware install running") case constants.FirmwareInstallFailed: From 6cf01686c513a1d94b75b69bed6e7689fd26e5ce Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Fri, 25 Aug 2023 17:16:35 +0200 Subject: [PATCH 3/5] providers/redfish: return error instead of panic, include a few comments --- providers/redfish/firmware.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/providers/redfish/firmware.go b/providers/redfish/firmware.go index 2f3a17e2..d0293b73 100644 --- a/providers/redfish/firmware.go +++ b/providers/redfish/firmware.go @@ -208,10 +208,13 @@ type pipeReaderFakeSeeker struct { // Seek impelements the io.Seeker interface only to panic if called func (p pipeReaderFakeSeeker) Seek(offset int64, whence int) (int64, error) { - panic("Seek() not implemented for fake pipe reader seeker.") + 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 []map[string]io.Reader) (int64, *bytes.Buffer, error) { body := &bytes.Buffer{} form := multipart.NewWriter(body) @@ -290,7 +293,11 @@ func (c *Conn) runRequestWithMultipartPayload(method, url string, payload []map[ // A content-length header is passed in to indicate the payload size // - // The content-length is required since the + // 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") From 059b754ae098c55eb3b2466e5aaf849e131ea974 Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Wed, 6 Sep 2023 09:53:02 +0200 Subject: [PATCH 4/5] providers/redfish: simplify the multipart payload build - use a struct instead This makes the code easier to read and maintain. As of now we limit the method to expect an os.File as an io.Reader, once its clear theres other implementations of io.Readers required we can accept those and figure how to determine the file size for the multipart payload --- providers/redfish/firmware.go | 147 +++++++++++++++-------------- providers/redfish/firmware_test.go | 21 ++++- 2 files changed, 91 insertions(+), 77 deletions(-) diff --git a/providers/redfish/firmware.go b/providers/redfish/firmware.go index d0293b73..a91bedca 100644 --- a/providers/redfish/firmware.go +++ b/providers/redfish/firmware.go @@ -25,6 +25,7 @@ import ( var ( errInsufficientCtxTimeout = errors.New("remaining context timeout insufficient to install firmware") + errMultiPartPayload = errors.New("error preparing multipart payload") ) // SupportedFirmwareApplyAtValues returns the supported redfish firmware applyAt values @@ -37,10 +38,16 @@ func SupportedFirmwareApplyAtValues() []string { // FirmwareInstall uploads and initiates the firmware install process func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt 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") + } + // validate firmware update mechanism is supported err = c.firmwareUpdateCompatible(ctx) if err != nil { - return "", err + return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, err.Error()) } // validate applyAt parameter @@ -59,7 +66,7 @@ func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt string, f // list redfish firmware install task if theres one present task, err := c.GetFirmwareInstallTaskQueued(ctx, component) if err != nil { - return "", err + return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, err.Error()) } if task != nil { @@ -101,9 +108,9 @@ func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt string, f c.redfishwrapper.SetHttpClientTimeout(time.Until(ctxDeadline)) - payload := []map[string]io.Reader{ - {"UpdateParameters": bytes.NewReader(updateParameters)}, - {"UpdateFile": reader}, + payload := &multipartPayload{ + updateParameters: bytes.NewReader(updateParameters), + updateFile: updateFile, } resp, err := c.runRequestWithMultipartPayload(http.MethodPost, "/redfish/v1/UpdateService/MultipartUpload", payload) @@ -127,6 +134,11 @@ func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt string, f return taskID, nil } +type multipartPayload struct { + updateParameters io.Reader + 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) @@ -215,54 +227,46 @@ func (p pipeReaderFakeSeeker) Seek(offset int64, whence int) (int64, error) { // // It creates a temporary form without reading in the update file payload and returns // sizeOf(form) + sizeOf(update file) -func multipartPayloadSize(payload []map[string]io.Reader) (int64, *bytes.Buffer, error) { +func multipartPayloadSize(payload *multipartPayload) (int64, *bytes.Buffer, error) { body := &bytes.Buffer{} form := multipart.NewWriter(body) - var size int64 - var err error - for idx, elem := range payload { - for key, reader := range elem { - var part io.Writer - if file, ok := reader.(*os.File); ok { - // Add update file fields - if _, err = form.CreateFormFile(key, filepath.Base(file.Name())); err != nil { - return 0, body, err - } - - // determine file size - finfo, err := file.Stat() - if err != nil { - return 0, body, err - } - - size = finfo.Size() - - } else { - // Add other fields - if part, err = updateParametersFormField(key, form); err != nil { - return 0, body, err - } - - buf := bytes.Buffer{} - teeReader := io.TeeReader(reader, &buf) - - if _, err = io.Copy(part, teeReader); err != nil { - return 0, body, err - } - - // place it back so its available to be read again - payload[idx][key] = bytes.NewReader(buf.Bytes()) - } - } + // Add UpdateParameters field part + part, err := updateParametersFormField("UpdateParameters", form) + if err != nil { + return 0, body, err + } + + // a buffer to save the contents of the updateParameters reader + buf := bytes.Buffer{} + teeReader := io.TeeReader(payload.updateParameters, &buf) + + if _, err = io.Copy(part, teeReader); err != nil { + return 0, body, err + } + + // restore the reader + payload.updateParameters = bytes.NewReader(buf.Bytes()) + + // 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()) + size, body, nil + return int64(body.Len()) + finfo.Size(), body, nil } // runRequestWithMultipartPayload is a copy of https://github.com/stmcginnis/gofish/blob/main/client.go#L349 @@ -286,7 +290,7 @@ func multipartPayloadSize(payload []map[string]io.Reader) (int64, *bytes.Buffer, // hey. // --------------------------1771f60800cb2801-- -func (c *Conn) runRequestWithMultipartPayload(method, url string, payload []map[string]io.Reader) (*http.Response, error) { +func (c *Conn) runRequestWithMultipartPayload(method, url string, payload *multipartPayload) (*http.Response, error) { if url == "" { return nil, fmt.Errorf("unable to execute request, no target provided") } @@ -314,6 +318,7 @@ func (c *Conn) runRequestWithMultipartPayload(method, url string, payload []map[ // 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() { @@ -324,34 +329,32 @@ func (c *Conn) runRequestWithMultipartPayload(method, url string, payload []map[ defer pipeWriter.Close() - for _, elem := range payload { - for key, reader := range elem { - var part io.Writer - // add update file multipart form header - // - // Content-Disposition: form-data; name="UpdateFile"; filename="dum - // myfile" - // Content-Type: application/octet-stream - if file, ok := reader.(*os.File); ok { - // Add a file stream - if part, err = form.CreateFormFile(key, filepath.Base(file.Name())); err != nil { - return - } - } else { - // add update parameters multipart form header - // - // Content-Disposition: form-data; name="UpdateParameters" - // Content-Type: application/json - if part, err = updateParametersFormField(key, form); err != nil { - return - } - } - - // copy from source into form part writer - if _, err = io.Copy(part, reader); err != nil { - return - } - } + // 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, 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 diff --git a/providers/redfish/firmware_test.go b/providers/redfish/firmware_test.go index 26c9131e..62763ce5 100644 --- a/providers/redfish/firmware_test.go +++ b/providers/redfish/firmware_test.go @@ -97,6 +97,17 @@ func TestFirmwareInstall(t *testing.T) { false, nil, "", + bmclibErrs.ErrFirmwareInstall, + "method expects an *os.File object", + "expect *os.File object", + }, + { + common.SlugBIOS, + constants.FirmwareApplyOnReset, + false, + false, + &os.File{}, + "", errInsufficientCtxTimeout, "", "remaining context deadline", @@ -106,7 +117,7 @@ func TestFirmwareInstall(t *testing.T) { "invalidApplyAt", false, true, - nil, + &os.File{}, "", bmclibErrs.ErrFirmwareInstall, "invalid applyAt parameter", @@ -189,15 +200,15 @@ func TestMultipartPayloadSize(t *testing.T) { testCases := []struct { testName string - payload []map[string]io.Reader + payload *multipartPayload expectedSize int64 errorMsg string }{ { "content length as expected", - []map[string]io.Reader{ - {"UpdateParameters": bytes.NewReader(updateParameters)}, - {"UpdateFile": testfileFH}, + &multipartPayload{ + updateParameters: bytes.NewReader(updateParameters), + updateFile: testfileFH, }, 475, "", From 08172f2d91032140fb93567da8046fbcf5b76b0a Mon Sep 17 00:00:00 2001 From: Joel Rebello Date: Sat, 9 Sep 2023 06:56:00 +0200 Subject: [PATCH 5/5] redfish/firmware: simpler UpdateParameters handling This makes the UpdateParameters handling easier to grok and maintain --- providers/redfish/firmware.go | 15 ++++----------- providers/redfish/firmware_test.go | 3 +-- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/providers/redfish/firmware.go b/providers/redfish/firmware.go index a91bedca..ca6231f4 100644 --- a/providers/redfish/firmware.go +++ b/providers/redfish/firmware.go @@ -109,7 +109,7 @@ func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt string, f c.redfishwrapper.SetHttpClientTimeout(time.Until(ctxDeadline)) payload := &multipartPayload{ - updateParameters: bytes.NewReader(updateParameters), + updateParameters: updateParameters, updateFile: updateFile, } @@ -135,7 +135,7 @@ func (c *Conn) FirmwareInstall(ctx context.Context, component, applyAt string, f } type multipartPayload struct { - updateParameters io.Reader + updateParameters []byte updateFile *os.File } @@ -237,17 +237,10 @@ func multipartPayloadSize(payload *multipartPayload) (int64, *bytes.Buffer, erro return 0, body, err } - // a buffer to save the contents of the updateParameters reader - buf := bytes.Buffer{} - teeReader := io.TeeReader(payload.updateParameters, &buf) - - if _, err = io.Copy(part, teeReader); err != nil { + if _, err = io.Copy(part, bytes.NewReader(payload.updateParameters)); err != nil { return 0, body, err } - // restore the reader - payload.updateParameters = bytes.NewReader(buf.Bytes()) - // Add updateFile form _, err = form.CreateFormFile("UpdateFile", filepath.Base(payload.updateFile.Name())) if err != nil { @@ -337,7 +330,7 @@ func (c *Conn) runRequestWithMultipartPayload(method, url string, payload *multi return } - if _, err = io.Copy(parametersPart, payload.updateParameters); err != nil { + if _, err = io.Copy(parametersPart, bytes.NewReader(payload.updateParameters)); err != nil { c.Log.Error(errMultiPartPayload, err.Error()+": UpdateParameters part copy error") return diff --git a/providers/redfish/firmware_test.go b/providers/redfish/firmware_test.go index 62763ce5..b0fbedec 100644 --- a/providers/redfish/firmware_test.go +++ b/providers/redfish/firmware_test.go @@ -1,7 +1,6 @@ package redfish import ( - "bytes" "context" "encoding/json" "fmt" @@ -207,7 +206,7 @@ func TestMultipartPayloadSize(t *testing.T) { { "content length as expected", &multipartPayload{ - updateParameters: bytes.NewReader(updateParameters), + updateParameters: updateParameters, updateFile: testfileFH, }, 475,