Merge "Add iDRAC ephemeral boot media support"

This commit is contained in:
Zuul 2020-04-21 12:58:20 +00:00 committed by Gerrit Code Review
commit 3040cfbfef
5 changed files with 130 additions and 6 deletions

View File

@ -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)
}

View File

@ -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{})

View File

@ -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,

View File

@ -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)
}

View File

@ -48,7 +48,10 @@ func GetVirtualMedia(types []string) redfishClient.VirtualMedia {
mediaTypes = append(mediaTypes, t)
}
inserted := false
vMedia.MediaTypes = mediaTypes
vMedia.Inserted = &inserted
return vMedia
}