Skip to content

Commit

Permalink
Merge branch 'main' into add-sel-support
Browse files Browse the repository at this point in the history
  • Loading branch information
mattcburns authored Sep 19, 2023
2 parents 3cca54a + b9b7ef8 commit cf178b2
Show file tree
Hide file tree
Showing 16 changed files with 554 additions and 308 deletions.
2 changes: 2 additions & 0 deletions examples/rpc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ func testConsumer(ctx context.Context) error {

case rpc.BootDeviceMethod:

case rpc.PingMethod:
rp.Result = "pong"
default:
w.WriteHeader(http.StatusNotFound)
}
Expand Down
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
// 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)
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

0 comments on commit cf178b2

Please sign in to comment.