From cb59c859cb224c6800c111b6b52d963557940f5a Mon Sep 17 00:00:00 2001 From: Drew Walters Date: Thu, 2 Apr 2020 21:14:03 +0000 Subject: [PATCH] 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 --- pkg/config/constants.go | 4 +- pkg/remote/redfish/client.go | 31 +++++----- pkg/remote/redfish/client_test.go | 18 +++--- pkg/remote/redfish/constants.go | 15 +++++ pkg/remote/redfish/utils.go | 23 ++++---- pkg/remote/redfish/vendors/dell/client.go | 56 +++++++++++++++++++ .../redfish/vendors/dell/client_test.go | 34 +++++++++++ pkg/remote/remote_direct.go | 36 ++++++++---- 8 files changed, 169 insertions(+), 48 deletions(-) create mode 100644 pkg/remote/redfish/constants.go create mode 100644 pkg/remote/redfish/vendors/dell/client.go create mode 100644 pkg/remote/redfish/vendors/dell/client_test.go diff --git a/pkg/config/constants.go b/pkg/config/constants.go index 2b30752ed..d2cde8bc2 100644 --- a/pkg/config/constants.go +++ b/pkg/config/constants.go @@ -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 ( diff --git a/pkg/remote/redfish/client.go b/pkg/remote/redfish/client.go index e22c0fdcd..e98111641 100644 --- a/pkg/remote/redfish/client.go +++ b/pkg/remote/redfish/client.go @@ -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 diff --git a/pkg/remote/redfish/client_test.go b/pkg/remote/redfish/client_test.go index 91f4f0178..c8e84d865 100644 --- a/pkg/remote/redfish/client_test.go +++ b/pkg/remote/redfish/client_test.go @@ -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) diff --git a/pkg/remote/redfish/constants.go b/pkg/remote/redfish/constants.go new file mode 100644 index 000000000..a1c2a4316 --- /dev/null +++ b/pkg/remote/redfish/constants.go @@ -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" diff --git a/pkg/remote/redfish/utils.go b/pkg/remote/redfish/utils.go index 4660c28bd..9c8ee73be 100644 --- a/pkg/remote/redfish/utils.go +++ b/pkg/remote/redfish/utils.go @@ -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 -} diff --git a/pkg/remote/redfish/vendors/dell/client.go b/pkg/remote/redfish/vendors/dell/client.go new file mode 100644 index 000000000..c1adc7c6f --- /dev/null +++ b/pkg/remote/redfish/vendors/dell/client.go @@ -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 +} diff --git a/pkg/remote/redfish/vendors/dell/client_test.go b/pkg/remote/redfish/vendors/dell/client_test.go new file mode 100644 index 000000000..afdbec3b1 --- /dev/null +++ b/pkg/remote/redfish/vendors/dell/client_test.go @@ -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) +} diff --git a/pkg/remote/remote_direct.go b/pkg/remote/remote_direct.go index 2dc86eeb1..f0ebe9575 100644 --- a/pkg/remote/remote_direct.go +++ b/pkg/remote/remote_direct.go @@ -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: