Add Redfish Authentication Support

This commit also introduces a dochelper concept.  This provides
some convenience methods to the document pkg that help extract data
from known document types as well as walk document relationships to
discover related information such as BMC credentials for baremetal
hosts.

Once merged, a follow up patchset will leverage these within the
cloud-init code to deduplicate some of these lookups.

Change-Id: Ie6a770ce4b34adbea30281917f0cb2fdc460b4fb
This commit is contained in:
Alan Meadows 2020-03-27 14:09:32 -07:00
parent 21c7c166ff
commit e2cca32748
15 changed files with 393 additions and 65 deletions

View File

@ -74,7 +74,7 @@ func getNetworkData(docBundle document.Bundle) ([]byte, error) {
}
// try and find these documents in our bundle
selector, err = document.NewEphemeralNetworkDataSelector(bmhDoc)
selector, err = document.NewNetworkDataSelector(bmhDoc)
if err != nil {
return nil, err
}

View File

@ -47,6 +47,7 @@ type Bundle interface {
SetFileSystem(FileSystem) error
GetFileSystem() FileSystem
Select(selector Selector) ([]Document, error)
SelectOne(selector Selector) (Document, error)
SelectBundle(selector Selector) (Bundle, error)
SelectByFieldValue(string, func(interface{}) bool) (Bundle, error)
GetByGvk(string, string, string) ([]Document, error)
@ -220,6 +221,29 @@ func (b *BundleFactory) Select(selector Selector) ([]Document, error) {
return docSet, err
}
// SelectOne serves the common use case where you expect one match
// and only one match to your selector -- in other words, you want to
// error if you didn't find any documents, and error if you found
// more than one. This reduces code repetition that would otherwise
// be scattered around that evaluates the length of the doc set returned
// for this common case
func (b *BundleFactory) SelectOne(selector Selector) (Document, error) {
docSet, err := b.Select(selector)
if err != nil {
return nil, err
}
// evaluate docSet for at least one document, and no more than
// one document and raise errors as appropriate
switch numDocsFound := len(docSet); {
case numDocsFound == 0:
return nil, ErrDocNotFound{Selector: selector}
case numDocsFound > 1:
return nil, ErrMultipleDocsFound{Selector: selector}
}
return docSet[0], nil
}
// SelectBundle offers an interface to pass a Selector, built on top of kustomize Selector
// to the bundle returning a new Bundle that matches the criteria. This is useful
// where you want to actually prune the underlying bundle you are working with

View File

@ -0,0 +1,60 @@
package document
// GetBMHNetworkData retrieves the associated network data string
// for the bmh document supplied from the bundle supplied
func GetBMHNetworkData(bmh Document, bundle Bundle) (string, error) {
// try and find these documents in our bundle
selector, err := NewNetworkDataSelector(bmh)
if err != nil {
return "", err
}
doc, err := bundle.SelectOne(selector)
if err != nil {
return "", err
}
networkData, err := GetSecretDataKey(doc, "networkData")
if err != nil {
return "", err
}
return networkData, nil
}
// GetBMHBMCAddress returns the bmc address for a particular the document supplied
func GetBMHBMCAddress(bmh Document) (string, error) {
bmcAddress, err := bmh.GetString("spec.bmc.address")
if err != nil {
return "", err
}
return bmcAddress, nil
}
// GetBMHBMCCredentials returns the BMC credentials for the bmh document supplied from
// the supplied bundle
func GetBMHBMCCredentials(bmh Document, bundle Bundle) (username string, password string, err error) {
// extract the secret document name
bmcCredentialsName, err := bmh.GetString("spec.bmc.credentialsName")
if err != nil {
return "", "", err
}
// find the secret within the bundle supplied
selector := NewBMCCredentialsSelector(bmcCredentialsName)
doc, err := bundle.SelectOne(selector)
if err != nil {
return "", "", err
}
username, err = GetSecretDataKey(doc, "username")
if err != nil {
return "", "", err
}
password, err = GetSecretDataKey(doc, "password")
if err != nil {
return "", "", err
}
// extract the username and password from them
return username, password, nil
}

View File

@ -0,0 +1,55 @@
package document_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"opendev.org/airship/airshipctl/pkg/document"
"opendev.org/airship/airshipctl/testutil"
)
func TestDocHelpers(t *testing.T) {
require := require.New(t)
assert := assert.New(t)
fSys := testutil.SetupTestFs(t, "testdata/dochelper")
bundle, err := document.NewBundle(fSys, "/", "/")
require.NoError(err, "Building Bundle Failed")
require.NotNil(bundle)
t.Run("GetBMHNetworkData", func(t *testing.T) {
// retrieve our single bmh in the dataset
selector := document.NewSelector().ByKind("BareMetalHost")
doc, err := bundle.SelectOne(selector)
require.NoError(err)
networkData, err := document.GetBMHNetworkData(doc, bundle)
require.NoError(err, "Unexpected error trying to GetBMHNetworkData")
assert.Equal(networkData, "some network data")
})
t.Run("GetBMHBMCAddress", func(t *testing.T) {
// retrieve our single bmh in the dataset
selector := document.NewSelector().ByKind("BareMetalHost")
doc, err := bundle.SelectOne(selector)
require.NoError(err)
bmcAddress, err := document.GetBMHBMCAddress(doc)
require.NoError(err, "Unexpected error trying to GetBMHBMCAddress")
assert.Equal(bmcAddress, "redfish+https://192.168.111.1/v1/Redfish/Foo/Bar")
})
t.Run("GetBMHBMCCredentials", func(t *testing.T) {
// retrieve our single bmh in the dataset
selector := document.NewSelector().ByKind("BareMetalHost")
doc, err := bundle.SelectOne(selector)
require.NoError(err)
bmcUsername, bmcPassword, err := document.GetBMHBMCCredentials(doc, bundle)
require.NoError(err, "Unexpected error trying to GetBMHBMCCredentials")
assert.Equal(bmcUsername, "username")
assert.Equal(bmcPassword, "password")
})
}

