Drew Walters 9a905cd9c4 Add validation to Redfish media ejection
Currently, airshipctl relies on a wait period after ejecting media
before trying to insert new media; however, this may not be safe if the
sleep time is not long enough.

This change replaces the sleep time with validation logic that polls the
media status before attempting to insert new media.

Change-Id: I64e8892686dbfa2fc780039331c341e14cc6752c
Signed-off-by: Drew Walters <andrew.walters@att.com>
2020-04-22 16:45:55 +00:00

281 lines
8.5 KiB
Go

// 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 redfish
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"net/url"
"strings"
"time"
redfishAPI "opendev.org/airship/go-redfish/api"
redfishClient "opendev.org/airship/go-redfish/client"
"opendev.org/airship/airshipctl/pkg/log"
)
const (
// ClientType is used by other packages as the identifier of the Redfish client.
ClientType string = "redfish"
systemActionRetries = 30
systemRebootDelay = 2 * time.Second
)
// Client holds details about a Redfish out-of-band system required for out-of-band management.
type Client struct {
ephemeralNodeID string
isoPath string
redfishURL url.URL
RedfishAPI redfishAPI.RedfishAPI
RedfishCFG *redfishClient.Configuration
}
// EphemeralNodeID retrieves the ephemeral node ID.
func (c *Client) EphemeralNodeID() string {
return c.ephemeralNodeID
}
// RebootSystem power cycles a host by sending a shutdown signal followed by a power on signal.
func (c *Client) RebootSystem(ctx context.Context, systemID string) error {
waitForPowerState := func(desiredState redfishClient.PowerState) error {
// Check if number of retries is defined in context
totalRetries, ok := ctx.Value("numRetries").(int)
if !ok {
totalRetries = systemActionRetries
}
for retry := 0; retry < totalRetries; retry++ {
system, httpResp, err := c.RedfishAPI.GetSystem(ctx, systemID)
if err = ScreenRedfishError(httpResp, err); err != nil {
return err
}
if system.PowerState == desiredState {
return nil
}
time.Sleep(systemRebootDelay)
}
return ErrOperationRetriesExceeded{
What: fmt.Sprintf("reboot system %s", c.EphemeralNodeID()),
Retries: totalRetries,
}
}
resetReq := redfishClient.ResetRequestBody{}
// Send PowerOff request
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
_, httpResp, err := c.RedfishAPI.ResetSystem(ctx, systemID, resetReq)
if err = ScreenRedfishError(httpResp, err); err != nil {
return err
}
// Check that node is powered off
if err = waitForPowerState(redfishClient.POWERSTATE_OFF); err != nil {
return err
}
// Send PowerOn request
resetReq.ResetType = redfishClient.RESETTYPE_ON
_, httpResp, err = c.RedfishAPI.ResetSystem(ctx, systemID, resetReq)
if err = ScreenRedfishError(httpResp, err); err != nil {
return err
}
// Check that node is powered on and return
return waitForPowerState(redfishClient.POWERSTATE_ON)
}
// SetEphemeralBootSourceByType sets the boot source of the ephemeral node to one that's compatible with the boot
// source type.
func (c *Client) SetEphemeralBootSourceByType(ctx context.Context) error {
_, vMediaType, err := GetVirtualMediaID(ctx, c.RedfishAPI, c.ephemeralNodeID)
if err != nil {
return err
}
// Retrieve system information, containing available boot sources
system, _, err := c.RedfishAPI.GetSystem(ctx, c.ephemeralNodeID)
if err != nil {
return ErrRedfishClient{Message: fmt.Sprintf("Get System[%s] failed with err: %v", c.ephemeralNodeID, err)}
}
allowableValues := system.Boot.BootSourceOverrideTargetRedfishAllowableValues
for _, bootSource := range allowableValues {
if strings.EqualFold(string(bootSource), vMediaType) {
/* set boot source */
systemReq := redfishClient.ComputerSystem{}
systemReq.Boot.BootSourceOverrideTarget = bootSource
_, httpResp, err := c.RedfishAPI.SetSystem(ctx, c.ephemeralNodeID, systemReq)
return ScreenRedfishError(httpResp, err)
}
}
return ErrRedfishClient{Message: fmt.Sprintf("failed to set system[%s] boot source", c.ephemeralNodeID)}
}
// SetVirtualMedia injects a virtual media device to an established virtual media ID. This assumes that isoPath is
// accessible to the redfish server and virtualMedia device is either of type CD or DVD.
func (c *Client) SetVirtualMedia(ctx context.Context, isoPath string) error {
waitForEjectMedia := func(managerID string, vMediaID string) error {
// Check if number of retries is defined in context
totalRetries, ok := ctx.Value("numRetries").(int)
if !ok {
totalRetries = systemActionRetries
}
for retry := 0; retry < totalRetries; retry++ {
vMediaMgr, httpResp, err := c.RedfishAPI.GetManagerVirtualMedia(ctx, managerID, vMediaID)
if err = ScreenRedfishError(httpResp, err); err != nil {
return err
}
if *vMediaMgr.Inserted == false {
log.Debugf("Successfully ejected virtual media.")
return nil
}
}
return ErrOperationRetriesExceeded{What: fmt.Sprintf("eject media %s", vMediaID), Retries: totalRetries}
}
log.Debugf("Ephemeral Node System ID: '%s'", c.ephemeralNodeID)
managerID, err := GetManagerID(ctx, c.RedfishAPI, c.ephemeralNodeID)
if err != nil {
return err
}
log.Debugf("Ephemeral node managerID: '%s'", managerID)
vMediaID, _, err := GetVirtualMediaID(ctx, c.RedfishAPI, c.ephemeralNodeID)
if err != nil {
return err
}
// Eject virtual media if it is already inserted
vMediaMgr, httpResp, err := c.RedfishAPI.GetManagerVirtualMedia(ctx, managerID, vMediaID)
if err = ScreenRedfishError(httpResp, err); err != nil {
return err
}
if *vMediaMgr.Inserted == true {
log.Debugf("Manager %s media type %s inserted. Attempting to eject.", managerID, vMediaID)
var emptyBody map[string]interface{}
_, httpResp, err = c.RedfishAPI.EjectVirtualMedia(ctx, managerID, vMediaID, emptyBody)
if err = ScreenRedfishError(httpResp, err); err != nil {
return err
}
if err = waitForEjectMedia(managerID, vMediaID); err != nil {
return err
}
}
vMediaReq := redfishClient.InsertMediaRequestBody{}
vMediaReq.Image = isoPath
vMediaReq.Inserted = true
_, httpResp, err = c.RedfishAPI.InsertVirtualMedia(ctx, managerID, vMediaID, vMediaReq)
return ScreenRedfishError(httpResp, err)
}
// SystemPowerOff shuts down a host.
func (c *Client) SystemPowerOff(ctx context.Context, systemID string) error {
resetReq := redfishClient.ResetRequestBody{}
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
_, httpResp, err := c.RedfishAPI.ResetSystem(ctx, systemID, resetReq)
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,
redfishURL string,
insecure bool,
useProxy bool,
username string,
password string) (context.Context, *Client, error) {
var ctx context.Context
if username != "" && password != "" {
ctx = context.WithValue(
context.Background(),
redfishClient.ContextBasicAuth,
redfishClient.BasicAuth{UserName: username, Password: password},
)
} else {
ctx = context.Background()
}
if redfishURL == "" {
return ctx, nil, ErrRedfishMissingConfig{What: "Redfish URL"}
}
parsedURL, err := url.Parse(redfishURL)
if err != nil {
return ctx, nil, err
}
cfg := &redfishClient.Configuration{
BasePath: redfishURL,
DefaultHeader: make(map[string]string),
UserAgent: headerUserAgent,
}
// see https://github.com/golang/go/issues/26013
// We clone the default transport to ensure when we customize the transport
// that we are providing it sane timeouts and other defaults that we would
// normally get when not overriding the transport
defaultTransportCopy := http.DefaultTransport.(*http.Transport) //nolint:errcheck
transport := defaultTransportCopy.Clone()
if insecure {
transport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: true, //nolint:gosec
}
}
if !useProxy {
transport.Proxy = nil
}
cfg.HTTPClient = &http.Client{
Transport: transport,
}
c := &Client{
ephemeralNodeID: ephemeralNodeID,
isoPath: isoPath,
redfishURL: *parsedURL,
RedfishAPI: redfishClient.NewAPIClient(cfg).DefaultApi,
RedfishCFG: cfg,
}
return ctx, c, nil
}