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": "<SystemConfiguration> + <Component FQDD=\"iDRAC.Embedded.1\"> + <Attribute Name=\"ServerBoot.1#BootOnce\">Enabled</Attribute> + <Attribute Name=\"ServerBoot.1#FirstBootDevice\">VCD-DVD</Attribute> + </Component> + </SystemConfiguration>" + }` ) // 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 }