From fa03b40f4affa891cab94061014999881747483f Mon Sep 17 00:00:00 2001
From: Kostiantyn Kalynovskyi <kkalynovskyi@mirantis.com>
Date: Tue, 10 Nov 2020 16:40:22 -0600
Subject: [PATCH] Implement RunAction method of the baremetal interface

Change-Id: I86b2cd7824c68ccd4fac875a5ad1d3ff9cd69072
Relates-To: #397
Relates-To: #362
Relates-To: #359
---
 pkg/inventory/baremetal/baremetal.go        | 52 ++++++++++-
 pkg/inventory/baremetal/baremetal_test.go   | 96 +++++++++++++++++++++
 pkg/inventory/baremetal/errors.go           |  9 ++
 pkg/inventory/baremetal/testdata/hosts.yaml |  2 +-
 4 files changed, 156 insertions(+), 3 deletions(-)

diff --git a/pkg/inventory/baremetal/baremetal.go b/pkg/inventory/baremetal/baremetal.go
index 62bf30815..9b2a49c57 100644
--- a/pkg/inventory/baremetal/baremetal.go
+++ b/pkg/inventory/baremetal/baremetal.go
@@ -19,7 +19,6 @@ import (
 
 	"opendev.org/airship/airshipctl/pkg/config"
 	"opendev.org/airship/airshipctl/pkg/document"
-	"opendev.org/airship/airshipctl/pkg/errors"
 	"opendev.org/airship/airshipctl/pkg/inventory/ifc"
 	"opendev.org/airship/airshipctl/pkg/log"
 	remoteifc "opendev.org/airship/airshipctl/pkg/remote/ifc"
@@ -89,7 +88,33 @@ func (i Inventory) RunOperation(
 	op ifc.BaremetalOperation,
 	selector ifc.BaremetalHostSelector,
 	_ ifc.BaremetalBatchRunOptions) error {
-	return errors.ErrNotImplemented{What: "RunOperation of the baremetal inventory interface"}
+	log.Debugf("Running operation '%s' against hosts selected by selector '%v'", op, selector)
+
+	hostAction, err := action(ctx, op)
+	if err != nil {
+		return err
+	}
+
+	hosts, err := i.Select(selector)
+	if err != nil {
+		return err
+	}
+
+	if len(hosts) == 0 {
+		log.Printf("Filtering using selector %v' didn't return any hosts to perform operation '%s'", selector, op)
+		return ErrNoBaremetalHostsFound{Selector: selector}
+	}
+
+	// TODO add concurent action execution
+	// TODO consider adding FailFast flag to BaremetalBatchRunOptions that would allow
+	// not fail on first error, but accumulate errors and return them at the end.
+	for _, host := range hosts {
+		if hostErr := hostAction(host); hostErr != nil {
+			return hostErr
+		}
+	}
+
+	return nil
 }
 
 // Host implements baremetal host interface
@@ -138,6 +163,29 @@ func (i Inventory) newHost(doc document.Document) (Host, error) {
 	return Host{Client: client}, nil
 }
 
+func action(ctx context.Context, op ifc.BaremetalOperation) (func(remoteifc.Client) error, error) {
+	switch op {
+	case ifc.BaremetalOperationReboot:
+		return func(host remoteifc.Client) error {
+			return host.RebootSystem(ctx)
+		}, nil
+	case ifc.BaremetalOperationPowerOff:
+		return func(host remoteifc.Client) error {
+			return host.SystemPowerOff(ctx)
+		}, nil
+	case ifc.BaremetalOperationPowerOn:
+		return func(host remoteifc.Client) error {
+			return host.SystemPowerOn(ctx)
+		}, nil
+	case ifc.BaremetalOperationEjectVirtualMedia:
+		return func(host remoteifc.Client) error {
+			return host.EjectVirtualMedia(ctx)
+		}, nil
+	default:
+		return nil, ErrBaremetalOperationNotSupported{Operation: op}
+	}
+}
+
 func toDocumentSelector(selector ifc.BaremetalHostSelector) document.Selector {
 	return document.NewSelector().
 		ByKind(document.BareMetalHostKind).
diff --git a/pkg/inventory/baremetal/baremetal_test.go b/pkg/inventory/baremetal/baremetal_test.go
index 454c64e17..c38ff6fce 100644
--- a/pkg/inventory/baremetal/baremetal_test.go
+++ b/pkg/inventory/baremetal/baremetal_test.go
@@ -15,6 +15,7 @@
 package baremetal
 
 import (
+	"context"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -119,6 +120,101 @@ func TestSelectOne(t *testing.T) {
 	}
 }
 
+func TestRunAction(t *testing.T) {
+	tests := []struct {
+		name, remoteDriver, expectedErr string
+		operation                       ifc.BaremetalOperation
+
+		selector ifc.BaremetalHostSelector
+	}{
+		{
+			name:         "success return one host",
+			remoteDriver: "redfish",
+			operation:    ifc.BaremetalOperation("not supported"),
+			selector:     (ifc.BaremetalHostSelector{}).ByName("master-0"),
+			expectedErr:  "Baremetal operation not supported",
+		},
+		{
+			name:         "success return one host",
+			remoteDriver: "redfish",
+			operation:    ifc.BaremetalOperationPowerOn,
+			selector:     (ifc.BaremetalHostSelector{}).ByName("does not exist"),
+			expectedErr:  "No baremetal hosts matched selector",
+		},
+		{
+			name:         "success return one host",
+			remoteDriver: "redfish",
+			operation:    ifc.BaremetalOperationPowerOn,
+			selector:     (ifc.BaremetalHostSelector{}).ByName("master-0"),
+			expectedErr:  "HTTP request failed",
+		},
+	}
+
+	bundle := testBundle(t)
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			mgmCfg := config.ManagementConfiguration{Type: tt.remoteDriver}
+			inventory := NewInventory(mgmCfg, bundle)
+			err := inventory.RunOperation(
+				context.Background(),
+				tt.operation,
+				tt.selector,
+				ifc.BaremetalBatchRunOptions{})
+			if tt.expectedErr != "" {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), tt.expectedErr)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestAction(t *testing.T) {
+	tests := []struct {
+		name      string
+		action    ifc.BaremetalOperation
+		expectErr bool
+	}{
+		{
+			name:   "poweron",
+			action: ifc.BaremetalOperationPowerOn,
+		},
+		{
+			name:   "poweroff",
+			action: ifc.BaremetalOperationPowerOff,
+		},
+		{
+			name:   "ejectvirtualmedia",
+			action: ifc.BaremetalOperationEjectVirtualMedia,
+		},
+		{
+			name:   "reboot",
+			action: ifc.BaremetalOperationReboot,
+		},
+		{
+			name:      "reboot",
+			action:    ifc.BaremetalOperation("not supported"),
+			expectErr: true,
+		},
+	}
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			actionFunc, err := action(context.Background(), tt.action)
+			if tt.expectErr {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				// TODO inject fake host interface here to validate
+				// that correct actions were selected
+				assert.NotNil(t, actionFunc)
+			}
+		})
+	}
+}
+
 func testBundle(t *testing.T) document.Bundle {
 	t.Helper()
 	bundle, err := document.NewBundleByPath("testdata")
diff --git a/pkg/inventory/baremetal/errors.go b/pkg/inventory/baremetal/errors.go
index 746bbae18..407626ed6 100644
--- a/pkg/inventory/baremetal/errors.go
+++ b/pkg/inventory/baremetal/errors.go
@@ -40,3 +40,12 @@ type ErrNoBaremetalHostsFound struct {
 func (e ErrNoBaremetalHostsFound) Error() string {
 	return fmt.Sprintf("No baremetal hosts matched selector %v", e.Selector)
 }
+
+// ErrBaremetalOperationNotSupported is returned when baremetal operation is not supported
+type ErrBaremetalOperationNotSupported struct {
+	Operation ifc.BaremetalOperation
+}
+
+func (e ErrBaremetalOperationNotSupported) Error() string {
+	return fmt.Sprintf("Baremetal operation not supported: '%s'", e.Operation)
+}
diff --git a/pkg/inventory/baremetal/testdata/hosts.yaml b/pkg/inventory/baremetal/testdata/hosts.yaml
index d8a67f9a9..3f01ddbdf 100644
--- a/pkg/inventory/baremetal/testdata/hosts.yaml
+++ b/pkg/inventory/baremetal/testdata/hosts.yaml
@@ -9,7 +9,7 @@ spec:
   online: true
   bootMACAddress: 00:3b:8b:0c:ec:8b
   bmc:
-    address: redfish+http://nolocalhost:8888/redfish/v1/Systems/ephemeral
+    address: redfish+http://nolocalhost:32201/redfish/v1/Systems/ephemeral
     credentialsName: master-0-bmc-secret
 ---
 apiVersion: v1