diff --git a/pkg/bootstrap/cloudinit/cloud-init.go b/pkg/bootstrap/cloudinit/cloud-init.go index c8aef2e5b..77da0a6f3 100644 --- a/pkg/bootstrap/cloudinit/cloud-init.go +++ b/pkg/bootstrap/cloudinit/cloud-init.go @@ -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 } diff --git a/pkg/document/bundle.go b/pkg/document/bundle.go index 80326ad2b..7ecaa908b 100644 --- a/pkg/document/bundle.go +++ b/pkg/document/bundle.go @@ -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 diff --git a/pkg/document/dochelper_baremetalhost.go b/pkg/document/dochelper_baremetalhost.go new file mode 100644 index 000000000..95a6caeb4 --- /dev/null +++ b/pkg/document/dochelper_baremetalhost.go @@ -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 +} diff --git a/pkg/document/dochelper_test.go b/pkg/document/dochelper_test.go new file mode 100644 index 000000000..dde62ef9f --- /dev/null +++ b/pkg/document/dochelper_test.go @@ -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") + }) +} diff --git a/pkg/document/dochelper_utils.go b/pkg/document/dochelper_utils.go new file mode 100644 index 000000000..76d4750a4 --- /dev/null +++ b/pkg/document/dochelper_utils.go @@ -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 +} diff --git a/pkg/document/errors.go b/pkg/document/errors.go index 769c7f208..84ca11334 100644 --- a/pkg/document/errors.go +++ b/pkg/document/errors.go @@ -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) } diff --git a/pkg/document/selectors.go b/pkg/document/selectors.go index 9dfd0595e..e4367c49c 100644 --- a/pkg/document/selectors.go +++ b/pkg/document/selectors.go @@ -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") diff --git a/pkg/document/selectors_test.go b/pkg/document/selectors_test.go index fc038d58b..949221f94 100644 --- a/pkg/document/selectors_test.go +++ b/pkg/document/selectors_test.go @@ -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) diff --git a/pkg/document/testdata/dochelper/baremetalhost.yaml b/pkg/document/testdata/dochelper/baremetalhost.yaml new file mode 100644 index 000000000..f20e8c0da --- /dev/null +++ b/pkg/document/testdata/dochelper/baremetalhost.yaml @@ -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 diff --git a/pkg/document/testdata/dochelper/kustomization.yaml b/pkg/document/testdata/dochelper/kustomization.yaml new file mode 100644 index 000000000..3935bdb09 --- /dev/null +++ b/pkg/document/testdata/dochelper/kustomization.yaml @@ -0,0 +1,3 @@ +resources: + - baremetalhost.yaml + - secret.yaml \ No newline at end of file diff --git a/pkg/document/testdata/dochelper/secret.yaml b/pkg/document/testdata/dochelper/secret.yaml new file mode 100644 index 000000000..4bafed861 --- /dev/null +++ b/pkg/document/testdata/dochelper/secret.yaml @@ -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 \ No newline at end of file diff --git a/pkg/document/testdata/selectors/valid/secret.yaml b/pkg/document/testdata/selectors/valid/secret.yaml index 25a11a87c..f39541cd2 100644 --- a/pkg/document/testdata/selectors/valid/secret.yaml +++ b/pkg/document/testdata/selectors/valid/secret.yaml @@ -17,5 +17,4 @@ metadata: namespace: validNamespace type: Opaque stringData: - userData: cloud-init - + userData: cloud-init \ No newline at end of file diff --git a/pkg/remote/redfish/redfish.go b/pkg/remote/redfish/redfish.go index 6bacc6289..a20ab81db 100644 --- a/pkg/remote/redfish/redfish.go +++ b/pkg/remote/redfish/redfish.go @@ -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{ diff --git a/pkg/remote/redfish/redfish_test.go b/pkg/remote/redfish/redfish_test.go index a1765c084..29914122a 100644 --- a/pkg/remote/redfish/redfish_test.go +++ b/pkg/remote/redfish/redfish_test.go @@ -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, diff --git a/pkg/remote/remote_direct.go b/pkg/remote/remote_direct.go index 0a328ef21..0c8784d6c 100644 --- a/pkg/remote/remote_direct.go +++ b/pkg/remote/remote_direct.go @@ -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 }