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

[2/4] Supermicro redfish methods #370

Merged
merged 17 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
2 changes: 0 additions & 2 deletions providers/redfish/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,7 @@ func TestMain(m *testing.M) {
handler := http.NewServeMux()
handler.HandleFunc("/redfish/v1/", serviceRoot)
handler.HandleFunc("/redfish/v1/SessionService/Sessions", sessionService)
handler.HandleFunc("/redfish/v1/UpdateService/MultipartUpload", multipartUpload)
handler.HandleFunc("/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs?$expand=*($levels=1)", dellJobs)
handler.HandleFunc("/redfish/v1/TaskService/Tasks/", openbmcStatus)

return httptest.NewTLSServer(handler)
}()
Expand Down
21 changes: 0 additions & 21 deletions providers/redfish/tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,27 +99,6 @@ func (c *Conn) GetFirmwareInstallTaskQueued(ctx context.Context, component strin
return task, nil
}

// purgeQueuedFirmwareInstallTask removes any existing queued firmware install task for the given component slug
func (c *Conn) purgeQueuedFirmwareInstallTask(ctx context.Context, component string) error {
vendor, _, err := c.redfishwrapper.DeviceVendorModel(ctx)
if err != nil {
return errors.Wrap(err, "unable to determine device vendor, model attributes")
}

// check an update task for the component is currently scheduled
switch {
case strings.Contains(vendor, constants.Dell):
err = c.dellPurgeScheduledFirmwareInstallJob(component)
default:
err = errors.Wrap(
bmclibErrs.ErrFirmwareInstall,
"Update is already running",
)
}

return err
}

// GetTask returns the current Task fir the given TaskID
func (c *Conn) GetTask(taskID string) (task *gofishrf.Task, err error) {
resp, err := c.redfishwrapper.Get("/redfish/v1/TaskService/Tasks/" + taskID)
Expand Down
31 changes: 31 additions & 0 deletions providers/supermicro/docs/x11.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#### x11 XML API power commands

power-off - immediate - `op=POWER_INFO.XML&r=(1,0)&_=`
power-on - `op=POWER_INFO.XML&r=(1,1)&_=`
power-off - `acpi/orderly - op=POWER_INFO.XML&r=(1,5)&_=`
reset server - cold powercycle - `op=POWER_INFO.XML&r=(1,3)&_=`
power cycle - `op=POWER_INFO.XML&r=(1,2)&_=`


ref invocation
```go
// powerCycle using SMC XML API
func (c *x11) powerCycle(ctx context.Context) (bool, error) {
payload := []byte(`op=POWER_INFO.XML&r=(1,3)&_=`)

headers := map[string]string{
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
}

body, status, err := c.serviceClient.query(ctx, "cgi/ipmi.cgi", http.MethodPost, bytes.NewBuffer(payload), headers, 0)
if err != nil {
return false, err
}

if status != http.StatusOK {
return false, unexpectedResponseErr(payload, body, status)
}

return true, nil
}
```
62 changes: 62 additions & 0 deletions providers/supermicro/fixtures/serviceroot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"@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-3CECEFCEFEDA",
"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"
}
},
"ProtocolFeaturesSupported": {
"FilterQuery": true,
"SelectQuery": true,
"ExcerptQuery": false,
"OnlyMemberQuery": false,
"ExpandQuery": {
"Links": true,
"NoLinks": true,
"ExpandAll": true,
"Levels": true,
"MaxLevels": 2
}
}
}
2 changes: 1 addition & 1 deletion providers/supermicro/floppy.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ var (
)

func (c *Client) floppyImageMounted(ctx context.Context) (bool, error) {
if err := c.openRedfish(ctx); err != nil {
if err := c.serviceClient.redfishSession(ctx); err != nil {
return false, err
}

Expand Down
94 changes: 46 additions & 48 deletions providers/supermicro/supermicro.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"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"
Expand All @@ -46,6 +47,9 @@ var (
providers.FeatureFirmwareInstallUploaded,
providers.FeatureFirmwareTaskStatus,
providers.FeatureFirmwareInstallSteps,
providers.FeatureInventoryRead,
providers.FeaturePowerSet,
providers.FeaturePowerState,
}
)

Expand Down Expand Up @@ -181,9 +185,40 @@ func (c *Client) Open(ctx context.Context) (err error) {
return errors.Wrap(bmclibErrs.ErrLoginFailed, err.Error())
}

if err := c.serviceClient.redfishSession(ctx); err != nil {
return errors.Wrap(bmclibErrs.ErrLoginFailed, err.Error())
}

return nil
}

// PowerStateGet gets the power state of a BMC machine
func (c *Client) PowerStateGet(ctx context.Context) (state string, err error) {
if c.serviceClient == nil || c.serviceClient.redfish == nil {
return "", errors.Wrap(bmclibErrs.ErrLoginFailed, "client not initialized")
}

return c.serviceClient.redfish.SystemPowerStatus(ctx)
}

// PowerSet sets the power state of a server
func (c *Client) PowerSet(ctx context.Context, state string) (ok bool, err error) {
if c.serviceClient == nil || c.serviceClient.redfish == nil {
return false, errors.Wrap(bmclibErrs.ErrLoginFailed, "client not initialized")
}

return c.serviceClient.redfish.PowerSet(ctx, state)
}
Comment on lines +196 to +211
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the state for these methods be a constant with a specific type, something like PowerState?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep thats the plan, although since this will affect all providers and it changes the interface, it needs to be done separately


