Skip to content

Commit

Permalink
Merge pull request #377 from bmc-toolbox/openbmc
Browse files Browse the repository at this point in the history
Add OpenBMC support
  • Loading branch information
joelrebel authored Nov 30, 2023
2 parents 0df7b75 + 17b3809 commit 3688b8c
Show file tree
Hide file tree
Showing 3 changed files with 301 additions and 0 deletions.
21 changes: 21 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -105,6 +107,9 @@ func NewClient(host, user, pass string, opts ...Option) *Client {
Port: "443",
},
rpc: rpc.Provider{},
openbmc: openbmc.Config{
Port: "443",
},
},
}

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
100 changes: 100 additions & 0 deletions providers/openbmc/firmware.go
Original file line number Diff line number Diff line change
@@ -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)
}
180 changes: 180 additions & 0 deletions providers/openbmc/openbmc.go
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit 3688b8c

Please sign in to comment.