View File

@ -0,0 +1,44 @@
package document
import (
b64 "encoding/base64"
)
// GetSecretDataKey understands how to retrieve a specific top level key from a secret
// that may have the data stored under a data or stringData field in which
// case the key may be base64 encoded or it may be plain text
//
// it is meant to be used by other high level dochelpers
func GetSecretDataKey(cfg Document, key string) (string, error) {
var needsBase64Decode = true
var docName = cfg.GetName()
// this purposely doesn't handle binaryData as that isn't
// something we could support anyways
data, err := cfg.GetStringMap("stringData")
if err == nil {
needsBase64Decode = false
} else {
data, err = cfg.GetStringMap("data")
if err != nil {
return "", ErrDocumentMalformed{
DocName: docName,
Message: "The secret document lacks a data or stringData top level field",
}
}
}
res, ok := data[key]
if !ok {
return "", ErrDocumentDataKeyNotFound{DocName: docName, Key: key}
}
if needsBase64Decode {
byteSlice, err := b64.StdEncoding.DecodeString(res)
if err != nil {
return "", err
}
return string(byteSlice), nil
}
return res, nil
}

View File

@ -9,6 +9,19 @@ type ErrDocNotFound struct {
Selector Selector
}
// ErrDocumentDataKeyNotFound returned if desired key within a document not found
type ErrDocumentDataKeyNotFound struct {
DocName string
Key string
}
// ErrDocumentMalformed returned if the document is structurally malformed
// (e.g. missing required low level keys)
type ErrDocumentMalformed struct {
DocName string
Message string
}
// ErrMultipleDocsFound returned if desired document not found
type ErrMultipleDocsFound struct {
Selector Selector
@ -18,6 +31,14 @@ func (e ErrDocNotFound) Error() string {
return fmt.Sprintf("Document filtered by selector %q found no documents", e.Selector)
}
func (e ErrDocumentDataKeyNotFound) Error() string {
return fmt.Sprintf("Document %q cannot retrieve data key %q", e.DocName, e.Key)
}
func (e ErrDocumentMalformed) Error() string {
return fmt.Sprintf("Document %q is malformed: %q", e.DocName, e.Message)
}
func (e ErrMultipleDocsFound) Error() string {
return fmt.Sprintf("Document filtered by selector %q found more than one document", e.Selector)
}

View File

@ -64,7 +64,7 @@ func (s Selector) ByAnnotation(annotationSelector string) Selector {
return s
}
// EphemeralCloudDataSelector returns selector to get BaremetalHost for ephemeral node
// NewEphemeralCloudDataSelector returns selector to get BaremetalHost for ephemeral node
func NewEphemeralCloudDataSelector() Selector {
return NewSelector().ByKind(SecretKind).ByLabel(EphemeralUserDataSelector)
}
@ -74,11 +74,16 @@ func NewEphemeralBMHSelector() Selector {
return NewSelector().ByKind(BareMetalHostKind).ByLabel(EphemeralHostSelector)
}
// NewEphemeralNetworkDataSelector returns selector that can be used to get secret with
// NewBMCCredentialsSelector returns selector to get BaremetalHost BMC credentials
func NewBMCCredentialsSelector(name string) Selector {
return NewSelector().ByKind(SecretKind).ByName(name)
}
// NewNetworkDataSelector returns selector that can be used to get secret with
// network data bmhDoc argument is a document interface, that should hold fields
// spec.networkData.name and spec.networkData.namespace where to find the secret,
// if either of these fields are not defined in Document error will be returned
func NewEphemeralNetworkDataSelector(bmhDoc Document) (Selector, error) {
func NewNetworkDataSelector(bmhDoc Document) (Selector, error) {
selector := NewSelector()
// extract the network data document pointer from the bmh document
netConfDocName, err := bmhDoc.GetString("spec.networkData.name")

View File

@ -24,7 +24,7 @@ func TestSelectorsPositive(t *testing.T) {
require.NoError(t, err)
assert.Len(t, docs, 1)
bmhDoc := docs[0]
selector, err := document.NewEphemeralNetworkDataSelector(bmhDoc)
selector, err := document.NewNetworkDataSelector(bmhDoc)
require.NoError(t, err)
assert.Equal(t, "validName", selector.Name)
})
@ -42,12 +42,12 @@ func TestSelectorsNegative(t *testing.T) {
// test coverage
bundle := testutil.NewTestBundle(t, "testdata/selectors/invalid")
t.Run("TestNewEphemeralNetworkDataSelectorErr", func(t *testing.T) {
t.Run("TestNewNetworkDataSelectorErr", func(t *testing.T) {
docs, err := bundle.Select(document.NewEphemeralBMHSelector())
require.NoError(t, err)
assert.Len(t, docs, 2)
bmhDoc := docs[0]
_, err = document.NewEphemeralNetworkDataSelector(bmhDoc)
_, err = document.NewNetworkDataSelector(bmhDoc)
assert.Error(t, err)
})
@ -56,7 +56,7 @@ func TestSelectorsNegative(t *testing.T) {
require.NoError(t, err)
assert.Len(t, docs, 2)
bmhDoc := docs[1]
_, err = document.NewEphemeralNetworkDataSelector(bmhDoc)
_, err = document.NewNetworkDataSelector(bmhDoc)
assert.Error(t, err)
})
}
@ -67,7 +67,7 @@ func TestSelectorsSkip(t *testing.T) {
// test coverage
bundle := testutil.NewTestBundle(t, "testdata/selectors/exclude-from-k8s")
t.Run("TestNewEphemeralNetworkDataSelectorErr", func(t *testing.T) {
t.Run("TestNewNetworkDataSelectorErr", func(t *testing.T) {
selector := document.NewDeployToK8sSelector()
docs, err := bundle.Select(selector)
require.NoError(t, err)

View File

@ -0,0 +1,16 @@
---
apiVersion: metal3.io/v1alpha1
kind: BareMetalHost
metadata:
labels:
airshipit.org/ephemeral-node: "true"
name: master-0
spec:
online: true
bootMACAddress: 00:3b:8b:0c:ec:8b
bmc:
address: redfish+https://192.168.111.1/v1/Redfish/Foo/Bar
credentialsName: master-0-bmc
networkData:
name: master-0-networkdata
namespace: metal3

View File

@ -0,0 +1,3 @@
resources:
- baremetalhost.yaml
- secret.yaml

View File

@ -0,0 +1,28 @@
apiVersion: v1
kind: Secret
metadata:
labels:
airshipit.org/ephemeral-user-data: "true"
name: ephemeral-user-data
type: Opaque
stringData:
userData: cloud-init
---
apiVersion: v1
kind: Secret
metadata:
name: master-0-bmc
namespace: metal3
type: Opaque
stringData:
username: username
password: password
---
apiVersion: v1
kind: Secret
metadata:
name: master-0-networkdata
namespace: metal3
type: Opaque
stringData:
networkData: some network data

View File

@ -17,5 +17,4 @@ metadata:
namespace: validNamespace
type: Opaque
stringData:
userData: cloud-init
userData: cloud-init

View File

@ -29,14 +29,18 @@ type RemoteDirect struct {
// Redfish Client implementation
RedfishAPI redfishApi.RedfishAPI
// optional Username Authentication
Username string
// optional Password
Password string
}
// Top level function to handle Redfish remote direct
func (cfg RemoteDirect) DoRemoteDirect() error {
alog.Debugf("Using Redfish Endpoint: '%s'", cfg.RemoteURL.String())
/* TODO: Add Authentication when redfish library supports it. */
/* Get system details */
systemID := cfg.EphemeralNodeID
system, _, err := cfg.RedfishAPI.GetSystem(cfg.Context, systemID)
@ -79,10 +83,12 @@ func (cfg RemoteDirect) DoRemoteDirect() error {
return nil
}
// Creates a new Redfish remote direct client.
func NewRedfishRemoteDirectClient(ctx context.Context,
// NewRedfishRemoteDirectClient creates a new Redfish remote direct client.
func NewRedfishRemoteDirectClient(
remoteURL string,
ephNodeID string,
username string,
password string,
isoPath string,
insecure bool,
useproxy bool,
@ -101,6 +107,17 @@ func NewRedfishRemoteDirectClient(ctx context.Context,
}
}
var ctx context.Context
if username != "" && password != "" {
ctx = context.WithValue(
context.Background(),
redfishClient.ContextBasicAuth,
redfishClient.BasicAuth{UserName: username, Password: password},
)
} else {
ctx = context.Background()
}
if isoPath == "" {
return RemoteDirect{},
ErrRedfishMissingConfig{

View File

@ -28,37 +28,44 @@ func TestRedfishRemoteDirectNormal(t *testing.T) {
systemID := computerSystemID
httpResp := &http.Response{StatusCode: 200}
m.On("GetSystem", context.Background(), systemID).Times(1).
ctx := context.WithValue(
context.Background(),
redfishClient.ContextBasicAuth,
redfishClient.BasicAuth{UserName: "username", Password: "password"},
)
m.On("GetSystem", ctx, systemID).Times(1).
Return(getTestSystem(), httpResp, nil)
m.On("InsertVirtualMedia", context.Background(), "manager-1", "Cd", mock.Anything).
m.On("InsertVirtualMedia", ctx, "manager-1", "Cd", mock.Anything).
Return(redfishClient.RedfishError{}, httpResp, nil)
m.On("GetSystem", context.Background(), systemID).Times(1).
m.On("GetSystem", ctx, systemID).Times(1).
Return(getTestSystem(), httpResp, nil)
systemReq := redfishClient.ComputerSystem{
Boot: redfishClient.Boot{
BootSourceOverrideTarget: redfishClient.BOOTSOURCE_CD,
},
}
m.On("SetSystem", context.Background(), systemID, systemReq).
m.On("SetSystem", ctx, systemID, systemReq).
Times(1).
Return(redfishClient.ComputerSystem{}, httpResp, nil)
resetReq := redfishClient.ResetRequestBody{}
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
m.On("ResetSystem", context.Background(), systemID, resetReq).
m.On("ResetSystem", ctx, systemID, resetReq).
Times(1).
Return(redfishClient.RedfishError{}, httpResp, nil)
m.On("GetSystem", context.Background(), systemID).Times(1).
m.On("GetSystem", ctx, systemID).Times(1).
Return(redfishClient.ComputerSystem{PowerState: redfishClient.POWERSTATE_OFF}, httpResp, nil)
resetReq.ResetType = redfishClient.RESETTYPE_ON
m.On("ResetSystem", context.Background(), systemID, resetReq).
m.On("ResetSystem", ctx, systemID, resetReq).
Times(1).
Return(redfishClient.RedfishError{}, httpResp, nil)
m.On("GetSystem", context.Background(), systemID).Times(1).
m.On("GetSystem", ctx, systemID).Times(1).
Return(redfishClient.ComputerSystem{PowerState: redfishClient.POWERSTATE_ON}, httpResp, nil)
rDCfg := getDefaultRedfishRemoteDirectObj(t, m)
@ -72,13 +79,19 @@ func TestRedfishRemoteDirectInvalidSystemId(t *testing.T) {
m := &redfishMocks.RedfishAPI{}
defer m.AssertExpectations(t)
ctx := context.WithValue(
context.Background(),
redfishClient.ContextBasicAuth,
redfishClient.BasicAuth{UserName: "username", Password: "password"},
)
systemID := "invalid-server"
localRDCfg := getDefaultRedfishRemoteDirectObj(t, m)
localRDCfg.EphemeralNodeID = systemID
realErr := fmt.Errorf("%s system do not exist", systemID)
m.On("GetSystem", context.Background(), systemID).
m.On("GetSystem", ctx, systemID).
Times(1).
Return(redfishClient.ComputerSystem{}, nil, realErr)
@ -92,10 +105,16 @@ func TestRedfishRemoteDirectGetSystemNetworkError(t *testing.T) {
m := &redfishMocks.RedfishAPI{}
defer m.AssertExpectations(t)
ctx := context.WithValue(
context.Background(),
redfishClient.ContextBasicAuth,
redfishClient.BasicAuth{UserName: "username", Password: "password"},
)
systemID := computerSystemID
realErr := fmt.Errorf("server request timeout")
httpResp := &http.Response{StatusCode: 408}
m.On("GetSystem", context.Background(), systemID).
m.On("GetSystem", ctx, systemID).
Times(1).
Return(redfishClient.ComputerSystem{}, httpResp, realErr)
@ -111,6 +130,12 @@ func TestRedfishRemoteDirectInvalidIsoPath(t *testing.T) {
m := &redfishMocks.RedfishAPI{}
defer m.AssertExpectations(t)
ctx := context.WithValue(
context.Background(),
redfishClient.ContextBasicAuth,
redfishClient.BasicAuth{UserName: "username", Password: "password"},
)
systemID := computerSystemID
rDCfg := getDefaultRedfishRemoteDirectObj(t, m)
localRDCfg := rDCfg
@ -118,11 +143,11 @@ func TestRedfishRemoteDirectInvalidIsoPath(t *testing.T) {
realErr := redfishClient.GenericOpenAPIError{}
httpResp := &http.Response{StatusCode: 500}
m.On("GetSystem", context.Background(), systemID).
m.On("GetSystem", ctx, systemID).
Times(1).
Return(getTestSystem(), nil, nil)
m.On("InsertVirtualMedia", context.Background(), "manager-1", "Cd", mock.Anything).
m.On("InsertVirtualMedia", ctx, "manager-1", "Cd", mock.Anything).
Return(redfishClient.RedfishError{}, httpResp, realErr)
err := localRDCfg.DoRemoteDirect()
@ -135,6 +160,12 @@ func TestRedfishRemoteDirectCdDvdNotAvailableInBootSources(t *testing.T) {
m := &redfishMocks.RedfishAPI{}
defer m.AssertExpectations(t)
ctx := context.WithValue(
context.Background(),
redfishClient.ContextBasicAuth,
redfishClient.BasicAuth{UserName: "username", Password: "password"},
)
systemID := computerSystemID
invalidSystem := getTestSystem()
invalidSystem.Boot.BootSourceOverrideTargetRedfishAllowableValues = []redfishClient.BootSource{
@ -142,10 +173,10 @@ func TestRedfishRemoteDirectCdDvdNotAvailableInBootSources(t *testing.T) {
redfishClient.BOOTSOURCE_PXE,
}
m.On("GetSystem", context.Background(), systemID).
m.On("GetSystem", ctx, systemID).
Return(invalidSystem, nil, nil)
m.On("InsertVirtualMedia", context.Background(), "manager-1", "Cd", mock.Anything).
m.On("InsertVirtualMedia", ctx, "manager-1", "Cd", mock.Anything).
Return(redfishClient.RedfishError{}, nil, nil)
rDCfg := getDefaultRedfishRemoteDirectObj(t, m)
@ -160,15 +191,21 @@ func TestRedfishRemoteDirectSetSystemBootSourceFailed(t *testing.T) {
m := &redfishMocks.RedfishAPI{}
defer m.AssertExpectations(t)
ctx := context.WithValue(
context.Background(),
redfishClient.ContextBasicAuth,
redfishClient.BasicAuth{UserName: "username", Password: "password"},
)
systemID := computerSystemID
httpSuccResp := &http.Response{StatusCode: 200}
m.On("GetSystem", context.Background(), systemID).
m.On("GetSystem", ctx, systemID).
Return(getTestSystem(), httpSuccResp, nil)
m.On("InsertVirtualMedia", context.Background(), "manager-1", "Cd", mock.Anything).
m.On("InsertVirtualMedia", ctx, "manager-1", "Cd", mock.Anything).
Return(redfishClient.RedfishError{}, httpSuccResp, nil)
m.On("SetSystem", context.Background(), systemID, mock.Anything).
m.On("SetSystem", ctx, systemID, mock.Anything).
Times(1).
Return(redfishClient.ComputerSystem{}, &http.Response{StatusCode: 401},
redfishClient.GenericOpenAPIError{})
@ -185,21 +222,27 @@ func TestRedfishRemoteDirectSystemRebootFailed(t *testing.T) {
m := &redfishMocks.RedfishAPI{}
defer m.AssertExpectations(t)
ctx := context.WithValue(
context.Background(),
redfishClient.ContextBasicAuth,
redfishClient.BasicAuth{UserName: "username", Password: "password"},
)
systemID := computerSystemID
httpSuccResp := &http.Response{StatusCode: 200}
m.On("GetSystem", context.Background(), systemID).
m.On("GetSystem", ctx, systemID).
Return(getTestSystem(), httpSuccResp, nil)
m.On("InsertVirtualMedia", context.Background(), mock.Anything, mock.Anything, mock.Anything).
m.On("InsertVirtualMedia", ctx, mock.Anything, mock.Anything, mock.Anything).
Return(redfishClient.RedfishError{}, httpSuccResp, nil)
m.On("SetSystem", context.Background(), systemID, mock.Anything).
m.On("SetSystem", ctx, systemID, mock.Anything).
Times(1).
Return(redfishClient.ComputerSystem{}, httpSuccResp, nil)
resetReq := redfishClient.ResetRequestBody{}
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
m.On("ResetSystem", context.Background(), systemID, resetReq).
m.On("ResetSystem", ctx, systemID, resetReq).
Times(1).
Return(redfishClient.RedfishError{}, &http.Response{StatusCode: 401},
redfishClient.GenericOpenAPIError{})
@ -244,9 +287,10 @@ func TestNewRedfishRemoteDirectClient(t *testing.T) {
defer m.AssertExpectations(t)
_, err := NewRedfishRemoteDirectClient(
context.Background(),
defaultURL,
computerSystemID,
"username",
"password",
"/tmp/test.iso",
true,
false,
@ -255,9 +299,10 @@ func TestNewRedfishRemoteDirectClient(t *testing.T) {
// Test with empty remote URL
_, err = NewRedfishRemoteDirectClient(
context.Background(),
"",
computerSystemID,
"username",
"password",
"/tmp/test.iso",
false,
false,
@ -267,9 +312,10 @@ func TestNewRedfishRemoteDirectClient(t *testing.T) {
// Test with empty ephemeral NodeID
_, err = NewRedfishRemoteDirectClient(
context.Background(),
defaultURL,
"",
"username",
"password",
"/tmp/test.iso",
false,
false,
@ -279,9 +325,10 @@ func TestNewRedfishRemoteDirectClient(t *testing.T) {
// Test with empty Iso Path
_, err = NewRedfishRemoteDirectClient(
context.Background(),
defaultURL,
computerSystemID,
"username",
"password",
"",
false,
false,
@ -294,9 +341,10 @@ func getDefaultRedfishRemoteDirectObj(t *testing.T, api redfishAPI.RedfishAPI) R
t.Helper()
rDCfg, err := NewRedfishRemoteDirectClient(
context.Background(),
defaultURL,
computerSystemID,
"username",
"password",
"/tmp/test.iso",
false,
false,

View File

@ -1,7 +1,6 @@
package remote
import (
"context"
"fmt"
"net/url"
"strings"
@ -23,7 +22,11 @@ type Client interface {
}
// Get remotedirect client based on config
func getRemoteDirectClient(remoteConfig *config.RemoteDirect, remoteURL string) (Client, error) {
func getRemoteDirectClient(
remoteConfig *config.RemoteDirect,
remoteURL string,
username string,
password string) (Client, error) {
var client Client
switch remoteConfig.RemoteType {
case AirshipRemoteTypeRedfish:
@ -44,9 +47,10 @@ func getRemoteDirectClient(remoteConfig *config.RemoteDirect, remoteURL string)
nodeID := urlPath[len(urlPath)-1]
client, err = redfish.NewRedfishRemoteDirectClient(
context.Background(),
baseURL,
nodeID,
username,
password,
remoteConfig.IsoURL,
remoteConfig.Insecure,
remoteConfig.UseProxy,
@ -63,56 +67,60 @@ func getRemoteDirectClient(remoteConfig *config.RemoteDirect, remoteURL string)
return client, nil
}
func getRemoteDirectConfig(settings *environment.AirshipCTLSettings) (*config.RemoteDirect, string, error) {
func getRemoteDirectConfig(settings *environment.AirshipCTLSettings) (
remoteConfig *config.RemoteDirect,
remoteURL string,
username string,
password string,
err error) {
cfg := settings.Config()
bootstrapSettings, err := cfg.CurrentContextBootstrapInfo()
if err != nil {
return nil, "", err
return nil, "", "", "", err
}
remoteConfig := bootstrapSettings.RemoteDirect
remoteConfig = bootstrapSettings.RemoteDirect
if remoteConfig == nil {
return nil, "", config.ErrMissingConfig{What: "RemoteDirect options not defined in bootstrap config"}
return nil, "", "", "", config.ErrMissingConfig{What: "RemoteDirect options not defined in bootstrap config"}
}
root, err := cfg.CurrentContextEntryPoint(config.Ephemeral, "")
bundlePath, err := cfg.CurrentContextEntryPoint(config.Ephemeral, "")
if err != nil {
return nil, "", err
return nil, "", "", "", err
}
docBundle, err := document.NewBundleByPath(root)
docBundle, err := document.NewBundleByPath(bundlePath)
if err != nil {
return nil, "", err
return nil, "", "", "", err
}
selector := document.NewEphemeralBMHSelector()
docs, err := docBundle.Select(selector)
doc, err := docBundle.SelectOne(selector)
if err != nil {
return nil, "", err
}
if len(docs) == 0 {
return nil, "", document.ErrDocNotFound{
Selector: selector,
}
return nil, "", "", "", err
}
// NOTE If filter returned more than one document chose first
remoteURL, err := docs[0].GetString("spec.bmc.address")
remoteURL, err = document.GetBMHBMCAddress(doc)
if err != nil {
return nil, "", err
return nil, "", "", "", err
}
return remoteConfig, remoteURL, nil
username, password, err = document.GetBMHBMCCredentials(doc, docBundle)
if err != nil {
return nil, "", "", "", err
}
return remoteConfig, remoteURL, username, password, nil
}
// Top level function to execute remote direct based on remote type
func DoRemoteDirect(settings *environment.AirshipCTLSettings) error {
remoteConfig, remoteURL, err := getRemoteDirectConfig(settings)
remoteConfig, remoteURL, username, password, err := getRemoteDirectConfig(settings)
if err != nil {
return err
}
client, err := getRemoteDirectClient(remoteConfig, remoteURL)
client, err := getRemoteDirectClient(remoteConfig, remoteURL, username, password)
if err != nil {
return err
}