![Drew Walters](/assets/img/avatar_default.png)
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>
283 lines
9.0 KiB
Go
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,
|
|
}
|
|
}
|