0a5f1e2ca4
Change-Id: I17726cae6f4d1f99e1cec4fa7a29c1a9e1dee3d5
403 lines
12 KiB
Go
403 lines
12 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"
|
|
"strings"
|
|
"time"
|
|
|
|
redfishAPI "opendev.org/airship/go-redfish/api"
|
|
redfishClient "opendev.org/airship/go-redfish/client"
|
|
|
|
"opendev.org/airship/airshipctl/pkg/log"
|
|
"opendev.org/airship/airshipctl/pkg/remote/ifc"
|
|
"opendev.org/airship/airshipctl/pkg/remote/power"
|
|
)
|
|
|
|
const (
|
|
// ClientType is used by other packages as the identifier of the Redfish client.
|
|
ClientType string = "redfish"
|
|
)
|
|
|
|
// Client holds details about a Redfish out-of-band system required for out-of-band management.
|
|
type Client struct {
|
|
nodeID string
|
|
username string
|
|
password string
|
|
redfishURL string
|
|
RedfishAPI redfishAPI.RedfishAPI
|
|
RedfishCFG *redfishClient.Configuration
|
|
systemActionRetries int
|
|
systemRebootDelay int
|
|
|
|
// Sleep is meant to be mocked out for tests
|
|
Sleep func(d time.Duration)
|
|
}
|
|
|
|
// NodeID retrieves the ephemeral node ID.
|
|
func (c *Client) NodeID() string {
|
|
return c.nodeID
|
|
}
|
|
|
|
// SystemActionRetries returns number of attempts to reach host during reboot process and ejecting virtual media
|
|
func (c *Client) SystemActionRetries() int {
|
|
return c.systemActionRetries
|
|
}
|
|
|
|
// SystemRebootDelay returns number of seconds to wait after reboot if host isn't available
|
|
func (c *Client) SystemRebootDelay() int {
|
|
return c.systemRebootDelay
|
|
}
|
|
|
|
// EjectVirtualMedia ejects a virtual media device attached to a host.
|
|
func (c *Client) EjectVirtualMedia(ctx context.Context) error {
|
|
ctx = SetAuth(ctx, c.username, c.password)
|
|
waitForEjectMedia := func(managerID string, mediaID string) error {
|
|
for retry := 0; retry < c.systemActionRetries; retry++ {
|
|
vMediaMgr, httpResp, err := c.RedfishAPI.GetManagerVirtualMedia(ctx, managerID, mediaID)
|
|
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", mediaID), Retries: c.systemActionRetries}
|
|
}
|
|
|
|
managerID, err := getManagerID(ctx, c.RedfishAPI, c.nodeID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
mediaCollection, httpResp, err := c.RedfishAPI.ListManagerVirtualMedia(ctx, managerID)
|
|
if err = ScreenRedfishError(httpResp, err); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Walk all virtual media devices and eject if inserted
|
|
for _, mediaURI := range mediaCollection.Members {
|
|
mediaID := GetResourceIDFromURL(mediaURI.OdataId)
|
|
|
|
vMediaMgr, httpResp, err := c.RedfishAPI.GetManagerVirtualMedia(ctx, managerID, mediaID)
|
|
if err = ScreenRedfishError(httpResp, err); err != nil {
|
|
return err
|
|
}
|
|
|
|
if *vMediaMgr.Inserted == true {
|
|
log.Debugf("'%s' has virtual media inserted. Attempting to eject.", vMediaMgr.Name)
|
|
|
|
var emptyBody map[string]interface{}
|
|
_, httpResp, err = c.RedfishAPI.EjectVirtualMedia(ctx, managerID, mediaID, emptyBody)
|
|
if err = ScreenRedfishError(httpResp, err); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = waitForEjectMedia(managerID, mediaID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RebootSystem power cycles a host by sending a shutdown signal followed by a power on signal.
|
|
func (c *Client) RebootSystem(ctx context.Context) error {
|
|
log.Debugf("Rebooting node '%s': powering off.", c.nodeID)
|
|
ctx = SetAuth(ctx, c.username, c.password)
|
|
resetReq := redfishClient.ResetRequestBody{}
|
|
|
|
// Send PowerOff request
|
|
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
|
|
_, httpResp, err := c.RedfishAPI.ResetSystem(ctx, c.nodeID, resetReq)
|
|
if err = ScreenRedfishError(httpResp, err); err != nil {
|
|
log.Debugf("Failed to reboot node '%s': shutdown failure.", c.nodeID)
|
|
return err
|
|
}
|
|
|
|
// Check that node is powered off
|
|
if err = c.waitForPowerState(ctx, redfishClient.POWERSTATE_OFF); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Debugf("Rebooting node '%s': powering on.", c.nodeID)
|
|
|
|
// Send PowerOn request
|
|
resetReq.ResetType = redfishClient.RESETTYPE_ON
|
|
_, httpResp, err = c.RedfishAPI.ResetSystem(ctx, c.nodeID, resetReq)
|
|
if err = ScreenRedfishError(httpResp, err); err != nil {
|
|
log.Debugf("Failed to reboot node '%s': startup failure.", c.nodeID)
|
|
return err
|
|
}
|
|
|
|
// Check that node is powered on and return
|
|
return c.waitForPowerState(ctx, redfishClient.POWERSTATE_ON)
|
|
}
|
|
|
|
// SetBootSourceByType sets the boot source of the ephemeral node to one that's compatible with the boot
|
|
// source type.
|
|
func (c *Client) SetBootSourceByType(ctx context.Context) error {
|
|
ctx = SetAuth(ctx, c.username, c.password)
|
|
_, vMediaType, err := GetVirtualMediaID(ctx, c.RedfishAPI, c.nodeID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Debugf("Setting boot device to '%s'.", vMediaType)
|
|
|
|
// Retrieve system information, containing available boot sources
|
|
system, _, err := c.RedfishAPI.GetSystem(ctx, c.nodeID)
|
|
if err != nil {
|
|
return ErrRedfishClient{Message: fmt.Sprintf("Get System[%s] failed with err: %v", c.nodeID, 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.nodeID, systemReq)
|
|
if err = ScreenRedfishError(httpResp, err); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Debug("Successfully set boot device.")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return ErrRedfishClient{Message: fmt.Sprintf("failed to set system[%s] boot source", c.nodeID)}
|
|
}
|
|
|
|
// 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 {
|
|
ctx = SetAuth(ctx, c.username, c.password)
|
|
log.Debugf("Inserting virtual media '%s'.", isoPath)
|
|
// Eject all previously-inserted media
|
|
if err := c.EjectVirtualMedia(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Retrieve the ID of a compatible media type
|
|
vMediaID, _, err := GetVirtualMediaID(ctx, c.RedfishAPI, c.nodeID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
managerID, err := getManagerID(ctx, c.RedfishAPI, c.nodeID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Insert media
|
|
vMediaReq := redfishClient.InsertMediaRequestBody{}
|
|
vMediaReq.Image = isoPath
|
|
vMediaReq.Inserted = true
|
|
_, httpResp, err := c.RedfishAPI.InsertVirtualMedia(ctx, managerID, vMediaID, vMediaReq)
|
|
|
|
if err = ScreenRedfishError(httpResp, err); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Debug("Successfully set virtual media.")
|
|
return nil
|
|
}
|
|
|
|
// SystemPowerOff shuts down a host.
|
|
func (c *Client) SystemPowerOff(ctx context.Context) error {
|
|
ctx = SetAuth(ctx, c.username, c.password)
|
|
resetReq := redfishClient.ResetRequestBody{}
|
|
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
|
|
|
|
_, httpResp, err := c.RedfishAPI.ResetSystem(ctx, c.nodeID, resetReq)
|
|
if err = ScreenRedfishError(httpResp, err); err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.waitForPowerState(ctx, redfishClient.POWERSTATE_OFF)
|
|
}
|
|
|
|
// SystemPowerOn powers on a host.
|
|
func (c *Client) SystemPowerOn(ctx context.Context) error {
|
|
ctx = SetAuth(ctx, c.username, c.password)
|
|
resetReq := redfishClient.ResetRequestBody{}
|
|
resetReq.ResetType = redfishClient.RESETTYPE_ON
|
|
|
|
_, httpResp, err := c.RedfishAPI.ResetSystem(ctx, c.nodeID, resetReq)
|
|
if err = ScreenRedfishError(httpResp, err); err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.waitForPowerState(ctx, redfishClient.POWERSTATE_ON)
|
|
}
|
|
|
|
// SystemPowerStatus retrieves the power status of a host as a human-readable string.
|
|
func (c *Client) SystemPowerStatus(ctx context.Context) (power.Status, error) {
|
|
ctx = SetAuth(ctx, c.username, c.password)
|
|
computerSystem, httpResp, err := c.RedfishAPI.GetSystem(ctx, c.nodeID)
|
|
if screenErr := ScreenRedfishError(httpResp, err); screenErr != nil {
|
|
return power.StatusUnknown, screenErr
|
|
}
|
|
|
|
switch computerSystem.PowerState {
|
|
case redfishClient.POWERSTATE_ON:
|
|
return power.StatusOn, nil
|
|
case redfishClient.POWERSTATE_OFF:
|
|
return power.StatusOff, nil
|
|
case redfishClient.POWERSTATE_POWERING_ON:
|
|
return power.StatusPoweringOn, nil
|
|
case redfishClient.POWERSTATE_POWERING_OFF:
|
|
return power.StatusPoweringOff, nil
|
|
default:
|
|
return power.StatusUnknown, nil
|
|
}
|
|
}
|
|
|
|
// RemoteDirect implements remote direct interface
|
|
func (c *Client) RemoteDirect(ctx context.Context, isoURL string) error {
|
|
return RemoteDirect(ctx, isoURL, c.redfishURL, c)
|
|
}
|
|
|
|
// RemoteDirect allows to perform remotedirect
|
|
func RemoteDirect(ctx context.Context, isoURL, redfishURL string, c ifc.Client) error {
|
|
log.Debugf("Bootstrapping ephemeral host with ID '%s' and BMC Address '%s'.", c.NodeID(),
|
|
redfishURL)
|
|
|
|
powerStatus, err := c.SystemPowerStatus(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Power on node if it is off
|
|
if powerStatus != power.StatusOn {
|
|
log.Debugf("Ephemeral node has power status '%s'. Attempting to power on.", powerStatus.String())
|
|
if err = c.SystemPowerOn(ctx); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Perform remote direct operations
|
|
if isoURL == "" {
|
|
return ErrRedfishMissingConfig{What: "isoURL"}
|
|
}
|
|
|
|
err = c.SetVirtualMedia(ctx, isoURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = c.SetBootSourceByType(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = c.RebootSystem(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Printf("Successfully bootstrapped ephemeral host '%s'.", c.NodeID())
|
|
|
|
return nil
|
|
}
|
|
|
|
// NewClient returns a client with the capability to make Redfish requests.
|
|
func NewClient(redfishURL string,
|
|
insecure bool,
|
|
useProxy bool,
|
|
username string,
|
|
password string,
|
|
systemActionRetries int,
|
|
systemRebootDelay int) (*Client, error) {
|
|
if redfishURL == "" {
|
|
return nil, ErrRedfishMissingConfig{What: "Redfish URL"}
|
|
}
|
|
|
|
basePath, err := getBasePath(redfishURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cfg := &redfishClient.Configuration{
|
|
BasePath: basePath,
|
|
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,
|
|
}
|
|
|
|
// Retrieve system ID from end of Redfish URL
|
|
systemID := GetResourceIDFromURL(redfishURL)
|
|
if len(systemID) == 0 {
|
|
return nil, ErrRedfishMissingConfig{What: "management URL system ID"}
|
|
}
|
|
|
|
c := &Client{
|
|
nodeID: systemID,
|
|
RedfishAPI: redfishClient.NewAPIClient(cfg).DefaultApi,
|
|
RedfishCFG: cfg,
|
|
systemActionRetries: systemActionRetries,
|
|
systemRebootDelay: systemRebootDelay,
|
|
password: password,
|
|
username: username,
|
|
redfishURL: redfishURL,
|
|
|
|
Sleep: func(d time.Duration) {
|
|
time.Sleep(d)
|
|
},
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// ClientFactory is a constructor for redfish ifc.Client implementation
|
|
var ClientFactory ifc.ClientFactory = func(redfishURL string,
|
|
insecure bool,
|
|
useProxy bool,
|
|
username string,
|
|
password string,
|
|
systemActionRetries int,
|
|
systemRebootDelay int) (ifc.Client, error) {
|
|
return NewClient(redfishURL, insecure, useProxy,
|
|
username, password, systemActionRetries, systemRebootDelay)
|
|
}
|