Skip to content
Open
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
156 changes: 156 additions & 0 deletions bmc/power_cap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package bmc

import (
"context"
"fmt"

"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"

bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors"
)

// PowerMetrics holds the chassis power readings and the configured power cap.
type PowerMetrics struct {
// ConsumedWatts is the power currently drawn by the chassis.
ConsumedWatts float64
// CapacityWatts is the total power capacity available to the chassis.
CapacityWatts float64
// LimitInWatts is the configured power cap; nil when no cap is set.
LimitInWatts *float64
}

// PowerMetricsGetter provides retrieval of the chassis power readings and power cap.
type PowerMetricsGetter interface {
GetPowerMetrics(ctx context.Context) (metrics PowerMetrics, err error)
}

type powerMetricsGetterProvider struct {
name string
PowerMetricsGetter
}

// PowerCapSetter provides setting the chassis power cap. A nil limitWatts clears
// the cap.
type PowerCapSetter interface {
SetPowerCap(ctx context.Context, limitWatts *float64) (err error)
}

type powerCapSetterProvider struct {
name string
PowerCapSetter
}

func getPowerMetrics(ctx context.Context, generic []powerMetricsGetterProvider) (metrics PowerMetrics, metadata Metadata, err error) {
metadata = newMetadata()
Loop:
for _, elem := range generic {
if elem.PowerMetricsGetter == nil {
continue
}
select {
case <-ctx.Done():
err = multierror.Append(err, ctx.Err())
break Loop
default:
metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, elem.name)
metrics, vErr := elem.GetPowerMetrics(ctx)
if vErr != nil {
err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name))
continue
}
metadata.SuccessfulProvider = elem.name
return metrics, metadata, nil
}
}

return metrics, metadata, multierror.Append(err, errors.New("failure to get power metrics"))
}

func setPowerCap(ctx context.Context, generic []powerCapSetterProvider, limitWatts *float64) (metadata Metadata, err error) {
metadata = newMetadata()
Loop:
for _, elem := range generic {
if elem.PowerCapSetter == nil {
continue
}
select {
case <-ctx.Done():
err = multierror.Append(err, ctx.Err())
break Loop
default:
metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, elem.name)
vErr := elem.SetPowerCap(ctx, limitWatts)
if vErr != nil {
err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name))
continue
}
metadata.SuccessfulProvider = elem.name
return metadata, nil
}
}

return metadata, multierror.Append(err, errors.New("failure to set power cap"))
}

// GetPowerMetricsFromInterfaces identifies implementations of the PowerMetricsGetter
// interface and passes them to the getPowerMetrics() wrapper.
func GetPowerMetricsFromInterfaces(ctx context.Context, generic []interface{}) (metrics PowerMetrics, metadata Metadata, err error) {
implementations := make([]powerMetricsGetterProvider, 0)
for _, elem := range generic {
if elem == nil {
continue
}
temp := powerMetricsGetterProvider{name: getProviderName(elem)}
switch p := elem.(type) {
case PowerMetricsGetter:
temp.PowerMetricsGetter = p
implementations = append(implementations, temp)
default:
e := fmt.Sprintf("not a PowerMetricsGetter implementation: %T", p)
err = multierror.Append(err, errors.New(e))
}
}
if len(implementations) == 0 {
return metrics, metadata, multierror.Append(
err,
errors.Wrap(
bmclibErrs.ErrProviderImplementation,
"no PowerMetricsGetter implementations found",
),
)
}

return getPowerMetrics(ctx, implementations)
}

// SetPowerCapFromInterfaces identifies implementations of the PowerCapSetter
// interface and passes them to the setPowerCap() wrapper.
func SetPowerCapFromInterfaces(ctx context.Context, generic []interface{}, limitWatts *float64) (metadata Metadata, err error) {
implementations := make([]powerCapSetterProvider, 0)
for _, elem := range generic {
if elem == nil {
continue
}
temp := powerCapSetterProvider{name: getProviderName(elem)}
switch p := elem.(type) {
case PowerCapSetter:
temp.PowerCapSetter = p
implementations = append(implementations, temp)
default:
e := fmt.Sprintf("not a PowerCapSetter implementation: %T", p)
err = multierror.Append(err, errors.New(e))
}
}
if len(implementations) == 0 {
return metadata, multierror.Append(
err,
errors.Wrap(
bmclibErrs.ErrProviderImplementation,
"no PowerCapSetter implementations found",
),
)
}

return setPowerCap(ctx, implementations, limitWatts)
}
127 changes: 127 additions & 0 deletions bmc/power_cap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package bmc

