Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redfish provider to support unstructured http push uploads #346

Merged
merged 16 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand All @@ -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
)
10 changes: 5 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
5 changes: 5 additions & 0 deletions internal/redfishwrapper/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"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"
)

Expand Down Expand Up @@ -208,3 +209,7 @@
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()

Check warning on line 214 in internal/redfishwrapper/client.go

View check run for this annotation

Codecov / codecov/patch

internal/redfishwrapper/client.go#L213-L214

Added lines #L213 - L214 were not covered by tests
}
222 changes: 121 additions & 101 deletions providers/redfish/firmware.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@
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{
Expand All @@ -44,8 +51,7 @@
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())
}
Expand Down Expand Up @@ -83,39 +89,36 @@
}
}

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)
}()

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())

Check warning on line 110 in providers/redfish/firmware.go

View check run for this annotation

Codecov / codecov/patch

providers/redfish/firmware.go#L110

Added line #L110 was not covered by tests
}

case unstructuredHttpPush:
var uploadErr error
resp, uploadErr = c.unstructuredHttpUpload(ctx, installURI, applyAt, reader)
if uploadErr != nil {
return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, uploadErr.Error())

Check warning on line 117 in providers/redfish/firmware.go

View check run for this annotation

Codecov / codecov/patch

providers/redfish/firmware.go#L113-L117

Added lines #L113 - L117 were not covered by tests
}

default:
return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, "unsupported install method: "+string(installMethod))

Check warning on line 121 in providers/redfish/firmware.go

View check run for this annotation

Codecov / codecov/patch

providers/redfish/firmware.go#L120-L121

Added lines #L120 - L121 were not covered by tests
}

if resp.StatusCode != http.StatusAccepted {
Expand All @@ -127,8 +130,30 @@

// The response contains a location header pointing to the task URI
ofaurax marked this conversation as resolved.
Show resolved Hide resolved
// 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 = ""

Check warning on line 151 in providers/redfish/firmware.go

View check run for this annotation

Codecov / codecov/patch

providers/redfish/firmware.go#L151

Added line #L151 was not covered by tests
}
}

if taskID == "" {
return "", bmclibErrs.ErrTaskNotFound
}

return taskID, nil
Expand All @@ -139,74 +164,67 @@
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")

Check warning on line 169 in providers/redfish/firmware.go

View check run for this annotation

Codecov / codecov/patch

providers/redfish/firmware.go#L169

Added line #L169 was not covered by tests
}

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")

Check warning on line 183 in providers/redfish/firmware.go

View check run for this annotation

Codecov / codecov/patch

providers/redfish/firmware.go#L183

Added line #L183 was not covered by tests
}

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")

Check warning on line 197 in providers/redfish/firmware.go

View check run for this annotation

Codecov / codecov/patch

providers/redfish/firmware.go#L195-L197

Added lines #L195 - L197 were not covered by tests
}

// TODO: transform this to read the update so that we don't hold the data in memory
b, _ := io.ReadAll(update)
ofaurax marked this conversation as resolved.
Show resolved Hide resolved
payloadReadSeeker := bytes.NewReader(b)

Check warning on line 202 in providers/redfish/firmware.go

View check run for this annotation

Codecov / codecov/patch

providers/redfish/firmware.go#L201-L202

Added lines #L201 - L202 were not covered by tests

return c.redfishwrapper.RunRawRequestWithHeaders(http.MethodPost, url, payloadReadSeeker, "application/octet-stream", nil)

Check warning on line 204 in providers/redfish/firmware.go

View check run for this annotation

Codecov / codecov/patch

providers/redfish/firmware.go#L204

Added line #L204 was not covered by tests

}

// 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())

Check warning on line 212 in providers/redfish/firmware.go

View check run for this annotation

Codecov / codecov/patch

providers/redfish/firmware.go#L212

Added line #L212 was not covered by tests
}

// 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")

Check warning on line 217 in providers/redfish/firmware.go

View check run for this annotation

Codecov / codecov/patch

providers/redfish/firmware.go#L217

Added line #L217 was not covered by tests
}

// 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

Check warning on line 224 in providers/redfish/firmware.go

View check run for this annotation

Codecov / codecov/patch

providers/redfish/firmware.go#L223-L224

Added lines #L223 - L224 were not covered by tests
}

return nil
return "", "", errors.Wrap(bmclibErrs.ErrRedfishUpdateService, "unsupported update method")

Check warning on line 227 in providers/redfish/firmware.go

View check run for this annotation

Codecov / codecov/patch

providers/redfish/firmware.go#L227

Added line #L227 was not covered by tests
}

// pipeReaderFakeSeeker wraps the io.PipeReader and implements the io.Seeker interface
Expand Down Expand Up @@ -283,7 +301,7 @@

// 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")
}
Expand Down Expand Up @@ -357,7 +375,7 @@
// 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
Expand All @@ -375,50 +393,52 @@
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")

Check warning on line 400 in providers/redfish/firmware.go

View check run for this annotation

Codecov / codecov/patch

providers/redfish/firmware.go#L400

Added line #L400 was not covered by tests
}

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)

Check warning on line 411 in providers/redfish/firmware.go

View check run for this annotation

Codecov / codecov/patch

providers/redfish/firmware.go#L411

Added line #L411 was not covered by tests
default:
err = errors.Wrap(
bmclibErrs.ErrNotImplemented,
"GetFirmwareInstallTask() for vendor: "+vendor,
)
task, err = c.GetTask(taskID)
}

if err != nil {
return nil, err
return state, err

Check warning on line 417 in providers/redfish/firmware.go

View check run for this annotation

Codecov / codecov/patch

providers/redfish/firmware.go#L417

Added line #L417 was not covered by tests
}

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 warning on line 421 in providers/redfish/firmware.go

View check run for this annotation

Codecov / codecov/patch

providers/redfish/firmware.go#L421

Added line #L421 was not covered by tests
}

// 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

Check warning on line 439 in providers/redfish/firmware.go

View check run for this annotation

Codecov / codecov/patch

providers/redfish/firmware.go#L428-L439

Added lines #L428 - L439 were not covered by tests
default:
err = errors.Wrap(
bmclibErrs.ErrNotImplemented,
"purgeFirmwareInstallTask() for vendor: "+vendor,
)
return constants.FirmwareInstallUnknown + ": " + state, nil
}

return err
}
Loading