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

Expand Down Expand Up @@ -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()
}
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 @@ 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{
Expand All @@ -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())
}
Expand Down Expand Up @@ -83,39 +89,36 @@ 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)
}()

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 {
Expand All @@ -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
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 = ""
}
}

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

return taskID, nil
Expand All @@ -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)
ofaurax marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Loading