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:
Drew Walters 2020-03-20 18:19:32 +00:00
parent bca0a96603
commit a15f978cad
13 changed files with 426 additions and 973 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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