Skip to content

Commit

Permalink
support BootProgress on SMC X12/X13 (#396)
Browse files Browse the repository at this point in the history
* WIP: support BootProgress on SMC X12/X13

* add some requested comments
  • Loading branch information
DoctorVin authored Oct 4, 2024
1 parent 6a25804 commit e0bb584
Show file tree
Hide file tree
Showing 11 changed files with 233 additions and 0 deletions.
3 changes: 3 additions & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ var (
// ErrUserAccountUpdate is returned when the user account failed to be updated
ErrUserAccountUpdate = errors.New("user account attributes could not be updated")

// ErrRedfishVersionIncompatible is returned when a given version of redfish doesn't support a feature
ErrRedfishVersionIncompatible = errors.New("operation not supported in this redfish version")

// ErrRedfishChassisOdataID is returned when no compatible Chassis Odata IDs were identified
ErrRedfishChassisOdataID = errors.New("no compatible Chassis Odata IDs identified")

Expand Down
57 changes: 57 additions & 0 deletions internal/redfishwrapper/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package redfishwrapper
import (
"context"
"crypto/x509"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -227,6 +229,61 @@ func (c *Client) VersionCompatible() bool {
return !slices.Contains(c.versionsNotCompatible, c.client.Service.RedfishVersion)
}

// redfishVersionMeetsOrExceeds compares this connection's redfish version to what is provided
// as a requirement. We rely on the stated structure of the version string as described in the
// Protocol Version (section 6.6) of the Redfish spec. If an implementation's version string is
// non-conforming this function returns false.
func redfishVersionMeetsOrExceeds(version string, major, minor, patch int) bool {
if version == "" {
return false
}

parts := strings.Split(version, ".")
if len(parts) != 3 {
return false
}

var rfVer []int64
for _, part := range parts {
ver, err := strconv.ParseInt(part, 10, 32)
if err != nil {
return false
}
rfVer = append(rfVer, ver)
}

if rfVer[0] < int64(major) {
return false
}

if rfVer[1] < int64(minor) {
return false
}

return rfVer[2] >= int64(patch)
}

func (c *Client) GetBootProgress() ([]*redfish.BootProgress, error) {
// The redfish standard adopts the BootProgress object in 1.13.0. Earlier versions of redfish return
// json NULL, which gofish turns into a zero-value object of BootProgress. We gate this on the RedfishVersion
// to avoid the complexity of interpreting whether a given value is legitimate.
if !redfishVersionMeetsOrExceeds(c.client.Service.RedfishVersion, 1, 13, 0) {
return nil, fmt.Errorf("%w: %s", bmclibErrs.ErrRedfishVersionIncompatible, c.client.Service.RedfishVersion)
}

systems, err := c.client.Service.Systems()
if err != nil {
return nil, fmt.Errorf("retrieving redfish systems collection: %w", err)
}

bps := []*redfish.BootProgress{}
for _, sys := range systems {
bps = append(bps, &sys.BootProgress)
}

return bps, nil
}

func (c *Client) PostWithHeaders(ctx context.Context, url string, payload interface{}, headers map[string]string) (*http.Response, error) {
return c.client.PostWithHeaders(url, payload, headers)
}
Expand Down
123 changes: 123 additions & 0 deletions internal/redfishwrapper/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"net/url"
"testing"

bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors"
"github.com/stmcginnis/gofish/redfish"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -218,3 +220,124 @@ func TestSystemsBIOSOdataID(t *testing.T) {
})
}
}

func TestRedfishVersionMeetsOrExceeds(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
version string
exp bool
}{
{
"empty string",
"",
false,
},
{
"short string",
"1.2",
false,
},
{
"bogus component",
"1.asdf.2",
false,
},
{
"major too low",
"0.3.4",
false,
},
{
"minor too low",
"1.1.3",
false,
},
{
"patch too low",
"1.2.2",
false,
},
{
"meets",
"1.2.3",
true,
},
{
"exceeds",
"1.2.4",
true,
},
}

