From 8958d7093c224ad68aff283db3ad18b2dd7e6b44 Mon Sep 17 00:00:00 2001 From: Kostiantyn Kalynovskyi Date: Thu, 21 Jan 2021 23:18:55 +0000 Subject: [PATCH] Add general inventory interface implementation This commit also adds function to be used with command line pkg Change-Id: Ifdebfd62817b071f06cad90a14897fda63808a7a --- pkg/config/config.go | 7 +- pkg/inventory/baremetal/baremetal.go | 4 +- pkg/inventory/baremetal/baremetal_test.go | 6 +- pkg/inventory/command.go | 102 ++++++++++++++ pkg/inventory/command_test.go | 162 ++++++++++++++++++++++ pkg/inventory/ifc/selectors.go | 14 +- pkg/inventory/inventory.go | 74 ++++++++++ pkg/inventory/inventory_test.go | 95 +++++++++++++ pkg/inventory/testdata/hosts.yaml | 13 ++ pkg/inventory/testdata/kustomization.yaml | 2 + pkg/inventory/testdata/metadata.yaml | 2 + 11 files changed, 463 insertions(+), 18 deletions(-) create mode 100644 pkg/inventory/command.go create mode 100644 pkg/inventory/command_test.go create mode 100644 pkg/inventory/inventory.go create mode 100644 pkg/inventory/inventory_test.go create mode 100644 pkg/inventory/testdata/hosts.yaml create mode 100644 pkg/inventory/testdata/kustomization.yaml create mode 100644 pkg/inventory/testdata/metadata.yaml diff --git a/pkg/config/config.go b/pkg/config/config.go index 1f8f9297b..eb07f4872 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -322,7 +322,12 @@ func (c *Config) CurrentContextManifest() (*Manifest, error) { return nil, err } - return c.Manifests[currentContext.Manifest], nil + manifest, exist := c.Manifests[currentContext.Manifest] + if !exist { + return nil, ErrMissingConfig{What: "manifest named " + currentContext.Manifest} + } + + return manifest, nil } // CurrentContextTargetPath returns target path from current context's manifest diff --git a/pkg/inventory/baremetal/baremetal.go b/pkg/inventory/baremetal/baremetal.go index 9b2a49c57..a720ad3b8 100644 --- a/pkg/inventory/baremetal/baremetal.go +++ b/pkg/inventory/baremetal/baremetal.go @@ -28,7 +28,7 @@ import ( // Inventory implements baremetal invenotry interface type Inventory struct { - mgmtCfg config.ManagementConfiguration + mgmtCfg *config.ManagementConfiguration inventoryBundle document.Bundle } @@ -36,7 +36,7 @@ var _ ifc.BaremetalInventory = Inventory{} // NewInventory returns inventory implementation based on BaremetalHost objects func NewInventory( - mgmtCfg config.ManagementConfiguration, + mgmtCfg *config.ManagementConfiguration, inventoryBundle document.Bundle) ifc.BaremetalInventory { return Inventory{ mgmtCfg: mgmtCfg, diff --git a/pkg/inventory/baremetal/baremetal_test.go b/pkg/inventory/baremetal/baremetal_test.go index c38ff6fce..9f3f83643 100644 --- a/pkg/inventory/baremetal/baremetal_test.go +++ b/pkg/inventory/baremetal/baremetal_test.go @@ -69,7 +69,7 @@ func TestSelect(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - mgmCfg := config.ManagementConfiguration{Type: tt.remoteDriver} + mgmCfg := &config.ManagementConfiguration{Type: tt.remoteDriver} inventory := NewInventory(mgmCfg, bundle) hosts, err := inventory.Select(tt.selector) if tt.expectedErr != "" { @@ -106,7 +106,7 @@ func TestSelectOne(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - mgmCfg := config.ManagementConfiguration{Type: tt.remoteDriver} + mgmCfg := &config.ManagementConfiguration{Type: tt.remoteDriver} inventory := NewInventory(mgmCfg, bundle) host, err := inventory.SelectOne(tt.selector) if tt.expectedErr != "" { @@ -155,7 +155,7 @@ func TestRunAction(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { mgmCfg := config.ManagementConfiguration{Type: tt.remoteDriver} - inventory := NewInventory(mgmCfg, bundle) + inventory := NewInventory(&mgmCfg, bundle) err := inventory.RunOperation( context.Background(), tt.operation, diff --git a/pkg/inventory/command.go b/pkg/inventory/command.go new file mode 100644 index 000000000..0729adc44 --- /dev/null +++ b/pkg/inventory/command.go @@ -0,0 +1,102 @@ +/* + 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 inventory + +import ( + "context" + "fmt" + "io" + "time" + + "opendev.org/airship/airshipctl/pkg/inventory/ifc" + remoteifc "opendev.org/airship/airshipctl/pkg/remote/ifc" +) + +// CommandOptions is used to store common variables from cmd flags for baremetal command group +type CommandOptions struct { + Labels string + Name string + Namespace string + IsoURL string + Timeout time.Duration + + Invetnory ifc.Inventory +} + +// NewOptions options constructor +func NewOptions(i ifc.Inventory) *CommandOptions { + return &CommandOptions{ + Invetnory: i, + } +} + +// BMHAction performs an action against BaremetalHost objects +func (o *CommandOptions) BMHAction(op ifc.BaremetalOperation) error { + bmhInventory, err := o.Invetnory.BaremetalInventory() + if err != nil { + return err + } + ctx, cancel := context.WithTimeout(context.Background(), o.Timeout) + defer cancel() + return bmhInventory.RunOperation( + ctx, + op, + o.selector(), + ifc.BaremetalBatchRunOptions{}) +} + +// RemoteDirect perform RemoteDirect operation against single host +func (o *CommandOptions) RemoteDirect() error { + host, err := o.getHost() + if err != nil { + return err + } + ctx, cancel := context.WithTimeout(context.Background(), o.Timeout) + defer cancel() + return host.RemoteDirect(ctx, o.IsoURL) +} + +// PowerStatus get power status of the single host +func (o *CommandOptions) PowerStatus(w io.Writer) error { + host, err := o.getHost() + if err != nil { + return err + } + ctx, cancel := context.WithTimeout(context.Background(), o.Timeout) + defer cancel() + status, err := host.SystemPowerStatus(ctx) + if err != nil { + return err + } + // TODO support different output formats + fmt.Fprintf(w, "Host with node id '%s' has power status: '%s'\n", host.NodeID(), status) + return nil +} + +func (o *CommandOptions) getHost() (remoteifc.Client, error) { + bmhInventory, err := o.Invetnory.BaremetalInventory() + if err != nil { + return nil, err + } + + return bmhInventory.SelectOne(o.selector()) +} + +func (o *CommandOptions) selector() ifc.BaremetalHostSelector { + return (ifc.BaremetalHostSelector{}). + ByLabel(o.Labels). + ByName(o.Name). + ByNamespace(o.Namespace) +} diff --git a/pkg/inventory/command_test.go b/pkg/inventory/command_test.go new file mode 100644 index 000000000..4a57c6891 --- /dev/null +++ b/pkg/inventory/command_test.go @@ -0,0 +1,162 @@ +/* + 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 inventory_test + +import ( + "bytes" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "opendev.org/airship/airshipctl/pkg/inventory" + "opendev.org/airship/airshipctl/pkg/inventory/ifc" + "opendev.org/airship/airshipctl/pkg/remote/power" + mockinventory "opendev.org/airship/airshipctl/testutil/inventory" + "opendev.org/airship/airshipctl/testutil/redfishutils" +) + +func TestCommandOptions(t *testing.T) { + t.Run("error BMHAction bmh inventory", func(t *testing.T) { + inv := &mockinventory.MockInventory{} + expetedErr := fmt.Errorf("bmh inventory error") + inv.On("BaremetalInventory").Once().Return(nil, expetedErr) + + co := inventory.NewOptions(inv) + actualErr := co.BMHAction(ifc.BaremetalOperationPowerOn) + assert.Equal(t, expetedErr, actualErr) + }) + + t.Run("success BMHAction", func(t *testing.T) { + bmhInv := &mockinventory.MockBMHInventory{} + bmhInv.On("RunOperation").Once().Return(nil) + + inv := &mockinventory.MockInventory{} + inv.On("BaremetalInventory").Once().Return(bmhInv, nil) + + co := inventory.NewOptions(inv) + actualErr := co.BMHAction(ifc.BaremetalOperationPowerOn) + assert.Equal(t, nil, actualErr) + }) + + t.Run("error PowerStatus SelectOne", func(t *testing.T) { + expetedErr := fmt.Errorf("SelectOne inventory error") + bmhInv := &mockinventory.MockBMHInventory{} + bmhInv.On("SelectOne").Once().Return(nil, expetedErr) + + inv := &mockinventory.MockInventory{} + inv.On("BaremetalInventory").Once().Return(bmhInv, nil) + + co := inventory.NewOptions(inv) + buf := bytes.NewBuffer([]byte{}) + actualErr := co.PowerStatus(buf) + assert.Equal(t, expetedErr, actualErr) + assert.Len(t, buf.Bytes(), 0) + }) + + t.Run("error PowerStatus BMHInventory", func(t *testing.T) { + inv := &mockinventory.MockInventory{} + + expetedErr := fmt.Errorf("bmh inventory error") + inv.On("BaremetalInventory").Once().Return(nil, expetedErr) + + co := inventory.NewOptions(inv) + buf := bytes.NewBuffer([]byte{}) + actualErr := co.PowerStatus(buf) + assert.Equal(t, expetedErr, actualErr) + assert.Len(t, buf.Bytes(), 0) + }) + + t.Run("error PowerStatus SystemPowerStatus", func(t *testing.T) { + expetedErr := fmt.Errorf("SystemPowerStatus error") + host := &redfishutils.MockClient{} + host.On("SystemPowerStatus").Once().Return(power.StatusUnknown, expetedErr) + + bmhInv := &mockinventory.MockBMHInventory{} + bmhInv.On("SelectOne").Once().Return(host, nil) + + inv := &mockinventory.MockInventory{} + inv.On("BaremetalInventory").Once().Return(bmhInv, nil) + + co := inventory.NewOptions(inv) + buf := bytes.NewBuffer([]byte{}) + actualErr := co.PowerStatus(buf) + assert.Equal(t, expetedErr, actualErr) + assert.Len(t, buf.Bytes(), 0) + }) + + t.Run("success PowerStatus", func(t *testing.T) { + host := &redfishutils.MockClient{} + nodeID := "node01" + host.On("SystemPowerStatus").Once().Return(power.StatusPoweringOn, nil) + host.On("NodeID").Once().Return(nodeID) + + bmhInv := &mockinventory.MockBMHInventory{} + bmhInv.On("SelectOne").Once().Return(host, nil) + + inv := &mockinventory.MockInventory{} + inv.On("BaremetalInventory").Once().Return(bmhInv, nil) + + co := inventory.NewOptions(inv) + buf := bytes.NewBuffer([]byte{}) + actualErr := co.PowerStatus(buf) + assert.Equal(t, nil, actualErr) + assert.Contains(t, buf.String(), nodeID) + assert.Contains(t, buf.String(), power.StatusPoweringOn.String()) + }) + + t.Run("success RemoteDirect", func(t *testing.T) { + host := &redfishutils.MockClient{} + host.On("RemoteDirect").Once().Return(nil) + + bmhInv := &mockinventory.MockBMHInventory{} + bmhInv.On("SelectOne").Once().Return(host, nil) + + inv := &mockinventory.MockInventory{} + inv.On("BaremetalInventory").Once().Return(bmhInv, nil) + + co := inventory.NewOptions(inv) + co.IsoURL = "http://some-url" + actualErr := co.RemoteDirect() + assert.Equal(t, nil, actualErr) + }) + + t.Run("error RemoteDirect no isoURL", func(t *testing.T) { + host := &redfishutils.MockClient{} + host.On("RemoteDirect").Once() + + bmhInv := &mockinventory.MockBMHInventory{} + bmhInv.On("SelectOne").Once().Return(host, nil) + + inv := &mockinventory.MockInventory{} + inv.On("BaremetalInventory").Once().Return(bmhInv, nil) + + co := inventory.NewOptions(inv) + actualErr := co.RemoteDirect() + // Simply check if error is returned in isoURL is not specified + assert.Error(t, actualErr) + }) + + t.Run("error RemoteDirect BMHInventory", func(t *testing.T) { + inv := &mockinventory.MockInventory{} + + expetedErr := fmt.Errorf("bmh inventory error") + inv.On("BaremetalInventory").Once().Return(nil, expetedErr) + + co := inventory.NewOptions(inv) + actualErr := co.RemoteDirect() + assert.Equal(t, expetedErr, actualErr) + }) +} diff --git a/pkg/inventory/ifc/selectors.go b/pkg/inventory/ifc/selectors.go index dc1e1c467..e10755e04 100644 --- a/pkg/inventory/ifc/selectors.go +++ b/pkg/inventory/ifc/selectors.go @@ -17,18 +17,11 @@ package ifc // BaremetalHostSelector allows to select baremetal hosts, if used empty all possible hosts // will be should be returned by Select() method of BaremetalInvenotry interface type BaremetalHostSelector struct { - PhaseSelector PhaseSelector Name string Namespace string LabelSelector string } -// PhaseSelector allows to select hosts based on phase they belong to -type PhaseSelector struct { - Name string - Namespace string -} - // ByName allows to select hosts based on their name func (s BaremetalHostSelector) ByName(name string) BaremetalHostSelector { s.Name = name @@ -36,11 +29,8 @@ func (s BaremetalHostSelector) ByName(name string) BaremetalHostSelector { } // ByNamespace allows to select hosts based on their namespace -func (s BaremetalHostSelector) ByNamespace(name, namespace string) BaremetalHostSelector { - s.PhaseSelector = PhaseSelector{ - Name: name, - Namespace: namespace, - } +func (s BaremetalHostSelector) ByNamespace(namespace string) BaremetalHostSelector { + s.Namespace = namespace return s } diff --git a/pkg/inventory/inventory.go b/pkg/inventory/inventory.go new file mode 100644 index 000000000..10fe79fe9 --- /dev/null +++ b/pkg/inventory/inventory.go @@ -0,0 +1,74 @@ +/* + 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 inventory + +import ( + "path/filepath" + + "opendev.org/airship/airshipctl/pkg/config" + "opendev.org/airship/airshipctl/pkg/document" + "opendev.org/airship/airshipctl/pkg/inventory/baremetal" + "opendev.org/airship/airshipctl/pkg/inventory/ifc" +) + +var _ ifc.Inventory = Invetnory{} + +// Invetnory implementation of the interface +type Invetnory struct { + config.Factory +} + +// NewInventory inventory constructor +func NewInventory(f config.Factory) ifc.Inventory { + return Invetnory{ + Factory: f, + } +} + +// BaremetalInventory implementation of the interface +func (i Invetnory) BaremetalInventory() (ifc.BaremetalInventory, error) { + cfg, err := i.Factory() + if err != nil { + return nil, err + } + + mgmCfg, err := cfg.CurrentContextManagementConfig() + if err != nil { + return nil, err + } + + targetPath, err := cfg.CurrentContextTargetPath() + if err != nil { + return nil, err + } + + phaseDir, err := cfg.CurrentContextPhaseRepositoryDir() + if err != nil { + return nil, err + } + + metadata, err := cfg.CurrentContextManifestMetadata() + if err != nil { + return nil, err + } + + inventoryBundle := filepath.Join(targetPath, phaseDir, metadata.Inventory.Path) + + bundle, err := document.NewBundleByPath(inventoryBundle) + if err != nil { + return nil, err + } + return baremetal.NewInventory(mgmCfg, bundle), nil +} diff --git a/pkg/inventory/inventory_test.go b/pkg/inventory/inventory_test.go new file mode 100644 index 000000000..d3c3dcdd0 --- /dev/null +++ b/pkg/inventory/inventory_test.go @@ -0,0 +1,95 @@ +/* + 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 inventory_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "opendev.org/airship/airshipctl/pkg/config" + "opendev.org/airship/airshipctl/pkg/inventory" +) + +func TestBaremetalInventory(t *testing.T) { + tests := []struct { + name string + errString string + + factory config.Factory + }{ + { + name: "error no metadata file", + errString: "no such file or directory", + + factory: func() (*config.Config, error) { + return config.NewConfig(), nil + }, + }, + { + name: "error no management config", + errString: "Management configuration", + + factory: func() (*config.Config, error) { + cfg := config.NewConfig() + cfg.ManagementConfiguration = nil + return cfg, nil + }, + }, + { + name: "error no manifest defined", + errString: "Missing configuration: manifest named", + + factory: func() (*config.Config, error) { + cfg := config.NewConfig() + // empty manifest map + cfg.Manifests = make(map[string]*config.Manifest) + return cfg, nil + }, + }, + { + name: "success config", + + factory: func() (*config.Config, error) { + cfg := config.NewConfig() + manifest, err := cfg.CurrentContextManifest() + require.NoError(t, err) + manifest.MetadataPath = "metadata.yaml" + manifest.PhaseRepositoryName = "testdata" + manifest.Repositories["testdata"] = &config.Repository{ + URLString: "/myrepo/testdata", + } + manifest.TargetPath = "." + return cfg, nil + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + i := inventory.NewInventory(tt.factory) + bmhInv, err := i.BaremetalInventory() + if tt.errString != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errString) + } else { + require.NoError(t, err) + assert.NotNil(t, bmhInv) + } + }) + } +} diff --git a/pkg/inventory/testdata/hosts.yaml b/pkg/inventory/testdata/hosts.yaml new file mode 100644 index 000000000..5b7065c77 --- /dev/null +++ b/pkg/inventory/testdata/hosts.yaml @@ -0,0 +1,13 @@ +--- +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+http://nolocalhost:32201/redfish/v1/Systems/ephemeral + credentialsName: master-0-bmc-secret \ No newline at end of file diff --git a/pkg/inventory/testdata/kustomization.yaml b/pkg/inventory/testdata/kustomization.yaml new file mode 100644 index 000000000..498247f48 --- /dev/null +++ b/pkg/inventory/testdata/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - hosts.yaml \ No newline at end of file diff --git a/pkg/inventory/testdata/metadata.yaml b/pkg/inventory/testdata/metadata.yaml new file mode 100644 index 000000000..e40febd8f --- /dev/null +++ b/pkg/inventory/testdata/metadata.yaml @@ -0,0 +1,2 @@ +inventory: + path: "." \ No newline at end of file