import (
"context"
"testing"

"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)

type mockPowerMetricsGetter struct {
metrics PowerMetrics
err error
}

func (m *mockPowerMetricsGetter) GetPowerMetrics(ctx context.Context) (PowerMetrics, error) {
return m.metrics, m.err
}

func (m *mockPowerMetricsGetter) Name() string {
return "mock"
}

type mockPowerCapSetter struct {
err error
}

func (m *mockPowerCapSetter) SetPowerCap(ctx context.Context, _ *float64) error {
return m.err
}

func (m *mockPowerCapSetter) Name() string {
return "mock"
}

func TestGetPowerMetricsFromInterfaces(t *testing.T) {
limit := 800.0
testCases := []struct {
name string
generic []interface{}
errMsg string
expectedMetrics PowerMetrics
}{
{
name: "success",
generic: []interface{}{&mockPowerMetricsGetter{metrics: PowerMetrics{ConsumedWatts: 234, CapacityWatts: 1400, LimitInWatts: &limit}}},
expectedMetrics: PowerMetrics{ConsumedWatts: 234, CapacityWatts: 1400, LimitInWatts: &limit},
},
{
name: "not an implementation",
generic: []interface{}{"foo"},
errMsg: "no PowerMetricsGetter implementations found",
},
{
name: "no implementations",
generic: []interface{}{},
errMsg: "no PowerMetricsGetter implementations found",
},
{
name: "error from getter",
generic: []interface{}{&mockPowerMetricsGetter{err: errors.New("foobar")}},
errMsg: "foobar",
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
metrics, _, err := GetPowerMetricsFromInterfaces(context.Background(), tt.generic)

if tt.errMsg == "" {
assert.NoError(t, err)
} else {
assert.ErrorContains(t, err, tt.errMsg)
}

assert.Equal(t, tt.expectedMetrics, metrics)
})
}
}

func TestSetPowerCapFromInterfaces(t *testing.T) {
limit := 800.0
testCases := []struct {
name string
generic []interface{}
limit *float64
errMsg string
}{
{
name: "success, set",
generic: []interface{}{&mockPowerCapSetter{}},
limit: &limit,
},
{
name: "success, clear",
generic: []interface{}{&mockPowerCapSetter{}},
},
{
name: "not an implementation",
generic: []interface{}{"foo"},
errMsg: "no PowerCapSetter implementations found",
},
{
name: "no implementations",
generic: []interface{}{},
errMsg: "no PowerCapSetter implementations found",
},
{
name: "error from setter",
generic: []interface{}{&mockPowerCapSetter{err: errors.New("foobar")}},
limit: &limit,
errMsg: "foobar",
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
_, err := SetPowerCapFromInterfaces(context.Background(), tt.generic, tt.limit)

if tt.errMsg == "" {
assert.NoError(t, err)
} else {
assert.ErrorContains(t, err, tt.errMsg)
}
})
}
}
24 changes: 24 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,30 @@ func (c *Client) AllowCustomSecureBootKeys(ctx context.Context, enable bool) (re
return rebootRequired, err
}

// GetPowerMetrics returns the chassis power readings and the configured power cap.
func (c *Client) GetPowerMetrics(ctx context.Context) (metrics bmc.PowerMetrics, err error) {
ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "GetPowerMetrics")
defer span.End()

metrics, metadata, err := bmc.GetPowerMetricsFromInterfaces(ctx, c.registry().GetDriverInterfaces())
c.setMetadata(metadata)
metadata.RegisterSpanAttributes(c.Auth.Host, span)

return metrics, err
}

// SetPowerCap sets the chassis power cap in watts. A nil limitWatts clears the cap.
func (c *Client) SetPowerCap(ctx context.Context, limitWatts *float64) (err error) {
ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "SetPowerCap")
defer span.End()

metadata, err := bmc.SetPowerCapFromInterfaces(ctx, c.registry().GetDriverInterfaces(), limitWatts)
c.setMetadata(metadata)
metadata.RegisterSpanAttributes(c.Auth.Host, span)

return err
}

// FirmwareInstall pass through library function to upload firmware and install firmware
func (c *Client) FirmwareInstall(ctx context.Context, component, operationApplyTime string, forceInstall bool, reader io.Reader) (taskID string, err error) {
ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "FirmwareInstall")
Expand Down
6 changes: 6 additions & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ var (
// ErrBMCUpdating is returned when the BMC is going through an update and will not serve other queries.
ErrBMCUpdating = errors.New("a BMC firmware update is in progress")

// ErrPowerControlNotFound is returned when no chassis exposes a Power resource with PowerControl
ErrPowerControlNotFound = errors.New("no chassis exposes a Power resource with PowerControl")

// ErrPowerCapSet is returned when the BMC rejects a power cap update
ErrPowerCapSet = errors.New("failed to set power cap")

// ErrSecureBootDatabaseNotFound is returned when the requested UEFI Secure Boot key database
// (db, KEK, PK, dbx, ...) is not present in the SecureBootDatabases collection reported by the BMC.
ErrSecureBootDatabaseNotFound = errors.New("secure boot key database not found")
Expand Down
Loading
Loading