Add general inventory interface implementation

This commit also adds function to be used with command line pkg

Change-Id: Ifdebfd62817b071f06cad90a14897fda63808a7a
This commit is contained in:
Kostiantyn Kalynovskyi 2021-01-21 23:18:55 +00:00
parent decc12b0a9
commit 8958d7093c
11 changed files with 463 additions and 18 deletions

View File

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

View File

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

View File

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

102
pkg/inventory/command.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

13
pkg/inventory/testdata/hosts.yaml vendored Normal file
View File

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

View File

@ -0,0 +1,2 @@
resources:
- hosts.yaml

2
pkg/inventory/testdata/metadata.yaml vendored Normal file
View File

@ -0,0 +1,2 @@
inventory:
path: "."