// Inventory collects hardware inventory and install firmware information
func (c *Client) Inventory(ctx context.Context) (device *common.Device, err error) {
if c.serviceClient == nil || c.serviceClient.redfish == nil {
return nil, errors.Wrap(bmclibErrs.ErrLoginFailed, "client not initialized")
}

return c.serviceClient.redfish.Inventory(ctx, false)
}

func (c *Client) bmcQueryor(ctx context.Context) (bmcQueryor, error) {
x11 := newX11Client(c.serviceClient, c.log)
x12 := newX12Client(c.serviceClient, c.log)
Expand Down Expand Up @@ -218,21 +253,6 @@ func (c *Client) bmcQueryor(ctx context.Context) (bmcQueryor, error) {
return queryor, nil
}

func (c *Client) openRedfish(ctx context.Context) error {
if c.serviceClient.redfish != nil && c.serviceClient.redfish.SessionActive() == nil {
return nil
}

rfclient := redfishwrapper.NewClient(c.serviceClient.host, "", c.serviceClient.user, c.serviceClient.pass)
if err := rfclient.Open(ctx); err != nil {
return err
}

c.serviceClient.redfish = rfclient

return nil
}

func parseToken(body []byte) string {
var key string
if bytes.Contains(body, []byte(`CSRF-TOKEN`)) {
Expand Down Expand Up @@ -349,38 +369,6 @@ func (c *Client) initScreenPreview(ctx context.Context) error {
return nil
}

// PowerSet sets the power state of a server
func (c *Client) PowerSet(ctx context.Context, state string) (ok bool, err error) {
switch strings.ToLower(state) {
case "cycle":
return c.powerCycle(ctx)
default:
return false, errors.New("action not implemented for provider")
}
}

// powerCycle using SMC XML API
//
// This method is only here for the case when firmware updates are being applied using this provider.
func (c *Client) powerCycle(ctx context.Context) (bool, error) {
payload := []byte(`op=SET_POWER_INFO.XML&r=(1,3)&_=`)

headers := map[string]string{
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
}

body, status, err := c.serviceClient.query(ctx, "cgi/ipmi.cgi", http.MethodPost, bytes.NewBuffer(payload), headers, 0)
if err != nil {
return false, err
}

if status != http.StatusOK {
return false, unexpectedResponseErr(payload, body, status)
}

return true, nil
}

type serviceClient struct {
host string
port string
Expand All @@ -404,7 +392,17 @@ func (c *serviceClient) setCsrfToken(t string) {
}

func (c *serviceClient) redfishSession(ctx context.Context) (err error) {
c.redfish = redfishwrapper.NewClient(c.host, "", c.user, c.pass, redfishwrapper.WithHTTPClient(c.client))
if c.redfish != nil && c.redfish.SessionActive() == nil {
return nil
}

c.redfish = redfishwrapper.NewClient(
c.host,
c.port,
c.user,
c.pass,
redfishwrapper.WithHTTPClient(c.client),
)
if err := c.redfish.Open(ctx); err != nil {
return err
}
Expand Down
48 changes: 47 additions & 1 deletion providers/supermicro/supermicro_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"

"github.com/bmc-toolbox/bmclib/v2/internal/redfishwrapper"
"github.com/go-logr/logr"
"github.com/stretchr/testify/assert"
)

const (
fixturesDir = "./fixtures"
)

func TestParseToken(t *testing.T) {
testcases := []struct {
name string
Expand Down Expand Up @@ -66,6 +72,37 @@ func TestParseToken(t *testing.T) {
}
}

func mustReadFile(t *testing.T, filename string) []byte {
t.Helper()

fixture := fixturesDir + "/" + filename
fh, err := os.Open(fixture)
if err != nil {
log.Fatal(err)
}

defer fh.Close()

b, err := io.ReadAll(fh)
if err != nil {
log.Fatal(err)
}

return b
}

var endpointFunc = func(t *testing.T, file string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// expect either GET or Delete methods
if r.Method != http.MethodGet && r.Method != http.MethodPost && r.Method != http.MethodDelete {
w.WriteHeader(http.StatusNotFound)
return
}

_, _ = w.Write(mustReadFile(t, file))
}
}

func TestOpen(t *testing.T) {
type handlerFuncMap map[string]func(http.ResponseWriter, *http.Request)
testcases := []struct {
Expand All @@ -84,6 +121,7 @@ func TestOpen(t *testing.T) {
"/": func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
},
"/redfish/v1/": endpointFunc(t, "serviceroot.json"),
// first request to login
"/cgi/login.cgi": func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, r.Method, http.MethodPost)
Expand Down Expand Up @@ -182,13 +220,21 @@ func TestOpen(t *testing.T) {
server := httptest.NewTLSServer(mux)
defer server.Close()

server.Config.ErrorLog = log.Default()
server.Config.ErrorLog = log.New(os.Stdout, "foo", 3)
parsedURL, err := url.Parse(server.URL)
if err != nil {
t.Fatal(err)
}

client := NewClient(parsedURL.Hostname(), tc.user, tc.pass, logr.Discard(), WithPort(parsedURL.Port()))
client.serviceClient.redfish = redfishwrapper.NewClient(
parsedURL.Hostname(),
parsedURL.Port(),
tc.user,
tc.pass,
redfishwrapper.WithHTTPClient(client.serviceClient.client),
)

err = client.Open(context.Background())
if tc.errorContains != "" {
assert.ErrorContains(t, err, tc.errorContains)
Expand Down
Loading