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 <andrew.walters@att.com> Change-Id: Ic9fd9e1493b1ff1bb20e956ae5f821d137c74760
This commit is contained in:
parent
cb59c859cb
commit
e2076191b7
@ -30,6 +30,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
// ClientType is used by other packages as the identifier of the Redfish client.
|
// ClientType is used by other packages as the identifier of the Redfish client.
|
||||||
ClientType string = "redfish"
|
ClientType string = "redfish"
|
||||||
|
mediaEjectDelay = 30 * time.Second
|
||||||
systemActionRetries = 30
|
systemActionRetries = 30
|
||||||
systemRebootDelay = 2 * time.Second
|
systemRebootDelay = 2 * time.Second
|
||||||
)
|
)
|
||||||
@ -140,10 +141,27 @@ func (c *Client) SetVirtualMedia(ctx context.Context, isoPath string) error {
|
|||||||
return err
|
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 := redfishClient.InsertMediaRequestBody{}
|
||||||
vMediaReq.Image = isoPath
|
vMediaReq.Image = isoPath
|
||||||
vMediaReq.Inserted = true
|
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)
|
return ScreenRedfishError(httpResp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,6 +267,8 @@ func TestSetVirtualMediaInsertVirtualMediaError(t *testing.T) {
|
|||||||
Return(testutil.GetMediaCollection([]string{"Cd"}), httpResp, nil)
|
Return(testutil.GetMediaCollection([]string{"Cd"}), httpResp, nil)
|
||||||
m.On("GetManagerVirtualMedia", ctx, testutil.ManagerID, "Cd").Times(1).
|
m.On("GetManagerVirtualMedia", ctx, testutil.ManagerID, "Cd").Times(1).
|
||||||
Return(testutil.GetVirtualMedia([]string{"CD"}), httpResp, nil)
|
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(
|
m.On("InsertVirtualMedia", context.Background(), testutil.ManagerID, "Cd", mock.Anything).Return(
|
||||||
redfishClient.RedfishError{}, &http.Response{StatusCode: 500}, redfishClient.GenericOpenAPIError{})
|
redfishClient.RedfishError{}, &http.Response{StatusCode: 500}, redfishClient.GenericOpenAPIError{})
|
||||||
|
|
||||||
|
85
pkg/remote/redfish/vendors/dell/client.go
vendored
85
pkg/remote/redfish/vendors/dell/client.go
vendored
@ -15,17 +15,35 @@
|
|||||||
package dell
|
package dell
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
redfishAPI "opendev.org/airship/go-redfish/api"
|
redfishAPI "opendev.org/airship/go-redfish/api"
|
||||||
redfishClient "opendev.org/airship/go-redfish/client"
|
redfishClient "opendev.org/airship/go-redfish/client"
|
||||||
|
|
||||||
|
"opendev.org/airship/airshipctl/pkg/log"
|
||||||
"opendev.org/airship/airshipctl/pkg/remote/redfish"
|
"opendev.org/airship/airshipctl/pkg/remote/redfish"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ClientType is used by other packages as the identifier of the Redfish client.
|
// 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
|
// 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
|
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.
|
// NewClient returns a client with the capability to make Redfish requests.
|
||||||
func NewClient(ephemeralNodeID string,
|
func NewClient(ephemeralNodeID string,
|
||||||
isoPath string,
|
isoPath string,
|
||||||
|
26
pkg/remote/redfish/vendors/dell/client_test.go
vendored
26
pkg/remote/redfish/vendors/dell/client_test.go
vendored
@ -13,9 +13,13 @@
|
|||||||
package dell
|
package dell
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
redfishMocks "opendev.org/airship/go-redfish/api/mocks"
|
||||||
|
redfishClient "opendev.org/airship/go-redfish/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -25,10 +29,24 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestNewClient(t *testing.T) {
|
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")
|
_, _, err := NewClient(ephemeralNodeID, isoPath, redfishURL, false, false, "username", "password")
|
||||||
assert.NoError(t, err)
|
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)
|
||||||
|
}
|
||||||
|
@ -48,7 +48,10 @@ func GetVirtualMedia(types []string) redfishClient.VirtualMedia {
|
|||||||
mediaTypes = append(mediaTypes, t)
|
mediaTypes = append(mediaTypes, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inserted := false
|
||||||
|
|
||||||
vMedia.MediaTypes = mediaTypes
|
vMedia.MediaTypes = mediaTypes
|
||||||
|
vMedia.Inserted = &inserted
|
||||||
|
|
||||||
return vMedia
|
return vMedia
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user