Merge "Add Dell Redfish client"

This commit is contained in:
Zuul 2020-04-20 14:52:39 +00:00 committed by Gerrit Code Review
commit 60eff66884
8 changed files with 169 additions and 48 deletions

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

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

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

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