diff --git a/client.go b/client.go index 161f0ea7..8ef536bf 100644 --- a/client.go +++ b/client.go @@ -20,6 +20,7 @@ import ( "github.com/bmc-toolbox/bmclib/v2/providers/dell" "github.com/bmc-toolbox/bmclib/v2/providers/intelamt" "github.com/bmc-toolbox/bmclib/v2/providers/ipmitool" + "github.com/bmc-toolbox/bmclib/v2/providers/openbmc" "github.com/bmc-toolbox/bmclib/v2/providers/redfish" "github.com/bmc-toolbox/bmclib/v2/providers/rpc" "github.com/bmc-toolbox/bmclib/v2/providers/supermicro" @@ -71,6 +72,7 @@ type providerConfig struct { dell dell.Config supermicro supermicro.Config rpc rpc.Provider + openbmc openbmc.Config } // NewClient returns a new Client struct @@ -105,6 +107,9 @@ func NewClient(host, user, pass string, opts ...Option) *Client { Port: "443", }, rpc: rpc.Provider{}, + openbmc: openbmc.Config{ + Port: "443", + }, }, } @@ -243,6 +248,21 @@ func (c *Client) registerSupermicroProvider() { c.Registry.Register(supermicro.ProviderName, supermicro.ProviderProtocol, supermicro.Features, nil, driverSupermicro) } +func (c *Client) registerOpenBMCProvider() { + httpClient := *c.httpClient + httpClient.Transport = c.httpClient.Transport.(*http.Transport).Clone() + driver := openbmc.New( + c.Auth.Host, + c.Auth.User, + c.Auth.Pass, + c.Logger, + openbmc.WithHttpClient(&httpClient), + openbmc.WithPort(c.providerConfig.openbmc.Port), + ) + + c.Registry.Register(openbmc.ProviderName, openbmc.ProviderProtocol, openbmc.Features, nil, driver) +} + func (c *Client) registerProviders() { // register the rpc provider // without the consumer URL there is no way to send RPC requests. @@ -265,6 +285,7 @@ func (c *Client) registerProviders() { c.registerIntelAMTProvider() c.registerDellProvider() c.registerSupermicroProvider() + c.registerOpenBMCProvider() } // GetMetadata returns the metadata that is populated after each BMC function/method call diff --git a/providers/openbmc/firmware.go b/providers/openbmc/firmware.go new file mode 100644 index 00000000..d5fd492a --- /dev/null +++ b/providers/openbmc/firmware.go @@ -0,0 +1,100 @@ +package openbmc + +import ( + "context" + "fmt" + "os" + "strings" + "time" + + "github.com/bmc-toolbox/bmclib/v2/constants" + "github.com/bmc-toolbox/common" + + bmcliberrs "github.com/bmc-toolbox/bmclib/v2/errors" + rfw "github.com/bmc-toolbox/bmclib/v2/internal/redfishwrapper" + "github.com/pkg/errors" + "github.com/stmcginnis/gofish/redfish" +) + +// bmc client interface implementations methods +func (c *Conn) FirmwareInstallSteps(ctx context.Context, component string) ([]constants.FirmwareInstallStep, error) { + if err := c.deviceSupported(ctx); err != nil { + return nil, err + } + + switch strings.ToUpper(component) { + case common.SlugBIOS: + return []constants.FirmwareInstallStep{ + constants.FirmwareInstallStepPowerOffHost, + constants.FirmwareInstallStepUploadInitiateInstall, + constants.FirmwareInstallStepInstallStatus, + }, nil + case common.SlugBMC: + return []constants.FirmwareInstallStep{ + constants.FirmwareInstallStepUploadInitiateInstall, + constants.FirmwareInstallStepInstallStatus, + }, nil + default: + return nil, errors.New("component firmware install not supported: " + component) + } +} + +func (c *Conn) FirmwareInstallUploadAndInitiate(ctx context.Context, component string, file *os.File) (taskID string, err error) { + if err := c.deviceSupported(ctx); err != nil { + return "", errNotOpenBMCDevice + } + + // // expect atleast 5 minutes left in the deadline to proceed with the upload + d, _ := ctx.Deadline() + if time.Until(d) < 10*time.Minute { + return "", errors.New("remaining context deadline insufficient to perform update: " + time.Until(d).String()) + } + + // list current tasks on BMC + tasks, err := c.redfishwrapper.Tasks(ctx) + if err != nil { + return "", errors.Wrap(err, "error listing bmc redfish tasks") + } + + // validate a new firmware install task can be queued + if err := c.checkQueueability(component, tasks); err != nil { + return "", errors.Wrap(bmcliberrs.ErrFirmwareInstall, err.Error()) + } + + params := &rfw.RedfishUpdateServiceParameters{ + Targets: []string{}, + OperationApplyTime: constants.OnReset, + Oem: []byte(`{}`), + } + + return c.redfishwrapper.FirmwareUpload(ctx, file, params) +} + +// returns an error when a bmc firmware install is active +func (c *Conn) checkQueueability(component string, tasks []*redfish.Task) error { + errTaskActive := errors.New("A firmware job was found active for component: " + component) + + for _, t := range tasks { + // taskInfo returned in error if any. + taskInfo := fmt.Sprintf("id: %s, state: %s, status: %s", t.ID, t.TaskState, t.TaskStatus) + + // convert redfish task state to bmclib state + convstate := c.redfishwrapper.ConvertTaskState(string(t.TaskState)) + // check if task is active based on converted state + active, err := c.redfishwrapper.TaskStateActive(convstate) + if err != nil { + return errors.Wrap(err, taskInfo) + } + + if active { + return errors.Wrap(errTaskActive, taskInfo) + } + } + + return nil +} + +// FirmwareTaskStatus returns the status of a firmware related task queued on the BMC. +func (c *Conn) FirmwareTaskStatus(ctx context.Context, kind constants.FirmwareInstallStep, component, taskID, installVersion string) (state constants.TaskState, status string, err error) { + return c.redfishwrapper.TaskStatus(ctx, taskID) +} diff --git a/providers/openbmc/openbmc.go b/providers/openbmc/openbmc.go new file mode 100644 index 00000000..259c49ba --- /dev/null +++ b/providers/openbmc/openbmc.go @@ -0,0 +1,180 @@ +package openbmc + +import ( + "bytes" + "context" + "crypto/x509" + "io" + "net/http" + "strings" + + "github.com/bmc-toolbox/bmclib/v2/internal/httpclient" + "github.com/bmc-toolbox/bmclib/v2/internal/redfishwrapper" + "github.com/bmc-toolbox/bmclib/v2/providers" + "github.com/bmc-toolbox/common" + "github.com/go-logr/logr" + "github.com/jacobweinstock/registrar" + "github.com/pkg/errors" +) + +const ( + // ProviderName for the OpenBMC provider implementation + ProviderName = "openbmc" + // ProviderProtocol for the OpenBMC provider implementation + ProviderProtocol = "redfish" +) + +var ( + // Features implemented by dell redfish + Features = registrar.Features{ + providers.FeaturePowerState, + providers.FeaturePowerSet, + providers.FeatureFirmwareInstallSteps, + providers.FeatureFirmwareUploadInitiateInstall, + providers.FeatureFirmwareTaskStatus, + providers.FeatureInventoryRead, + } + + errNotOpenBMCDevice = errors.New("not an OpenBMC device") +) + +type Config struct { + HttpClient *http.Client + Port string + VersionsNotCompatible []string + RootCAs *x509.CertPool + UseBasicAuth bool +} + +// Option for setting optional Client values +type Option func(*Config) + +func WithHttpClient(httpClient *http.Client) Option { + return func(c *Config) { + c.HttpClient = httpClient + } +} + +func WithPort(port string) Option { + return func(c *Config) { + c.Port = port + } +} + +func WithRootCAs(rootCAs *x509.CertPool) Option { + return func(c *Config) { + c.RootCAs = rootCAs + } +} + +func WithUseBasicAuth(useBasicAuth bool) Option { + return func(c *Config) { + c.UseBasicAuth = useBasicAuth + } +} + +// Conn details for redfish client +type Conn struct { + host string + httpClient *http.Client + redfishwrapper *redfishwrapper.Client + Log logr.Logger +} + +// New returns connection with a redfish client initialized +func New(host, user, pass string, log logr.Logger, opts ...Option) *Conn { + defaultConfig := &Config{ + HttpClient: httpclient.Build(), + Port: "443", + VersionsNotCompatible: []string{}, + } + + for _, opt := range opts { + opt(defaultConfig) + } + + rfOpts := []redfishwrapper.Option{ + redfishwrapper.WithHTTPClient(defaultConfig.HttpClient), + redfishwrapper.WithBasicAuthEnabled(defaultConfig.UseBasicAuth), + redfishwrapper.WithEtagMatchDisabled(true), + } + + if defaultConfig.RootCAs != nil { + rfOpts = append(rfOpts, redfishwrapper.WithSecureTLS(defaultConfig.RootCAs)) + } + + return &Conn{ + host: host, + httpClient: defaultConfig.HttpClient, + Log: log, + redfishwrapper: redfishwrapper.NewClient(host, defaultConfig.Port, user, pass, rfOpts...), + } +} + +// Open a connection to a BMC via redfish +func (c *Conn) Open(ctx context.Context) (err error) { + if err := c.deviceSupported(ctx); err != nil { + return nil + } + + if err := c.redfishwrapper.Open(ctx); err != nil { + return err + } + + return nil +} + +func (c *Conn) deviceSupported(ctx context.Context) error { + var host = c.host + if !strings.HasPrefix(host, "https://") && !strings.HasPrefix(host, "http://") { + host = "https://" + host + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, host, nil) + if err != nil { + return err + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + if !bytes.Contains(b, []byte(`OpenBMC`)) { + return errNotOpenBMCDevice + } + + return nil +} + +// Close a connection to a BMC via redfish +func (c *Conn) Close(ctx context.Context) error { + return c.redfishwrapper.Close(ctx) +} + +// Name returns the client provider name. +func (c *Conn) Name() string { + return ProviderName +} + +// PowerStateGet gets the power state of a BMC machine +func (c *Conn) PowerStateGet(ctx context.Context) (state string, err error) { + return c.redfishwrapper.SystemPowerStatus(ctx) +} + +// PowerSet sets the power state of a server +func (c *Conn) PowerSet(ctx context.Context, state string) (ok bool, err error) { + return c.redfishwrapper.PowerSet(ctx, state) +} + +// Inventory collects hardware inventory and install firmware information +func (c *Conn) Inventory(ctx context.Context) (device *common.Device, err error) { + return c.redfishwrapper.Inventory(ctx, false) +}