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: