[#52] Provide Redfish feedback in RemoteDirect

Currently, errors thrown by RemoteDirect are simply HTTP status codes
from the Redfish client HTTP requests. This change adds feedback to
RemoteDirect errors when the Redfish client provides error feedback.

Closes #52

Change-Id: I4441baf3b98405e92613a2d4e8c44ef63a5d4001
Signed-off-by: Drew Walters <andrew.walters@att.com>
This commit is contained in:
Drew Walters 2020-03-09 22:00:10 +00:00
parent 0cf1068c5f
commit 4ba23313c5
3 changed files with 76 additions and 82 deletions

View File

@ -1,9 +1,12 @@
package redfish package redfish
import ( import (
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
redfishClient "opendev.org/airship/go-redfish/client"
aerror "opendev.org/airship/airshipctl/pkg/errors" aerror "opendev.org/airship/airshipctl/pkg/errors"
) )
@ -26,16 +29,29 @@ func (e ErrRedfishMissingConfig) Error() string {
return "missing configuration: " + e.What return "missing configuration: " + e.What
} }
// Redfish client return error if response has no body. // ScreenRedfishError provides detailed error checking on a Redfish client response.
// In this case this function further checks the func ScreenRedfishError(httpResp *http.Response, clientErr error) error {
// HTTP response code. // NOTE(drewwalters96): clientErr may not be nil even though the request was successful. The HTTP status code
func ScreenRedfishError(httpResp *http.Response, err error) error { // has to be verified for success on each request. The Redfish client uses HTTP codes 200 and 204 to indicate
if err != nil && httpResp != nil { // success.
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 400 { if httpResp != nil && httpResp.StatusCode != 200 && httpResp.StatusCode != 204 {
if clientErr == nil {
return ErrRedfishClient{Message: http.StatusText(httpResp.StatusCode)}
}
oAPIErr, ok := clientErr.(redfishClient.GenericOpenAPIError)
if !ok {
return ErrRedfishClient{Message: "Unable to decode client error."}
}
var resp redfishClient.RedfishError
if err := json.Unmarshal(oAPIErr.Body(), &resp); err != nil {
// No JSON response included; use generic error text.
return ErrRedfishClient{Message: err.Error()} return ErrRedfishClient{Message: err.Error()}
} }
} else if err != nil { } else if httpResp == nil {
return ErrRedfishClient{Message: err.Error()} return ErrRedfishClient{Message: "HTTP request failed. Please try again."}
} }
return nil return nil
} }

View File

@ -27,10 +27,11 @@ func TestRedfishRemoteDirectNormal(t *testing.T) {
defer m.AssertExpectations(t) defer m.AssertExpectations(t)
systemID := computerSystemID systemID := computerSystemID
httpResp := &http.Response{StatusCode: 200}
m.On("GetSystem", context.Background(), systemID). m.On("GetSystem", context.Background(), systemID).
Return(getTestSystem(), nil, nil) Return(getTestSystem(), httpResp, nil)
m.On("InsertVirtualMedia", context.Background(), "manager-1", "Cd", mock.Anything). m.On("InsertVirtualMedia", context.Background(), "manager-1", "Cd", mock.Anything).
Return(redfishClient.RedfishError{}, nil, nil) Return(redfishClient.RedfishError{}, httpResp, nil)
systemReq := redfishClient.ComputerSystem{ systemReq := redfishClient.ComputerSystem{
Boot: redfishClient.Boot{ Boot: redfishClient.Boot{
@ -39,18 +40,18 @@ func TestRedfishRemoteDirectNormal(t *testing.T) {
} }
m.On("SetSystem", context.Background(), systemID, systemReq). m.On("SetSystem", context.Background(), systemID, systemReq).
Times(1). Times(1).
Return(redfishClient.ComputerSystem{}, nil, nil) Return(redfishClient.ComputerSystem{}, httpResp, nil)
resetReq := redfishClient.ResetRequestBody{} resetReq := redfishClient.ResetRequestBody{}
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
m.On("ResetSystem", context.Background(), systemID, resetReq). m.On("ResetSystem", context.Background(), systemID, resetReq).
Times(1). Times(1).
Return(redfishClient.RedfishError{}, nil, nil) Return(redfishClient.RedfishError{}, httpResp, nil)
resetReq.ResetType = redfishClient.RESETTYPE_ON resetReq.ResetType = redfishClient.RESETTYPE_ON
m.On("ResetSystem", context.Background(), systemID, resetReq). m.On("ResetSystem", context.Background(), systemID, resetReq).
Times(1). Times(1).
Return(redfishClient.RedfishError{}, nil, nil) Return(redfishClient.RedfishError{}, httpResp, nil)
rDCfg := getDefaultRedfishRemoteDirectObj(t, m) rDCfg := getDefaultRedfishRemoteDirectObj(t, m)
@ -85,9 +86,7 @@ func TestRedfishRemoteDirectGetSystemNetworkError(t *testing.T) {
systemID := computerSystemID systemID := computerSystemID
realErr := fmt.Errorf("server request timeout") realErr := fmt.Errorf("server request timeout")
httpResp := &http.Response{ httpResp := &http.Response{StatusCode: 408}
StatusCode: 408,
}
m.On("GetSystem", context.Background(), systemID). m.On("GetSystem", context.Background(), systemID).
Times(1). Times(1).
Return(redfishClient.ComputerSystem{}, httpResp, realErr) Return(redfishClient.ComputerSystem{}, httpResp, realErr)
@ -109,11 +108,8 @@ func TestRedfishRemoteDirectInvalidIsoPath(t *testing.T) {
localRDCfg := rDCfg localRDCfg := rDCfg
localRDCfg.IsoPath = "bogus/path/to.iso" localRDCfg.IsoPath = "bogus/path/to.iso"
errStr := "invalid remote boot path" realErr := redfishClient.GenericOpenAPIError{}
realErr := fmt.Errorf(errStr) httpResp := &http.Response{StatusCode: 500}
httpResp := &http.Response{
StatusCode: 500,
}
m.On("GetSystem", context.Background(), systemID). m.On("GetSystem", context.Background(), systemID).
Times(1). Times(1).
Return(getTestSystem(), nil, nil) Return(getTestSystem(), nil, nil)
@ -157,19 +153,17 @@ func TestRedfishRemoteDirectSetSystemBootSourceFailed(t *testing.T) {
defer m.AssertExpectations(t) defer m.AssertExpectations(t)
systemID := computerSystemID systemID := computerSystemID
httpSuccResp := &http.Response{StatusCode: 200}
m.On("GetSystem", context.Background(), systemID). m.On("GetSystem", context.Background(), systemID).
Return(getTestSystem(), nil, nil) Return(getTestSystem(), httpSuccResp, nil)
m.On("InsertVirtualMedia", context.Background(), "manager-1", "Cd", mock.Anything). m.On("InsertVirtualMedia", context.Background(), "manager-1", "Cd", mock.Anything).
Return(redfishClient.RedfishError{}, nil, nil) Return(redfishClient.RedfishError{}, httpSuccResp, nil)
realErr := fmt.Errorf("unauthorized")
httpResp := &http.Response{
StatusCode: 401,
}
m.On("SetSystem", context.Background(), systemID, mock.Anything). m.On("SetSystem", context.Background(), systemID, mock.Anything).
Times(1). Times(1).
Return(redfishClient.ComputerSystem{}, httpResp, realErr) Return(redfishClient.ComputerSystem{}, &http.Response{StatusCode: 401},
redfishClient.GenericOpenAPIError{})
rDCfg := getDefaultRedfishRemoteDirectObj(t, m) rDCfg := getDefaultRedfishRemoteDirectObj(t, m)
@ -184,26 +178,23 @@ func TestRedfishRemoteDirectSystemRebootFailed(t *testing.T) {
defer m.AssertExpectations(t) defer m.AssertExpectations(t)
systemID := computerSystemID systemID := computerSystemID
httpSuccResp := &http.Response{StatusCode: 200}
m.On("GetSystem", context.Background(), systemID). m.On("GetSystem", context.Background(), systemID).
Return(getTestSystem(), nil, nil) Return(getTestSystem(), httpSuccResp, nil)
m.On("InsertVirtualMedia", context.Background(), mock.Anything, mock.Anything, mock.Anything). m.On("InsertVirtualMedia", context.Background(), mock.Anything, mock.Anything, mock.Anything).
Return(redfishClient.RedfishError{}, nil, nil) Return(redfishClient.RedfishError{}, httpSuccResp, nil)
m.On("SetSystem", context.Background(), systemID, mock.Anything). m.On("SetSystem", context.Background(), systemID, mock.Anything).
Times(1). Times(1).
Return(redfishClient.ComputerSystem{}, nil, nil) Return(redfishClient.ComputerSystem{}, httpSuccResp, nil)
realErr := fmt.Errorf("unauthorized")
httpResp := &http.Response{
StatusCode: 401,
}
resetReq := redfishClient.ResetRequestBody{} resetReq := redfishClient.ResetRequestBody{}
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
m.On("ResetSystem", context.Background(), systemID, resetReq). m.On("ResetSystem", context.Background(), systemID, resetReq).
Times(1). Times(1).
Return(redfishClient.RedfishError{}, httpResp, realErr) Return(redfishClient.RedfishError{}, &http.Response{StatusCode: 401},
redfishClient.GenericOpenAPIError{})
rDCfg := getDefaultRedfishRemoteDirectObj(t, m) rDCfg := getDefaultRedfishRemoteDirectObj(t, m)

View File

@ -2,7 +2,6 @@ package redfish
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"testing" "testing"
@ -17,47 +16,41 @@ const (
) )
func TestRedfishErrorNoError(t *testing.T) { func TestRedfishErrorNoError(t *testing.T) {
err := ScreenRedfishError(nil, nil) err := ScreenRedfishError(&http.Response{StatusCode: 200}, nil)
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestRedfishErrorNonNilErrorWithoutHttpResp(t *testing.T) { func TestRedfishErrorNonNilErrorWithoutHttpResp(t *testing.T) {
realErr := fmt.Errorf("sample error") err := ScreenRedfishError(nil, redfishClient.GenericOpenAPIError{})
err := ScreenRedfishError(nil, realErr)
assert.Error(t, err)
_, ok := err.(ErrRedfishClient) _, ok := err.(ErrRedfishClient)
assert.True(t, ok) assert.True(t, ok)
} }
func TestRedfishErrorNonNilErrorWithHttpRespError(t *testing.T) { func TestRedfishErrorNonNilErrorWithHttpRespError(t *testing.T) {
realErr := fmt.Errorf("sample error") respErr := redfishClient.GenericOpenAPIError{}
httpResp := &http.Response{StatusCode: 408} err := ScreenRedfishError(&http.Response{StatusCode: 408}, respErr)
err := ScreenRedfishError(httpResp, realErr) _, ok := err.(ErrRedfishClient)
assert.Equal(t, err, ErrRedfishClient{Message: realErr.Error()}) assert.True(t, ok)
httpResp.StatusCode = 400 err = ScreenRedfishError(&http.Response{StatusCode: 500}, respErr)
err = ScreenRedfishError(httpResp, realErr) _, ok = err.(ErrRedfishClient)
assert.Equal(t, err, ErrRedfishClient{Message: realErr.Error()}) assert.True(t, ok)
httpResp.StatusCode = 199 err = ScreenRedfishError(&http.Response{StatusCode: 199}, respErr)
err = ScreenRedfishError(httpResp, realErr) _, ok = err.(ErrRedfishClient)
assert.Equal(t, err, ErrRedfishClient{Message: realErr.Error()}) assert.True(t, ok)
} }
func TestRedfishErrorNonNilErrorWithHttpRespOK(t *testing.T) { func TestRedfishErrorNonNilErrorWithHttpRespOK(t *testing.T) {
realErr := fmt.Errorf("sample error") respErr := redfishClient.GenericOpenAPIError{}
httpResp := &http.Response{StatusCode: 204} // NOTE: Redfish client only uses HTTP 200 & HTTP 204 for success.
err := ScreenRedfishError(httpResp, realErr) err := ScreenRedfishError(&http.Response{StatusCode: 204}, respErr)
assert.NoError(t, err) assert.NoError(t, err)
httpResp.StatusCode = 200 err = ScreenRedfishError(&http.Response{StatusCode: 200}, respErr)
err = ScreenRedfishError(httpResp, realErr)
assert.NoError(t, err)
httpResp.StatusCode = 399
err = ScreenRedfishError(httpResp, realErr)
assert.NoError(t, err) assert.NoError(t, err)
} }
@ -106,12 +99,12 @@ func TestRedfishUtilRebootSystemOK(t *testing.T) {
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
m.On("ResetSystem", ctx, systemID, resetReq). m.On("ResetSystem", ctx, systemID, resetReq).
Times(1). Times(1).
Return(redfishClient.RedfishError{}, nil, nil) Return(redfishClient.RedfishError{}, &http.Response{StatusCode: 200}, nil)
resetReq.ResetType = redfishClient.RESETTYPE_ON resetReq.ResetType = redfishClient.RESETTYPE_ON
m.On("ResetSystem", ctx, systemID, resetReq). m.On("ResetSystem", ctx, systemID, resetReq).
Times(1). Times(1).
Return(redfishClient.RedfishError{}, nil, nil) Return(redfishClient.RedfishError{}, &http.Response{StatusCode: 200}, nil)
err := RebootSystem(ctx, m, systemID) err := RebootSystem(ctx, m, systemID)
assert.NoError(t, err) assert.NoError(t, err)
@ -122,19 +115,17 @@ func TestRedfishUtilRebootSystemForceOffError2(t *testing.T) {
defer m.AssertExpectations(t) defer m.AssertExpectations(t)
ctx := context.Background() ctx := context.Background()
realErr := fmt.Errorf("unauthorized")
httpResp := &http.Response{
StatusCode: 401,
}
resetReq := redfishClient.ResetRequestBody{} resetReq := redfishClient.ResetRequestBody{}
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
m.On("ResetSystem", ctx, systemID, resetReq). m.On("ResetSystem", ctx, systemID, resetReq).
Times(1). Times(1).
Return(redfishClient.RedfishError{}, httpResp, realErr) Return(redfishClient.RedfishError{}, &http.Response{StatusCode: 401},
redfishClient.GenericOpenAPIError{})
err := RebootSystem(ctx, m, systemID) err := RebootSystem(ctx, m, systemID)
assert.Error(t, err) _, ok := err.(ErrRedfishClient)
assert.True(t, ok)
} }
func TestRedfishUtilRebootSystemForceOffError(t *testing.T) { func TestRedfishUtilRebootSystemForceOffError(t *testing.T) {
@ -142,18 +133,16 @@ func TestRedfishUtilRebootSystemForceOffError(t *testing.T) {
defer m.AssertExpectations(t) defer m.AssertExpectations(t)
ctx := context.Background() ctx := context.Background()
realErr := fmt.Errorf("unauthorized")
httpResp := &http.Response{
StatusCode: 401,
}
resetReq := redfishClient.ResetRequestBody{} resetReq := redfishClient.ResetRequestBody{}
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
m.On("ResetSystem", ctx, systemID, resetReq). m.On("ResetSystem", ctx, systemID, resetReq).
Times(1). Times(1).
Return(redfishClient.RedfishError{}, httpResp, realErr) Return(redfishClient.RedfishError{}, &http.Response{StatusCode: 401},
redfishClient.GenericOpenAPIError{})
err := RebootSystem(ctx, m, systemID) err := RebootSystem(ctx, m, systemID)
assert.Error(t, err) _, ok := err.(ErrRedfishClient)
assert.True(t, ok)
} }
func TestRedfishUtilRebootSystemTurningOnError(t *testing.T) { func TestRedfishUtilRebootSystemTurningOnError(t *testing.T) {
@ -165,18 +154,16 @@ func TestRedfishUtilRebootSystemTurningOnError(t *testing.T) {
resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
m.On("ResetSystem", ctx, systemID, resetReq). m.On("ResetSystem", ctx, systemID, resetReq).
Times(1). Times(1).
Return(redfishClient.RedfishError{}, nil, nil) Return(redfishClient.RedfishError{}, &http.Response{StatusCode: 200}, nil)
realErr := fmt.Errorf("unauthorized")
httpResp := &http.Response{
StatusCode: 401,
}
resetOnReq := redfishClient.ResetRequestBody{} resetOnReq := redfishClient.ResetRequestBody{}
resetOnReq.ResetType = redfishClient.RESETTYPE_ON resetOnReq.ResetType = redfishClient.RESETTYPE_ON
m.On("ResetSystem", ctx, systemID, resetOnReq). m.On("ResetSystem", ctx, systemID, resetOnReq).
Times(1). Times(1).
Return(redfishClient.RedfishError{}, httpResp, realErr) Return(redfishClient.RedfishError{}, &http.Response{StatusCode: 401},
redfishClient.GenericOpenAPIError{})
err := RebootSystem(ctx, m, systemID) err := RebootSystem(ctx, m, systemID)
assert.Error(t, err) _, ok := err.(ErrRedfishClient)
assert.True(t, ok)
} }