for _, tc := range testCases {
got := redfishVersionMeetsOrExceeds(tc.version, 1, 2, 3)
assert.Equal(t, tc.exp, got, "testcase %s", tc.name)
}
}

func TestGetBootProgress(t *testing.T) {
tests := map[string]struct {
hfunc map[string]func(http.ResponseWriter, *http.Request)
expect []*redfish.BootProgress
err error
}{
"happy case": {
hfunc: map[string]func(http.ResponseWriter, *http.Request){
// service root
"/redfish/v1/": endpointFunc(t, "smc_1.14.0_serviceroot.json"),
"/redfish/v1/Systems": endpointFunc(t, "smc_1.14.0_systems.json"),
"/redfish/v1/Systems/1": endpointFunc(t, "smc_1.14.0_systems_1.json"),
},
expect: []*redfish.BootProgress{
&redfish.BootProgress{
LastState: redfish.SystemHardwareInitializationCompleteBootProgressTypes,
},
},
err: nil,
},
"insufficient redfish version": {
hfunc: map[string]func(http.ResponseWriter, *http.Request){
"/redfish/v1/": endpointFunc(t, "smc_1.9.0_serviceroot.json"),
},
expect: nil,
err: bmclibErrs.ErrRedfishVersionIncompatible,
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
mux := http.NewServeMux()
handleFunc := tc.hfunc
for endpoint, handler := range handleFunc {
mux.HandleFunc(endpoint, handler)
}

server := httptest.NewTLSServer(mux)
defer server.Close()

parsedURL, err := url.Parse(server.URL)
if err != nil {
t.Fatal(err)
}

client := NewClient(parsedURL.Hostname(), parsedURL.Port(), "", "")

err = client.Open(context.TODO())
if err != nil {
t.Fatal(err)
}
defer client.Close(context.TODO())

got, err := client.GetBootProgress()
if err != nil {
assert.ErrorIs(t, err, tc.err)
return
}

assert.ElementsMatch(t, tc.expect, got)
})
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"@odata.type":"#ServiceRoot.v1_14_0.ServiceRoot","@odata.id":"/redfish/v1","Id":"ServiceRoot","Name":"Root Service","RedfishVersion":"1.14.0","UUID":"00000000-0000-0000-0000-3CECEFC84895","Vendor":"Supermicro","Systems":{"@odata.id":"/redfish/v1/Systems"},"Chassis":{"@odata.id":"/redfish/v1/Chassis"},"Managers":{"@odata.id":"/redfish/v1/Managers"},"Tasks":{"@odata.id":"/redfish/v1/TaskService"},"SessionService":{"@odata.id":"/redfish/v1/SessionService"},"AccountService":{"@odata.id":"/redfish/v1/AccountService"},"EventService":{"@odata.id":"/redfish/v1/EventService"},"UpdateService":{"@odata.id":"/redfish/v1/UpdateService"},"CertificateService":{"@odata.id":"/redfish/v1/CertificateService"},"Registries":{"@odata.id":"/redfish/v1/Registries"},"JsonSchemas":{"@odata.id":"/redfish/v1/JsonSchemas"},"TelemetryService":{"@odata.id":"/redfish/v1/TelemetryService"},"Product":null,"ServiceIdentification":"S482931X2814218","Links":{"Sessions":{"@odata.id":"/redfish/v1/SessionService/Sessions"}},"Oem":{"Supermicro":{"DumpService":{"@odata.id":"/redfish/v1/Oem/Supermicro/DumpService"}}},"ProtocolFeaturesSupported":{"FilterQuery":true,"SelectQuery":true,"ExcerptQuery":false,"OnlyMemberQuery":false,"DeepOperations":{"DeepPATCH":false,"DeepPOST":false,"MaxLevels":1},"ExpandQuery":{"Links":true,"NoLinks":true,"ExpandAll":true,"Levels":true,"MaxLevels":2}},"@odata.etag":"\"a3ee7c2898ae386781519de584c4dacd\""}
1 change: 1 addition & 0 deletions internal/redfishwrapper/fixtures/smc_1.14.0_systems.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"@odata.type":"#ComputerSystemCollection.ComputerSystemCollection","@odata.id":"/redfish/v1/Systems","Name":"Computer System Collection","Description":"Computer System Collection","[email protected]":1,"Members":[{"@odata.id":"/redfish/v1/Systems/1"}],"@odata.etag":"\"e310554bb25b657853dd0b5f36f07991\""}
1 change: 1 addition & 0 deletions internal/redfishwrapper/fixtures/smc_1.14.0_systems_1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"@odata.type":"#ComputerSystem.v1_16_0.ComputerSystem","@odata.id":"/redfish/v1/Systems/1","Id":"1","Name":"System","Description":"Description of server","Status":{"State":"Enabled","Health":"Critical"},"SerialNumber":"S482931X2814218","PartNumber":"SYS-510T-MR-EI018","AssetTag":null,"IndicatorLED":"Off","LocationIndicatorActive":false,"SystemType":"Physical","BiosVersion":"2.0","Manufacturer":"Supermicro","Model":"SYS-510T-MR-EI018","SKU":"To be filled by O.E.M.","UUID":"B11CC600-6D10-11EC-8000-3CECEFC846F8","ProcessorSummary":{"Count":1,"Model":"Intel(R) Xeon(R) processor","Status":{"State":"Enabled","Health":"OK","HealthRollup":"OK"},"Metrics":{"@odata.id":"/redfish/v1/Systems/1/ProcessorSummary/ProcessorMetrics"}},"MemorySummary":{"TotalSystemMemoryGiB":64,"MemoryMirroring":"System","Status":{"State":"Enabled","Health":"OK","HealthRollup":"OK"},"Metrics":{"@odata.id":"/redfish/v1/Systems/1/MemorySummary/MemoryMetrics"}},"PowerState":"On","PowerOnDelaySeconds":3,"[email protected]":["3:254:1"],"PowerOffDelaySeconds":3,"[email protected]":["3:254:1"],"PowerCycleDelaySeconds":5,"[email protected]":["5:254:1"],"Boot":{"AutomaticRetryConfig":"Disabled","BootSourceOverrideEnabled":"Continuous","BootSourceOverrideMode":"UEFI","BootSourceOverrideTarget":"Hdd","[email protected]":["None","Pxe","Floppy","Cd","Usb","Hdd","BiosSetup","UsbCd","UefiBootNext","UefiHttp"],"BootOptions":{"@odata.id":"/redfish/v1/Systems/1/BootOptions"},"BootNext":null,"BootOrder":["Boot0003","Boot0004","Boot0005","Boot0006","Boot0007","Boot0008","Boot0009","Boot000A","Boot000B","Boot0002"]},"GraphicalConsole":{"ServiceEnabled":true,"Port":5900,"MaxConcurrentSessions":4,"ConnectTypesSupported":["KVMIP"]},"SerialConsole":{"MaxConcurrentSessions":1,"SSH":{"ServiceEnabled":true,"Port":22,"SharedWithManagerCLI":true,"ConsoleEntryCommand":"cd system1/sol1; start","HotKeySequenceDisplay":"press <Enter>, <Esc>, and then <T> to terminate session"},"IPMI":{"HotKeySequenceDisplay":"Press ~. - terminate connection","ServiceEnabled":true,"Port":623}},"VirtualMediaConfig":{"ServiceEnabled":true,"Port":623},"BootProgress":{"OemLastState":null,"LastState":"SystemHardwareInitializationComplete"},"Processors":{"@odata.id":"/redfish/v1/Systems/1/Processors"},"Memory":{"@odata.id":"/redfish/v1/Systems/1/Memory"},"EthernetInterfaces":{"@odata.id":"/redfish/v1/Systems/1/EthernetInterfaces"},"NetworkInterfaces":{"@odata.id":"/redfish/v1/Systems/1/NetworkInterfaces"},"Storage":{"@odata.id":"/redfish/v1/Systems/1/Storage"},"LogServices":{"@odata.id":"/redfish/v1/Systems/1/LogServices"},"SecureBoot":{"@odata.id":"/redfish/v1/Systems/1/SecureBoot"},"Bios":{"@odata.id":"/redfish/v1/Systems/1/Bios"},"VirtualMedia":{"@odata.id":"/redfish/v1/Managers/1/VirtualMedia"},"Links":{"Chassis":[{"@odata.id":"/redfish/v1/Chassis/1"}],"ManagedBy":[{"@odata.id":"/redfish/v1/Managers/1"}],"PoweredBy":[{"@odata.id":"/redfish/v1/Chassis/1/PowerSubsystem/PowerSupplies/1"},{"@odata.id":"/redfish/v1/Chassis/1/PowerSubsystem/PowerSupplies/2"}]},"Actions":{"Oem":{},"#ComputerSystem.Reset":{"target":"/redfish/v1/Systems/1/Actions/ComputerSystem.Reset","@Redfish.ActionInfo":"/redfish/v1/Systems/1/ResetActionInfo","[email protected]":["On","ForceOff","GracefulShutdownGracefulRestart","ForceRestart","Nmi","ForceOn"]}},"Oem":{"Supermicro":{"@odata.type":"#SmcSystemExtensions.v1_0_0.System","NodeManager":{"@odata.id":"/redfish/v1/Systems/1/Oem/Supermicro/NodeManager"}}},"@odata.etag":"\"27ffd39c216000b3013c84008394dffd\""}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"@odata.type":"#ServiceRoot.v1_5_2.ServiceRoot","@odata.id":"/redfish/v1","Id":"ServiceRoot","Name":"Root Service","RedfishVersion":"1.9.0","UUID":"00000000-0000-0000-0000-3CECEFC8484F","Systems":{"@odata.id":"/redfish/v1/Systems"},"Chassis":{"@odata.id":"/redfish/v1/Chassis"},"Managers":{"@odata.id":"/redfish/v1/Managers"},"Tasks":{"@odata.id":"/redfish/v1/TaskService"},"SessionService":{"@odata.id":"/redfish/v1/SessionService"},"AccountService":{"@odata.id":"/redfish/v1/AccountService"},"EventService":{"@odata.id":"/redfish/v1/EventService"},"UpdateService":{"@odata.id":"/redfish/v1/UpdateService"},"CertificateService":{"@odata.id":"/redfish/v1/CertificateService"},"Registries":{"@odata.id":"/redfish/v1/Registries"},"JsonSchemas":{"@odata.id":"/redfish/v1/JsonSchemas"},"TelemetryService":{"@odata.id":"/redfish/v1/TelemetryService"},"Links":{"Sessions":{"@odata.id":"/redfish/v1/SessionService/Sessions"}},"Oem":{"Supermicro":{"DumpService":{"@odata.id":"/redfish/v1/Oem/Supermicro/DumpService"}}},"ProtocolFeaturesSupported":{"FilterQuery":true,"SelectQuery":true,"ExcerptQuery":false,"OnlyMemberQuery":false,"ExpandQuery":{"Links":true,"NoLinks":true,"ExpandAll":true,"Levels":true,"MaxLevels":2}}}
3 changes: 3 additions & 0 deletions providers/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,7 @@ const (

// FeatureGetBiosConfiguration means an implementation that can get bios configuration in a simple k/v map
FeatureGetBiosConfiguration registrar.Feature = "getbiosconfig"

// FeatureBootProgress indicates that the implementation supports reading the BootProgress from the BMC
FeatureBootProgress registrar.Feature = "bootprogress"
)
16 changes: 16 additions & 0 deletions providers/supermicro/supermicro.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/bmc-toolbox/bmclib/v2/internal/sum"
"github.com/bmc-toolbox/bmclib/v2/providers"
"github.com/bmc-toolbox/common"
"github.com/stmcginnis/gofish/redfish"

