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:
parent
1100217edd
commit
cb59c859cb
@ -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 (
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
15
pkg/remote/redfish/constants.go
Normal file
15
pkg/remote/redfish/constants.go
Normal 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"
|
@ -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
|
||||
}
|
||||
|
56
pkg/remote/redfish/vendors/dell/client.go
vendored
Normal file
56
pkg/remote/redfish/vendors/dell/client.go
vendored
Normal 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
|
||||
}
|
34
pkg/remote/redfish/vendors/dell/client_test.go
vendored
Normal file
34
pkg/remote/redfish/vendors/dell/client_test.go
vendored
Normal 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)
|
||||
}
|
@ -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,6 +65,18 @@ func (a *Adapter) configureClient(remoteURL string) error {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@ -74,8 +85,9 @@ func (a *Adapter) configureClient(remoteURL string) error {
|
||||
a.remoteConfig.UseProxy,
|
||||
a.username,
|
||||
a.password)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
alog.Debugf("redfish remotedirect client creation failed")
|
||||
return err
|
||||
}
|
||||
default:
|
||||
|
Loading…
Reference in New Issue
Block a user