Files
airshipctl/pkg/remote/redfish/utils.go
Drew Walters 7ac6362350 Decode Redfish error responses as raw JSON
Some BMCs return response formats that differ from the DMTF Redfish
specification. When the response format varies, airshipctl is unable to
decode the extended information field, leaving the user with the
general, top-level error message "A general error has occurred. See
ExtendedInfo for more information".

This change introduces raw JSON processing for all Redfish error
responses so both observed formats can be handled until this issue is
addressed by the manufacturers.

Change-Id: I6a6e87ecda65292b3cd58a0525985105db350ab8
Signed-off-by: Drew Walters <andrew.walters@att.com>
2020-04-27 13:35:17 +00:00

223 lines
7.1 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"
)
const (
URLSchemeSeparator = "+"
)
// 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 {
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) {
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" {
return mediaID, mediaType, nil
}
}
}
return "", "", ErrRedfishClient{Message: "Unable to find virtual media with type CD or DVD"}
}
// 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
}