Drew Walters ede8e8da4f Add state validation to Redfish shutdown/startup
The Redfish client reports success when Redfish returns success after a
shutdown or startup signal is sent; however, airshipctl needs to verify
that the host successfully reaches the desired power state before moving
on. This change adds the same wait functionality from reboot to the
power on and power off functionality.

Change-Id: I56c6696394b547f97da79cec56726002c3f225db
Signed-off-by: Drew Walters <andrew.walters@att.com>
2020-05-04 20:48:59 +00:00

283 lines
9.0 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"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
redfishAPI "opendev.org/airship/go-redfish/api"
redfishClient "opendev.org/airship/go-redfish/client"
"opendev.org/airship/airshipctl/pkg/log"
)
// URLSchemeSeparator holds the separator for URL scheme
// Example: git+ssh
const (
redfishURLSchemeSeparator = "+"
)
// DecodeRawError decodes a raw Redfish HTTP response and retrieves the extended information and available resolutions
// returned by the BMC.
func DecodeRawError(rawResponse []byte) (string, error) {
processExtendedInfo := func(extendedInfo map[string]interface{}) (string, error) {
message, ok := extendedInfo["Message"]
if !ok {
return "", ErrUnrecognizedRedfishResponse{Key: "error.@Message.ExtendedInfo.Message"}
}
messageContent, ok := message.(string)
if !ok {
return "", ErrUnrecognizedRedfishResponse{Key: "error.@Message.ExtendedInfo.Message"}
}
// Resolution may be omitted in some responses
if resolution, ok := extendedInfo["Resolution"]; ok {
return fmt.Sprintf("%s %s", messageContent, resolution), nil
}
return messageContent, nil
}
// Unmarshal raw Redfish response as arbitrary JSON map
var arbitraryJSON map[string]interface{}
if err := json.Unmarshal(rawResponse, &arbitraryJSON); err != nil {
return "", ErrUnrecognizedRedfishResponse{Key: "error"}
}
errObject, ok := arbitraryJSON["error"]
if !ok {
return "", ErrUnrecognizedRedfishResponse{Key: "error"}
}
errContent, ok := errObject.(map[string]interface{})
if !ok {
return "", ErrUnrecognizedRedfishResponse{Key: "error"}
}
extendedInfoContent, ok := errContent["@Message.ExtendedInfo"]
if !ok {
return "", ErrUnrecognizedRedfishResponse{Key: "error.@Message.ExtendedInfo"}
}
// NOTE(drewwalters96): The official specification dictates that "@Message.ExtendedInfo" should be a JSON array;
// however, some BMCs have returned a single JSON dictionary. Handle both types here.
switch extendedInfo := extendedInfoContent.(type) {
case []interface{}:
if len(extendedInfo) == 0 {
return "", ErrUnrecognizedRedfishResponse{Key: "error.@MessageExtendedInfo"}
}
var errorMessage string
for _, info := range extendedInfo {
infoContent, ok := info.(map[string]interface{})
if !ok {
return "", ErrUnrecognizedRedfishResponse{Key: "error.@Message.ExtendedInfo"}
}
message, err := processExtendedInfo(infoContent)
if err != nil {
return "", err
}
errorMessage = fmt.Sprintf("%s\n%s", message, errorMessage)
}
return errorMessage, nil
case map[string]interface{}:
return processExtendedInfo(extendedInfo)
default:
return "", ErrUnrecognizedRedfishResponse{Key: "error.@Message.ExtendedInfo"}
}
}
// GetManagerID retrieves the manager ID for a redfish system.
func GetManagerID(ctx context.Context, api redfishAPI.RedfishAPI, systemID string) (string, error) {
system, httpResp, err := api.GetSystem(ctx, systemID)
if err = ScreenRedfishError(httpResp, err); err != nil {
log.Debugf("Unable to find manager for node '%s'.", systemID)
return "", err
}
return GetResourceIDFromURL(system.Links.ManagedBy[0].OdataId), nil
}
// GetResourceIDFromURL returns a parsed Redfish resource ID
// from the Redfish URL
func GetResourceIDFromURL(refURL string) string {
u, err := url.Parse(refURL)
if err != nil {
log.Fatal(err)
}
trimmedURL := strings.TrimSuffix(u.Path, "/")
elems := strings.Split(trimmedURL, "/")
id := elems[len(elems)-1]
return id
}
// IsIDInList checks whether an ID exists in Redfish IDref collection
func IsIDInList(idRefList []redfishClient.IdRef, id string) bool {
for _, r := range idRefList {
rID := GetResourceIDFromURL(r.OdataId)
if rID == id {
return true
}
}
return false
}
// GetVirtualMediaID retrieves the ID of a Redfish virtual media resource if it supports type "CD" or "DVD".
func GetVirtualMediaID(ctx context.Context, api redfishAPI.RedfishAPI, systemID string) (string, string, error) {
log.Debug("Searching for compatible media types.")
managerID, err := GetManagerID(ctx, api, systemID)
if err != nil {
return "", "", err
}
mediaCollection, httpResp, err := api.ListManagerVirtualMedia(ctx, managerID)
if err = ScreenRedfishError(httpResp, err); err != nil {
return "", "", err
}
for _, mediaURI := range mediaCollection.Members {
// Retrieve the virtual media ID from the request URI
mediaID := GetResourceIDFromURL(mediaURI.OdataId)
vMedia, httpResp, err := api.GetManagerVirtualMedia(ctx, managerID, mediaID)
if err = ScreenRedfishError(httpResp, err); err != nil {
return "", "", err
}
for _, mediaType := range vMedia.MediaTypes {
if mediaType == "CD" || mediaType == "DVD" {
log.Debugf("Found virtual media type '%s' with ID '%s' on manager '%s'.", mediaType,
mediaID, managerID)
return mediaID, mediaType, nil
}
}
}
return "", "", ErrRedfishClient{Message: fmt.Sprintf("Manager '%s' does not have virtual media type CD or DVD.",
managerID)}
}
// ScreenRedfishError provides a detailed error message for end user consumption by inspecting all Redfish client
// responses and errors.
func ScreenRedfishError(httpResp *http.Response, clientErr error) error {
if httpResp == nil {
return ErrRedfishClient{
Message: "HTTP request failed. Redfish may be temporarily unavailable. Please try again.",
}
}
// NOTE(drewwalters96): The error, clientErr, may not be nil even though the request was successful. The HTTP
// status code is the most reliable way to determine the result of a Redfish request using the go-redfish
// library. The Redfish client uses HTTP codes 200 and 204 to indicate success.
var finalError ErrRedfishClient
switch httpResp.StatusCode {
case http.StatusOK:
return nil
case http.StatusNoContent:
return nil
case http.StatusNotFound:
finalError = ErrRedfishClient{Message: "System not found. Correct the system name and try again."}
case http.StatusBadRequest:
finalError = ErrRedfishClient{Message: "Invalid request. Verify the system name and try again."}
case http.StatusMethodNotAllowed:
finalError = ErrRedfishClient{
Message: fmt.Sprintf("%s. BMC returned status '%s'.",
"This operation is likely unsupported by the BMC Redfish version, or the BMC is busy",
httpResp.Status),
}
default:
finalError = ErrRedfishClient{Message: fmt.Sprintf("BMC responded '%s'.", httpResp.Status)}
log.Debugf("BMC responded '%s'. Attempting to unmarshal the raw BMC error response.",
httpResp.Status)
}
// Retrieve the raw HTTP response body
oAPIErr, ok := clientErr.(redfishClient.GenericOpenAPIError)
if !ok {
log.Debug("Unable to decode BMC response.")
}
// Attempt to decode the BMC response from the raw HTTP response
if bmcResponse, err := DecodeRawError(oAPIErr.Body()); err == nil {
finalError.Message = fmt.Sprintf("%s BMC responded: '%s'", finalError.Message, bmcResponse)
} else {
log.Debugf("Unable to decode BMC response. %q", err)
}
return finalError
}
func getManagerID(ctx context.Context, api redfishAPI.RedfishAPI, systemID string) (string, error) {
system, _, err := api.GetSystem(ctx, systemID)
if err != nil {
return "", err
}
return GetResourceIDFromURL(system.Links.ManagedBy[0].OdataId), nil
}
func getBasePath(redfishURL string) (string, error) {
parsedURL, err := url.Parse(redfishURL)
if err != nil {
return "", ErrRedfishClient{Message: fmt.Sprintf("Redfish URL malformed %s", err.Error())}
}
baseURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)
schemeSplit := strings.Split(parsedURL.Scheme, redfishURLSchemeSeparator)
if len(schemeSplit) > 1 {
baseURL = fmt.Sprintf("%s://%s", schemeSplit[len(schemeSplit)-1], parsedURL.Host)
}
return baseURL, nil
}
func (c Client) waitForPowerState(ctx context.Context, desiredState redfishClient.PowerState) error {
log.Debugf("Waiting for node '%s' to reach power state '%s'.", c.nodeID, desiredState)
// Check if number of retries is defined in context
totalRetries, ok := ctx.Value(ctxKeyNumRetries).(int)
if !ok {
totalRetries = systemActionRetries
}
for retry := 0; retry <= totalRetries; retry++ {
system, httpResp, err := c.RedfishAPI.GetSystem(ctx, c.NodeID())
if err = ScreenRedfishError(httpResp, err); err != nil {
return err
}
if system.PowerState == desiredState {
log.Debugf("Node '%s' reached power state '%s'.", c.nodeID, desiredState)
return nil
}
c.Sleep(systemRebootDelay)
}
return ErrOperationRetriesExceeded{
What: fmt.Sprintf("reach desired power state %s", desiredState),
Retries: totalRetries,
}
}