Add remote host power status command

This change introduces a command that allows the user to retrieve the
power status of a remote host.

Relates-To: #5

Change-Id: I4d3ded6667a5427ad6814c3d000da3becaec50a1
Signed-off-by: Drew Walters <andrew.walters@att.com>
This commit is contained in:
Drew Walters 2020-04-01 19:12:31 +00:00
parent d25c22a538
commit 0c3eefe036
9 changed files with 100 additions and 68 deletions

View File

@ -16,7 +16,6 @@ import (
"github.com/spf13/cobra"
"opendev.org/airship/airshipctl/pkg/environment"
"opendev.org/airship/airshipctl/pkg/errors"
)
// NewRemoteCommand creates a new command that provides functionality to control remote entities.
@ -24,10 +23,10 @@ func NewRemoteCommand(rootSettings *environment.AirshipCTLSettings) *cobra.Comma
remoteRootCmd := &cobra.Command{
Use: "remote",
Short: "Control remote entities, i.e. hosts.",
RunE: func(cmd *cobra.Command, args []string) error {
return errors.ErrNotImplemented{}
},
}
powerStatusCmd := NewPowerStatusCommand(rootSettings)
remoteRootCmd.AddCommand(powerStatusCmd)
return remoteRootCmd
}

View File

@ -0,0 +1,48 @@
// 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 remote
import (
"fmt"
"github.com/spf13/cobra"
"opendev.org/airship/airshipctl/pkg/environment"
"opendev.org/airship/airshipctl/pkg/remote"
)
// NewPowerStatusCommand provides a command to retrieve the power status of a remote host.
func NewPowerStatusCommand(rootSettings *environment.AirshipCTLSettings) *cobra.Command {
powerStatusCmd := &cobra.Command{
Use: "powerstatus SYSTEM_ID",
Short: "Retrieve the power status of a host",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
a, err := remote.NewAdapter(rootSettings)
if err != nil {
return err
}
powerStatus, err := a.OOBClient.SystemPowerStatus(a.Context, args[0])
if err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "Remote host %s has power status: %s\n", args[0], powerStatus)
return nil
},
}
return powerStatusCmd
}

View File

@ -1,35 +0,0 @@
// 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 remote
import (
"testing"
"opendev.org/airship/airshipctl/pkg/errors"
"opendev.org/airship/airshipctl/testutil"
)
func TestRemoteCommand(t *testing.T) {
tests := []*testutil.CmdTest{
{
Name: "remote-error-not-implemented",
CmdLine: "",
Cmd: NewRemoteCommand(nil),
Error: errors.ErrNotImplemented{},
},
}
for _, tt := range tests {
testutil.RunTest(t, tt)
}
}

View File

@ -1,7 +0,0 @@
Error: Not implemented
Usage:
remote [flags]
Flags:
-h, --help help for remote

View File