"github.com/go-logr/logr"
"github.com/jacobweinstock/registrar"
Expand Down Expand Up @@ -56,6 +57,7 @@ var (
providers.FeatureSetBiosConfiguration,
providers.FeatureSetBiosConfigurationFromFile,
providers.FeatureResetBiosConfiguration,
providers.FeatureBootProgress,
}
)

Expand Down Expand Up @@ -120,6 +122,8 @@ type bmcQueryor interface {
// returns the device model, that was queried previously with queryDeviceModel
deviceModel() (model string)
supportsInstall(component string) error
getBootProgress() (*redfish.BootProgress, error)
bootComplete() (bool, error)
}

// New returns connection with a Supermicro client initialized
Expand Down Expand Up @@ -285,6 +289,8 @@ func (c *Client) bmcQueryor(ctx context.Context) (bmcQueryor, error) {
for _, bmc := range []bmcQueryor{x11, x12} {
var err error

// Note to maintainers: x12 lacks support for the ipmi.cgi endpoint,
// which will lead to our graceful handling of ErrXMLAPIUnsupported below.
_, err = bmc.queryDeviceModel(ctx)
if err != nil {
if errors.Is(err, ErrXMLAPIUnsupported) {
Expand Down Expand Up @@ -597,3 +603,13 @@ func hostIP(hostURL string) (string, error) {
func (c *Client) SendNMI(ctx context.Context) error {
return c.serviceClient.redfish.SendNMI(ctx)
}

// GetBootProgress allows a caller to follow along as the system goes through its boot sequence
func (c *Client) GetBootProgress() (*redfish.BootProgress, error) {
return c.bmc.getBootProgress()
}

// BootComplete checks if this system has reached the last state for boot
func (c *Client) BootComplete() (bool, error) {
return c.bmc.bootComplete()
}
9 changes: 9 additions & 0 deletions providers/supermicro/x11.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/bmc-toolbox/common"
"github.com/go-logr/logr"
"github.com/pkg/errors"
"github.com/stmcginnis/gofish/redfish"
"golang.org/x/exp/slices"
)

Expand Down Expand Up @@ -143,3 +144,11 @@ func (c *x11) firmwareTaskStatus(ctx context.Context, component, _ string) (stat

return "", "", errors.Wrap(bmclibErrs.ErrFirmwareTaskStatus, "component unsupported: "+component)
}

func (c *x11) getBootProgress() (*redfish.BootProgress, error) {
return nil, fmt.Errorf("%w: not supported on x11 models", bmclibErrs.ErrRedfishVersionIncompatible)
}

func (c *x11) bootComplete() (bool, error) {
return false, fmt.Errorf("%w: not supported on x11 models", bmclibErrs.ErrRedfishVersionIncompatible)
}
18 changes: 18 additions & 0 deletions providers/supermicro/x12.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,3 +314,21 @@ func (c *x12) firmwareTaskStatus(ctx context.Context, component, taskID string)

return c.redfish.TaskStatus(ctx, taskID)
}

func (c *x12) getBootProgress() (*redfish.BootProgress, error) {
bps, err := c.redfish.GetBootProgress()
if err != nil {
return nil, err
}
return bps[0], nil
}

// this is some syntactic sugar to avoid having to code potentially provider- or model-specific knowledge into a caller
func (c *x12) bootComplete() (bool, error) {
bp, err := c.getBootProgress()
if err != nil {
return false, err
}
// we determined this by experiment on X12STH-SYS with redfish 1.14.0
return bp.LastState == redfish.SystemHardwareInitializationCompleteBootProgressTypes, nil
}

0 comments on commit e0bb584

Please sign in to comment.