From e0bb584397a2e08ff603d37141d64977f099398a Mon Sep 17 00:00:00 2001 From: Doctor Vince Date: Fri, 4 Oct 2024 14:47:01 -0400 Subject: [PATCH] support BootProgress on SMC X12/X13 (#396) * WIP: support BootProgress on SMC X12/X13 * add some requested comments --- errors/errors.go | 3 + internal/redfishwrapper/client.go | 57 ++++++++ internal/redfishwrapper/client_test.go | 123 ++++++++++++++++++ .../fixtures/smc_1.14.0_serviceroot.json | 1 + .../fixtures/smc_1.14.0_systems.json | 1 + .../fixtures/smc_1.14.0_systems_1.json | 1 + .../fixtures/smc_1.9.0_serviceroot.json | 1 + providers/providers.go | 3 + providers/supermicro/supermicro.go | 16 +++ providers/supermicro/x11.go | 9 ++ providers/supermicro/x12.go | 18 +++ 11 files changed, 233 insertions(+) create mode 100644 internal/redfishwrapper/fixtures/smc_1.14.0_serviceroot.json create mode 100644 internal/redfishwrapper/fixtures/smc_1.14.0_systems.json create mode 100644 internal/redfishwrapper/fixtures/smc_1.14.0_systems_1.json create mode 100644 internal/redfishwrapper/fixtures/smc_1.9.0_serviceroot.json diff --git a/errors/errors.go b/errors/errors.go index 19c8c133d..37a7d5d16 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -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") diff --git a/internal/redfishwrapper/client.go b/internal/redfishwrapper/client.go index 5b40cd003..9f7fb0788 100644 --- a/internal/redfishwrapper/client.go +++ b/internal/redfishwrapper/client.go @@ -3,9 +3,11 @@ package redfishwrapper import ( "context" "crypto/x509" + "fmt" "io" "net/http" "os" + "strconv" "strings" "time" @@ -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) } diff --git a/internal/redfishwrapper/client_test.go b/internal/redfishwrapper/client_test.go index a08ba2698..40190753e 100644 --- a/internal/redfishwrapper/client_test.go +++ b/internal/redfishwrapper/client_test.go @@ -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" ) @@ -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) + }) + } + +} diff --git a/internal/redfishwrapper/fixtures/smc_1.14.0_serviceroot.json b/internal/redfishwrapper/fixtures/smc_1.14.0_serviceroot.json new file mode 100644 index 000000000..14c2af1e8 --- /dev/null +++ b/internal/redfishwrapper/fixtures/smc_1.14.0_serviceroot.json @@ -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\""} \ No newline at end of file diff --git a/internal/redfishwrapper/fixtures/smc_1.14.0_systems.json b/internal/redfishwrapper/fixtures/smc_1.14.0_systems.json new file mode 100644 index 000000000..f25f65dd6 --- /dev/null +++ b/internal/redfishwrapper/fixtures/smc_1.14.0_systems.json @@ -0,0 +1 @@ +{"@odata.type":"#ComputerSystemCollection.ComputerSystemCollection","@odata.id":"/redfish/v1/Systems","Name":"Computer System Collection","Description":"Computer System Collection","Members@odata.count":1,"Members":[{"@odata.id":"/redfish/v1/Systems/1"}],"@odata.etag":"\"e310554bb25b657853dd0b5f36f07991\""} \ No newline at end of file diff --git a/internal/redfishwrapper/fixtures/smc_1.14.0_systems_1.json b/internal/redfishwrapper/fixtures/smc_1.14.0_systems_1.json new file mode 100644 index 000000000..a508f9ca2 --- /dev/null +++ b/internal/redfishwrapper/fixtures/smc_1.14.0_systems_1.json @@ -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,"PowerOnDelaySeconds@Redfish.AllowableNumbers":["3:254:1"],"PowerOffDelaySeconds":3,"PowerOffDelaySeconds@Redfish.AllowableNumbers":["3:254:1"],"PowerCycleDelaySeconds":5,"PowerCycleDelaySeconds@Redfish.AllowableNumbers":["5:254:1"],"Boot":{"AutomaticRetryConfig":"Disabled","BootSourceOverrideEnabled":"Continuous","BootSourceOverrideMode":"UEFI","BootSourceOverrideTarget":"Hdd","BootSourceOverrideTarget@Redfish.AllowableValues":["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 , , and then 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","ResetType@Redfish.AllowableValues":["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\""} \ No newline at end of file diff --git a/internal/redfishwrapper/fixtures/smc_1.9.0_serviceroot.json b/internal/redfishwrapper/fixtures/smc_1.9.0_serviceroot.json new file mode 100644 index 000000000..7c22b3267 --- /dev/null +++ b/internal/redfishwrapper/fixtures/smc_1.9.0_serviceroot.json @@ -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}}} \ No newline at end of file diff --git a/providers/providers.go b/providers/providers.go index 2791c7776..c87425808 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -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" ) diff --git a/providers/supermicro/supermicro.go b/providers/supermicro/supermicro.go index e40f4a66c..e7d224f08 100644 --- a/providers/supermicro/supermicro.go +++ b/providers/supermicro/supermicro.go @@ -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" @@ -56,6 +57,7 @@ var ( providers.FeatureSetBiosConfiguration, providers.FeatureSetBiosConfigurationFromFile, providers.FeatureResetBiosConfiguration, + providers.FeatureBootProgress, } ) @@ -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 @@ -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) { @@ -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() +} diff --git a/providers/supermicro/x11.go b/providers/supermicro/x11.go index 08525e7a4..db24f0283 100644 --- a/providers/supermicro/x11.go +++ b/providers/supermicro/x11.go @@ -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" ) @@ -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) +} diff --git a/providers/supermicro/x12.go b/providers/supermicro/x12.go index bcac78596..52893a996 100644 --- a/providers/supermicro/x12.go +++ b/providers/supermicro/x12.go @@ -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 +}