Add Dell Redfish client

The Dell Redfish implementation slightly deviates from the DMTF Redfish
specification. One variation is the Dell specification's classification
of virtual media, in which the Dell variation adds support for virtual
CDs and virtual floppy drives [0]. In order to perform some actions on Dell
hardware, airshipctl needs support for vendor specific clients.

This change introduces a vendor-specific Dell client to handle some
Redfish operations.

[0] https://www.dell.com/support/manuals/us/en/04/idrac9-lifecycle-controller-v3.2-series/idrac_3.21.21.21_redfishapiguide/virtualmedia?guid=guid-d9e76cf6-627d-4cb9-a3de-3f2b88b74cfb&lang=en-us

Relates-To: #139

Change-Id: Icc4500177e859d5a4607b98ec2bd2737521d00b1
Signed-off-by: Drew Walters <andrew.walters@att.com>
This commit is contained in:
Drew Walters 2020-04-02 21:14:03 +00:00
parent 1100217edd
commit cb59c859cb
8 changed files with 169 additions and 48 deletions

View File

@ -14,6 +14,8 @@
package config package config
import "opendev.org/airship/airshipctl/pkg/remote/redfish"
// Constants related to the ClusterType type // Constants related to the ClusterType type
const ( const (
Ephemeral = "ephemeral" Ephemeral = "ephemeral"
@ -50,7 +52,7 @@ const (
// Modules // Modules
AirshipDefaultBootstrapImage = "quay.io/airshipit/isogen:latest" AirshipDefaultBootstrapImage = "quay.io/airshipit/isogen:latest"
AirshipDefaultIsoURL = "http://localhost:8099/debian-custom.iso" AirshipDefaultIsoURL = "http://localhost:8099/debian-custom.iso"
AirshipDefaultRemoteType = "redfish" AirshipDefaultRemoteType = redfish.ClientType
) )
const ( const (

View File

@ -39,7 +39,8 @@ type Client struct {
ephemeralNodeID string ephemeralNodeID string
isoPath string isoPath string
redfishURL url.URL redfishURL url.URL
redfishAPI redfishAPI.RedfishAPI RedfishAPI redfishAPI.RedfishAPI
RedfishCFG *redfishClient.Configuration
} }
// EphemeralNodeID retrieves the ephemeral node ID. // EphemeralNodeID retrieves the ephemeral node ID.
@ -57,7 +58,7 @@ func (c *Client) RebootSystem(ctx context.Context, systemID string) error {
} }
for retry := 0; retry <= totalRetries; retry++ { for retry := 0; retry <= totalRetries; retry++ {
system, httpResp, err := c.redfishAPI.GetSystem(ctx, systemID) system, httpResp, err := c.RedfishAPI.GetSystem(ctx, systemID)
if err = ScreenRedfishError(httpResp, err); err != nil { if err = ScreenRedfishError(httpResp, err); err != nil {
return err return err
} }
@ -73,7 +74,7 @@ func (c *Client) RebootSystem(ctx context.Context, systemID string) error {
// Send PowerOff request // Send PowerOff request
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
_, httpResp, err := c.redfishAPI.ResetSystem(ctx, systemID, resetReq) _, httpResp, err := c.RedfishAPI.ResetSystem(ctx, systemID, resetReq)
if err = ScreenRedfishError(httpResp, err); err != nil { if err = ScreenRedfishError(httpResp, err); err != nil {
return err return err
} }
@ -85,7 +86,7 @@ func (c *Client) RebootSystem(ctx context.Context, systemID string) error {
// Send PowerOn request // Send PowerOn request
resetReq.ResetType = redfishClient.RESETTYPE_ON resetReq.ResetType = redfishClient.RESETTYPE_ON
_, httpResp, err = c.redfishAPI.ResetSystem(ctx, systemID, resetReq) _, httpResp, err = c.RedfishAPI.ResetSystem(ctx, systemID, resetReq)
if err = ScreenRedfishError(httpResp, err); err != nil { if err = ScreenRedfishError(httpResp, err); err != nil {
return err return err
} }
@ -97,13 +98,13 @@ 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 // SetEphemeralBootSourceByType sets the boot source of the ephemeral node to one that's compatible with the boot
// source type. // source type.
func (c *Client) SetEphemeralBootSourceByType(ctx context.Context) error { func (c *Client) SetEphemeralBootSourceByType(ctx context.Context) error {
_, vMediaType, err := GetVirtualMediaID(ctx, c.redfishAPI, c.ephemeralNodeID) _, vMediaType, err := GetVirtualMediaID(ctx, c.RedfishAPI, c.ephemeralNodeID)
if err != nil { if err != nil {
return err return err
} }
// Retrieve system information, containing available boot sources // Retrieve system information, containing available boot sources
system, _, err := c.redfishAPI.GetSystem(ctx, c.ephemeralNodeID) system, _, err := c.RedfishAPI.GetSystem(ctx, c.ephemeralNodeID)
if err != nil { if err != nil {
return ErrRedfishClient{Message: fmt.Sprintf("Get System[%s] failed with err: %v", c.ephemeralNodeID, err)} return ErrRedfishClient{Message: fmt.Sprintf("Get System[%s] failed with err: %v", c.ephemeralNodeID, err)}
} }
@ -114,7 +115,7 @@ func (c *Client) SetEphemeralBootSourceByType(ctx context.Context) error {
/* set boot source */ /* set boot source */
systemReq := redfishClient.ComputerSystem{} systemReq := redfishClient.ComputerSystem{}
systemReq.Boot.BootSourceOverrideTarget = bootSource systemReq.Boot.BootSourceOverrideTarget = bootSource
_, httpResp, err := c.redfishAPI.SetSystem(ctx, c.ephemeralNodeID, systemReq) _, httpResp, err := c.RedfishAPI.SetSystem(ctx, c.ephemeralNodeID, systemReq)
return ScreenRedfishError(httpResp, err) return ScreenRedfishError(httpResp, err)
} }
} }
@ -127,14 +128,14 @@ func (c *Client) SetEphemeralBootSourceByType(ctx context.Context) error {
func (c *Client) SetVirtualMedia(ctx context.Context, isoPath string) error { func (c *Client) SetVirtualMedia(ctx context.Context, isoPath string) error {
log.Debugf("Ephemeral Node System ID: '%s'", c.ephemeralNodeID) log.Debugf("Ephemeral Node System ID: '%s'", c.ephemeralNodeID)
managerID, err := getManagerID(ctx, c.redfishAPI, c.ephemeralNodeID) managerID, err := GetManagerID(ctx, c.RedfishAPI, c.ephemeralNodeID)
if err != nil { if err != nil {
return err return err
} }
log.Debugf("Ephemeral node managerID: '%s'", managerID) log.Debugf("Ephemeral node managerID: '%s'", managerID)
vMediaID, _, err := GetVirtualMediaID(ctx, c.redfishAPI, c.ephemeralNodeID) vMediaID, _, err := GetVirtualMediaID(ctx, c.RedfishAPI, c.ephemeralNodeID)
if err != nil { if err != nil {
return err return err
} }
@ -142,7 +143,7 @@ func (c *Client) SetVirtualMedia(ctx context.Context, isoPath string) error {
vMediaReq := redfishClient.InsertMediaRequestBody{} vMediaReq := redfishClient.InsertMediaRequestBody{}
vMediaReq.Image = isoPath vMediaReq.Image = isoPath
vMediaReq.Inserted = true vMediaReq.Inserted = true
_, httpResp, err := c.redfishAPI.InsertVirtualMedia(ctx, managerID, vMediaID, vMediaReq) _, httpResp, err := c.RedfishAPI.InsertVirtualMedia(ctx, managerID, vMediaID, vMediaReq)
return ScreenRedfishError(httpResp, err) return ScreenRedfishError(httpResp, err)
} }
@ -151,14 +152,14 @@ func (c *Client) SystemPowerOff(ctx context.Context, systemID string) error {
resetReq := redfishClient.ResetRequestBody{} resetReq := redfishClient.ResetRequestBody{}
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
_, httpResp, err := c.redfishAPI.ResetSystem(ctx, systemID, resetReq) _, httpResp, err := c.RedfishAPI.ResetSystem(ctx, systemID, resetReq)
return ScreenRedfishError(httpResp, err) return ScreenRedfishError(httpResp, err)
} }
// SystemPowerStatus retrieves the power status of a host as a human-readable string. // SystemPowerStatus retrieves the power status of a host as a human-readable string.
func (c *Client) SystemPowerStatus(ctx context.Context, systemID string) (string, error) { func (c *Client) SystemPowerStatus(ctx context.Context, systemID string) (string, error) {
computerSystem, httpResp, err := c.redfishAPI.GetSystem(ctx, systemID) computerSystem, httpResp, err := c.RedfishAPI.GetSystem(ctx, systemID)
if err = ScreenRedfishError(httpResp, err); err != nil { if err = ScreenRedfishError(httpResp, err); err != nil {
return "", err return "", err
} }
@ -197,8 +198,9 @@ func NewClient(ephemeralNodeID string,
cfg := &redfishClient.Configuration{ cfg := &redfishClient.Configuration{
BasePath: redfishURL, BasePath: redfishURL,
DefaultHeader: make(map[string]string), DefaultHeader: make(map[string]string),
UserAgent: "airshipctl/client", UserAgent: headerUserAgent,
} }
// see https://github.com/golang/go/issues/26013 // see https://github.com/golang/go/issues/26013
// We clone the default transport to ensure when we customize the transport // 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 // that we are providing it sane timeouts and other defaults that we would
@ -224,7 +226,8 @@ func NewClient(ephemeralNodeID string,
ephemeralNodeID: ephemeralNodeID, ephemeralNodeID: ephemeralNodeID,
isoPath: isoPath, isoPath: isoPath,
redfishURL: *parsedURL, redfishURL: *parsedURL,
redfishAPI: redfishClient.NewAPIClient(cfg).DefaultApi, RedfishAPI: redfishClient.NewAPIClient(cfg).DefaultApi,
RedfishCFG: cfg,
} }
return ctx, c, nil return ctx, c, nil

View File

@ -78,7 +78,7 @@ func TestRebootSystem(t *testing.T) {
Return(redfishClient.ComputerSystem{PowerState: redfishClient.POWERSTATE_ON}, httpResp, nil) Return(redfishClient.ComputerSystem{PowerState: redfishClient.POWERSTATE_ON}, httpResp, nil)
// Replace normal API client with mocked API client // Replace normal API client with mocked API client
client.redfishAPI = m client.RedfishAPI = m
err = client.RebootSystem(ctx, ephemeralNodeID) err = client.RebootSystem(ctx, ephemeralNodeID)
assert.NoError(t, err) assert.NoError(t, err)
@ -99,7 +99,7 @@ func TestRebootSystemShutdownError(t *testing.T) {
&http.Response{StatusCode: 401}, redfishClient.GenericOpenAPIError{}) &http.Response{StatusCode: 401}, redfishClient.GenericOpenAPIError{})
// Replace normal API client with mocked API client // Replace normal API client with mocked API client
client.redfishAPI = m client.RedfishAPI = m
err = client.RebootSystem(ctx, ephemeralNodeID) err = client.RebootSystem(ctx, ephemeralNodeID)
_, ok := err.(ErrRedfishClient) _, ok := err.(ErrRedfishClient)
@ -133,7 +133,7 @@ func TestRebootSystemStartupError(t *testing.T) {
&http.Response{StatusCode: 401}, redfishClient.GenericOpenAPIError{}) &http.Response{StatusCode: 401}, redfishClient.GenericOpenAPIError{})
// Replace normal API client with mocked API client // Replace normal API client with mocked API client
client.redfishAPI = m client.RedfishAPI = m
err = client.RebootSystem(ctx, systemID) err = client.RebootSystem(ctx, systemID)
_, ok := err.(ErrRedfishClient) _, ok := err.(ErrRedfishClient)
@ -160,7 +160,7 @@ func TestRebootSystemTimeout(t *testing.T) {
Return(redfishClient.ComputerSystem{}, &http.Response{StatusCode: 200}, nil) Return(redfishClient.ComputerSystem{}, &http.Response{StatusCode: 200}, nil)
// Replace normal API client with mocked API client // Replace normal API client with mocked API client
client.redfishAPI = m client.RedfishAPI = m
err = client.RebootSystem(ctx, systemID) err = client.RebootSystem(ctx, systemID)
assert.Equal(t, ErrOperationRetriesExceeded{}, err) assert.Equal(t, ErrOperationRetriesExceeded{}, err)
@ -178,7 +178,7 @@ func TestSetEphemeralBootSourceByTypeGetSystemError(t *testing.T) {
&http.Response{StatusCode: 500}, redfishClient.GenericOpenAPIError{}) &http.Response{StatusCode: 500}, redfishClient.GenericOpenAPIError{})
// Replace normal API client with mocked API client // Replace normal API client with mocked API client
client.redfishAPI = m client.RedfishAPI = m
err = client.SetEphemeralBootSourceByType(ctx) err = client.SetEphemeralBootSourceByType(ctx)
assert.Error(t, err) assert.Error(t, err)
@ -201,7 +201,7 @@ func TestSetEphemeralBootSourceByTypeSetSystemError(t *testing.T) {
redfishClient.ComputerSystem{}, &http.Response{StatusCode: 401}, redfishClient.GenericOpenAPIError{}) redfishClient.ComputerSystem{}, &http.Response{StatusCode: 401}, redfishClient.GenericOpenAPIError{})
// Replace normal API client with mocked API client // Replace normal API client with mocked API client
client.redfishAPI = m client.RedfishAPI = m
err = client.SetEphemeralBootSourceByType(ctx) err = client.SetEphemeralBootSourceByType(ctx)
assert.Error(t, err) assert.Error(t, err)
@ -228,7 +228,7 @@ func TestSetEphemeralBootSourceByTypeBootSourceUnavailable(t *testing.T) {
Return(testutil.GetVirtualMedia([]string{"CD"}), httpResp, nil) Return(testutil.GetVirtualMedia([]string{"CD"}), httpResp, nil)
// Replace normal API client with mocked API client // Replace normal API client with mocked API client
client.redfishAPI = m client.RedfishAPI = m
err = client.SetEphemeralBootSourceByType(ctx) err = client.SetEphemeralBootSourceByType(ctx)
_, ok := err.(ErrRedfishClient) _, ok := err.(ErrRedfishClient)
@ -247,7 +247,7 @@ func TestSetVirtualMediaGetSystemError(t *testing.T) {
nil, redfishClient.GenericOpenAPIError{}) nil, redfishClient.GenericOpenAPIError{})
// Replace normal API client with mocked API client // Replace normal API client with mocked API client
client.redfishAPI = m client.RedfishAPI = m
err = client.SetVirtualMedia(ctx, client.isoPath) err = client.SetVirtualMedia(ctx, client.isoPath)
assert.Error(t, err) assert.Error(t, err)
@ -271,7 +271,7 @@ func TestSetVirtualMediaInsertVirtualMediaError(t *testing.T) {
redfishClient.RedfishError{}, &http.Response{StatusCode: 500}, redfishClient.GenericOpenAPIError{}) redfishClient.RedfishError{}, &http.Response{StatusCode: 500}, redfishClient.GenericOpenAPIError{})
// Replace normal API client with mocked API client // Replace normal API client with mocked API client
client.redfishAPI = m client.RedfishAPI = m
err = client.SetVirtualMedia(ctx, client.isoPath) err = client.SetVirtualMedia(ctx, client.isoPath)
_, ok := err.(ErrRedfishClient) _, ok := err.(ErrRedfishClient)

View File

@ -0,0 +1,15 @@
// 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 redfish
const headerUserAgent string = "airshipctl/client"

View File

@ -29,10 +29,18 @@ const (
URLSchemeSeparator = "+" URLSchemeSeparator = "+"
) )
// GetManagerID retrieves the manager ID for a redfish system.
func GetManagerID(ctx context.Context, api redfishAPI.RedfishAPI, systemID string) (string, error) {
system, _, err := api.GetSystem(ctx, systemID)
if err != nil {
return "", err
}
return GetResourceIDFromURL(system.Links.ManagedBy[0].OdataId), nil
}
// GetResourceIDFromURL returns a parsed Redfish resource ID // GetResourceIDFromURL returns a parsed Redfish resource ID
// from the Redfish URL // from the Redfish URL
// Redfish Id ref is a URI which contains resource Id
// as the last part.
func GetResourceIDFromURL(refURL string) string { func GetResourceIDFromURL(refURL string) string {
u, err := url.Parse(refURL) u, err := url.Parse(refURL)
if err != nil { if err != nil {
@ -59,7 +67,7 @@ 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". // 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, systemID string) (string, string, error) { func GetVirtualMediaID(ctx context.Context, api redfishAPI.RedfishAPI, systemID string) (string, string, error) {
managerID, err := getManagerID(ctx, api, systemID) managerID, err := GetManagerID(ctx, api, systemID)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
@ -119,12 +127,3 @@ func ScreenRedfishError(httpResp *http.Response, clientErr error) error {
return ErrRedfishClient{Message: resp.Error.Message} 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 "", err
}
return GetResourceIDFromURL(system.Links.ManagedBy[0].OdataId), nil
}

View File

@ -0,0 +1,56 @@
// 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 dell wraps the standard Redfish client in order to provide additional functionality required to perform
// actions on iDRAC servers.
package dell
import (
"context"
redfishAPI "opendev.org/airship/go-redfish/api"
redfishClient "opendev.org/airship/go-redfish/client"
"opendev.org/airship/airshipctl/pkg/remote/redfish"
)
const (
// ClientType is used by other packages as the identifier of the Redfish client.
ClientType string = "redfish-dell"
)
// Client is a wrapper around the standard airshipctl Redfish client. This allows vendor specific Redfish clients to
// override methods without duplicating the entire client.
type Client struct {
redfish.Client
RedfishAPI redfishAPI.RedfishAPI
RedfishCFG *redfishClient.Configuration
}
// NewClient returns a client with the capability to make Redfish requests.
func NewClient(ephemeralNodeID string,
isoPath string,
redfishURL string,
insecure bool,
useProxy bool,
username string,
password string) (context.Context, *Client, error) {
ctx, genericClient, err := redfish.NewClient(
ephemeralNodeID, isoPath, redfishURL, insecure, useProxy, username, password)
if err != nil {
return ctx, nil, err
}
c := &Client{*genericClient, genericClient.RedfishAPI, genericClient.RedfishCFG}
return ctx, c, nil
}

View File

@ -0,0 +1,34 @@
// 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 dell
import (
"testing"
"github.com/stretchr/testify/assert"
)
const (
ephemeralNodeID = "System.Embedded.1"
isoPath = "https://localhost:8080/debian.iso"
redfishURL = "redfish+https://localhost/Systems/System.Embedded.1"
)
func TestNewClient(t *testing.T) {
// NOTE(drewwalters96): The Dell client implementation of this method simply creates the standard Redfish
// client. This test verifies that the Dell client creates and stores an instance of the standard client.
// Create the Dell client
_, _, err := NewClient(ephemeralNodeID, isoPath, redfishURL, false, false, "username", "password")
assert.NoError(t, err)
}

View File

@ -23,6 +23,7 @@ import (
"opendev.org/airship/airshipctl/pkg/environment" "opendev.org/airship/airshipctl/pkg/environment"
alog "opendev.org/airship/airshipctl/pkg/log" alog "opendev.org/airship/airshipctl/pkg/log"
"opendev.org/airship/airshipctl/pkg/remote/redfish" "opendev.org/airship/airshipctl/pkg/remote/redfish"
redfishDell "opendev.org/airship/airshipctl/pkg/remote/redfish/vendors/dell"
) )
// Adapter bridges the gap between out-of-band clients. It can hold any type of OOB client, e.g. Redfish. // Adapter bridges the gap between out-of-band clients. It can hold any type of OOB client, e.g. Redfish.
@ -38,9 +39,7 @@ type Adapter struct {
// configureClient retrieves a client for remoteDirect requests based on the RemoteType in the Airship config file. // configureClient retrieves a client for remoteDirect requests based on the RemoteType in the Airship config file.
func (a *Adapter) configureClient(remoteURL string) error { func (a *Adapter) configureClient(remoteURL string) error {
switch a.remoteConfig.RemoteType { switch a.remoteConfig.RemoteType {
case redfish.ClientType: case redfish.ClientType, redfishDell.ClientType:
alog.Debug("Remote type redfish")
rfURL, err := url.Parse(remoteURL) rfURL, err := url.Parse(remoteURL)
if err != nil { if err != nil {
return err return err
@ -66,16 +65,29 @@ func (a *Adapter) configureClient(remoteURL string) error {
} }
} }
a.Context, a.OOBClient, err = redfish.NewClient( if a.remoteConfig.RemoteType == redfishDell.ClientType {
nodeID, alog.Debug("Remote type: Redfish for Integrated Dell Remote Access Controller (iDrac) systems")
a.remoteConfig.IsoURL, a.Context, a.OOBClient, err = redfishDell.NewClient(
baseURL, nodeID,
a.remoteConfig.Insecure, a.remoteConfig.IsoURL,
a.remoteConfig.UseProxy, baseURL,
a.username, a.remoteConfig.Insecure,
a.password) a.remoteConfig.UseProxy,
a.username,
a.password)
} else {
alog.Debug("Remote type: Redfish")
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 { if err != nil {
alog.Debugf("redfish remotedirect client creation failed")
return err return err
} }
default: default: