Use new Redfish client
The remote package in airshipctl is tightly coupled to redfish. In the future, we may need to introduce IPMI or SMASH; however, adding those clients now would be difficult because of our tight dependence on redfish. This change implements usage of the new Redfish client, which implements the generic remote client. It also separates remoteDirect functionality from the redfish package in order to make it more loosely coupled. Relates #5 Closes #122 Change-Id: I45d4ea6e2a4146ea519e94ea701a3ad527e50ca0 Signed-off-by: Drew Walters <andrew.walters@att.com>
This commit is contained in:
parent
bca0a96603
commit
a15f978cad
@ -7,13 +7,18 @@ import (
|
||||
"opendev.org/airship/airshipctl/pkg/remote"
|
||||
)
|
||||
|
||||
// New Bootstrap remote direct subcommand
|
||||
// NewRemoteDirectCommand provides a command with the capability to perform remote direct operations.
|
||||
func NewRemoteDirectCommand(rootSettings *environment.AirshipCTLSettings) *cobra.Command {
|
||||
remoteDirect := &cobra.Command{
|
||||
Use: "remotedirect",
|
||||
Short: "Bootstrap ephemeral node",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return remote.DoRemoteDirect(rootSettings)
|
||||
a, err := remote.NewAdapter(rootSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.DoRemoteDirect()
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,12 @@ func (c *Client) RebootSystem(ctx context.Context, systemID string) error {
|
||||
|
||||
// SetEphemeralBootSourceByType sets the boot source of the ephemeral node to one that's compatible with the boot
|
||||
// source type.
|
||||
func (c *Client) SetEphemeralBootSourceByType(ctx context.Context, mediaType string) error {
|
||||
func (c *Client) SetEphemeralBootSourceByType(ctx context.Context) error {
|
||||
_, vMediaType, err := GetVirtualMediaID(ctx, c.redfishAPI, c.ephemeralNodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Retrieve system information, containing available boot sources
|
||||
system, _, err := c.redfishAPI.GetSystem(ctx, c.ephemeralNodeID)
|
||||
if err != nil {
|
||||
@ -105,7 +110,7 @@ func (c *Client) SetEphemeralBootSourceByType(ctx context.Context, mediaType str
|
||||
|
||||
allowableValues := system.Boot.BootSourceOverrideTargetRedfishAllowableValues
|
||||
for _, bootSource := range allowableValues {
|
||||
if strings.EqualFold(string(bootSource), mediaType) {
|
||||
if strings.EqualFold(string(bootSource), vMediaType) {
|
||||
/* set boot source */
|
||||
systemReq := redfishClient.ComputerSystem{}
|
||||
systemReq.Boot.BootSourceOverrideTarget = bootSource
|
||||
@ -119,17 +124,21 @@ func (c *Client) SetEphemeralBootSourceByType(ctx context.Context, mediaType str
|
||||
|
||||
// SetVirtualMedia injects a virtual media device to an established virtual media ID. This assumes that isoPath is
|
||||
// accessible to the redfish server and virtualMedia device is either of type CD or DVD.
|
||||
func (c *Client) SetVirtualMedia(ctx context.Context, vMediaID string, isoPath string) error {
|
||||
system, _, err := c.redfishAPI.GetSystem(ctx, c.ephemeralNodeID)
|
||||
if err != nil {
|
||||
return ErrRedfishClient{Message: fmt.Sprintf("Get System[%s] failed with err: %v", c.ephemeralNodeID, err)}
|
||||
}
|
||||
|
||||
func (c *Client) SetVirtualMedia(ctx context.Context, isoPath string) error {
|
||||
log.Debugf("Ephemeral Node System ID: '%s'", c.ephemeralNodeID)
|
||||
|
||||
managerID := GetResourceIDFromURL(system.Links.ManagedBy[0].OdataId)
|
||||
managerID, err := getManagerID(ctx, c.redfishAPI, c.ephemeralNodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("Ephemeral node managerID: '%s'", managerID)
|
||||
|
||||
vMediaID, _, err := GetVirtualMediaID(ctx, c.redfishAPI, c.ephemeralNodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vMediaReq := redfishClient.InsertMediaRequestBody{}
|
||||
vMediaReq.Image = isoPath
|
||||
vMediaReq.Inserted = true
|
||||
|
@ -10,6 +10,8 @@ import (
|
||||
|
||||
redfishMocks "opendev.org/airship/go-redfish/api/mocks"
|
||||
redfishClient "opendev.org/airship/go-redfish/client"
|
||||
|
||||
testutil "opendev.org/airship/airshipctl/testutil/redfishutils/helpers"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -18,33 +20,6 @@ const (
|
||||
redfishURL = "https://localhost:1234"
|
||||
)
|
||||
|
||||
func getTestSystem() redfishClient.ComputerSystem {
|
||||
return redfishClient.ComputerSystem{
|
||||
Id: "serverid-00",
|
||||
Name: "server-100",
|
||||
UUID: "58893887-8974-2487-2389-841168418919",
|
||||
Status: redfishClient.Status{
|
||||
State: "Enabled",
|
||||
Health: "OK",
|
||||
},
|
||||
Links: redfishClient.SystemLinks{
|
||||
ManagedBy: []redfishClient.IdRef{
|
||||
{OdataId: "/redfish/v1/Managers/manager-1"},
|
||||
},
|
||||
},
|
||||
Boot: redfishClient.Boot{
|
||||
BootSourceOverrideTarget: redfishClient.BOOTSOURCE_CD,
|
||||
BootSourceOverrideEnabled: redfishClient.BOOTSOURCEOVERRIDEENABLED_CONTINUOUS,
|
||||
BootSourceOverrideTargetRedfishAllowableValues: []redfishClient.BootSource{
|
||||
redfishClient.BOOTSOURCE_CD,
|
||||
redfishClient.BOOTSOURCE_FLOPPY,
|
||||
redfishClient.BOOTSOURCE_HDD,
|
||||
redfishClient.BOOTSOURCE_PXE,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
_, _, err := NewClient(ephemeralNodeID, isoPath, redfishURL, false, false, "", "")
|
||||
assert.NoError(t, err)
|
||||
@ -128,6 +103,7 @@ func TestRebootSystemStartupError(t *testing.T) {
|
||||
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
|
||||
|
||||
// Mock redfish shutdown request
|
||||
systemID := ephemeralNodeID
|
||||
m.On("ResetSystem", ctx, systemID, resetReq).Times(1).Return(redfishClient.RedfishError{},
|
||||
&http.Response{StatusCode: 200}, nil)
|
||||
|
||||
@ -160,6 +136,8 @@ func TestRebootSystemTimeout(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), "numRetries", 1)
|
||||
resetReq := redfishClient.ResetRequestBody{}
|
||||
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
|
||||
|
||||
systemID := ephemeralNodeID
|
||||
m.On("ResetSystem", ctx, systemID, resetReq).
|
||||
Times(1).
|
||||
Return(redfishClient.RedfishError{}, &http.Response{StatusCode: 200}, nil)
|
||||
@ -183,14 +161,13 @@ func TestSetEphemeralBootSourceByTypeGetSystemError(t *testing.T) {
|
||||
|
||||
// Mock redfish get system request
|
||||
m.On("GetSystem", ctx, client.ephemeralNodeID).Times(1).Return(redfishClient.ComputerSystem{},
|
||||
nil, redfishClient.GenericOpenAPIError{})
|
||||
&http.Response{StatusCode: 500}, redfishClient.GenericOpenAPIError{})
|
||||
|
||||
// Replace normal API client with mocked API client
|
||||
client.redfishAPI = m
|
||||
|
||||
err = client.SetEphemeralBootSourceByType(ctx, "CD")
|
||||
_, ok := err.(ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
err = client.SetEphemeralBootSourceByType(ctx)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSetEphemeralBootSourceByTypeSetSystemError(t *testing.T) {
|
||||
@ -200,17 +177,20 @@ func TestSetEphemeralBootSourceByTypeSetSystemError(t *testing.T) {
|
||||
ctx, client, err := NewClient("invalid-server", isoPath, redfishURL, false, false, "", "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
m.On("GetSystem", ctx, client.ephemeralNodeID).Return(getTestSystem(),
|
||||
&http.Response{StatusCode: 200}, nil)
|
||||
httpResp := &http.Response{StatusCode: 200}
|
||||
m.On("GetSystem", ctx, client.ephemeralNodeID).Return(testutil.GetTestSystem(), httpResp, nil)
|
||||
m.On("ListManagerVirtualMedia", ctx, testutil.ManagerID).Times(1).
|
||||
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("SetSystem", ctx, client.ephemeralNodeID, mock.Anything).Times(1).Return(
|
||||
redfishClient.ComputerSystem{}, &http.Response{StatusCode: 401}, redfishClient.GenericOpenAPIError{})
|
||||
|
||||
// Replace normal API client with mocked API client
|
||||
client.redfishAPI = m
|
||||
|
||||
err = client.SetEphemeralBootSourceByType(ctx, "CD")
|
||||
_, ok := err.(ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
err = client.SetEphemeralBootSourceByType(ctx)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSetEphemeralBootSourceByTypeBootSourceUnavailable(t *testing.T) {
|
||||
@ -220,18 +200,23 @@ func TestSetEphemeralBootSourceByTypeBootSourceUnavailable(t *testing.T) {
|
||||
ctx, client, err := NewClient("invalid-server", isoPath, redfishURL, false, false, "", "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
invalidSystem := getTestSystem()
|
||||
invalidSystem := testutil.GetTestSystem()
|
||||
invalidSystem.Boot.BootSourceOverrideTargetRedfishAllowableValues = []redfishClient.BootSource{
|
||||
redfishClient.BOOTSOURCE_HDD,
|
||||
redfishClient.BOOTSOURCE_PXE,
|
||||
}
|
||||
|
||||
httpResp := &http.Response{StatusCode: 200}
|
||||
m.On("GetSystem", ctx, client.ephemeralNodeID).Return(invalidSystem, nil, nil)
|
||||
m.On("ListManagerVirtualMedia", ctx, testutil.ManagerID).Times(1).
|
||||
Return(testutil.GetMediaCollection([]string{"Cd"}), httpResp, nil)
|
||||
m.On("GetManagerVirtualMedia", ctx, testutil.ManagerID, "Cd").Times(1).
|
||||
Return(testutil.GetVirtualMedia([]string{"CD"}), httpResp, nil)
|
||||
|
||||
// Replace normal API client with mocked API client
|
||||
client.redfishAPI = m
|
||||
|
||||
err = client.SetEphemeralBootSourceByType(ctx, "Cd")
|
||||
err = client.SetEphemeralBootSourceByType(ctx)
|
||||
_, ok := err.(ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
@ -250,29 +235,31 @@ func TestSetVirtualMediaGetSystemError(t *testing.T) {
|
||||
// Replace normal API client with mocked API client
|
||||
client.redfishAPI = m
|
||||
|
||||
err = client.SetVirtualMedia(ctx, "CD", client.isoPath)
|
||||
_, ok := err.(ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
err = client.SetVirtualMedia(ctx, client.isoPath)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSetVirtualMediaInsertVirtualMediaError(t *testing.T) {
|
||||
m := &redfishMocks.RedfishAPI{}
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
systemID := ephemeralNodeID
|
||||
ctx, client, err := NewClient(systemID, isoPath, redfishURL, false, false, "", "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
httpResp := &http.Response{StatusCode: 500}
|
||||
m.On("GetSystem", context.Background(), systemID).Times(1).Return(getTestSystem(), nil, nil)
|
||||
|
||||
realErr := redfishClient.GenericOpenAPIError{}
|
||||
m.On("InsertVirtualMedia", context.Background(), "manager-1", "Cd", mock.Anything).Return(
|
||||
redfishClient.RedfishError{}, httpResp, realErr)
|
||||
httpResp := &http.Response{StatusCode: 200}
|
||||
m.On("GetSystem", ctx, client.ephemeralNodeID).Return(testutil.GetTestSystem(), httpResp, nil)
|
||||
m.On("ListManagerVirtualMedia", ctx, testutil.ManagerID).Times(1).
|
||||
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("InsertVirtualMedia", context.Background(), testutil.ManagerID, "Cd", mock.Anything).Return(
|
||||
redfishClient.RedfishError{}, &http.Response{StatusCode: 500}, redfishClient.GenericOpenAPIError{})
|
||||
|
||||
// Replace normal API client with mocked API client
|
||||
client.redfishAPI = m
|
||||
|
||||
err = client.SetVirtualMedia(ctx, "Cd", client.isoPath)
|
||||
err = client.SetVirtualMedia(ctx, client.isoPath)
|
||||
_, ok := err.(ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
@ -1,11 +1,7 @@
|
||||
package redfish
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
redfishClient "opendev.org/airship/go-redfish/client"
|
||||
|
||||
aerror "opendev.org/airship/airshipctl/pkg/errors"
|
||||
)
|
||||
@ -36,35 +32,3 @@ type ErrOperationRetriesExceeded struct {
|
||||
func (e ErrOperationRetriesExceeded) Error() string {
|
||||
return "maximum retries exceeded"
|
||||
}
|
||||
|
||||
// ScreenRedfishError provides detailed error checking on a Redfish client response.
|
||||
func ScreenRedfishError(httpResp *http.Response, clientErr error) error {
|
||||
if httpResp == nil {
|
||||
return ErrRedfishClient{Message: "HTTP request failed. Please try again."}
|
||||
}
|
||||
|
||||
// NOTE(drewwalters96): clientErr may not be nil even though the request was successful. The HTTP status code
|
||||
// has to be verified for success on each request. The Redfish client uses HTTP codes 200 and 204 to indicate
|
||||
// success.
|
||||
if httpResp.StatusCode >= http.StatusOK && httpResp.StatusCode <= http.StatusNoContent {
|
||||
// This range of status codes indicate success
|
||||
return nil
|
||||
}
|
||||
|
||||
if clientErr == nil {
|
||||
return ErrRedfishClient{Message: http.StatusText(httpResp.StatusCode)}
|
||||
}
|
||||
|
||||
oAPIErr, ok := clientErr.(redfishClient.GenericOpenAPIError)
|
||||
if !ok {
|
||||
return ErrRedfishClient{Message: "Unable to decode client error."}
|
||||
}
|
||||
|
||||
var resp redfishClient.RedfishError
|
||||
if err := json.Unmarshal(oAPIErr.Body(), &resp); err != nil {
|
||||
// No JSON response included; use generic error text.
|
||||
return ErrRedfishClient{Message: err.Error()}
|
||||
}
|
||||
|
||||
return ErrRedfishClient{Message: resp.Error.Message}
|
||||
}
|
||||
|
@ -1,173 +0,0 @@
|
||||
package redfish
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
redfishApi "opendev.org/airship/go-redfish/api"
|
||||
redfishClient "opendev.org/airship/go-redfish/client"
|
||||
|
||||
alog "opendev.org/airship/airshipctl/pkg/log"
|
||||
)
|
||||
|
||||
type RemoteDirect struct {
|
||||
|
||||
// Context
|
||||
Context context.Context
|
||||
|
||||
// remote URL
|
||||
RemoteURL url.URL
|
||||
|
||||
// ephemeral Host ID
|
||||
EphemeralNodeID string
|
||||
|
||||
// ISO URL
|
||||
IsoPath string
|
||||
|
||||
// Redfish Client implementation
|
||||
RedfishAPI redfishApi.RedfishAPI
|
||||
|
||||
// optional Username Authentication
|
||||
Username string
|
||||
|
||||
// optional Password
|
||||
Password string
|
||||
}
|
||||
|
||||
// Top level function to handle Redfish remote direct
|
||||
func (cfg RemoteDirect) DoRemoteDirect() error {
|
||||
alog.Debugf("Using Redfish Endpoint: '%s'", cfg.RemoteURL.String())
|
||||
|
||||
/* Get system details */
|
||||
systemID := cfg.EphemeralNodeID
|
||||
system, _, err := cfg.RedfishAPI.GetSystem(cfg.Context, systemID)
|
||||
if err != nil {
|
||||
return ErrRedfishClient{Message: fmt.Sprintf("Get System[%s] failed with err: %v", systemID, err)}
|
||||
}
|
||||
alog.Debugf("Ephemeral Node System ID: '%s'", systemID)
|
||||
|
||||
/* get manager for system */
|
||||
managerID := GetResourceIDFromURL(system.Links.ManagedBy[0].OdataId)
|
||||
alog.Debugf("Ephemeral node managerID: '%s'", managerID)
|
||||
|
||||
/* Get manager's Cd or DVD virtual media ID */
|
||||
vMediaID, vMediaType, err := GetVirtualMediaID(cfg.Context, cfg.RedfishAPI, managerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
alog.Debugf("Ephemeral Node Virtual Media Id: '%s'", vMediaID)
|
||||
|
||||
/* Load ISO in manager's virtual media */
|
||||
err = SetVirtualMedia(cfg.Context, cfg.RedfishAPI, managerID, vMediaID, cfg.IsoPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
alog.Debugf("Successfully loaded virtual media: '%s'", cfg.IsoPath)
|
||||
|
||||
/* Set system's bootsource to selected media */
|
||||
err = SetSystemBootSourceForMediaType(cfg.Context, cfg.RedfishAPI, systemID, vMediaType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
/* Reboot system */
|
||||
err = RebootSystem(cfg.Context, cfg.RedfishAPI, systemID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
alog.Debug("Restarted ephemeral host")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewRedfishRemoteDirectClient creates a new Redfish remote direct client.
|
||||
func NewRedfishRemoteDirectClient(
|
||||
remoteURL string,
|
||||
ephNodeID string,
|
||||
username string,
|
||||
password string,
|
||||
isoPath string,
|
||||
insecure bool,
|
||||
useproxy bool,
|
||||
) (RemoteDirect, error) {
|
||||
if remoteURL == "" {
|
||||
return RemoteDirect{},
|
||||
ErrRedfishMissingConfig{
|
||||
What: "redfish remote url empty",
|
||||
}
|
||||
}
|
||||
|
||||
if ephNodeID == "" {
|
||||
return RemoteDirect{},
|
||||
ErrRedfishMissingConfig{
|
||||
What: "redfish ephemeral node id empty",
|
||||
}
|
||||
}
|
||||
|
||||
var ctx context.Context
|
||||
if username != "" && password != "" {
|
||||
ctx = context.WithValue(
|
||||
context.Background(),
|
||||
redfishClient.ContextBasicAuth,
|
||||
redfishClient.BasicAuth{UserName: username, Password: password},
|
||||
)
|
||||
} else {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
if isoPath == "" {
|
||||
return RemoteDirect{},
|
||||
ErrRedfishMissingConfig{
|
||||
What: "redfish ephemeral node iso Path empty",
|
||||
}
|
||||
}
|
||||
|
||||
cfg := &redfishClient.Configuration{
|
||||
BasePath: remoteURL,
|
||||
DefaultHeader: make(map[string]string),
|
||||
UserAgent: "airshipctl/client",
|
||||
}
|
||||
|
||||
// see https://github.com/golang/go/issues/26013
|
||||
// We clone the default transport to ensure when we customize the transport
|
||||
// that we are providing it sane timeouts and other defaults that we would
|
||||
// normally get when not overriding the transport
|
||||
defaultTransportCopy := (http.DefaultTransport.(*http.Transport))
|
||||
transport := defaultTransportCopy.Clone()
|
||||
|
||||
if insecure {
|
||||
transport.TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: true, //nolint:gosec
|
||||
}
|
||||
}
|
||||
if !useproxy {
|
||||
transport.Proxy = nil
|
||||
}
|
||||
|
||||
cfg.HTTPClient = &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
var api redfishApi.RedfishAPI = redfishClient.NewAPIClient(cfg).DefaultApi
|
||||
|
||||
parsedURL, err := url.Parse(remoteURL)
|
||||
if err != nil {
|
||||
return RemoteDirect{},
|
||||
ErrRedfishMissingConfig{
|
||||
What: fmt.Sprintf("invalid url format: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
client := RemoteDirect{
|
||||
Context: ctx,
|
||||
RemoteURL: *parsedURL,
|
||||
EphemeralNodeID: ephNodeID,
|
||||
IsoPath: isoPath,
|
||||
RedfishAPI: api,
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
@ -1,398 +0,0 @@
|
||||
package redfish_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
redfishAPI "opendev.org/airship/go-redfish/api"
|
||||
redfishMocks "opendev.org/airship/go-redfish/api/mocks"
|
||||
redfishClient "opendev.org/airship/go-redfish/client"
|
||||
|
||||
. "opendev.org/airship/airshipctl/pkg/remote/redfish"
|
||||
testutil "opendev.org/airship/airshipctl/testutil/redfish"
|
||||
)
|
||||
|
||||
const (
|
||||
computerSystemID = "server-100"
|
||||
defaultURL = "https://localhost:1234"
|
||||
)
|
||||
|
||||
func TestRedfishRemoteDirectNormal(t *testing.T) {
|
||||
m := &redfishMocks.RedfishAPI{}
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
systemID := computerSystemID
|
||||
httpResp := &http.Response{StatusCode: 200}
|
||||
|
||||
ctx := context.WithValue(
|
||||
context.Background(),
|
||||
redfishClient.ContextBasicAuth,
|
||||
redfishClient.BasicAuth{UserName: "username", Password: "password"},
|
||||
)
|
||||
|
||||
m.On("GetSystem", ctx, systemID).Times(1).
|
||||
Return(getTestSystem(), httpResp, nil)
|
||||
|
||||
// GetVirtualMediaID() mocks
|
||||
m.On("ListManagerVirtualMedia", ctx, "manager-1").Times(1).
|
||||
Return(testutil.GetMediaCollection([]string{"Cd", "Floppy"}), httpResp, nil)
|
||||
|
||||
m.On("GetManagerVirtualMedia", ctx, "manager-1", "Cd").Times(1).
|
||||
Return(testutil.GetVirtualMedia([]string{"CD"}), httpResp, nil)
|
||||
|
||||
m.On("InsertVirtualMedia", ctx, "manager-1", "Cd", mock.Anything).
|
||||
Return(redfishClient.RedfishError{}, httpResp, nil)
|
||||
|
||||
m.On("GetSystem", ctx, systemID).Times(1).
|
||||
Return(getTestSystem(), httpResp, nil)
|
||||
systemReq := redfishClient.ComputerSystem{
|
||||
Boot: redfishClient.Boot{
|
||||
BootSourceOverrideTarget: redfishClient.BOOTSOURCE_CD,
|
||||
},
|
||||
}
|
||||
m.On("SetSystem", ctx, systemID, systemReq).
|
||||
Times(1).
|
||||
Return(redfishClient.ComputerSystem{}, httpResp, nil)
|
||||
|
||||
resetReq := redfishClient.ResetRequestBody{}
|
||||
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
|
||||
m.On("ResetSystem", ctx, systemID, resetReq).
|
||||
Times(1).
|
||||
Return(redfishClient.RedfishError{}, httpResp, nil)
|
||||
|
||||
m.On("GetSystem", ctx, systemID).Times(1).
|
||||
Return(redfishClient.ComputerSystem{PowerState: redfishClient.POWERSTATE_OFF}, httpResp, nil)
|
||||
|
||||
resetReq.ResetType = redfishClient.RESETTYPE_ON
|
||||
m.On("ResetSystem", ctx, systemID, resetReq).
|
||||
Times(1).
|
||||
Return(redfishClient.RedfishError{}, httpResp, nil)
|
||||
|
||||
m.On("GetSystem", ctx, systemID).Times(1).
|
||||
Return(redfishClient.ComputerSystem{PowerState: redfishClient.POWERSTATE_ON}, httpResp, nil)
|
||||
|
||||
rDCfg := getDefaultRedfishRemoteDirectObj(t, m)
|
||||
|
||||
err := rDCfg.DoRemoteDirect()
|
||||
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRedfishRemoteDirectInvalidSystemId(t *testing.T) {
|
||||
m := &redfishMocks.RedfishAPI{}
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
ctx := context.WithValue(
|
||||
context.Background(),
|
||||
redfishClient.ContextBasicAuth,
|
||||
redfishClient.BasicAuth{UserName: "username", Password: "password"},
|
||||
)
|
||||
|
||||
systemID := "invalid-server"
|
||||
localRDCfg := getDefaultRedfishRemoteDirectObj(t, m)
|
||||
|
||||
localRDCfg.EphemeralNodeID = systemID
|
||||
|
||||
realErr := fmt.Errorf("%s system do not exist", systemID)
|
||||
m.On("GetSystem", ctx, systemID).
|
||||
Times(1).
|
||||
Return(redfishClient.ComputerSystem{}, nil, realErr)
|
||||
|
||||
err := localRDCfg.DoRemoteDirect()
|
||||
|
||||
_, ok := err.(ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestRedfishRemoteDirectGetSystemNetworkError(t *testing.T) {
|
||||
m := &redfishMocks.RedfishAPI{}
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
ctx := context.WithValue(
|
||||
context.Background(),
|
||||
redfishClient.ContextBasicAuth,
|
||||
redfishClient.BasicAuth{UserName: "username", Password: "password"},
|
||||
)
|
||||
|
||||
systemID := computerSystemID
|
||||
realErr := fmt.Errorf("server request timeout")
|
||||
httpResp := &http.Response{StatusCode: 408}
|
||||
m.On("GetSystem", ctx, systemID).
|
||||
Times(1).
|
||||
Return(redfishClient.ComputerSystem{}, httpResp, realErr)
|
||||
|
||||
rDCfg := getDefaultRedfishRemoteDirectObj(t, m)
|
||||
|
||||
err := rDCfg.DoRemoteDirect()
|
||||
|
||||
_, ok := err.(ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestRedfishRemoteDirectInvalidIsoPath(t *testing.T) {
|
||||
m := &redfishMocks.RedfishAPI{}
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
ctx := context.WithValue(
|
||||
context.Background(),
|
||||
redfishClient.ContextBasicAuth,
|
||||
redfishClient.BasicAuth{UserName: "username", Password: "password"},
|
||||
)
|
||||
|
||||
systemID := computerSystemID
|
||||
rDCfg := getDefaultRedfishRemoteDirectObj(t, m)
|
||||
localRDCfg := rDCfg
|
||||
localRDCfg.IsoPath = "bogus/path/to.iso"
|
||||
|
||||
realErr := redfishClient.GenericOpenAPIError{}
|
||||
m.On("GetSystem", ctx, systemID).
|
||||
Times(1).
|
||||
Return(getTestSystem(), nil, nil)
|
||||
|
||||
httpSuccResp := &http.Response{StatusCode: 200}
|
||||
// GetVirtualMediaID() mocks
|
||||
m.On("ListManagerVirtualMedia", ctx, "manager-1").Times(1).
|
||||
Return(testutil.GetMediaCollection([]string{"Cd", "Floppy"}), httpSuccResp, nil)
|
||||
|
||||
m.On("GetManagerVirtualMedia", ctx, "manager-1", "Cd").Times(1).
|
||||
Return(testutil.GetVirtualMedia([]string{"CD"}), httpSuccResp, nil)
|
||||
|
||||
httpResp := &http.Response{StatusCode: 500}
|
||||
m.On("InsertVirtualMedia", ctx, "manager-1", "Cd", mock.Anything).
|
||||
Return(redfishClient.RedfishError{}, httpResp, realErr)
|
||||
|
||||
err := localRDCfg.DoRemoteDirect()
|
||||
|
||||
_, ok := err.(ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestRedfishRemoteDirectCdDvdNotAvailableInBootSources(t *testing.T) {
|
||||
m := &redfishMocks.RedfishAPI{}
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
ctx := context.WithValue(
|
||||
context.Background(),
|
||||
redfishClient.ContextBasicAuth,
|
||||
redfishClient.BasicAuth{UserName: "username", Password: "password"},
|
||||
)
|
||||
|
||||
systemID := computerSystemID
|
||||
invalidSystem := getTestSystem()
|
||||
invalidSystem.Boot.BootSourceOverrideTargetRedfishAllowableValues = []redfishClient.BootSource{
|
||||
redfishClient.BOOTSOURCE_HDD,
|
||||
redfishClient.BOOTSOURCE_PXE,
|
||||
}
|
||||
|
||||
m.On("GetSystem", ctx, systemID).
|
||||
Return(invalidSystem, nil, nil)
|
||||
|
||||
// GetVirtualMediaID() mocks
|
||||
httpResp := &http.Response{StatusCode: 200}
|
||||
m.On("ListManagerVirtualMedia", ctx, "manager-1").Times(1).
|
||||
Return(testutil.GetMediaCollection([]string{"Cd", "Floppy"}), httpResp, nil)
|
||||
|
||||
m.On("GetManagerVirtualMedia", ctx, "manager-1", "Cd").Times(1).
|
||||
Return(testutil.GetVirtualMedia([]string{"CD"}), httpResp, nil)
|
||||
|
||||
m.On("InsertVirtualMedia", ctx, "manager-1", "Cd", mock.Anything).
|
||||
Return(redfishClient.RedfishError{}, nil, nil)
|
||||
|
||||
rDCfg := getDefaultRedfishRemoteDirectObj(t, m)
|
||||
|
||||
err := rDCfg.DoRemoteDirect()
|
||||
|
||||
_, ok := err.(ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestRedfishRemoteDirectSetSystemBootSourceFailed(t *testing.T) {
|
||||
m := &redfishMocks.RedfishAPI{}
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
ctx := context.WithValue(
|
||||
context.Background(),
|
||||
redfishClient.ContextBasicAuth,
|
||||
redfishClient.BasicAuth{UserName: "username", Password: "password"},
|
||||
)
|
||||
|
||||
systemID := computerSystemID
|
||||
httpSuccResp := &http.Response{StatusCode: 200}
|
||||
|
||||
m.On("GetSystem", ctx, systemID).
|
||||
Return(getTestSystem(), httpSuccResp, nil)
|
||||
|
||||
// GetVirtualMediaID() mocks
|
||||
m.On("ListManagerVirtualMedia", ctx, "manager-1").Times(1).
|
||||
Return(testutil.GetMediaCollection([]string{"Cd", "Floppy"}), httpSuccResp, nil)
|
||||
|
||||
m.On("GetManagerVirtualMedia", ctx, "manager-1", "Cd").Times(1).
|
||||
Return(testutil.GetVirtualMedia([]string{"CD"}), httpSuccResp, nil)
|
||||
|
||||
m.On("InsertVirtualMedia", ctx, "manager-1", "Cd", mock.Anything).
|
||||
Return(redfishClient.RedfishError{}, httpSuccResp, nil)
|
||||
|
||||
m.On("SetSystem", ctx, systemID, mock.Anything).
|
||||
Times(1).
|
||||
Return(redfishClient.ComputerSystem{}, &http.Response{StatusCode: 401},
|
||||
redfishClient.GenericOpenAPIError{})
|
||||
|
||||
rDCfg := getDefaultRedfishRemoteDirectObj(t, m)
|
||||
|
||||
err := rDCfg.DoRemoteDirect()
|
||||
|
||||
_, ok := err.(ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestRedfishRemoteDirectSystemRebootFailed(t *testing.T) {
|
||||
m := &redfishMocks.RedfishAPI{}
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
ctx := context.WithValue(
|
||||
context.Background(),
|
||||
redfishClient.ContextBasicAuth,
|
||||
redfishClient.BasicAuth{UserName: "username", Password: "password"},
|
||||
)
|
||||
|
||||
systemID := computerSystemID
|
||||
httpSuccResp := &http.Response{StatusCode: 200}
|
||||
m.On("GetSystem", ctx, systemID).
|
||||
Return(getTestSystem(), httpSuccResp, nil)
|
||||
|
||||
// GetVirtualMediaID() mocks
|
||||
m.On("ListManagerVirtualMedia", ctx, "manager-1").Times(1).
|
||||
Return(testutil.GetMediaCollection([]string{"Cd", "Floppy"}), httpSuccResp, nil)
|
||||
|
||||
m.On("GetManagerVirtualMedia", ctx, "manager-1", "Cd").Times(1).
|
||||
Return(testutil.GetVirtualMedia([]string{"CD"}), httpSuccResp, nil)
|
||||
|
||||
m.On("InsertVirtualMedia", ctx, mock.Anything, mock.Anything, mock.Anything).
|
||||
Return(redfishClient.RedfishError{}, httpSuccResp, nil)
|
||||
|
||||
m.On("SetSystem", ctx, systemID, mock.Anything).
|
||||
Times(1).
|
||||
Return(redfishClient.ComputerSystem{}, httpSuccResp, nil)
|
||||
|
||||
resetReq := redfishClient.ResetRequestBody{}
|
||||
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
|
||||
m.On("ResetSystem", ctx, systemID, resetReq).
|
||||
Times(1).
|
||||
Return(redfishClient.RedfishError{}, &http.Response{StatusCode: 401},
|
||||
redfishClient.GenericOpenAPIError{})
|
||||
|
||||
rDCfg := getDefaultRedfishRemoteDirectObj(t, m)
|
||||
|
||||
err := rDCfg.DoRemoteDirect()
|
||||
|
||||
_, ok := err.(ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func getTestSystem() redfishClient.ComputerSystem {
|
||||
return redfishClient.ComputerSystem{
|
||||
Id: computerSystemID,
|
||||
Name: "server-100",
|
||||
UUID: "58893887-8974-2487-2389-841168418919",
|
||||
Status: redfishClient.Status{
|
||||
State: "Enabled",
|
||||
Health: "OK",
|
||||
},
|
||||
Links: redfishClient.SystemLinks{
|
||||
ManagedBy: []redfishClient.IdRef{
|
||||
{OdataId: "/redfish/v1/Managers/manager-1"},
|
||||
},
|
||||
},
|
||||
Boot: redfishClient.Boot{
|
||||
BootSourceOverrideTarget: redfishClient.BOOTSOURCE_CD,
|
||||
BootSourceOverrideEnabled: redfishClient.BOOTSOURCEOVERRIDEENABLED_CONTINUOUS,
|
||||
BootSourceOverrideTargetRedfishAllowableValues: []redfishClient.BootSource{
|
||||
redfishClient.BOOTSOURCE_CD,
|
||||
redfishClient.BOOTSOURCE_FLOPPY,
|
||||
redfishClient.BOOTSOURCE_HDD,
|
||||
redfishClient.BOOTSOURCE_PXE,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRedfishRemoteDirectClient(t *testing.T) {
|
||||
m := &redfishMocks.RedfishAPI{}
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
_, err := NewRedfishRemoteDirectClient(
|
||||
defaultURL,
|
||||
computerSystemID,
|
||||
"username",
|
||||
"password",
|
||||
"/tmp/test.iso",
|
||||
true,
|
||||
false,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test with empty remote URL
|
||||
_, err = NewRedfishRemoteDirectClient(
|
||||
"",
|
||||
computerSystemID,
|
||||
"username",
|
||||
"password",
|
||||
"/tmp/test.iso",
|
||||
false,
|
||||
false,
|
||||
)
|
||||
expectedError := "missing configuration: redfish remote url empty"
|
||||
assert.EqualError(t, err, expectedError)
|
||||
|
||||
// Test with empty ephemeral NodeID
|
||||
_, err = NewRedfishRemoteDirectClient(
|
||||
defaultURL,
|
||||
"",
|
||||
"username",
|
||||
"password",
|
||||
"/tmp/test.iso",
|
||||
false,
|
||||
false,
|
||||
)
|
||||
expectedError = "missing configuration: redfish ephemeral node id empty"
|
||||
assert.EqualError(t, err, expectedError)
|
||||
|
||||
// Test with empty Iso Path
|
||||
_, err = NewRedfishRemoteDirectClient(
|
||||
defaultURL,
|
||||
computerSystemID,
|
||||
"username",
|
||||
"password",
|
||||
"",
|
||||
false,
|
||||
false,
|
||||
)
|
||||
expectedError = "missing configuration: redfish ephemeral node iso Path empty"
|
||||
assert.EqualError(t, err, expectedError)
|
||||
}
|
||||
|
||||
func getDefaultRedfishRemoteDirectObj(t *testing.T, api redfishAPI.RedfishAPI) RemoteDirect {
|
||||
t.Helper()
|
||||
|
||||
rDCfg, err := NewRedfishRemoteDirectClient(
|
||||
defaultURL,
|
||||
computerSystemID,
|
||||
"username",
|
||||
"password",
|
||||
"/tmp/test.iso",
|
||||
false,
|
||||
false,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
rDCfg.RedfishAPI = api
|
||||
|
||||
return rDCfg
|
||||
}
|
@ -2,20 +2,18 @@ package redfish
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
redfishApi "opendev.org/airship/go-redfish/api"
|
||||
redfishAPI "opendev.org/airship/go-redfish/api"
|
||||
redfishClient "opendev.org/airship/go-redfish/client"
|
||||
|
||||
"opendev.org/airship/airshipctl/pkg/log"
|
||||
)
|
||||
|
||||
const (
|
||||
SystemRebootDelay = 2 * time.Second
|
||||
SystemActionRetries = 30
|
||||
RedfishURLSchemeSeparator = "+"
|
||||
)
|
||||
|
||||
@ -47,7 +45,12 @@ func IsIDInList(idRefList []redfishClient.IdRef, id string) bool {
|
||||
}
|
||||
|
||||
// GetVirtualMediaID retrieves the ID of a Redfish virtual media resource if it supports type "CD" or "DVD".
|
||||
func GetVirtualMediaID(ctx context.Context, api redfishApi.RedfishAPI, managerID string) (string, string, error) {
|
||||
func GetVirtualMediaID(ctx context.Context, api redfishAPI.RedfishAPI, systemID string) (string, string, error) {
|
||||
managerID, err := getManagerID(ctx, api, systemID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
mediaCollection, httpResp, err := api.ListManagerVirtualMedia(ctx, managerID)
|
||||
if err = ScreenRedfishError(httpResp, err); err != nil {
|
||||
return "", "", err
|
||||
@ -72,88 +75,43 @@ func GetVirtualMediaID(ctx context.Context, api redfishApi.RedfishAPI, managerID
|
||||
return "", "", ErrRedfishClient{Message: "Unable to find virtual media with type CD or DVD"}
|
||||
}
|
||||
|
||||
// This function walks through the bootsources of a system and sets the bootsource
|
||||
// which is compatible with the given media type.
|
||||
func SetSystemBootSourceForMediaType(ctx context.Context,
|
||||
api redfishApi.RedfishAPI,
|
||||
systemID string,
|
||||
mediaType string) error {
|
||||
/* Check available boot sources for system */
|
||||
// ScreenRedfishError provides detailed error checking on a Redfish client response.
|
||||
func ScreenRedfishError(httpResp *http.Response, clientErr error) error {
|
||||
if httpResp == nil {
|
||||
return ErrRedfishClient{Message: "HTTP request failed. Please try again."}
|
||||
}
|
||||
|
||||
// NOTE(drewwalters96): clientErr may not be nil even though the request was successful. The HTTP status code
|
||||
// has to be verified for success on each request. The Redfish client uses HTTP codes 200 and 204 to indicate
|
||||
// success.
|
||||
if httpResp.StatusCode >= http.StatusOK && httpResp.StatusCode <= http.StatusNoContent {
|
||||
// This range of status codes indicate success
|
||||
return nil
|
||||
}
|
||||
|
||||
if clientErr == nil {
|
||||
return ErrRedfishClient{Message: http.StatusText(httpResp.StatusCode)}
|
||||
}
|
||||
|
||||
oAPIErr, ok := clientErr.(redfishClient.GenericOpenAPIError)
|
||||
if !ok {
|
||||
return ErrRedfishClient{Message: "Unable to decode client error."}
|
||||
}
|
||||
|
||||
var resp redfishClient.RedfishError
|
||||
if err := json.Unmarshal(oAPIErr.Body(), &resp); err != nil {
|
||||
// No JSON response included; use generic error text.
|
||||
return ErrRedfishClient{Message: err.Error()}
|
||||
}
|
||||
|
||||
return ErrRedfishClient{Message: resp.Error.Message}
|
||||
}
|
||||
|
||||
func getManagerID(ctx context.Context, api redfishAPI.RedfishAPI, systemID string) (string, error) {
|
||||
system, _, err := api.GetSystem(ctx, systemID)
|
||||
if err != nil {
|
||||
return ErrRedfishClient{Message: fmt.Sprintf("Get System[%s] failed with err: %v", systemID, err)}
|
||||
return "", err
|
||||
}
|
||||
|
||||
allowableValues := system.Boot.BootSourceOverrideTargetRedfishAllowableValues
|
||||
for _, bootSource := range allowableValues {
|
||||
if strings.EqualFold(string(bootSource), mediaType) {
|
||||
/* set boot source */
|
||||
systemReq := redfishClient.ComputerSystem{}
|
||||
systemReq.Boot.BootSourceOverrideTarget = bootSource
|
||||
_, httpResp, err := api.SetSystem(ctx, systemID, systemReq)
|
||||
return ScreenRedfishError(httpResp, err)
|
||||
}
|
||||
}
|
||||
|
||||
return ErrRedfishClient{Message: fmt.Sprintf("failed to set system[%s] boot source", systemID)}
|
||||
}
|
||||
|
||||
// Reboots a system by force shutoff and turning on.
|
||||
func RebootSystem(ctx context.Context, api redfishApi.RedfishAPI, systemID string) error {
|
||||
waitForPowerState := func(desiredState redfishClient.PowerState) error {
|
||||
// Check if number of retries is defined in context
|
||||
totalRetries, ok := ctx.Value("numRetries").(int)
|
||||
if !ok {
|
||||
totalRetries = SystemActionRetries
|
||||
}
|
||||
|
||||
for retry := 0; retry <= totalRetries; retry++ {
|
||||
system, httpResp, err := api.GetSystem(ctx, systemID)
|
||||
if err = ScreenRedfishError(httpResp, err); err != nil {
|
||||
return err
|
||||
}
|
||||
if system.PowerState == desiredState {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(SystemRebootDelay)
|
||||
}
|
||||
return ErrOperationRetriesExceeded{}
|
||||
}
|
||||
|
||||
resetReq := redfishClient.ResetRequestBody{}
|
||||
|
||||
// Send PowerOff request
|
||||
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
|
||||
_, httpResp, err := api.ResetSystem(ctx, systemID, resetReq)
|
||||
if err = ScreenRedfishError(httpResp, err); err != nil {
|
||||
return err
|
||||
}
|
||||
// Check that node is powered off
|
||||
if err = waitForPowerState(redfishClient.POWERSTATE_OFF); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send PowerOn request
|
||||
resetReq.ResetType = redfishClient.RESETTYPE_ON
|
||||
_, httpResp, err = api.ResetSystem(ctx, systemID, resetReq)
|
||||
if err = ScreenRedfishError(httpResp, err); err != nil {
|
||||
return err
|
||||
}
|
||||
// Check that node is powered on and return
|
||||
return waitForPowerState(redfishClient.POWERSTATE_ON)
|
||||
}
|
||||
|
||||
// Insert the remote virtual media to the given virtual media id.
|
||||
// This assumes that isoPath is accessible to the redfish server and
|
||||
// virtualMedia device is either of type CD or DVD.
|
||||
func SetVirtualMedia(ctx context.Context,
|
||||
api redfishApi.RedfishAPI,
|
||||
managerID string,
|
||||
vMediaID string,
|
||||
isoPath string) error {
|
||||
vMediaReq := redfishClient.InsertMediaRequestBody{}
|
||||
vMediaReq.Image = isoPath
|
||||
vMediaReq.Inserted = true
|
||||
_, httpResp, err := api.InsertVirtualMedia(ctx, managerID, vMediaID, vMediaReq)
|
||||
return ScreenRedfishError(httpResp, err)
|
||||
return GetResourceIDFromURL(system.Links.ManagedBy[0].OdataId), nil
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package redfish
|
||||
package redfish_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -6,42 +6,40 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
redfishMocks "opendev.org/airship/go-redfish/api/mocks"
|
||||
redfishClient "opendev.org/airship/go-redfish/client"
|
||||
|
||||
testutil "opendev.org/airship/airshipctl/testutil/redfish"
|
||||
)
|
||||
|
||||
const (
|
||||
systemID = "123"
|
||||
"opendev.org/airship/airshipctl/pkg/remote/redfish"
|
||||
testutil "opendev.org/airship/airshipctl/testutil/redfishutils/helpers"
|
||||
)
|
||||
|
||||
func TestRedfishErrorNoError(t *testing.T) {
|
||||
err := ScreenRedfishError(&http.Response{StatusCode: 200}, nil)
|
||||
err := redfish.ScreenRedfishError(&http.Response{StatusCode: 200}, nil)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRedfishErrorNonNilErrorWithoutHttpResp(t *testing.T) {
|
||||
err := ScreenRedfishError(nil, redfishClient.GenericOpenAPIError{})
|
||||
err := redfish.ScreenRedfishError(nil, redfishClient.GenericOpenAPIError{})
|
||||
|
||||
_, ok := err.(ErrRedfishClient)
|
||||
_, ok := err.(redfish.ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestRedfishErrorNonNilErrorWithHttpRespError(t *testing.T) {
|
||||
respErr := redfishClient.GenericOpenAPIError{}
|
||||
|
||||
err := ScreenRedfishError(&http.Response{StatusCode: 408}, respErr)
|
||||
_, ok := err.(ErrRedfishClient)
|
||||
err := redfish.ScreenRedfishError(&http.Response{StatusCode: 408}, respErr)
|
||||
_, ok := err.(redfish.ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
|
||||
err = ScreenRedfishError(&http.Response{StatusCode: 500}, respErr)
|
||||
_, ok = err.(ErrRedfishClient)
|
||||
err = redfish.ScreenRedfishError(&http.Response{StatusCode: 500}, respErr)
|
||||
_, ok = err.(redfish.ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
|
||||
err = ScreenRedfishError(&http.Response{StatusCode: 199}, respErr)
|
||||
_, ok = err.(ErrRedfishClient)
|
||||
err = redfish.ScreenRedfishError(&http.Response{StatusCode: 199}, respErr)
|
||||
_, ok = err.(redfish.ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
@ -49,27 +47,27 @@ func TestRedfishErrorNonNilErrorWithHttpRespOK(t *testing.T) {
|
||||
respErr := redfishClient.GenericOpenAPIError{}
|
||||
|
||||
// NOTE: Redfish client only uses HTTP 200 & HTTP 204 for success.
|
||||
err := ScreenRedfishError(&http.Response{StatusCode: 204}, respErr)
|
||||
err := redfish.ScreenRedfishError(&http.Response{StatusCode: 204}, respErr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = ScreenRedfishError(&http.Response{StatusCode: 200}, respErr)
|
||||
err = redfish.ScreenRedfishError(&http.Response{StatusCode: 200}, respErr)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRedfishUtilGetResIDFromURL(t *testing.T) {
|
||||
// simple case
|
||||
url := "api/user/123"
|
||||
id := GetResourceIDFromURL(url)
|
||||
id := redfish.GetResourceIDFromURL(url)
|
||||
assert.Equal(t, id, "123")
|
||||
|
||||
// FQDN
|
||||
url = "http://www.abc.com/api/user/123"
|
||||
id = GetResourceIDFromURL(url)
|
||||
id = redfish.GetResourceIDFromURL(url)
|
||||
assert.Equal(t, id, "123")
|
||||
|
||||
//Trailing slash
|
||||
url = "api/user/123/"
|
||||
id = GetResourceIDFromURL(url)
|
||||
id = redfish.GetResourceIDFromURL(url)
|
||||
assert.Equal(t, id, "123")
|
||||
}
|
||||
|
||||
@ -82,13 +80,13 @@ func TestRedfishUtilIsIdInList(t *testing.T) {
|
||||
}
|
||||
var emptyList []redfishClient.IdRef
|
||||
|
||||
res := IsIDInList(idList, "1")
|
||||
res := redfish.IsIDInList(idList, "1")
|
||||
assert.True(t, res)
|
||||
|
||||
res = IsIDInList(idList, "100")
|
||||
res = redfish.IsIDInList(idList, "100")
|
||||
assert.False(t, res)
|
||||
|
||||
res = IsIDInList(emptyList, "1")
|
||||
res = redfish.IsIDInList(emptyList, "1")
|
||||
assert.False(t, res)
|
||||
}
|
||||
|
||||
@ -97,19 +95,21 @@ func TestGetVirtualMediaID(t *testing.T) {
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
ctx := context.Background()
|
||||
managerID := "manager-090102"
|
||||
httpResp := &http.Response{StatusCode: 200}
|
||||
|
||||
m.On("ListManagerVirtualMedia", ctx, managerID).Times(1).
|
||||
m.On("GetSystem", ctx, mock.Anything).
|
||||
Return(testutil.GetTestSystem(), &http.Response{StatusCode: 200}, nil)
|
||||
|
||||
m.On("ListManagerVirtualMedia", ctx, testutil.ManagerID).Times(1).
|
||||
Return(testutil.GetMediaCollection([]string{"Floppy", "Cd"}), httpResp, nil)
|
||||
|
||||
m.On("GetManagerVirtualMedia", ctx, managerID, "Floppy").Times(1).
|
||||
m.On("GetManagerVirtualMedia", ctx, testutil.ManagerID, "Floppy").Times(1).
|
||||
Return(testutil.GetVirtualMedia([]string{"Floppy", "USBStick"}), httpResp, nil)
|
||||
|
||||
m.On("GetManagerVirtualMedia", ctx, managerID, "Cd").Times(1).
|
||||
m.On("GetManagerVirtualMedia", ctx, testutil.ManagerID, "Cd").Times(1).
|
||||
Return(testutil.GetVirtualMedia([]string{"CD"}), httpResp, nil)
|
||||
|
||||
mediaID, mediaType, err := GetVirtualMediaID(ctx, m, managerID)
|
||||
mediaID, mediaType, err := redfish.GetVirtualMediaID(ctx, m, testutil.ManagerID)
|
||||
assert.Equal(t, mediaID, "Cd")
|
||||
assert.Equal(t, mediaType, "CD")
|
||||
assert.NoError(t, err)
|
||||
@ -120,12 +120,18 @@ func TestGetVirtualMediaIDNoMedia(t *testing.T) {
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
ctx := context.Background()
|
||||
managerID := "manager-090102"
|
||||
httpResp := &http.Response{StatusCode: 200}
|
||||
|
||||
m.On("ListManagerVirtualMedia", ctx, managerID).Times(1).Return(redfishClient.Collection{}, httpResp, nil)
|
||||
// Remove available media types from test system
|
||||
system := testutil.GetTestSystem()
|
||||
system.Boot.BootSourceOverrideTargetRedfishAllowableValues = []redfishClient.BootSource{}
|
||||
m.On("GetSystem", ctx, mock.Anything).
|
||||
Return(system, &http.Response{StatusCode: 200}, nil)
|
||||
|
||||
mediaID, mediaType, err := GetVirtualMediaID(ctx, m, managerID)
|
||||
m.On("ListManagerVirtualMedia", ctx, testutil.ManagerID).Times(1).
|
||||
Return(redfishClient.Collection{}, httpResp, nil)
|
||||
|
||||
mediaID, mediaType, err := redfish.GetVirtualMediaID(ctx, m, testutil.ManagerID)
|
||||
assert.Empty(t, mediaID)
|
||||
assert.Empty(t, mediaType)
|
||||
assert.Error(t, err)
|
||||
@ -136,122 +142,23 @@ func TestGetVirtualMediaIDUnacceptableMediaTypes(t *testing.T) {
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
ctx := context.Background()
|
||||
managerID := "manager-090102"
|
||||
httpResp := &http.Response{StatusCode: 200}
|
||||
|
||||
m.On("ListManagerVirtualMedia", ctx, managerID).Times(1).
|
||||
system := testutil.GetTestSystem()
|
||||
system.Boot.BootSourceOverrideTargetRedfishAllowableValues = []redfishClient.BootSource{
|
||||
redfishClient.BOOTSOURCE_PXE,
|
||||
}
|
||||
m.On("GetSystem", ctx, mock.Anything).
|
||||
Return(system, &http.Response{StatusCode: 200}, nil)
|
||||
|
||||
m.On("ListManagerVirtualMedia", ctx, testutil.ManagerID).Times(1).
|
||||
Return(testutil.GetMediaCollection([]string{"Floppy"}), httpResp, nil)
|
||||
|
||||
m.On("GetManagerVirtualMedia", ctx, managerID, "Floppy").Times(1).
|
||||
m.On("GetManagerVirtualMedia", ctx, testutil.ManagerID, "Floppy").Times(1).
|
||||
Return(testutil.GetVirtualMedia([]string{"Floppy", "USBStick"}), httpResp, nil)
|
||||
|
||||
mediaID, mediaType, err := GetVirtualMediaID(ctx, m, managerID)
|
||||
mediaID, mediaType, err := redfish.GetVirtualMediaID(ctx, m, testutil.ManagerID)
|
||||
assert.Empty(t, mediaID)
|
||||
assert.Empty(t, mediaType)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRedfishUtilRebootSystemOK(t *testing.T) {
|
||||
m := &redfishMocks.RedfishAPI{}
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
httpResp := &http.Response{StatusCode: 200}
|
||||
ctx := context.Background()
|
||||
resetReq := redfishClient.ResetRequestBody{}
|
||||
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
|
||||
m.On("ResetSystem", ctx, systemID, resetReq).
|
||||
Times(1).
|
||||
Return(redfishClient.RedfishError{}, httpResp, nil)
|
||||
|
||||
m.On("GetSystem", ctx, systemID).Times(1).
|
||||
Return(redfishClient.ComputerSystem{PowerState: redfishClient.POWERSTATE_OFF}, httpResp, nil)
|
||||
|
||||
resetReq.ResetType = redfishClient.RESETTYPE_ON
|
||||
m.On("ResetSystem", ctx, systemID, resetReq).
|
||||
Times(1).
|
||||
Return(redfishClient.RedfishError{}, httpResp, nil)
|
||||
|
||||
m.On("GetSystem", ctx, systemID).Times(1).
|
||||
Return(redfishClient.ComputerSystem{PowerState: redfishClient.POWERSTATE_ON}, httpResp, nil)
|
||||
|
||||
err := RebootSystem(ctx, m, systemID)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRedfishUtilRebootSystemForceOffError2(t *testing.T) {
|
||||
m := &redfishMocks.RedfishAPI{}
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
ctx := context.Background()
|
||||
resetReq := redfishClient.ResetRequestBody{}
|
||||
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
|
||||
|
||||
m.On("ResetSystem", ctx, systemID, resetReq).
|
||||
Times(1).
|
||||
Return(redfishClient.RedfishError{}, &http.Response{StatusCode: 401},
|
||||
redfishClient.GenericOpenAPIError{})
|
||||
|
||||
err := RebootSystem(ctx, m, systemID)
|
||||
_, ok := err.(ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestRedfishUtilRebootSystemForceOffError(t *testing.T) {
|
||||
m := &redfishMocks.RedfishAPI{}
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
ctx := context.Background()
|
||||
resetReq := redfishClient.ResetRequestBody{}
|
||||
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
|
||||
m.On("ResetSystem", ctx, systemID, resetReq).
|
||||
Times(1).
|
||||
Return(redfishClient.RedfishError{}, &http.Response{StatusCode: 401},
|
||||
redfishClient.GenericOpenAPIError{})
|
||||
|
||||
err := RebootSystem(ctx, m, systemID)
|
||||
_, ok := err.(ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestRedfishUtilRebootSystemTurningOnError(t *testing.T) {
|
||||
m := &redfishMocks.RedfishAPI{}
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
ctx := context.Background()
|
||||
resetReq := redfishClient.ResetRequestBody{}
|
||||
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
|
||||
m.On("ResetSystem", ctx, systemID, resetReq).
|
||||
Times(1).
|
||||
Return(redfishClient.RedfishError{}, &http.Response{StatusCode: 200}, nil)
|
||||
|
||||
m.On("GetSystem", ctx, systemID).Times(1).
|
||||
Return(redfishClient.ComputerSystem{PowerState: redfishClient.POWERSTATE_OFF}, &http.Response{StatusCode: 200}, nil)
|
||||
|
||||
resetOnReq := redfishClient.ResetRequestBody{}
|
||||
resetOnReq.ResetType = redfishClient.RESETTYPE_ON
|
||||
m.On("ResetSystem", ctx, systemID, resetOnReq).
|
||||
Times(1).
|
||||
Return(redfishClient.RedfishError{}, &http.Response{StatusCode: 401},
|
||||
redfishClient.GenericOpenAPIError{})
|
||||
|
||||
err := RebootSystem(ctx, m, systemID)
|
||||
_, ok := err.(ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestRedfishUtilRebootSystemTimeout(t *testing.T) {
|
||||
m := &redfishMocks.RedfishAPI{}
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
ctx := context.WithValue(context.Background(), "numRetries", 1)
|
||||
resetReq := redfishClient.ResetRequestBody{}
|
||||
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
|
||||
m.On("ResetSystem", ctx, systemID, resetReq).
|
||||
Times(1).
|
||||
Return(redfishClient.RedfishError{}, &http.Response{StatusCode: 200}, nil)
|
||||
|
||||
m.On("GetSystem", ctx, systemID).
|
||||
Return(redfishClient.ComputerSystem{}, &http.Response{StatusCode: 200}, nil)
|
||||
err := RebootSystem(ctx, m, systemID)
|
||||
assert.Equal(t, ErrOperationRetriesExceeded{}, err)
|
||||
}
|
||||
|
@ -1,6 +1,19 @@
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package remote
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
@ -13,28 +26,28 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
AirshipRemoteTypeRedfish string = "redfish"
|
||||
AirshipHostKind string = "BareMetalHost"
|
||||
)
|
||||
|
||||
// Interface to be implemented by remoteDirect implementation
|
||||
type RDClient interface {
|
||||
DoRemoteDirect() error
|
||||
// Adapter bridges the gap between out-of-band clients. It can hold any type of OOB client, e.g. Redfish.
|
||||
type Adapter struct {
|
||||
OOBClient Client
|
||||
context context.Context
|
||||
remoteConfig *config.RemoteDirect
|
||||
remoteURL string
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
// Get remotedirect client based on config
|
||||
func getRemoteDirectClient(
|
||||
remoteConfig *config.RemoteDirect,
|
||||
remoteURL string,
|
||||
username string,
|
||||
password string) (RDClient, error) {
|
||||
var client RDClient
|
||||
switch remoteConfig.RemoteType {
|
||||
case AirshipRemoteTypeRedfish:
|
||||
// configureClient retrieves a client for remoteDirect requests based on the RemoteType in the Airship config file.
|
||||
func (a *Adapter) configureClient(remoteURL string) error {
|
||||
switch a.remoteConfig.RemoteType {
|
||||
case redfish.ClientType:
|
||||
alog.Debug("Remote type redfish")
|
||||
|
||||
rfURL, err := url.Parse(remoteURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
baseURL := fmt.Sprintf("%s://%s", rfURL.Scheme, rfURL.Host)
|
||||
@ -45,93 +58,121 @@ func getRemoteDirectClient(
|
||||
|
||||
urlPath := strings.Split(rfURL.Path, "/")
|
||||
nodeID := urlPath[len(urlPath)-1]
|
||||
|
||||
client, err = redfish.NewRedfishRemoteDirectClient(
|
||||
baseURL,
|
||||
nodeID,
|
||||
username,
|
||||
password,
|
||||
remoteConfig.IsoURL,
|
||||
remoteConfig.Insecure,
|
||||
remoteConfig.UseProxy,
|
||||
)
|
||||
if err != nil {
|
||||
alog.Debugf("redfish remotedirect client creation failed")
|
||||
return nil, err
|
||||
if nodeID == "" {
|
||||
return redfish.ErrRedfishMissingConfig{
|
||||
What: "redfish ephemeral node id empty",
|
||||
}
|
||||
}
|
||||
|
||||
if a.remoteConfig.IsoURL == "" {
|
||||
return redfish.ErrRedfishMissingConfig{
|
||||
What: "redfish ephemeral node iso Path empty",
|
||||
}
|
||||
}
|
||||
|
||||
a.context, a.OOBClient, err = redfish.NewClient(
|
||||
nodeID,
|
||||
a.remoteConfig.IsoURL,
|
||||
baseURL,
|
||||
a.remoteConfig.Insecure,
|
||||
a.remoteConfig.UseProxy,
|
||||
a.username,
|
||||
a.password)
|
||||
if err != nil {
|
||||
alog.Debugf("redfish remotedirect client creation failed")
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return nil, NewRemoteDirectErrorf("invalid remote type")
|
||||
return NewRemoteDirectErrorf("invalid remote type")
|
||||
}
|
||||
|
||||
return client, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRemoteDirectConfig(settings *environment.AirshipCTLSettings) (
|
||||
remoteConfig *config.RemoteDirect,
|
||||
remoteURL string,
|
||||
username string,
|
||||
password string,
|
||||
err error) {
|
||||
// initializeAdapter retrieves the remote direct configuration defined in the Airship configuration file.
|
||||
func (a *Adapter) intializeAdapter(settings *environment.AirshipCTLSettings) error {
|
||||
cfg := settings.Config()
|
||||
bootstrapSettings, err := cfg.CurrentContextBootstrapInfo()
|
||||
if err != nil {
|
||||
return nil, "", "", "", err
|
||||
return err
|
||||
}
|
||||
|
||||
remoteConfig = bootstrapSettings.RemoteDirect
|
||||
if remoteConfig == nil {
|
||||
return nil, "", "", "", config.ErrMissingConfig{What: "RemoteDirect options not defined in bootstrap config"}
|
||||
a.remoteConfig = bootstrapSettings.RemoteDirect
|
||||
if a.remoteConfig == nil {
|
||||
return config.ErrMissingConfig{What: "RemoteDirect options not defined in bootstrap config"}
|
||||
}
|
||||
|
||||
bundlePath, err := cfg.CurrentContextEntryPoint(config.Ephemeral, "")
|
||||
if err != nil {
|
||||
return nil, "", "", "", err
|
||||
return err
|
||||
}
|
||||
|
||||
docBundle, err := document.NewBundleByPath(bundlePath)
|
||||
if err != nil {
|
||||
return nil, "", "", "", err
|
||||
return err
|
||||
}
|
||||
|
||||
selector := document.NewEphemeralBMHSelector()
|
||||
doc, err := docBundle.SelectOne(selector)
|
||||
if err != nil {
|
||||
return nil, "", "", "", err
|
||||
return err
|
||||
}
|
||||
|
||||
remoteURL, err = document.GetBMHBMCAddress(doc)
|
||||
if err != nil {
|
||||
return nil, "", "", "", err
|
||||
}
|
||||
|
||||
username, password, err = document.GetBMHBMCCredentials(doc, docBundle)
|
||||
if err != nil {
|
||||
return nil, "", "", "", err
|
||||
}
|
||||
|
||||
return remoteConfig, remoteURL, username, password, nil
|
||||
}
|
||||
|
||||
// Top level function to execute remote direct based on remote type
|
||||
func DoRemoteDirect(settings *environment.AirshipCTLSettings) error {
|
||||
remoteConfig, remoteURL, username, password, err := getRemoteDirectConfig(settings)
|
||||
a.remoteURL, err = document.GetBMHBMCAddress(doc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := getRemoteDirectClient(remoteConfig, remoteURL, username, password)
|
||||
a.username, a.password, err = document.GetBMHBMCCredentials(doc, docBundle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = client.DoRemoteDirect()
|
||||
if err != nil {
|
||||
alog.Debugf("remote direct failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
alog.Print("Remote direct successfully completed")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DoRemoteDirect executes remote direct based on remote type.
|
||||
func (a *Adapter) DoRemoteDirect() error {
|
||||
alog.Debugf("Using Remote Endpoint: %q", a.remoteURL)
|
||||
|
||||
/* Load ISO in manager's virtual media */
|
||||
err := a.OOBClient.SetVirtualMedia(a.context, a.remoteConfig.IsoURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
alog.Debugf("Successfully loaded virtual media: %q", a.remoteConfig.IsoURL)
|
||||
|
||||
/* Set system's bootsource to selected media */
|
||||
err = a.OOBClient.SetEphemeralBootSourceByType(a.context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
/* Reboot system */
|
||||
err = a.OOBClient.RebootSystem(a.context, a.OOBClient.EphemeralNodeID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
alog.Debug("Restarted ephemeral host")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewAdapter provides an adapter that exposes the capability to perform remote direct functionality with any
|
||||
// out-of-band client.
|
||||
func NewAdapter(settings *environment.AirshipCTLSettings) (*Adapter, error) {
|
||||
a := &Adapter{}
|
||||
a.context = context.Background()
|
||||
err := a.intializeAdapter(settings)
|
||||
if err != nil {
|
||||
return a, err
|
||||
}
|
||||
|
||||
if err := a.configureClient(a.remoteURL); err != nil {
|
||||
return a, err
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
@ -8,9 +8,15 @@ import (
|
||||
|
||||
"opendev.org/airship/airshipctl/pkg/config"
|
||||
"opendev.org/airship/airshipctl/pkg/environment"
|
||||
|
||||
"opendev.org/airship/airshipctl/pkg/remote/redfish"
|
||||
"opendev.org/airship/airshipctl/testutil"
|
||||
"opendev.org/airship/airshipctl/testutil/redfishutils"
|
||||
)
|
||||
|
||||
const (
|
||||
systemID = "server-100"
|
||||
isoURL = "https://localhost:8080/ubuntu.iso"
|
||||
redfishURL = "https://redfish.local"
|
||||
)
|
||||
|
||||
func initSettings(t *testing.T, rd *config.RemoteDirect, testdata string) *environment.AirshipCTLSettings {
|
||||
@ -36,7 +42,7 @@ func TestUnknownRemoteType(t *testing.T) {
|
||||
"base",
|
||||
)
|
||||
|
||||
err := DoRemoteDirect(s)
|
||||
_, err := NewAdapter(s)
|
||||
_, ok := err.(*GenericError)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
@ -51,7 +57,7 @@ func TestRedfishRemoteDirectWithEmptyURL(t *testing.T) {
|
||||
"emptyurl",
|
||||
)
|
||||
|
||||
err := DoRemoteDirect(s)
|
||||
_, err := NewAdapter(s)
|
||||
_, ok := err.(redfish.ErrRedfishMissingConfig)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
@ -66,8 +72,7 @@ func TestRedfishRemoteDirectWithEmptyIsoPath(t *testing.T) {
|
||||
"base",
|
||||
)
|
||||
|
||||
err := DoRemoteDirect(s)
|
||||
|
||||
_, err := NewAdapter(s)
|
||||
_, ok := err.(redfish.ErrRedfishMissingConfig)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
@ -79,8 +84,123 @@ func TestBootstrapRemoteDirectMissingConfigOpts(t *testing.T) {
|
||||
"base",
|
||||
)
|
||||
|
||||
err := DoRemoteDirect(s)
|
||||
|
||||
_, err := NewAdapter(s)
|
||||
_, ok := err.(config.ErrMissingConfig)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestDoRemoteDirectRedfish(t *testing.T) {
|
||||
cfg := &config.RemoteDirect{
|
||||
RemoteType: redfish.ClientType,
|
||||
IsoURL: isoURL,
|
||||
}
|
||||
|
||||
// Initialize a remote direct adapter
|
||||
settings := initSettings(t, cfg, "base")
|
||||
a, err := NewAdapter(settings)
|
||||
assert.NoError(t, err)
|
||||
|
||||
ctx, rMock, err := redfishutils.NewClient(systemID, isoURL, redfishURL, false, false, "admin", "password")
|
||||
assert.NoError(t, err)
|
||||
|
||||
rMock.On("SetVirtualMedia", a.context, isoURL).Times(1).Return(nil)
|
||||
rMock.On("SetEphemeralBootSourceByType", a.context).Times(1).Return(nil)
|
||||
rMock.On("EphemeralNodeID").Times(1).Return(systemID)
|
||||
rMock.On("RebootSystem", a.context, systemID).Times(1).Return(nil)
|
||||
|
||||
// Swap the redfish client initialized by the remote direct adapter with the above mocked client
|
||||
a.context = ctx
|
||||
a.OOBClient = rMock
|
||||
|
||||
err = a.DoRemoteDirect()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDoRemoteDirectRedfishVirtualMediaError(t *testing.T) {
|
||||
cfg := &config.RemoteDirect{
|
||||
RemoteType: redfish.ClientType,
|
||||
IsoURL: isoURL,
|
||||
}
|
||||
|
||||
// Initialize a remote direct adapter
|
||||
settings := initSettings(t, cfg, "base")
|
||||
a, err := NewAdapter(settings)
|
||||
assert.NoError(t, err)
|
||||
|
||||
ctx, rMock, err := redfishutils.NewClient(systemID, isoURL, redfishURL, false, false, "admin", "password")
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedErr := redfish.ErrRedfishClient{Message: "Unable to set virtual media."}
|
||||
rMock.On("SetVirtualMedia", a.context, isoURL).Times(1).Return(expectedErr)
|
||||
rMock.On("SetEphemeralBootSourceByType", a.context).Times(1).Return(nil)
|
||||
rMock.On("EphemeralNodeID").Times(1).Return(systemID)
|
||||
rMock.On("RebootSystem", a.context, systemID).Times(1).Return(nil)
|
||||
|
||||
// Swap the redfish client initialized by the remote direct adapter with the above mocked client
|
||||
a.context = ctx
|
||||
a.OOBClient = rMock
|
||||
|
||||
err = a.DoRemoteDirect()
|
||||
_, ok := err.(redfish.ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestDoRemoteDirectRedfishBootSourceError(t *testing.T) {
|
||||
cfg := &config.RemoteDirect{
|
||||
RemoteType: redfish.ClientType,
|
||||
IsoURL: isoURL,
|
||||
}
|
||||
|
||||
// Initialize a remote direct adapter
|
||||
settings := initSettings(t, cfg, "base")
|
||||
a, err := NewAdapter(settings)
|
||||
assert.NoError(t, err)
|
||||
|
||||
ctx, rMock, err := redfishutils.NewClient(systemID, isoURL, redfishURL, false, false, "admin", "password")
|
||||
assert.NoError(t, err)
|
||||
|
||||
rMock.On("SetVirtualMedia", a.context, isoURL).Times(1).Return(nil)
|
||||
|
||||
expectedErr := redfish.ErrRedfishClient{Message: "Unable to set boot source."}
|
||||
rMock.On("SetEphemeralBootSourceByType", a.context).Times(1).Return(expectedErr)
|
||||
rMock.On("EphemeralNodeID").Times(1).Return(systemID)
|
||||
rMock.On("RebootSystem", a.context, systemID).Times(1).Return(nil)
|
||||
|
||||
// Swap the redfish client initialized by the remote direct adapter with the above mocked client
|
||||
a.context = ctx
|
||||
a.OOBClient = rMock
|
||||
|
||||
err = a.DoRemoteDirect()
|
||||
_, ok := err.(redfish.ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestDoRemoteDirectRedfishRebootError(t *testing.T) {
|
||||
cfg := &config.RemoteDirect{
|
||||
RemoteType: redfish.ClientType,
|
||||
IsoURL: isoURL,
|
||||
}
|
||||
|
||||
// Initialize a remote direct adapter
|
||||
settings := initSettings(t, cfg, "base")
|
||||
a, err := NewAdapter(settings)
|
||||
assert.NoError(t, err)
|
||||
|
||||
ctx, rMock, err := redfishutils.NewClient(systemID, isoURL, redfishURL, false, false, "admin", "password")
|
||||
assert.NoError(t, err)
|
||||
|
||||
rMock.On("SetVirtualMedia", a.context, isoURL).Times(1).Return(nil)
|
||||
rMock.On("SetEphemeralBootSourceByType", a.context).Times(1).Return(nil)
|
||||
rMock.On("EphemeralNodeID").Times(1).Return(systemID)
|
||||
|
||||
expectedErr := redfish.ErrRedfishClient{Message: "Unable to set boot source."}
|
||||
rMock.On("RebootSystem", a.context, systemID).Times(1).Return(expectedErr)
|
||||
|
||||
// Swap the redfish client initialized by the remote direct adapter with the above mocked client
|
||||
a.context = ctx
|
||||
a.OOBClient = rMock
|
||||
|
||||
err = a.DoRemoteDirect()
|
||||
_, ok := err.(redfish.ErrRedfishClient)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
@ -24,9 +24,9 @@ type Client interface {
|
||||
|
||||
// TODO(drewwalters96): This function may be too tightly coupled to remoteDirect operations. This could probably
|
||||
// be combined with SetVirtualMedia.
|
||||
SetEphemeralBootSourceByType(context.Context, string) error
|
||||
SetEphemeralBootSourceByType(context.Context) error
|
||||
|
||||
// TODO(drewwalters96): This function is tightly coupled to Redfish. It should be combined with the
|
||||
// SetBootSource operation and removed from the client interface.
|
||||
SetVirtualMedia(context.Context, string, string) error
|
||||
SetVirtualMedia(context.Context, string) error
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package redfish
|
||||
package redfishutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -18,6 +18,11 @@ import (
|
||||
redfishClient "opendev.org/airship/go-redfish/client"
|
||||
)
|
||||
|
||||
const (
|
||||
// ManagerID is the Redfish manager ID used by helper functions and should be used in mock calls.
|
||||
ManagerID = "manager1"
|
||||
)
|
||||
|
||||
// GetMediaCollection builds a collection of media IDs returned by the "ListManagerVirtualMedia" function.
|
||||
func GetMediaCollection(refs []string) redfishClient.Collection {
|
||||
uri := "/redfish/v1/Managers/7832-09/VirtualMedia"
|
||||
@ -47,3 +52,31 @@ func GetVirtualMedia(types []string) redfishClient.VirtualMedia {
|
||||
|
||||
return vMedia
|
||||
}
|
||||
|
||||
// GetTestSystem builds a test computer system.
|
||||
func GetTestSystem() redfishClient.ComputerSystem {
|
||||
return redfishClient.ComputerSystem{
|
||||
Id: "serverid-00",
|
||||
Name: "server-100",
|
||||
UUID: "58893887-8974-2487-2389-841168418919",
|
||||
Status: redfishClient.Status{
|
||||
State: "Enabled",
|
||||
Health: "OK",
|
||||
},
|
||||
Links: redfishClient.SystemLinks{
|
||||
ManagedBy: []redfishClient.IdRef{
|
||||
{OdataId: fmt.Sprintf("/redfish/v1/Managers/%s", ManagerID)},
|
||||
},
|
||||
},
|
||||
Boot: redfishClient.Boot{
|
||||
BootSourceOverrideTarget: redfishClient.BOOTSOURCE_CD,
|
||||
BootSourceOverrideEnabled: redfishClient.BOOTSOURCEOVERRIDEENABLED_CONTINUOUS,
|
||||
BootSourceOverrideTargetRedfishAllowableValues: []redfishClient.BootSource{
|
||||
redfishClient.BOOTSOURCE_CD,
|
||||
redfishClient.BOOTSOURCE_FLOPPY,
|
||||
redfishClient.BOOTSOURCE_HDD,
|
||||
redfishClient.BOOTSOURCE_PXE,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@ -64,8 +64,8 @@ func (m *MockClient) RebootSystem(ctx context.Context, systemID string) error {
|
||||
// client.On("SetEphemeralBootSourceByType").Return(<return values>)
|
||||
//
|
||||
// err := client.setEphemeralBootSourceByType(<args>)
|
||||
func (m *MockClient) SetEphemeralBootSourceByType(ctx context.Context, mediaType string) error {
|
||||
args := m.Called(ctx, mediaType)
|
||||
func (m *MockClient) SetEphemeralBootSourceByType(ctx context.Context) error {
|
||||
args := m.Called(ctx)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
@ -77,8 +77,8 @@ func (m *MockClient) SetEphemeralBootSourceByType(ctx context.Context, mediaType
|
||||
// client.On("SetVirtualMedia").Return(<return values>)
|
||||
//
|
||||
// err := client.SetVirtualMedia(<args>)
|
||||
func (m *MockClient) SetVirtualMedia(ctx context.Context, vMediaID string, isoPath string) error {
|
||||
args := m.Called(ctx, vMediaID, isoPath)
|
||||
func (m *MockClient) SetVirtualMedia(ctx context.Context, isoPath string) error {
|
||||
args := m.Called(ctx, isoPath)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user