Add BaremetalManager executor

This will allow to peform remote BMH operations as a phase

Change-Id: I8e99285e0407d1922312a08ad4f766363f8855d2
This commit is contained in:
Kostiantyn Kalynovskyi 2021-01-26 02:38:12 +00:00 committed by Kostyantyn Kalynovskyi
parent 08681115d5
commit f0e80cfdc5
8 changed files with 397 additions and 8 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()),
},

View File

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

View File

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