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

View File

@ -39,7 +39,8 @@ type Client struct {
ephemeralNodeID string
isoPath string
redfishURL url.URL
redfishAPI redfishAPI.RedfishAPI
RedfishAPI redfishAPI.RedfishAPI
RedfishCFG *redfishClient.Configuration
}
// 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++ {
system, httpResp, err := c.redfishAPI.GetSystem(ctx, systemID)
system, httpResp, err := c.RedfishAPI.GetSystem(ctx, systemID)
if err = ScreenRedfishError(httpResp, err); err != nil {
return err
}
@ -73,7 +74,7 @@ func (c *Client) RebootSystem(ctx context.Context, systemID string) error {
// Send PowerOff request
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 {
return err
}
@ -85,7 +86,7 @@ func (c *Client) RebootSystem(ctx context.Context, systemID string) error {
// Send PowerOn request
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 {
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
// source type.
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 {
return err
}
// 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 {
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 */
systemReq := redfishClient.ComputerSystem{}
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)
}
}
@ -127,14 +128,14 @@ func (c *Client) SetEphemeralBootSourceByType(ctx context.Context) error {
func (c *Client) SetVirtualMedia(ctx context.Context, isoPath string) error {
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 {
return err
}
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 {
return err
}
@ -142,7 +143,7 @@ func (c *Client) SetVirtualMedia(ctx context.Context, isoPath string) error {
vMediaReq := redfishClient.InsertMediaRequestBody{}
vMediaReq.Image = isoPath
vMediaReq.Inserted = true
_, httpResp, err := c.redfishAPI.InsertVirtualMedia(ctx, managerID, vMediaID, vMediaReq)
_, httpResp, err := c.RedfishAPI.InsertVirtualMedia(ctx, managerID, vMediaID, vMediaReq)
return ScreenRedfishError(httpResp, err)
}
@ -151,14 +152,14 @@ func (c *Client) SystemPowerOff(ctx context.Context, systemID string) error {
resetReq := redfishClient.ResetRequestBody{}
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)
}
// SystemPowerStatus retrieves the power status of a host as a human-readable string.
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 {
return "", err
}
@ -197,8 +198,9 @@ func NewClient(ephemeralNodeID string,
cfg := &redfishClient.Configuration{
BasePath: redfishURL,
DefaultHeader: make(map[string]string),
UserAgent: "airshipctl/client",
UserAgent: headerUserAgent,
}
// 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
@ -224,7 +226,8 @@ func NewClient(ephemeralNodeID string,
ephemeralNodeID: ephemeralNodeID,
isoPath: isoPath,
redfishURL: *parsedURL,
redfishAPI: redfishClient.NewAPIClient(cfg).DefaultApi,
RedfishAPI: redfishClient.NewAPIClient(cfg).DefaultApi,
RedfishCFG: cfg,
}
return ctx, c, nil

View File

@ -78,7 +78,7 @@ func TestRebootSystem(t *testing.T) {
Return(redfishClient.ComputerSystem{PowerState: redfishClient.POWERSTATE_ON}, httpResp, nil)
// Replace normal API client with mocked API client
client.redfishAPI = m
client.RedfishAPI = m
err = client.RebootSystem(ctx, ephemeralNodeID)
assert.NoError(t, err)
@ -99,7 +99,7 @@ func TestRebootSystemShutdownError(t *testing.T) {
&http.Response{StatusCode: 401}, redfishClient.GenericOpenAPIError{})
// Replace normal API client with mocked API client
client.redfishAPI = m
client.RedfishAPI = m
err = client.RebootSystem(ctx, ephemeralNodeID)
_, ok := err.(ErrRedfishClient)
@ -133,7 +133,7 @@ func TestRebootSystemStartupError(t *testing.T) {
&http.Response{StatusCode: 401}, redfishClient.GenericOpenAPIError{})
// Replace normal API client with mocked API client
client.redfishAPI = m
client.RedfishAPI = m
err = client.RebootSystem(ctx, systemID)
_, ok := err.(ErrRedfishClient)
@ -160,7 +160,7 @@ func TestRebootSystemTimeout(t *testing.T) {
Return(redfishClient.ComputerSystem{}, &http.Response{StatusCode: 200}, nil)
// Replace normal API client with mocked API client
client.redfishAPI = m
client.RedfishAPI = m
err = client.RebootSystem(ctx, systemID)
assert.Equal(t, ErrOperationRetriesExceeded{}, err)
@ -178,7 +178,7 @@ func TestSetEphemeralBootSourceByTypeGetSystemError(t *testing.T) {
&http.Response{StatusCode: 500}, redfishClient.GenericOpenAPIError{})
// Replace normal API client with mocked API client
client.redfishAPI = m
client.RedfishAPI = m
err = client.SetEphemeralBootSourceByType(ctx)
assert.Error(t, err)
@ -201,7 +201,7 @@ func TestSetEphemeralBootSourceByTypeSetSystemError(t *testing.T) {
redfishClient.ComputerSystem{}, &http.Response{StatusCode: 401}, redfishClient.GenericOpenAPIError{})
// Replace normal API client with mocked API client
client.redfishAPI = m
client.RedfishAPI = m
err = client.SetEphemeralBootSourceByType(ctx)
assert.Error(t, err)
@ -228,7 +228,7 @@ func TestSetEphemeralBootSourceByTypeBootSourceUnavailable(t *testing.T) {
Return(testutil.GetVirtualMedia([]string{"CD"}), httpResp, nil)
// Replace normal API client with mocked API client
client.redfishAPI = m
client.RedfishAPI = m
err = client.SetEphemeralBootSourceByType(ctx)
_, ok := err.(ErrRedfishClient)
@ -247,7 +247,7 @@ func TestSetVirtualMediaGetSystemError(t *testing.T) {
nil, redfishClient.GenericOpenAPIError{})
// Replace normal API client with mocked API client
client.redfishAPI = m
client.RedfishAPI = m
err = client.SetVirtualMedia(ctx, client.isoPath)
assert.Error(t, err)
@ -271,7 +271,7 @@ func TestSetVirtualMediaInsertVirtualMediaError(t *testing.T) {
redfishClient.RedfishError{}, &http.Response{StatusCode: 500}, redfishClient.GenericOpenAPIError{})
// Replace normal API client with mocked API client
client.redfishAPI = m
client.RedfishAPI = m
err = client.SetVirtualMedia(ctx, client.isoPath)
_, 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 = "+"
)
// 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
// from the Redfish URL
// Redfish Id ref is a URI which contains resource Id
// as the last part.
func GetResourceIDFromURL(refURL string) string {
u, err := url.Parse(refURL)
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".
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 {
return "", "", err
}
@ -119,12 +127,3 @@ func ScreenRedfishError(httpResp *http.Response, clientErr error) 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 "", 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"
alog "opendev.org/airship/airshipctl/pkg/log"
"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.
@ -38,9 +39,7 @@ type Adapter struct {
// 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")
case redfish.ClientType, redfishDell.ClientType:
rfURL, err := url.Parse(remoteURL)
if err != nil {
return err
@ -66,16 +65,29 @@ func (a *Adapter) configureClient(remoteURL string) error {
}
}
a.Context, a.OOBClient, err = redfish.NewClient(
nodeID,
a.remoteConfig.IsoURL,
baseURL,
a.remoteConfig.Insecure,
a.remoteConfig.UseProxy,
a.username,
a.password)
if a.remoteConfig.RemoteType == redfishDell.ClientType {
alog.Debug("Remote type: Redfish for Integrated Dell Remote Access Controller (iDrac) systems")
a.Context, a.OOBClient, err = redfishDell.NewClient(
nodeID,
a.remoteConfig.IsoURL,
baseURL,
a.remoteConfig.Insecure,
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 {
alog.Debugf("redfish remotedirect client creation failed")
return err
}
default: