You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
293 lines
9.3 KiB
293 lines
9.3 KiB
// 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" |
|
"time" |
|
|
|
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 = "+" |
|
) |
|
|
|
func processExtendedInfo(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 |
|
} |
|
|
|
// 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) { |
|
// 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.@Message.ExtendedInfo"} |
|
} |
|
|
|
var errorMessage string |
|
for _, info := range extendedInfo { |
|
infoContent, ok := info.(map[string]interface{}) |
|
if !ok { |
|
return "", ErrUnrecognizedRedfishResponse{Key: "error.@Message.ExtendedInfo"} |
|
} |
|
|
|
if _, ok := infoContent["Message"]; !ok { |
|
return errContent["message"].(string), nil |
|
} |
|
|
|
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: 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\nBMC responded: '%s'", finalError.Message, bmcResponse) |
|
} else { |
|
log.Debugf("Unable to decode BMC response. %q", err) |
|
} |
|
|
|
return finalError |
|
} |
|
|
|
// SetAuth allows to set username and password to given context so that redfish client can |
|
// authenticate against redfish server |
|
func SetAuth(ctx context.Context, username string, password string) context.Context { |
|
authValue := redfishClient.BasicAuth{UserName: username, Password: password} |
|
if ctx.Value(redfishClient.ContextBasicAuth) == authValue { |
|
return ctx |
|
} |
|
return context.WithValue( |
|
ctx, |
|
redfishClient.ContextBasicAuth, |
|
redfishClient.BasicAuth{UserName: username, Password: password}, |
|
) |
|
} |
|
|
|
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) |
|
|
|
for retry := 0; retry <= c.systemActionRetries; 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(time.Duration(c.systemRebootDelay) * time.Second) |
|
} |
|
|
|
return ErrOperationRetriesExceeded{ |
|
What: fmt.Sprintf("reach desired power state %s", desiredState), |
|
Retries: c.systemActionRetries, |
|
} |
|
}
|
|
|