From f0e80cfdc55f1cec590a11db19705934fb53ebe9 Mon Sep 17 00:00:00 2001 From: Kostiantyn Kalynovskyi Date: Tue, 26 Jan 2021 02:38:12 +0000 Subject: [PATCH] Add BaremetalManager executor This will allow to peform remote BMH operations as a phase Change-Id: I8e99285e0407d1922312a08ad4f766363f8855d2 --- pkg/events/events.go | 31 ++- pkg/phase/client.go | 2 +- pkg/phase/executors/baremetal_manager.go | 142 ++++++++++++ pkg/phase/executors/baremetal_manager_test.go | 212 ++++++++++++++++++ pkg/phase/executors/clusterctl.go | 2 +- pkg/phase/executors/clusterctl_test.go | 2 +- pkg/phase/executors/common.go | 4 + pkg/phase/executors/errors.go | 10 +- 8 files changed, 397 insertions(+), 8 deletions(-) create mode 100644 pkg/phase/executors/baremetal_manager.go create mode 100644 pkg/phase/executors/baremetal_manager_test.go diff --git a/pkg/events/events.go b/pkg/events/events.go index 3eca9686b..f96b018f7 100644 --- a/pkg/events/events.go +++ b/pkg/events/events.go @@ -40,8 +40,10 @@ const ( IsogenType // BootstrapType event emitted by Bootstrap executor BootstrapType - //GenericContainerType event emitted by GenericContainer + // GenericContainerType event emitted by GenericContainer GenericContainerType + // BaremetalManagerEventType event emitted by BaremetalManager + BaremetalManagerEventType ) // Event holds all possible events that can be produced by airship @@ -55,6 +57,7 @@ type Event struct { IsogenEvent IsogenEvent BootstrapEvent BootstrapEvent GenericContainerEvent GenericContainerEvent + BaremetalManagerEvent BaremetalManagerEvent } //GenericEvent generalized type for custom events @@ -260,3 +263,29 @@ func (e Event) WithGenericContainerEvent(concreteEvent GenericContainerEvent) Ev e.GenericContainerEvent = concreteEvent return e } + +// BaremetalManagerStep indicates what operation baremetal manager is currently peforming +// Note that this is not baremetal +type BaremetalManagerStep int + +const ( + // BaremetalManagerStart operation + BaremetalManagerStart BaremetalManagerStep = iota + // BaremetalManagerComplete operation + BaremetalManagerComplete +) + +// BaremetalManagerEvent event emitted by BaremetalManager +type BaremetalManagerEvent struct { + Step BaremetalManagerStep + // HostOperation indicates which operation is performed against BMH Host + HostOperation string + Message string +} + +// WithBaremetalManagerEvent sets type and actual bootstrap event +func (e Event) WithBaremetalManagerEvent(concreteEvent BaremetalManagerEvent) Event { + e.Type = BaremetalManagerEventType + e.BaremetalManagerEvent = concreteEvent + return e +} diff --git a/pkg/phase/client.go b/pkg/phase/client.go index 686a2effe..14820816b 100644 --- a/pkg/phase/client.go +++ b/pkg/phase/client.go @@ -39,7 +39,7 @@ func DefaultExecutorRegistry() map[schema.GroupVersionKind]ifc.ExecutorFactory { execMap := make(map[schema.GroupVersionKind]ifc.ExecutorFactory) for _, execName := range []string{executors.Clusterctl, executors.KubernetesApply, - executors.Isogen, executors.GenericContainer, executors.Ephemeral} { + executors.Isogen, executors.GenericContainer, executors.Ephemeral, executors.BMHManager} { if err := executors.RegisterExecutor(execName, execMap); err != nil { log.Fatal(ErrExecutorRegistration{ExecutorName: execName, Err: err}) } diff --git a/pkg/phase/executors/baremetal_manager.go b/pkg/phase/executors/baremetal_manager.go new file mode 100644 index 000000000..49a0b23dc --- /dev/null +++ b/pkg/phase/executors/baremetal_manager.go @@ -0,0 +1,142 @@ +/* + 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 executors + +import ( + "fmt" + "io" + "time" + + "opendev.org/airship/airshipctl/pkg/api/v1alpha1" + airshipv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1" + "opendev.org/airship/airshipctl/pkg/events" + "opendev.org/airship/airshipctl/pkg/inventory" + inventoryifc "opendev.org/airship/airshipctl/pkg/inventory/ifc" + "opendev.org/airship/airshipctl/pkg/phase/ifc" +) + +// BaremetalManagerExectuor is abstraction built on top of baremetal commands of airshipctl +type BaremetalManagerExectuor struct { + inventory inventoryifc.Inventory + options *airshipv1.BaremetalManager +} + +// NewBaremetalExecutor constructor for baremetal executor +func NewBaremetalExecutor(cfg ifc.ExecutorConfig) (ifc.Executor, error) { + inv, err := cfg.Helper.Inventory() + if err != nil { + return nil, err + } + options := airshipv1.DefaultBaremetalManager() + if err := cfg.ExecutorDocument.ToAPIObject(options, airshipv1.Scheme); err != nil { + return nil, err + } + return &BaremetalManagerExectuor{ + inventory: inv, + options: options, + }, nil +} + +// Run runs baremetal operations as exectuor +func (e *BaremetalManagerExectuor) Run(evtCh chan events.Event, opts ifc.RunOptions) { + defer close(evtCh) + commandOptions := toCommandOptions(e.inventory, e.options.Spec, opts) + + evtCh <- events.NewEvent().WithBaremetalManagerEvent(events.BaremetalManagerEvent{ + Step: events.BaremetalManagerStart, + HostOperation: string(e.options.Spec.Operation), + Message: fmt.Sprintf("Starting remote operation, selector to be to filter hosts %v", + e.options.Spec.HostSelector), + }) + + op, err := e.validate() + if err != nil { + handleError(evtCh, err) + return + } + if !opts.DryRun { + switch e.options.Spec.Operation { + case airshipv1.BaremetalOperationPowerOn, airshipv1.BaremetalOperationPowerOff, + airshipv1.BaremetalOperationReboot, airshipv1.BaremetalOperationEjectVirtualMedia: + err = commandOptions.BMHAction(op) + case airshipv1.BaremetalOperationRemoteDirect: + err = commandOptions.RemoteDirect() + } + } + + if err != nil { + handleError(evtCh, err) + return + } + + evtCh <- events.NewEvent().WithBaremetalManagerEvent(events.BaremetalManagerEvent{ + Step: events.BaremetalManagerComplete, + HostOperation: string(e.options.Spec.Operation), + Message: fmt.Sprintf("Successfully completed operation against host selected by selector %v", + e.options.Spec.HostSelector), + }) +} + +// Validate executor configuration and documents +func (e *BaremetalManagerExectuor) Validate() error { + _, err := e.validate() + return err +} + +func (e *BaremetalManagerExectuor) validate() (inventoryifc.BaremetalOperation, error) { + var result inventoryifc.BaremetalOperation + var err error + switch e.options.Spec.Operation { + case airshipv1.BaremetalOperationPowerOn: + result = inventoryifc.BaremetalOperationPowerOn + case airshipv1.BaremetalOperationPowerOff: + result = inventoryifc.BaremetalOperationPowerOff + case airshipv1.BaremetalOperationEjectVirtualMedia: + result = inventoryifc.BaremetalOperationEjectVirtualMedia + case airshipv1.BaremetalOperationReboot: + result = inventoryifc.BaremetalOperationReboot + case airshipv1.BaremetalOperationRemoteDirect: + // TODO add remote direct validation, make sure that ISO-URL is specified + result = "" + default: + err = ErrUnknownExecutorAction{Action: string(e.options.Spec.Operation), ExecutorName: BMHManager} + } + return result, err +} + +// Render baremetal hosts +func (e *BaremetalManagerExectuor) Render(w io.Writer, _ ifc.RenderOptions) error { + // add printing of baremetal hosts here + _, err := w.Write([]byte{}) + return err +} + +func toCommandOptions(i inventoryifc.Inventory, + spec v1alpha1.BaremetalManagerSpec, + opts ifc.RunOptions) *inventory.CommandOptions { + timeout := time.Duration(spec.Timeout) * time.Second + if opts.Timeout != 0 { + timeout = opts.Timeout + } + + return &inventory.CommandOptions{ + Invetnory: i, + IsoURL: spec.OperationOptions.RemoteDirect.ISOURL, + Labels: spec.HostSelector.LabelSelector, + Name: spec.HostSelector.Name, + Namespace: spec.HostSelector.Namespace, + Timeout: timeout, + } +} diff --git a/pkg/phase/executors/baremetal_manager_test.go b/pkg/phase/executors/baremetal_manager_test.go new file mode 100644 index 000000000..480697dfd --- /dev/null +++ b/pkg/phase/executors/baremetal_manager_test.go @@ -0,0 +1,212 @@ +/* + 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 executors_test + +import ( + "bytes" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "opendev.org/airship/airshipctl/pkg/document" + "opendev.org/airship/airshipctl/pkg/events" + "opendev.org/airship/airshipctl/pkg/k8s/utils" + "opendev.org/airship/airshipctl/pkg/phase/executors" + "opendev.org/airship/airshipctl/pkg/phase/ifc" + testdoc "opendev.org/airship/airshipctl/testutil/document" +) + +var bmhExecutorTemplate = `apiVersion: airshipit.org/v1alpha1 +kind: BaremetalManager +metadata: + name: RemoteDirectEphemeral + labels: + airshipit.org/deploy-k8s: "false" +spec: + operation: "%s" + hostSelector: + name: node02 + operationOptions: + remoteDirect: + isoURL: %s` + +func TestNewBMHExecutor(t *testing.T) { + t.Run("success", func(t *testing.T) { + execDoc := executorDoc(t, fmt.Sprintf(bmhExecutorTemplate, "reboot", "/home/iso-url")) + executor, err := executors.NewBaremetalExecutor(ifc.ExecutorConfig{ + ExecutorDocument: execDoc, + BundleFactory: testBundleFactory(singleExecutorBundlePath), + Helper: makeDefaultHelper(t, "../testdata"), + }) + assert.NoError(t, err) + assert.NotNil(t, executor) + }) + + t.Run("error", func(t *testing.T) { + exepectedErr := fmt.Errorf("ToAPI error") + execDoc := &testdoc.MockDocument{ + MockToAPIObject: func() error { return exepectedErr }, + } + executor, actualErr := executors.NewBaremetalExecutor(ifc.ExecutorConfig{ + ExecutorDocument: execDoc, + BundleFactory: testBundleFactory(singleExecutorBundlePath), + Helper: makeDefaultHelper(t, "../testdata"), + }) + assert.Equal(t, exepectedErr, actualErr) + assert.Nil(t, executor) + }) +} + +func TestBMHExecutorRun(t *testing.T) { + tests := []struct { + name string + expectedErr string + runOptions ifc.RunOptions + execDoc document.Document + }{ + { + name: "error validate dry-run", + expectedErr: "unknown action type", + runOptions: ifc.RunOptions{ + DryRun: true, + // any value but zero + Timeout: 40, + }, + execDoc: executorDoc(t, fmt.Sprintf(bmhExecutorTemplate, "unknown", "")), + }, + { + name: "success validate dry-run", + runOptions: ifc.RunOptions{ + DryRun: true, + }, + execDoc: executorDoc(t, fmt.Sprintf(bmhExecutorTemplate, "remote-direct", "/some/url")), + }, + { + name: "error unknown action type", + runOptions: ifc.RunOptions{}, + execDoc: executorDoc(t, fmt.Sprintf(bmhExecutorTemplate, "unknown", "")), + expectedErr: "unknown action type", + }, + { + name: "error no kustomization.yaml for inventory remote-direct", + runOptions: ifc.RunOptions{}, + execDoc: executorDoc(t, fmt.Sprintf(bmhExecutorTemplate, "remote-direct", "")), + expectedErr: "kustomization.yaml", + }, + { + name: "error no kustomization.yaml for inventory remote-direct", + runOptions: ifc.RunOptions{}, + execDoc: executorDoc(t, fmt.Sprintf(bmhExecutorTemplate, "reboot", "")), + expectedErr: "kustomization.yaml", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + executor, err := executors.NewBaremetalExecutor(ifc.ExecutorConfig{ + ExecutorDocument: tt.execDoc, + BundleFactory: testBundleFactory(singleExecutorBundlePath), + Helper: makeDefaultHelper(t, "../testdata/"), + }) + require.NoError(t, err) + require.NotNil(t, executor) + ch := make(chan events.Event) + go func() { + executor.Run(ch, tt.runOptions) + }() + processor := events.NewDefaultProcessor(utils.Streams()) + defer processor.Close() + err = processor.Process(ch) + if tt.expectedErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedErr) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestBMHValidate(t *testing.T) { + tests := []struct { + name string + expectedErr string + execDoc document.Document + }{ + { + name: "error validate unknown action", + expectedErr: "unknown action type", + execDoc: executorDoc(t, fmt.Sprintf(bmhExecutorTemplate, "unknown", "")), + }, + { + name: "success validate remote-direct", + execDoc: executorDoc(t, fmt.Sprintf(bmhExecutorTemplate, "remote-direct", "/some/url")), + }, + { + name: "success validate reboot", + execDoc: executorDoc(t, fmt.Sprintf(bmhExecutorTemplate, "reboot", "/some/url")), + }, + { + name: "success validate power-off", + execDoc: executorDoc(t, fmt.Sprintf(bmhExecutorTemplate, "power-off", "/some/url")), + }, + { + name: "success validate power-on", + execDoc: executorDoc(t, fmt.Sprintf(bmhExecutorTemplate, "power-on", "/some/url")), + }, + { + name: "success validate eject-virtual-media", + execDoc: executorDoc(t, fmt.Sprintf(bmhExecutorTemplate, "eject-virtual-media", "/some/url")), + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + executor, err := executors.NewBaremetalExecutor(ifc.ExecutorConfig{ + ExecutorDocument: tt.execDoc, + BundleFactory: testBundleFactory(singleExecutorBundlePath), + Helper: makeDefaultHelper(t, "../testdata/"), + }) + require.NoError(t, err) + require.NotNil(t, executor) + + actualErr := executor.Validate() + if tt.expectedErr != "" { + require.Error(t, actualErr) + assert.Contains(t, actualErr.Error(), tt.expectedErr) + } else { + assert.NoError(t, actualErr) + } + }) + } +} + +// Dummy test to keep up with coverage, develop better testcases when render is implemented +func TestBMHManagerRender(t *testing.T) { + execDoc := executorDoc(t, fmt.Sprintf(bmhExecutorTemplate, "reboot", "/home/iso-url")) + executor, err := executors.NewBaremetalExecutor(ifc.ExecutorConfig{ + ExecutorDocument: execDoc, + BundleFactory: testBundleFactory(singleExecutorBundlePath), + Helper: makeDefaultHelper(t, "../testdata"), + }) + require.NoError(t, err) + require.NotNil(t, executor) + + err = executor.Render(bytes.NewBuffer([]byte{}), ifc.RenderOptions{}) + assert.NoError(t, err) +} diff --git a/pkg/phase/executors/clusterctl.go b/pkg/phase/executors/clusterctl.go index 9a6120155..daf5993e9 100755 --- a/pkg/phase/executors/clusterctl.go +++ b/pkg/phase/executors/clusterctl.go @@ -67,7 +67,7 @@ func (c *ClusterctlExecutor) Run(evtCh chan events.Event, opts ifc.RunOptions) { case airshipv1.Init: c.init(opts, evtCh) default: - handleError(evtCh, ErrUnknownExecutorAction{Action: string(c.options.Action)}) + handleError(evtCh, ErrUnknownExecutorAction{Action: string(c.options.Action), ExecutorName: "clusterctl"}) } } diff --git a/pkg/phase/executors/clusterctl_test.go b/pkg/phase/executors/clusterctl_test.go index a75bdd942..b6758ecd6 100755 --- a/pkg/phase/executors/clusterctl_test.go +++ b/pkg/phase/executors/clusterctl_test.go @@ -96,7 +96,7 @@ func TestExecutorRun(t *testing.T) { cfgDoc: executorDoc(t, fmt.Sprintf(executorConfigTmpl, "someAction")), bundlePath: "testdata/executor_init", expectedEvt: []events.Event{ - wrapError(executors.ErrUnknownExecutorAction{Action: "someAction"}), + wrapError(executors.ErrUnknownExecutorAction{Action: "someAction", ExecutorName: "clusterctl"}), }, clusterMap: clustermap.NewClusterMap(v1alpha1.DefaultClusterMap()), }, diff --git a/pkg/phase/executors/common.go b/pkg/phase/executors/common.go index 08d874165..5fb288f4d 100755 --- a/pkg/phase/executors/common.go +++ b/pkg/phase/executors/common.go @@ -29,6 +29,7 @@ const ( Isogen = "isogen" GenericContainer = "generic-container" Ephemeral = "ephemeral" + BMHManager = "BaremetalManager" ) // RegisterExecutor adds executor to phase executor registry @@ -53,6 +54,9 @@ func RegisterExecutor(executorName string, registry map[schema.GroupVersionKind] case Ephemeral: gvks, _, err = airshipv1.Scheme.ObjectKinds(airshipv1.DefaultBootConfiguration()) execObj = NewEphemeralExecutor + case BMHManager: + gvks, _, err = airshipv1.Scheme.ObjectKinds(&airshipv1.BaremetalManager{}) + execObj = NewBaremetalExecutor default: return ErrUnknownExecutorName{ExecutorName: executorName} } diff --git a/pkg/phase/executors/errors.go b/pkg/phase/executors/errors.go index c3a5c7e4f..c92d8b26f 100755 --- a/pkg/phase/executors/errors.go +++ b/pkg/phase/executors/errors.go @@ -18,14 +18,16 @@ import ( "fmt" ) -// ErrUnknownExecutorAction is returned for unknown action parameter -// in clusterctl configuration document +// ErrUnknownExecutorAction is returned when unknown action or operation is requested +// from one of the executors. type ErrUnknownExecutorAction struct { - Action string + Action string + ExecutorName string } func (e ErrUnknownExecutorAction) Error() string { - return fmt.Sprintf("unknown action type '%s'", e.Action) + return fmt.Sprintf("unknown action type '%s' was requested from executor '%s'", + e.Action, e.ExecutorName) } // ErrIsogenNilBundle is returned when isogen executor is not provided with bundle