@ -146,6 +146,16 @@ func (c *Client) SetVirtualMedia(ctx context.Context, isoPath string) error {
return ScreenRedfishError(httpResp, err)
}
// SystemPowerStatus retrieves the power status of a host as a human-readable string.
func (c *Client) SystemPowerStatus(ctx context.Context, systemID string) (string, error) {
computerSystem, httpResp, err := c.redfishAPI.GetSystem(ctx, systemID)
if err = ScreenRedfishError(httpResp, err); err != nil {
return "", err
}
return string(computerSystem.PowerState), nil
}
// NewClient returns a client with the capability to make Redfish requests.
func NewClient(ephemeralNodeID string,
isoPath string,

View File

@ -32,7 +32,7 @@ const (
// Adapter bridges the gap between out-of-band clients. It can hold any type of OOB client, e.g. Redfish.
type Adapter struct {
OOBClient Client
context context.Context
Context context.Context
remoteConfig *config.RemoteDirect
remoteURL string
username string
@ -70,7 +70,7 @@ func (a *Adapter) configureClient(remoteURL string) error {
}
}
a.context, a.OOBClient, err = redfish.NewClient(
a.Context, a.OOBClient, err = redfish.NewClient(
nodeID,
a.remoteConfig.IsoURL,
baseURL,
@ -136,7 +136,7 @@ func (a *Adapter) DoRemoteDirect() error {
alog.Debugf("Using Remote Endpoint: %q", a.remoteURL)
/* Load ISO in manager's virtual media */
err := a.OOBClient.SetVirtualMedia(a.context, a.remoteConfig.IsoURL)
err := a.OOBClient.SetVirtualMedia(a.Context, a.remoteConfig.IsoURL)
if err != nil {
return err
}
@ -144,13 +144,13 @@ func (a *Adapter) DoRemoteDirect() error {
alog.Debugf("Successfully loaded virtual media: %q", a.remoteConfig.IsoURL)
/* Set system's bootsource to selected media */
err = a.OOBClient.SetEphemeralBootSourceByType(a.context)
err = a.OOBClient.SetEphemeralBootSourceByType(a.Context)
if err != nil {
return err
}
/* Reboot system */
err = a.OOBClient.RebootSystem(a.context, a.OOBClient.EphemeralNodeID())
err = a.OOBClient.RebootSystem(a.Context, a.OOBClient.EphemeralNodeID())
if err != nil {
return err
}
@ -164,7 +164,7 @@ func (a *Adapter) DoRemoteDirect() error {
// out-of-band client.
func NewAdapter(settings *environment.AirshipCTLSettings) (*Adapter, error) {
a := &Adapter{}
a.context = context.Background()
a.Context = context.Background()
err := a.initializeAdapter(settings)
if err != nil {
return a, err

View File

@ -103,13 +103,13 @@ func TestDoRemoteDirectRedfish(t *testing.T) {
ctx, rMock, err := redfishutils.NewClient(systemID, isoURL, redfishURL, false, false, "admin", "password")
assert.NoError(t, err)
rMock.On("SetVirtualMedia", a.context, isoURL).Times(1).Return(nil)
rMock.On("SetEphemeralBootSourceByType", a.context).Times(1).Return(nil)
rMock.On("SetVirtualMedia", a.Context, isoURL).Times(1).Return(nil)
rMock.On("SetEphemeralBootSourceByType", a.Context).Times(1).Return(nil)
rMock.On("EphemeralNodeID").Times(1).Return(systemID)
rMock.On("RebootSystem", a.context, systemID).Times(1).Return(nil)
rMock.On("RebootSystem", a.Context, systemID).Times(1).Return(nil)
// Swap the redfish client initialized by the remote direct adapter with the above mocked client
a.context = ctx
a.Context = ctx
a.OOBClient = rMock
err = a.DoRemoteDirect()
@ -131,13 +131,13 @@ func TestDoRemoteDirectRedfishVirtualMediaError(t *testing.T) {
assert.NoError(t, err)
expectedErr := redfish.ErrRedfishClient{Message: "Unable to set virtual media."}
rMock.On("SetVirtualMedia", a.context, isoURL).Times(1).Return(expectedErr)
rMock.On("SetEphemeralBootSourceByType", a.context).Times(1).Return(nil)
rMock.On("SetVirtualMedia", a.Context, isoURL).Times(1).Return(expectedErr)
rMock.On("SetEphemeralBootSourceByType", a.Context).Times(1).Return(nil)
rMock.On("EphemeralNodeID").Times(1).Return(systemID)
rMock.On("RebootSystem", a.context, systemID).Times(1).Return(nil)
rMock.On("RebootSystem", a.Context, systemID).Times(1).Return(nil)
// Swap the redfish client initialized by the remote direct adapter with the above mocked client
a.context = ctx
a.Context = ctx
a.OOBClient = rMock
err = a.DoRemoteDirect()
@ -159,15 +159,15 @@ func TestDoRemoteDirectRedfishBootSourceError(t *testing.T) {
ctx, rMock, err := redfishutils.NewClient(systemID, isoURL, redfishURL, false, false, "admin", "password")
assert.NoError(t, err)
rMock.On("SetVirtualMedia", a.context, isoURL).Times(1).Return(nil)
rMock.On("SetVirtualMedia", a.Context, isoURL).Times(1).Return(nil)
expectedErr := redfish.ErrRedfishClient{Message: "Unable to set boot source."}
rMock.On("SetEphemeralBootSourceByType", a.context).Times(1).Return(expectedErr)
rMock.On("SetEphemeralBootSourceByType", a.Context).Times(1).Return(expectedErr)
rMock.On("EphemeralNodeID").Times(1).Return(systemID)
rMock.On("RebootSystem", a.context, systemID).Times(1).Return(nil)
rMock.On("RebootSystem", a.Context, systemID).Times(1).Return(nil)
// Swap the redfish client initialized by the remote direct adapter with the above mocked client
a.context = ctx
a.Context = ctx
a.OOBClient = rMock
err = a.DoRemoteDirect()
@ -189,15 +189,15 @@ func TestDoRemoteDirectRedfishRebootError(t *testing.T) {
ctx, rMock, err := redfishutils.NewClient(systemID, isoURL, redfishURL, false, false, "admin", "password")
assert.NoError(t, err)
rMock.On("SetVirtualMedia", a.context, isoURL).Times(1).Return(nil)
rMock.On("SetEphemeralBootSourceByType", a.context).Times(1).Return(nil)
rMock.On("SetVirtualMedia", a.Context, isoURL).Times(1).Return(nil)
rMock.On("SetEphemeralBootSourceByType", a.Context).Times(1).Return(nil)
rMock.On("EphemeralNodeID").Times(1).Return(systemID)
expectedErr := redfish.ErrRedfishClient{Message: "Unable to set boot source."}
rMock.On("RebootSystem", a.context, systemID).Times(1).Return(expectedErr)
rMock.On("RebootSystem", a.Context, systemID).Times(1).Return(expectedErr)
// Swap the redfish client initialized by the remote direct adapter with the above mocked client
a.context = ctx
a.Context = ctx
a.OOBClient = rMock
err = a.DoRemoteDirect()

View File

@ -20,6 +20,10 @@ import (
// functions within client are used by power management commands and remote direct functionality.
type Client interface {
RebootSystem(context.Context, string) error
// TODO(drewwalters96): Should this be a string forever? We may want to define our own custom type, as the
// string format will be client dependent when we add new clients.
SystemPowerStatus(context.Context, string) (string, error)
EphemeralNodeID() string
// TODO(drewwalters96): This function may be too tightly coupled to remoteDirect operations. This could probably

View File

@ -82,6 +82,19 @@ func (m *MockClient) SetVirtualMedia(ctx context.Context, isoPath string) error
return args.Error(0)
}
// SystemPowerStatus provides a stubbed method that can be mocked to test functions that use the
// Redfish client without making any Redfish API calls or requiring the appropriate Redfish client settings.
//
// Example usage:
// client := redfishutils.NewClient()
// client.On("SystemPowerStatus").Return(<return values>)
//
// err := client.SystemPowerStatus(<args>)
func (m *MockClient) SystemPowerStatus(ctx context.Context, systemID string) (string, error) {
args := m.Called(ctx, systemID)
return args.String(0), args.Error(1)
}
// NewClient returns a mocked Redfish client in order to test functions that use the Redfish client without making any
// Redfish API calls.
func NewClient(ephemeralNodeID string, isoPath string, redfishURL string, insecure bool,