From e2076191b73a1b09d15a7b772294bb28c2c7b001 Mon Sep 17 00:00:00 2001 From: Alan Meadows Date: Wed, 18 Mar 2020 11:01:52 -0700 Subject: [PATCH] Add iDRAC ephemeral boot media support The Dell Redfish implementation slightly deviates from the DMTF Redfish specification. One variation is the Dell specification's classification of virtual media, in which the Dell variation adds support for virtual CDs and virtual floppy drives [0]. In order to perform some actions on Dell hardware, airshipctl needs support for vendor specific clients. This change introduces ephemeral boot media support for iDRAC systems using a proprietary API in the Dell client. During the creation of this change, it was also observed that Redfish calls fail when media is already inserted. This change adds a check to eject media if media is already inserted. [0] https://www.dell.com/support/manuals/us/en/04/idrac9-lifecycle-controller-v3.2-series/idrac_3.21.21.21_redfishapiguide/virtualmedia?guid=guid-d9e76cf6-627d-4cb9-a3de-3f2b88b74cfb&lang=en-us Closes: #139 Co-authored-by: Drew Walters Change-Id: Ic9fd9e1493b1ff1bb20e956ae5f821d137c74760 --- pkg/remote/redfish/client.go | 20 ++++- pkg/remote/redfish/client_test.go | 2 + pkg/remote/redfish/vendors/dell/client.go | 85 ++++++++++++++++++- .../redfish/vendors/dell/client_test.go | 26 +++++- testutil/redfishutils/helpers/helpers.go | 3 + 5 files changed, 130 insertions(+), 6 deletions(-) diff --git a/pkg/remote/redfish/client.go b/pkg/remote/redfish/client.go index e98111641..6d997692c 100644 --- a/pkg/remote/redfish/client.go +++ b/pkg/remote/redfish/client.go @@ -30,6 +30,7 @@ import ( const ( // ClientType is used by other packages as the identifier of the Redfish client. ClientType string = "redfish" + mediaEjectDelay = 30 * time.Second systemActionRetries = 30 systemRebootDelay = 2 * time.Second ) @@ -140,10 +141,27 @@ func (c *Client) SetVirtualMedia(ctx context.Context, isoPath string) error { return err } + // Eject virtual media if it is already inserted + vMediaMgr, httpResp, err := c.RedfishAPI.GetManagerVirtualMedia(ctx, managerID, vMediaID) + if err = ScreenRedfishError(httpResp, err); err != nil { + return err + } + + if *vMediaMgr.Inserted == true { + var emptyBody map[string]interface{} + _, httpResp, err = c.RedfishAPI.EjectVirtualMedia(ctx, managerID, vMediaID, emptyBody) + if err = ScreenRedfishError(httpResp, err); err != nil { + return err + } + + time.Sleep(mediaEjectDelay) + } + vMediaReq := redfishClient.InsertMediaRequestBody{} vMediaReq.Image = isoPath vMediaReq.Inserted = true - _, httpResp, err := c.RedfishAPI.InsertVirtualMedia(ctx, managerID, vMediaID, vMediaReq) + _, httpResp, err = c.RedfishAPI.InsertVirtualMedia(ctx, managerID, vMediaID, vMediaReq) + return ScreenRedfishError(httpResp, err) } diff --git a/pkg/remote/redfish/client_test.go b/pkg/remote/redfish/client_test.go index c8e84d865..309eed1b9 100644 --- a/pkg/remote/redfish/client_test.go +++ b/pkg/remote/redfish/client_test.go @@ -267,6 +267,8 @@ func TestSetVirtualMediaInsertVirtualMediaError(t *testing.T) { Return(testutil.GetMediaCollection([]string{"Cd"}), httpResp, nil) m.On("GetManagerVirtualMedia", ctx, testutil.ManagerID, "Cd").Times(1). Return(testutil.GetVirtualMedia([]string{"CD"}), httpResp, nil) + m.On("GetManagerVirtualMedia", ctx, testutil.ManagerID, "Cd").Times(1). + Return(testutil.GetVirtualMedia([]string{"CD"}), httpResp, nil) m.On("InsertVirtualMedia", context.Background(), testutil.ManagerID, "Cd", mock.Anything).Return( redfishClient.RedfishError{}, &http.Response{StatusCode: 500}, redfishClient.GenericOpenAPIError{}) diff --git a/pkg/remote/redfish/vendors/dell/client.go b/pkg/remote/redfish/vendors/dell/client.go index c1adc7c6f..a9a91c036 100644 --- a/pkg/remote/redfish/vendors/dell/client.go +++ b/pkg/remote/redfish/vendors/dell/client.go @@ -15,17 +15,35 @@ package dell import ( + "bytes" "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" redfishAPI "opendev.org/airship/go-redfish/api" redfishClient "opendev.org/airship/go-redfish/client" + "opendev.org/airship/airshipctl/pkg/log" "opendev.org/airship/airshipctl/pkg/remote/redfish" ) const ( // ClientType is used by other packages as the identifier of the Redfish client. - ClientType string = "redfish-dell" + ClientType = "redfish-dell" + endpointImportSysCFG = "%s/redfish/v1/Managers/%s/Actions/Oem/EID_674_Manager.ImportSystemConfiguration" + vCDBootRequestBody = `{ + "ShareParameters": { + "Target": "ALL" + }, + "ImportBuffer": " + + Enabled + VCD-DVD + + " + }` ) // Client is a wrapper around the standard airshipctl Redfish client. This allows vendor specific Redfish clients to @@ -36,6 +54,71 @@ type Client struct { RedfishCFG *redfishClient.Configuration } +type iDRACAPIRespErr struct { + Err iDRACAPIErr `json:"error"` +} + +type iDRACAPIErr struct { + ExtendedInfo []iDRACAPIExtendedInfo `json:"@Message.ExtendedInfo"` + Code string `json:"code"` + Message string `json:"message"` +} + +type iDRACAPIExtendedInfo struct { + Message string `json:"Message"` + Resolution string `json:"Resolution,omitempty"` +} + +// SetEphemeralBootSourceByType sets the boot source of the ephemeral node to a virtual CD, "VCD-DVD". +func (c *Client) SetEphemeralBootSourceByType(ctx context.Context) error { + managerID, err := redfish.GetManagerID(ctx, c.RedfishAPI, c.EphemeralNodeID()) + if err != nil { + return err + } + + // NOTE(drewwalters96): Setting the boot device to a virtual media type requires an API request to the iDRAC + // actions API. The request is made below using the same HTTP client used by the Redfish API and exposed by the + // standard airshipctl Redfish client. Only iDRAC 9 >= 3.3 is supports this endpoint. + url := fmt.Sprintf(endpointImportSysCFG, c.RedfishCFG.BasePath, managerID) + req, err := http.NewRequest(http.MethodPost, url, bytes.NewBufferString(vCDBootRequestBody)) + if err != nil { + return err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + + if auth, ok := ctx.Value(redfishClient.ContextBasicAuth).(redfishClient.BasicAuth); ok { + req.SetBasicAuth(auth.UserName, auth.Password) + } + + httpResp, err := c.RedfishCFG.HTTPClient.Do(req) + if httpResp.StatusCode != http.StatusAccepted { + body, ok := ioutil.ReadAll(httpResp.Body) + if ok != nil { + log.Debugf("Malformed iDRAC response: %s", body) + return redfish.ErrRedfishClient{Message: "Unable to set boot device. Malformed iDRAC response."} + } + + var iDRACResp iDRACAPIRespErr + ok = json.Unmarshal(body, &iDRACResp) + if ok != nil { + log.Debugf("Malformed iDRAC response: %s", body) + return redfish.ErrRedfishClient{Message: "Unable to set boot device. Malformed iDrac response."} + } + + return redfish.ErrRedfishClient{ + Message: fmt.Sprintf("Unable to set boot device. %s", iDRACResp.Err.ExtendedInfo[0]), + } + } else if err != nil { + return redfish.ErrRedfishClient{Message: fmt.Sprintf("Unable to set boot device. %v", err)} + } + + defer httpResp.Body.Close() + + return nil +} + // NewClient returns a client with the capability to make Redfish requests. func NewClient(ephemeralNodeID string, isoPath string, diff --git a/pkg/remote/redfish/vendors/dell/client_test.go b/pkg/remote/redfish/vendors/dell/client_test.go index afdbec3b1..8a63b8a4a 100644 --- a/pkg/remote/redfish/vendors/dell/client_test.go +++ b/pkg/remote/redfish/vendors/dell/client_test.go @@ -13,9 +13,13 @@ package dell import ( + "net/http" "testing" "github.com/stretchr/testify/assert" + + redfishMocks "opendev.org/airship/go-redfish/api/mocks" + redfishClient "opendev.org/airship/go-redfish/client" ) const ( @@ -25,10 +29,24 @@ const ( ) func TestNewClient(t *testing.T) { - // NOTE(drewwalters96): The Dell client implementation of this method simply creates the standard Redfish - // client. This test verifies that the Dell client creates and stores an instance of the standard client. - - // Create the Dell client _, _, err := NewClient(ephemeralNodeID, isoPath, redfishURL, false, false, "username", "password") assert.NoError(t, err) } + +func TestSetEphemeralBootSourceByTypeGetSystemError(t *testing.T) { + m := &redfishMocks.RedfishAPI{} + defer m.AssertExpectations(t) + + ctx, client, err := NewClient("invalid-server", isoPath, redfishURL, false, false, "", "") + assert.NoError(t, err) + + // Mock redfish get system request + m.On("GetSystem", ctx, client.EphemeralNodeID()).Times(1).Return(redfishClient.ComputerSystem{}, + &http.Response{StatusCode: 500}, redfishClient.GenericOpenAPIError{}) + + // Replace normal API client with mocked API client + client.RedfishAPI = m + + err = client.SetEphemeralBootSourceByType(ctx) + assert.Error(t, err) +} diff --git a/testutil/redfishutils/helpers/helpers.go b/testutil/redfishutils/helpers/helpers.go index 8aa938eb9..c51ecec93 100644 --- a/testutil/redfishutils/helpers/helpers.go +++ b/testutil/redfishutils/helpers/helpers.go @@ -48,7 +48,10 @@ func GetVirtualMedia(types []string) redfishClient.VirtualMedia { mediaTypes = append(mediaTypes, t) } + inserted := false + vMedia.MediaTypes = mediaTypes + vMedia.Inserted = &inserted return vMedia }