Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support BootProgress on SMC X12/X13 #396

Merged
merged 3 commits into from
Oct 4, 2024
Merged
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
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) {
DoctorVin marked this conversation as resolved.
Show resolved Hide resolved
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) {
DoctorVin marked this conversation as resolved.
Show resolved Hide resolved
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
}
Loading