From d9161eb1ec3bc41b16623877732f64090dce6bd0 Mon Sep 17 00:00:00 2001 From: "C, Amarnath" Date: Tue, 26 May 2026 10:44:06 +0530 Subject: [PATCH] feat: implement redfish kvm user consent actions (#1000) 1. Implement KVM consent request/submit/cancel end-to-end. 2. Implement KVM and user consent status in graphical console. Signed-off-by: C, Amarnath --- .../controller/http/v1/handler/routes_test.go | 24 + .../http/v1/handler/systems_actions.go | 152 +++++- .../http/v1/handler/systems_actions_test.go | 364 ++++++++++++++- .../http/v1/handler/systems_test.go | 36 ++ redfish/internal/mocks/mock_repo.go | 27 ++ redfish/internal/usecase/computer_system.go | 36 ++ .../usecase/computer_system_fuzz_test.go | 24 + .../internal/usecase/computer_system_test.go | 136 ++++++ redfish/internal/usecase/consent_errors.go | 63 +++ .../internal/usecase/consent_errors_test.go | 92 ++++ redfish/internal/usecase/interfaces.go | 3 + redfish/internal/usecase/wsman_repo.go | 149 +++++- redfish/internal/usecase/wsman_repo_test.go | 434 +++++++++++++++++- 13 files changed, 1490 insertions(+), 50 deletions(-) create mode 100644 redfish/internal/usecase/consent_errors.go create mode 100644 redfish/internal/usecase/consent_errors_test.go diff --git a/redfish/internal/controller/http/v1/handler/routes_test.go b/redfish/internal/controller/http/v1/handler/routes_test.go index 4a3c2f2d..2c482303 100644 --- a/redfish/internal/controller/http/v1/handler/routes_test.go +++ b/redfish/internal/controller/http/v1/handler/routes_test.go @@ -108,6 +108,30 @@ func (r *TestComputerSystemRepository) UpdateSerialConsoleServiceEnabled(_ conte return usecase.ErrSystemNotFound } +func (r *TestComputerSystemRepository) RequestKVMConsent(_ context.Context, systemID string) error { + if _, exists := r.systems[systemID]; exists { + return nil + } + + return usecase.ErrSystemNotFound +} + +func (r *TestComputerSystemRepository) SubmitKVMConsentCode(_ context.Context, systemID, _ string) error { + if _, exists := r.systems[systemID]; exists { + return nil + } + + return usecase.ErrSystemNotFound +} + +func (r *TestComputerSystemRepository) CancelKVMConsent(_ context.Context, systemID string) error { + if _, exists := r.systems[systemID]; exists { + return nil + } + + return usecase.ErrSystemNotFound +} + // createTestSystemData creates a test system for the repository func createTestSystemData(systemID, name, manufacturer, model, serialNumber string) *redfishv1.ComputerSystem { return &redfishv1.ComputerSystem{ diff --git a/redfish/internal/controller/http/v1/handler/systems_actions.go b/redfish/internal/controller/http/v1/handler/systems_actions.go index 9ecadcac..a3b4ae8b 100644 --- a/redfish/internal/controller/http/v1/handler/systems_actions.go +++ b/redfish/internal/controller/http/v1/handler/systems_actions.go @@ -4,7 +4,10 @@ package v1 import ( "errors" "fmt" + "io" "net/http" + "regexp" + "strings" "time" "github.com/gin-gonic/gin" @@ -23,6 +26,11 @@ const ( nameKey = "Name" ) +var ( + sixDigitConsentCodeRe = regexp.MustCompile(`^\d{6}$`) + amtBadRequestRe = regexp.MustCompile(`(?i)\b400\s+bad\s+request\b`) +) + // PostRedfishV1SystemsComputerSystemIdActionsComputerSystemReset handles reset action for a computer system. // Validates system ID and reset type before executing power state change. // @@ -104,7 +112,6 @@ func (s *RedfishServer) PostRedfishV1SystemsComputerSystemIdActionsComputerSyste } // PostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemCancelKVMConsent handles canceling KVM consent for a computer system. -// This is a stub implementation. // //nolint:revive // Method name is generated from OpenAPI spec and cannot be changed func (s *RedfishServer) PostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemCancelKVMConsent(c *gin.Context, computerSystemID string) { @@ -114,7 +121,32 @@ func (s *RedfishServer) PostRedfishV1SystemsComputerSystemIdActionsOemIntelCompu return } - MethodNotAllowedError(c) + var req generated.PostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemCancelKVMConsentJSONRequestBody + + if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) { + MalformedJSONError(c) + + return + } + + if err := s.ComputerSystemUC.CancelKVMConsent(c.Request.Context(), computerSystemID); err != nil { + var consentErr *usecase.ConsentFailedError + + switch { + case errors.Is(err, usecase.ErrSystemNotFound): + NotFoundError(c, "System", computerSystemID) + case errors.As(err, &consentErr): + BadRequestError(c, consentErr.Error()) + case isAMTBadRequestError(err): + BadRequestError(c, err.Error()) + default: + InternalServerError(c, err) + } + + return + } + + sendActionSuccessResponse(c) } // PostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemCancelSolConsent handles canceling SOL consent for a computer system. @@ -166,7 +198,6 @@ func (s *RedfishServer) PostRedfishV1SystemsComputerSystemIdActionsOemIntelCompu } // PostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemRequestKVMConsent handles requesting KVM consent for a computer system. -// This is a stub implementation. // //nolint:revive // Method name is generated from OpenAPI spec and cannot be changed func (s *RedfishServer) PostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemRequestKVMConsent(c *gin.Context, computerSystemID string) { @@ -176,7 +207,32 @@ func (s *RedfishServer) PostRedfishV1SystemsComputerSystemIdActionsOemIntelCompu return } - MethodNotAllowedError(c) + var req generated.PostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemRequestKVMConsentJSONRequestBody + + if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) { + MalformedJSONError(c) + + return + } + + if err := s.ComputerSystemUC.RequestKVMConsent(c.Request.Context(), computerSystemID); err != nil { + var consentErr *usecase.ConsentFailedError + + switch { + case errors.Is(err, usecase.ErrSystemNotFound): + NotFoundError(c, "System", computerSystemID) + case errors.As(err, &consentErr): + BadRequestError(c, consentErr.Error()) + case isAMTBadRequestError(err): + BadRequestError(c, err.Error()) + default: + InternalServerError(c, err) + } + + return + } + + sendActionSuccessResponse(c) } // PostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemRequestSolConsent handles requesting SOL consent for a computer system. @@ -194,7 +250,6 @@ func (s *RedfishServer) PostRedfishV1SystemsComputerSystemIdActionsOemIntelCompu } // PostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemSubmitKVMConsentCode handles submitting a KVM consent code for a computer system. -// This is a stub implementation. // //nolint:revive // Method name is generated from OpenAPI spec and cannot be changed func (s *RedfishServer) PostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemSubmitKVMConsentCode(c *gin.Context, computerSystemID string) { @@ -204,7 +259,45 @@ func (s *RedfishServer) PostRedfishV1SystemsComputerSystemIdActionsOemIntelCompu return } - MethodNotAllowedError(c) + var req generated.PostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemSubmitKVMConsentCodeJSONRequestBody + + if err := c.ShouldBindJSON(&req); err != nil { + MalformedJSONError(c) + + return + } + + consentCode := strings.TrimSpace(req.ConsentCode) + if consentCode == "" { + PropertyMissingError(c, "ConsentCode") + + return + } + + if !sixDigitConsentCodeRe.MatchString(consentCode) { + BadRequestError(c, "Invalid ConsentCode: must be a six-digit numeric value") + + return + } + + if err := s.ComputerSystemUC.SubmitKVMConsentCode(c.Request.Context(), computerSystemID, consentCode); err != nil { + var consentErr *usecase.ConsentFailedError + + switch { + case errors.Is(err, usecase.ErrSystemNotFound): + NotFoundError(c, "System", computerSystemID) + case errors.As(err, &consentErr): + BadRequestError(c, consentErr.Error()) + case isAMTBadRequestError(err): + BadRequestError(c, err.Error()) + default: + InternalServerError(c, err) + } + + return + } + + sendActionSuccessResponse(c) } // PostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemSubmitSolConsentCode handles submitting a SOL consent code for a computer system. @@ -220,3 +313,50 @@ func (s *RedfishServer) PostRedfishV1SystemsComputerSystemIdActionsOemIntelCompu MethodNotAllowedError(c) } + +func sendActionSuccessResponse(c *gin.Context) { + sendActionSuccessResponseWithLookup(c, registryMgr.LookupMessage) +} + +func sendActionSuccessResponseWithLookup(c *gin.Context, lookupFn func(string, string) (*RegistryMessage, error)) { + SetRedfishHeaders(c) + + successMsg, err := lookupFn("Base", "Success") + if err != nil { + InternalServerError(c, err) + + return + } + + messageID := successMsg.MessageID + message := successMsg.Message + severity := mapSeverityToResourceHealth(successMsg.Severity) + resolution := successMsg.Resolution + + c.JSON(http.StatusOK, generated.RedfishError{ + Error: struct { + MessageExtendedInfo *[]generated.MessageMessage `json:"@Message.ExtendedInfo,omitempty"` + Code *string `json:"code,omitempty"` + Message *string `json:"message,omitempty"` + }{ + Code: &messageID, + Message: &message, + MessageExtendedInfo: &[]generated.MessageMessage{ + { + MessageId: &messageID, + Message: &message, + Severity: &severity, + Resolution: &resolution, + }, + }, + }, + }) +} + +func isAMTBadRequestError(err error) bool { + if err == nil { + return false + } + + return amtBadRequestRe.MatchString(err.Error()) +} diff --git a/redfish/internal/controller/http/v1/handler/systems_actions_test.go b/redfish/internal/controller/http/v1/handler/systems_actions_test.go index 8db3fd56..6697c30d 100644 --- a/redfish/internal/controller/http/v1/handler/systems_actions_test.go +++ b/redfish/internal/controller/http/v1/handler/systems_actions_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "net/http" "net/http/httptest" "sync" @@ -27,6 +28,9 @@ const ( testSystemID = "550e8400-e29b-41d4-a716-446655440001" resetActionEndpoint = "/redfish/v1/Systems/550e8400-e29b-41d4-a716-446655440001/Actions/ComputerSystem.Reset" generateTokenActionEndpoint = "/redfish/v1/Systems/550e8400-e29b-41d4-a716-446655440001/Actions/Oem/IntelComputerSystem.GenerateRedirectionToken" + requestKVMConsentEndpoint = "/redfish/v1/Systems/550e8400-e29b-41d4-a716-446655440001/Actions/Oem/IntelComputerSystem.RequestKVMConsent" + submitKVMConsentEndpoint = "/redfish/v1/Systems/550e8400-e29b-41d4-a716-446655440001/Actions/Oem/IntelComputerSystem.SubmitKVMConsentCode" + cancelKVMConsentEndpoint = "/redfish/v1/Systems/550e8400-e29b-41d4-a716-446655440001/Actions/Oem/IntelComputerSystem.CancelKVMConsent" taskServiceEndpoint = "/redfish/v1/TaskService/Tasks/" taskODataContext = "/redfish/v1/$metadata#Task.Task" taskODataType = "#Task.v1_6_0.Task" @@ -58,6 +62,21 @@ func setupSystemActionsTestRouter(server *RedfishServer) *gin.Engine { computerSystemID := c.Param("computerSystemId") server.PostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemGenerateRedirectionToken(c, computerSystemID) }) + router.POST("/redfish/v1/Systems/:computerSystemId/Actions/Oem/IntelComputerSystem.RequestKVMConsent", + func(c *gin.Context) { + computerSystemID := c.Param("computerSystemId") + server.PostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemRequestKVMConsent(c, computerSystemID) + }) + router.POST("/redfish/v1/Systems/:computerSystemId/Actions/Oem/IntelComputerSystem.SubmitKVMConsentCode", + func(c *gin.Context) { + computerSystemID := c.Param("computerSystemId") + server.PostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemSubmitKVMConsentCode(c, computerSystemID) + }) + router.POST("/redfish/v1/Systems/:computerSystemId/Actions/Oem/IntelComputerSystem.CancelKVMConsent", + func(c *gin.Context) { + computerSystemID := c.Param("computerSystemId") + server.PostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemCancelKVMConsent(c, computerSystemID) + }) return router } @@ -87,6 +106,15 @@ func createEmptyActionRequest() []byte { return []byte(`{}`) } +func createSubmitKVMConsentCodeRequest(code string) []byte { + req := generated.PostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemSubmitKVMConsentCodeJSONRequestBody{ + ConsentCode: code, + } + body, _ := json.Marshal(req) + + return body +} + func configureTestJWT(t *testing.T) { t.Helper() @@ -298,12 +326,13 @@ func TestPostRedfishV1SystemsComputerSystemIdActionsComputerSystemReset_EmptyRes assert.Equal(t, http.StatusBadRequest, w.Code) } -// Stub implementation tests for KVM actions +// KVM action tests -func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemCancelKVMConsent_Stub(t *testing.T) { +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemCancelKVMConsent_EmptyBody(t *testing.T) { t.Parallel() repo := NewTestSystemsComputerSystemRepository() + repo.AddSystem(testSystemID, &redfishv1.ComputerSystem{ID: testSystemID, Name: "Test System"}) server := setupSystemActionsTestServer(repo) req := httptest.NewRequest(http.MethodPost, "/redfish/v1/Systems/"+testSystemID+"/Actions/Oem/IntelComputerSystem.CancelKVMConsent", http.NoBody) @@ -314,7 +343,7 @@ func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemCancel server.PostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemCancelKVMConsent(ctx, testSystemID) - assert.Equal(t, http.StatusMethodNotAllowed, w.Code) + assert.Equal(t, http.StatusOK, w.Code) } func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemCancelKVMConsent_InvalidSystemID(t *testing.T) { @@ -440,21 +469,17 @@ func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemGenera assertErrorResponse(t, w) } -func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemRequestKVMConsent_Stub(t *testing.T) { +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemRequestKVMConsent_Success(t *testing.T) { t.Parallel() repo := NewTestSystemsComputerSystemRepository() + repo.AddSystem(testSystemID, &redfishv1.ComputerSystem{ID: testSystemID, Name: "Test System"}) server := setupSystemActionsTestServer(repo) + router := setupSystemActionsTestRouter(server) - req := httptest.NewRequest(http.MethodPost, "/redfish/v1/Systems/"+testSystemID+"/Actions/Oem/IntelComputerSystem.RequestKVMConsent", http.NoBody) - w := httptest.NewRecorder() - ctx, _ := gin.CreateTestContext(w) - ctx.Request = req - ctx.Params = gin.Params{{Key: "computerSystemId", Value: testSystemID}} - - server.PostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemRequestKVMConsent(ctx, testSystemID) + w := executeResetRequest(router, requestKVMConsentEndpoint, createEmptyActionRequest()) - assert.Equal(t, http.StatusMethodNotAllowed, w.Code) + assert.Equal(t, http.StatusOK, w.Code) } func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemRequestKVMConsent_InvalidSystemID(t *testing.T) { @@ -474,21 +499,90 @@ func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemReques assert.Equal(t, http.StatusBadRequest, w.Code) } -func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemSubmitKVMConsentCode_Stub(t *testing.T) { +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemRequestKVMConsent_SystemNotFound(t *testing.T) { t.Parallel() repo := NewTestSystemsComputerSystemRepository() server := setupSystemActionsTestServer(repo) + router := setupSystemActionsTestRouter(server) - req := httptest.NewRequest(http.MethodPost, "/redfish/v1/Systems/"+testSystemID+"/Actions/Oem/IntelComputerSystem.SubmitKVMConsentCode", http.NoBody) - w := httptest.NewRecorder() - ctx, _ := gin.CreateTestContext(w) - ctx.Request = req - ctx.Params = gin.Params{{Key: "computerSystemId", Value: testSystemID}} + endpoint := "/redfish/v1/Systems/999e8400-e29b-41d4-a716-446655440000/Actions/Oem/IntelComputerSystem.RequestKVMConsent" + w := executeResetRequest(router, endpoint, createEmptyActionRequest()) - server.PostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemSubmitKVMConsentCode(ctx, testSystemID) + assert.Equal(t, http.StatusNotFound, w.Code) + assertErrorResponse(t, w) +} - assert.Equal(t, http.StatusMethodNotAllowed, w.Code) +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemRequestKVMConsent_MalformedJSON(t *testing.T) { + t.Parallel() + + repo := NewTestSystemsComputerSystemRepository() + repo.AddSystem(testSystemID, &redfishv1.ComputerSystem{ID: testSystemID, Name: "Test System"}) + server := setupSystemActionsTestServer(repo) + router := setupSystemActionsTestRouter(server) + + w := executeResetRequest(router, requestKVMConsentEndpoint, []byte(`{"invalid":`)) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assertErrorResponse(t, w) +} + +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemRequestKVMConsent_ConsentFailure(t *testing.T) { + t.Parallel() + + repo := NewTestSystemsComputerSystemRepository() + repo.AddSystem(testSystemID, &redfishv1.ComputerSystem{ID: testSystemID, Name: "Test System"}) + repo.errorOnGetByID[testSystemID] = &usecase.ConsentFailedError{Operation: "RequestKVMConsent", ReturnValue: 5} + server := setupSystemActionsTestServer(repo) + router := setupSystemActionsTestRouter(server) + + w := executeResetRequest(router, requestKVMConsentEndpoint, createEmptyActionRequest()) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assertErrorResponse(t, w) +} + +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemRequestKVMConsent_AMTBadRequestMapped(t *testing.T) { + t.Parallel() + + repo := NewTestSystemsComputerSystemRepository() + repo.AddSystem(testSystemID, &redfishv1.ComputerSystem{ID: testSystemID, Name: "Test System"}) + repo.errorOnGetByID[testSystemID] = errors.New("wrapped AMT call failed: 400 Bad Request") + server := setupSystemActionsTestServer(repo) + router := setupSystemActionsTestRouter(server) + + w := executeResetRequest(router, requestKVMConsentEndpoint, createEmptyActionRequest()) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assertErrorResponse(t, w) +} + +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemRequestKVMConsent_InternalError(t *testing.T) { + t.Parallel() + + repo := NewTestSystemsComputerSystemRepository() + repo.AddSystem(testSystemID, &redfishv1.ComputerSystem{ID: testSystemID, Name: "Test System"}) + repo.errorOnGetByID[testSystemID] = errors.New("amt refused") + server := setupSystemActionsTestServer(repo) + router := setupSystemActionsTestRouter(server) + + w := executeResetRequest(router, requestKVMConsentEndpoint, createEmptyActionRequest()) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assertErrorResponse(t, w) +} + +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemSubmitKVMConsentCode_Success(t *testing.T) { + t.Parallel() + + repo := NewTestSystemsComputerSystemRepository() + repo.AddSystem(testSystemID, &redfishv1.ComputerSystem{ID: testSystemID, Name: "Test System"}) + server := setupSystemActionsTestServer(repo) + router := setupSystemActionsTestRouter(server) + + w := executeResetRequest(router, submitKVMConsentEndpoint, createSubmitKVMConsentCodeRequest("123456")) + + assert.Equal(t, http.StatusOK, w.Code) } func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemSubmitKVMConsentCode_InvalidSystemID(t *testing.T) { @@ -508,6 +602,236 @@ func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemSubmit assert.Equal(t, http.StatusBadRequest, w.Code) } +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemSubmitKVMConsentCode_SystemNotFound(t *testing.T) { + t.Parallel() + + repo := NewTestSystemsComputerSystemRepository() + server := setupSystemActionsTestServer(repo) + router := setupSystemActionsTestRouter(server) + + endpoint := "/redfish/v1/Systems/999e8400-e29b-41d4-a716-446655440000/Actions/Oem/IntelComputerSystem.SubmitKVMConsentCode" + w := executeResetRequest(router, endpoint, createSubmitKVMConsentCodeRequest("123456")) + + assert.Equal(t, http.StatusNotFound, w.Code) + assertErrorResponse(t, w) +} + +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemSubmitKVMConsentCode_InvalidConsentCode(t *testing.T) { + t.Parallel() + + repo := NewTestSystemsComputerSystemRepository() + repo.AddSystem(testSystemID, &redfishv1.ComputerSystem{ID: testSystemID, Name: "Test System"}) + server := setupSystemActionsTestServer(repo) + router := setupSystemActionsTestRouter(server) + + w := executeResetRequest(router, submitKVMConsentEndpoint, createSubmitKVMConsentCodeRequest("12ab")) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assertErrorResponse(t, w) +} + +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemSubmitKVMConsentCode_MissingConsentCode(t *testing.T) { + t.Parallel() + + repo := NewTestSystemsComputerSystemRepository() + repo.AddSystem(testSystemID, &redfishv1.ComputerSystem{ID: testSystemID, Name: "Test System"}) + server := setupSystemActionsTestServer(repo) + router := setupSystemActionsTestRouter(server) + + w := executeResetRequest(router, submitKVMConsentEndpoint, createSubmitKVMConsentCodeRequest("")) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assertErrorResponse(t, w) +} + +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemSubmitKVMConsentCode_MalformedJSON(t *testing.T) { + t.Parallel() + + repo := NewTestSystemsComputerSystemRepository() + repo.AddSystem(testSystemID, &redfishv1.ComputerSystem{ID: testSystemID, Name: "Test System"}) + server := setupSystemActionsTestServer(repo) + router := setupSystemActionsTestRouter(server) + + w := executeResetRequest(router, submitKVMConsentEndpoint, []byte(`{"ConsentCode":`)) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assertErrorResponse(t, w) +} + +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemSubmitKVMConsentCode_ConsentFailure(t *testing.T) { + t.Parallel() + + repo := NewTestSystemsComputerSystemRepository() + repo.AddSystem(testSystemID, &redfishv1.ComputerSystem{ID: testSystemID, Name: "Test System"}) + repo.errorOnGetByID[testSystemID] = &usecase.ConsentFailedError{Operation: "SubmitKVMConsentCode", ReturnValue: 7} + server := setupSystemActionsTestServer(repo) + router := setupSystemActionsTestRouter(server) + + w := executeResetRequest(router, submitKVMConsentEndpoint, createSubmitKVMConsentCodeRequest("123456")) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assertErrorResponse(t, w) +} + +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemSubmitKVMConsentCode_AMTBadRequestMapped(t *testing.T) { + t.Parallel() + + repo := NewTestSystemsComputerSystemRepository() + repo.AddSystem(testSystemID, &redfishv1.ComputerSystem{ID: testSystemID, Name: "Test System"}) + repo.errorOnGetByID[testSystemID] = errors.New("400 Bad Request") + server := setupSystemActionsTestServer(repo) + router := setupSystemActionsTestRouter(server) + + w := executeResetRequest(router, submitKVMConsentEndpoint, createSubmitKVMConsentCodeRequest("123456")) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assertErrorResponse(t, w) +} + +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemSubmitKVMConsentCode_InternalError(t *testing.T) { + t.Parallel() + + repo := NewTestSystemsComputerSystemRepository() + repo.AddSystem(testSystemID, &redfishv1.ComputerSystem{ID: testSystemID, Name: "Test System"}) + repo.errorOnGetByID[testSystemID] = errors.New("amt refused") + server := setupSystemActionsTestServer(repo) + router := setupSystemActionsTestRouter(server) + + w := executeResetRequest(router, submitKVMConsentEndpoint, createSubmitKVMConsentCodeRequest("123456")) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assertErrorResponse(t, w) +} + +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemCancelKVMConsent_Success(t *testing.T) { + t.Parallel() + + repo := NewTestSystemsComputerSystemRepository() + repo.AddSystem(testSystemID, &redfishv1.ComputerSystem{ID: testSystemID, Name: "Test System"}) + server := setupSystemActionsTestServer(repo) + router := setupSystemActionsTestRouter(server) + + w := executeResetRequest(router, cancelKVMConsentEndpoint, createEmptyActionRequest()) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemCancelKVMConsent_SystemNotFound(t *testing.T) { + t.Parallel() + + repo := NewTestSystemsComputerSystemRepository() + server := setupSystemActionsTestServer(repo) + router := setupSystemActionsTestRouter(server) + + endpoint := "/redfish/v1/Systems/999e8400-e29b-41d4-a716-446655440000/Actions/Oem/IntelComputerSystem.CancelKVMConsent" + w := executeResetRequest(router, endpoint, createEmptyActionRequest()) + + assert.Equal(t, http.StatusNotFound, w.Code) + assertErrorResponse(t, w) +} + +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemCancelKVMConsent_MalformedJSON(t *testing.T) { + t.Parallel() + + repo := NewTestSystemsComputerSystemRepository() + repo.AddSystem(testSystemID, &redfishv1.ComputerSystem{ID: testSystemID, Name: "Test System"}) + server := setupSystemActionsTestServer(repo) + router := setupSystemActionsTestRouter(server) + + w := executeResetRequest(router, cancelKVMConsentEndpoint, []byte(`{"invalid":`)) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assertErrorResponse(t, w) +} + +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemCancelKVMConsent_ConsentFailure(t *testing.T) { + t.Parallel() + + repo := NewTestSystemsComputerSystemRepository() + repo.AddSystem(testSystemID, &redfishv1.ComputerSystem{ID: testSystemID, Name: "Test System"}) + repo.errorOnGetByID[testSystemID] = &usecase.ConsentFailedError{Operation: "CancelKVMConsent", ReturnValue: 9} + server := setupSystemActionsTestServer(repo) + router := setupSystemActionsTestRouter(server) + + w := executeResetRequest(router, cancelKVMConsentEndpoint, createEmptyActionRequest()) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assertErrorResponse(t, w) +} + +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemCancelKVMConsent_AMTBadRequestMapped(t *testing.T) { + t.Parallel() + + repo := NewTestSystemsComputerSystemRepository() + repo.AddSystem(testSystemID, &redfishv1.ComputerSystem{ID: testSystemID, Name: "Test System"}) + repo.errorOnGetByID[testSystemID] = errors.New("wrapped AMT call failed: 400 Bad Request") + server := setupSystemActionsTestServer(repo) + router := setupSystemActionsTestRouter(server) + + w := executeResetRequest(router, cancelKVMConsentEndpoint, createEmptyActionRequest()) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assertErrorResponse(t, w) +} + +func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemCancelKVMConsent_InternalError(t *testing.T) { + t.Parallel() + + repo := NewTestSystemsComputerSystemRepository() + repo.AddSystem(testSystemID, &redfishv1.ComputerSystem{ID: testSystemID, Name: "Test System"}) + repo.errorOnGetByID[testSystemID] = errors.New("amt refused") + server := setupSystemActionsTestServer(repo) + router := setupSystemActionsTestRouter(server) + + w := executeResetRequest(router, cancelKVMConsentEndpoint, createEmptyActionRequest()) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assertErrorResponse(t, w) +} + +func TestIsAMTBadRequestError(t *testing.T) { + t.Parallel() + + assert.False(t, isAMTBadRequestError(nil)) + assert.False(t, isAMTBadRequestError(errors.New("amt refused"))) + assert.True(t, isAMTBadRequestError(errors.New("Wrapped call failed: 400 bad request"))) +} + +func TestSendActionSuccessResponseWithLookup_Error(t *testing.T) { + t.Parallel() + + w := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(w) + ctx.Request = httptest.NewRequest(http.MethodPost, requestKVMConsentEndpoint, bytes.NewBuffer(createEmptyActionRequest())) + + sendActionSuccessResponseWithLookup(ctx, func(_, _ string) (*RegistryMessage, error) { + return nil, errors.New("lookup failed") + }) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assertErrorResponse(t, w) +} + +func TestSendActionSuccessResponseWithLookup_Success(t *testing.T) { + t.Parallel() + + w := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(w) + ctx.Request = httptest.NewRequest(http.MethodPost, requestKVMConsentEndpoint, bytes.NewBuffer(createEmptyActionRequest())) + + sendActionSuccessResponseWithLookup(ctx, func(_, _ string) (*RegistryMessage, error) { + return &RegistryMessage{ + MessageID: "Base.1.22.0.Success", + Message: "Success", + Severity: "OK", + Resolution: "None", + }, nil + }) + + assert.Equal(t, http.StatusOK, w.Code) + assertErrorResponse(t, w) +} + // Stub implementation tests for SOL actions func TestPostRedfishV1SystemsComputerSystemIdActionsOemIntelComputerSystemCancelSolConsent_Stub(t *testing.T) { diff --git a/redfish/internal/controller/http/v1/handler/systems_test.go b/redfish/internal/controller/http/v1/handler/systems_test.go index 35dda27b..1e5765f4 100644 --- a/redfish/internal/controller/http/v1/handler/systems_test.go +++ b/redfish/internal/controller/http/v1/handler/systems_test.go @@ -177,6 +177,42 @@ func (r *TestSystemsComputerSystemRepository) UpdateSerialConsoleServiceEnabled( return usecase.ErrSystemNotFound } +func (r *TestSystemsComputerSystemRepository) RequestKVMConsent(_ context.Context, systemID string) error { + if err, exists := r.errorOnGetByID[systemID]; exists { + return err + } + + if _, exists := r.systems[systemID]; exists { + return nil + } + + return usecase.ErrSystemNotFound +} + +func (r *TestSystemsComputerSystemRepository) SubmitKVMConsentCode(_ context.Context, systemID, _ string) error { + if err, exists := r.errorOnGetByID[systemID]; exists { + return err + } + + if _, exists := r.systems[systemID]; exists { + return nil + } + + return usecase.ErrSystemNotFound +} + +func (r *TestSystemsComputerSystemRepository) CancelKVMConsent(_ context.Context, systemID string) error { + if err, exists := r.errorOnGetByID[systemID]; exists { + return err + } + + if _, exists := r.systems[systemID]; exists { + return nil + } + + return usecase.ErrSystemNotFound +} + // TestCase represents a generic test case structure type SystemsTestCase[T any] struct { name string diff --git a/redfish/internal/mocks/mock_repo.go b/redfish/internal/mocks/mock_repo.go index 19b823ce..027ae280 100644 --- a/redfish/internal/mocks/mock_repo.go +++ b/redfish/internal/mocks/mock_repo.go @@ -245,3 +245,30 @@ func (r *MockComputerSystemRepo) UpdateSerialConsoleServiceEnabled(_ context.Con return nil } + +// RequestKVMConsent starts a mock KVM consent flow for an existing system. +func (r *MockComputerSystemRepo) RequestKVMConsent(_ context.Context, systemID string) error { + if _, exists := r.systems[systemID]; !exists { + return usecase.ErrSystemNotFound + } + + return nil +} + +// SubmitKVMConsentCode accepts a mock six-digit consent code for an existing system. +func (r *MockComputerSystemRepo) SubmitKVMConsentCode(_ context.Context, systemID, _ string) error { + if _, exists := r.systems[systemID]; !exists { + return usecase.ErrSystemNotFound + } + + return nil +} + +// CancelKVMConsent cancels a mock KVM consent flow for an existing system. +func (r *MockComputerSystemRepo) CancelKVMConsent(_ context.Context, systemID string) error { + if _, exists := r.systems[systemID]; !exists { + return usecase.ErrSystemNotFound + } + + return nil +} diff --git a/redfish/internal/usecase/computer_system.go b/redfish/internal/usecase/computer_system.go index c9fc698a..1a6da0e3 100644 --- a/redfish/internal/usecase/computer_system.go +++ b/redfish/internal/usecase/computer_system.go @@ -505,6 +505,21 @@ func (uc *ComputerSystemUseCase) UpdateSerialConsoleServiceEnabled(ctx context.C return uc.Repo.UpdateSerialConsoleServiceEnabled(ctx, systemID, enabled) } +// RequestKVMConsent triggers a user consent request on the target system. +func (uc *ComputerSystemUseCase) RequestKVMConsent(ctx context.Context, systemID string) error { + return uc.Repo.RequestKVMConsent(ctx, systemID) +} + +// SubmitKVMConsentCode submits the user-provided KVM consent code. +func (uc *ComputerSystemUseCase) SubmitKVMConsentCode(ctx context.Context, systemID, consentCode string) error { + return uc.Repo.SubmitKVMConsentCode(ctx, systemID, consentCode) +} + +// CancelKVMConsent cancels a pending user consent request on the target system. +func (uc *ComputerSystemUseCase) CancelKVMConsent(ctx context.Context, systemID string) error { + return uc.Repo.CancelKVMConsent(ctx, systemID) +} + // GenerateRedirectionToken validates that the target system exists and returns a short-lived redirection token. func (uc *ComputerSystemUseCase) GenerateRedirectionToken(ctx context.Context, systemID string) (*generated.ComputerSystemOemIntelAmtGenerateRedirectionTokenResponse, error) { if _, err := uc.Repo.GetByID(ctx, systemID); err != nil { @@ -747,6 +762,12 @@ func (uc *ComputerSystemUseCase) createActionsStruct(systemID string) *generated title := "Reset" generateTokenTarget := fmt.Sprintf("/redfish/v1/Systems/%s/Actions/Oem/IntelComputerSystem.GenerateRedirectionToken", systemID) generateTokenTitle := "Generate Redirection Token" + requestConsentTarget := fmt.Sprintf("/redfish/v1/Systems/%s/Actions/Oem/IntelComputerSystem.RequestKVMConsent", systemID) + requestConsentTitle := "Request KVM Consent" + submitConsentCodeTarget := fmt.Sprintf("/redfish/v1/Systems/%s/Actions/Oem/IntelComputerSystem.SubmitKVMConsentCode", systemID) + submitConsentCodeTitle := "Submit KVM Consent Code" + cancelConsentTarget := fmt.Sprintf("/redfish/v1/Systems/%s/Actions/Oem/IntelComputerSystem.CancelKVMConsent", systemID) + cancelConsentTitle := "Cancel KVM Consent" // Create the ComputerSystem.Reset action resetAction := &generated.ComputerSystemReset{ @@ -759,12 +780,27 @@ func (uc *ComputerSystemUseCase) createActionsStruct(systemID string) *generated Target: &generateTokenTarget, Title: &generateTokenTitle, } + requestConsentAction := &generated.ComputerSystemOemIntelAmtRequestKVMConsent{ + Target: &requestConsentTarget, + Title: &requestConsentTitle, + } + submitConsentCodeAction := &generated.ComputerSystemOemIntelAmtSubmitKVMConsentCode{ + Target: &submitConsentCodeTarget, + Title: &submitConsentCodeTitle, + } + cancelConsentAction := &generated.ComputerSystemOemIntelAmtCancelKVMConsent{ + Target: &cancelConsentTarget, + Title: &cancelConsentTitle, + } // Create and return the Actions structure return &generated.ComputerSystemActions{ HashComputerSystemReset: resetAction, Oem: &generated.ComputerSystemOemActions{ HashOemIntelAMTGenerateRedirectionToken: generateTokenAction, + HashOemIntelAMTRequestKVMConsent: requestConsentAction, + HashOemIntelAMTSubmitKVMConsentCode: submitConsentCodeAction, + HashOemIntelAMTCancelKVMConsent: cancelConsentAction, }, } } diff --git a/redfish/internal/usecase/computer_system_fuzz_test.go b/redfish/internal/usecase/computer_system_fuzz_test.go index c3a104f3..5c4fc457 100644 --- a/redfish/internal/usecase/computer_system_fuzz_test.go +++ b/redfish/internal/usecase/computer_system_fuzz_test.go @@ -91,6 +91,30 @@ func (r *fuzzMockRepo) UpdateSerialConsoleServiceEnabled(_ context.Context, syst return nil } +func (r *fuzzMockRepo) RequestKVMConsent(_ context.Context, systemID string) error { + if _, ok := r.systems[systemID]; !ok { + return ErrSystemNotFound + } + + return nil +} + +func (r *fuzzMockRepo) SubmitKVMConsentCode(_ context.Context, systemID, _ string) error { + if _, ok := r.systems[systemID]; !ok { + return ErrSystemNotFound + } + + return nil +} + +func (r *fuzzMockRepo) CancelKVMConsent(_ context.Context, systemID string) error { + if _, ok := r.systems[systemID]; !ok { + return ErrSystemNotFound + } + + return nil +} + // newFuzzUseCase returns a ComputerSystemUseCase backed by the inline mock repository. func newFuzzUseCase() *ComputerSystemUseCase { return &ComputerSystemUseCase{Repo: newFuzzMockRepo()} diff --git a/redfish/internal/usecase/computer_system_test.go b/redfish/internal/usecase/computer_system_test.go index 92a6d92d..ae1d5dfe 100644 --- a/redfish/internal/usecase/computer_system_test.go +++ b/redfish/internal/usecase/computer_system_test.go @@ -14,6 +14,7 @@ type graphicalConsoleTestRepo struct { bootErr error kvmErr error solErr error + ccErr error } func (r *graphicalConsoleTestRepo) GetAll(_ context.Context) ([]string, error) { @@ -54,6 +55,18 @@ func (r *graphicalConsoleTestRepo) UpdateSerialConsoleServiceEnabled(_ context.C return r.solErr } +func (r *graphicalConsoleTestRepo) RequestKVMConsent(_ context.Context, _ string) error { + return r.ccErr +} + +func (r *graphicalConsoleTestRepo) SubmitKVMConsentCode(_ context.Context, _, _ string) error { + return r.ccErr +} + +func (r *graphicalConsoleTestRepo) CancelKVMConsent(_ context.Context, _ string) error { + return r.ccErr +} + func TestConvertGraphicalConsoleToGeneratedNil(t *testing.T) { t.Parallel() @@ -604,6 +617,96 @@ func TestUpdateSerialConsoleServiceEnabled(t *testing.T) { } } +func TestRequestKVMConsent(t *testing.T) { + t.Parallel() + + errAMT := errors.New("amt refused") + + tests := []struct { + name string + repoErr error + wantErr error + }{ + {name: "success"}, + {name: "repo error", repoErr: errAMT, wantErr: errAMT}, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + uc := &ComputerSystemUseCase{Repo: &graphicalConsoleTestRepo{ccErr: tt.repoErr}} + + err := uc.RequestKVMConsent(context.Background(), "system-1") + if !errors.Is(err, tt.wantErr) { + t.Fatalf("RequestKVMConsent() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestSubmitKVMConsentCode(t *testing.T) { + t.Parallel() + + errAMT := errors.New("amt refused") + + tests := []struct { + name string + repoErr error + wantErr error + }{ + {name: "success"}, + {name: "repo error", repoErr: errAMT, wantErr: errAMT}, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + uc := &ComputerSystemUseCase{Repo: &graphicalConsoleTestRepo{ccErr: tt.repoErr}} + + err := uc.SubmitKVMConsentCode(context.Background(), "system-1", "123456") + if !errors.Is(err, tt.wantErr) { + t.Fatalf("SubmitKVMConsentCode() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestCancelKVMConsent(t *testing.T) { + t.Parallel() + + errAMT := errors.New("amt refused") + + tests := []struct { + name string + repoErr error + wantErr error + }{ + {name: "success"}, + {name: "repo error", repoErr: errAMT, wantErr: errAMT}, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + uc := &ComputerSystemUseCase{Repo: &graphicalConsoleTestRepo{ccErr: tt.repoErr}} + + err := uc.CancelKVMConsent(context.Background(), "system-1") + if !errors.Is(err, tt.wantErr) { + t.Fatalf("CancelKVMConsent() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + func TestGetComputerSystemIncludesGenerateRedirectionTokenAction(t *testing.T) { t.Parallel() @@ -641,6 +744,18 @@ func TestGetComputerSystemIncludesGenerateRedirectionTokenAction(t *testing.T) { t.Fatal("expected #Oem.Intel.AMT.GenerateRedirectionToken action to be present") } + if result.Actions.Oem.HashOemIntelAMTRequestKVMConsent == nil { + t.Fatal("expected #Oem.Intel.AMT.RequestKVMConsent action to be present") + } + + if result.Actions.Oem.HashOemIntelAMTSubmitKVMConsentCode == nil { + t.Fatal("expected #Oem.Intel.AMT.SubmitKVMConsentCode action to be present") + } + + if result.Actions.Oem.HashOemIntelAMTCancelKVMConsent == nil { + t.Fatal("expected #Oem.Intel.AMT.CancelKVMConsent action to be present") + } + action := result.Actions.Oem.HashOemIntelAMTGenerateRedirectionToken expectedTarget := "/redfish/v1/Systems/system-1/Actions/Oem/IntelComputerSystem.GenerateRedirectionToken" @@ -651,4 +766,25 @@ func TestGetComputerSystemIncludesGenerateRedirectionTokenAction(t *testing.T) { if action.Title == nil || *action.Title != "Generate Redirection Token" { t.Fatalf("expected action title %q, got %#v", "Generate Redirection Token", action.Title) } + + requestAction := result.Actions.Oem.HashOemIntelAMTRequestKVMConsent + requestTarget := "/redfish/v1/Systems/system-1/Actions/Oem/IntelComputerSystem.RequestKVMConsent" + + submitAction := result.Actions.Oem.HashOemIntelAMTSubmitKVMConsentCode + submitTarget := "/redfish/v1/Systems/system-1/Actions/Oem/IntelComputerSystem.SubmitKVMConsentCode" + + cancelAction := result.Actions.Oem.HashOemIntelAMTCancelKVMConsent + cancelTarget := "/redfish/v1/Systems/system-1/Actions/Oem/IntelComputerSystem.CancelKVMConsent" + + assertActionTarget(t, "request", requestAction.Target, requestTarget) + assertActionTarget(t, "submit", submitAction.Target, submitTarget) + assertActionTarget(t, "cancel", cancelAction.Target, cancelTarget) +} + +func assertActionTarget(t *testing.T, actionName string, got *string, want string) { + t.Helper() + + if got == nil || *got != want { + t.Fatalf("expected %s action target %q, got %#v", actionName, want, got) + } } diff --git a/redfish/internal/usecase/consent_errors.go b/redfish/internal/usecase/consent_errors.go new file mode 100644 index 00000000..8e23d024 --- /dev/null +++ b/redfish/internal/usecase/consent_errors.go @@ -0,0 +1,63 @@ +package usecase + +import "fmt" + +const ( + consentOperationRequest = "RequestKVMConsent" + consentOperationSubmit = "SubmitKVMConsentCode" + consentOperationCancel = "CancelKVMConsent" + + consentReturnValueInvalidState = 2 + consentReturnValueOperationNotReady = 3 + consentReturnValueConsentCodeInvalid = 2066 +) + +// ConsentFailedError indicates that AMT processed a consent operation but returned a non-zero ReturnValue. +type ConsentFailedError struct { + Operation string + ReturnValue int +} + +func (e *ConsentFailedError) Error() string { + if msg, ok := consentReturnValueMessage(e.Operation, e.ReturnValue); ok { + return fmt.Sprintf("%s failed with ReturnValue=%d: %s", e.Operation, e.ReturnValue, msg) + } + + return fmt.Sprintf("%s failed with ReturnValue=%d", e.Operation, e.ReturnValue) +} + +func consentReturnValueMessage(operation string, returnValue int) (string, bool) { + switch returnValue { + case consentReturnValueInvalidState: + switch operation { + case consentOperationRequest: + return "cannot request consent in the current opt-in state (request may already be pending)", true + case consentOperationSubmit: + return "cannot submit consent code in the current opt-in state (request may be missing or code invalid)", true + case consentOperationCancel: + return "cannot cancel consent in the current opt-in state (no pending request)", true + default: + return "operation is not allowed in the current AMT opt-in state", true + } + case consentReturnValueOperationNotReady: + switch operation { + case consentOperationRequest: + return "cannot request consent because AMT is not ready for this operation in the current configuration/state", true + case consentOperationSubmit: + return "cannot submit consent code because AMT is not ready for this operation in the current configuration/state", true + case consentOperationCancel: + return "cannot cancel consent because AMT is not ready for this operation in the current configuration/state", true + default: + return "operation cannot proceed because AMT is not ready in the current configuration/state", true + } + case consentReturnValueConsentCodeInvalid: + switch operation { + case consentOperationSubmit: + return "consent code was rejected by AMT (code may be incorrect or expired)", true + default: + return "operation failed due to AMT consent validation error", true + } + default: + return "", false + } +} diff --git a/redfish/internal/usecase/consent_errors_test.go b/redfish/internal/usecase/consent_errors_test.go new file mode 100644 index 00000000..1178184a --- /dev/null +++ b/redfish/internal/usecase/consent_errors_test.go @@ -0,0 +1,92 @@ +package usecase + +import ( + "strings" + "testing" +) + +func TestConsentFailedErrorMessage_KnownReturnValue(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + err *ConsentFailedError + wantSubstr string + }{ + { + name: "request return value 2", + err: &ConsentFailedError{Operation: consentOperationRequest, ReturnValue: consentReturnValueInvalidState}, + wantSubstr: "cannot request consent in the current opt-in state", + }, + { + name: "submit return value 2", + err: &ConsentFailedError{Operation: consentOperationSubmit, ReturnValue: consentReturnValueInvalidState}, + wantSubstr: "cannot submit consent code in the current opt-in state", + }, + { + name: "cancel return value 2", + err: &ConsentFailedError{Operation: consentOperationCancel, ReturnValue: consentReturnValueInvalidState}, + wantSubstr: "cannot cancel consent in the current opt-in state", + }, + { + name: "unknown operation return value 2", + err: &ConsentFailedError{Operation: "OtherConsentOp", ReturnValue: consentReturnValueInvalidState}, + wantSubstr: "operation is not allowed in the current AMT opt-in state", + }, + { + name: "request return value 3", + err: &ConsentFailedError{Operation: consentOperationRequest, ReturnValue: consentReturnValueOperationNotReady}, + wantSubstr: "cannot request consent because AMT is not ready", + }, + { + name: "submit return value 3", + err: &ConsentFailedError{Operation: consentOperationSubmit, ReturnValue: consentReturnValueOperationNotReady}, + wantSubstr: "cannot submit consent code because AMT is not ready", + }, + { + name: "cancel return value 3", + err: &ConsentFailedError{Operation: consentOperationCancel, ReturnValue: consentReturnValueOperationNotReady}, + wantSubstr: "cannot cancel consent because AMT is not ready", + }, + { + name: "unknown operation return value 3", + err: &ConsentFailedError{Operation: "OtherConsentOp", ReturnValue: consentReturnValueOperationNotReady}, + wantSubstr: "operation cannot proceed because AMT is not ready", + }, + { + name: "submit return value 2066", + err: &ConsentFailedError{Operation: consentOperationSubmit, ReturnValue: consentReturnValueConsentCodeInvalid}, + wantSubstr: "consent code was rejected by AMT", + }, + { + name: "unknown operation return value 2066", + err: &ConsentFailedError{Operation: "OtherConsentOp", ReturnValue: consentReturnValueConsentCodeInvalid}, + wantSubstr: "operation failed due to AMT consent validation error", + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := tt.err.Error() + if !strings.Contains(got, tt.wantSubstr) { + t.Fatalf("ConsentFailedError.Error() = %q, want substring %q", got, tt.wantSubstr) + } + }) + } +} + +func TestConsentFailedErrorMessage_FallbackFormat(t *testing.T) { + t.Parallel() + + err := &ConsentFailedError{Operation: consentOperationRequest, ReturnValue: 5} + got := err.Error() + want := "RequestKVMConsent failed with ReturnValue=5" + + if got != want { + t.Fatalf("ConsentFailedError.Error() = %q, want %q", got, want) + } +} diff --git a/redfish/internal/usecase/interfaces.go b/redfish/internal/usecase/interfaces.go index 9bbf8f09..9b7f0c89 100644 --- a/redfish/internal/usecase/interfaces.go +++ b/redfish/internal/usecase/interfaces.go @@ -17,4 +17,7 @@ type ComputerSystemRepository interface { UpdateBootSettings(ctx context.Context, systemID string, boot *generated.ComputerSystemBoot) error UpdateGraphicalConsoleServiceEnabled(ctx context.Context, systemID string, enabled bool) error UpdateSerialConsoleServiceEnabled(ctx context.Context, systemID string, enabled bool) error + RequestKVMConsent(ctx context.Context, systemID string) error + SubmitKVMConsentCode(ctx context.Context, systemID, consentCode string) error + CancelKVMConsent(ctx context.Context, systemID string) error } diff --git a/redfish/internal/usecase/wsman_repo.go b/redfish/internal/usecase/wsman_repo.go index 2a5cb4ce..067ca1b0 100644 --- a/redfish/internal/usecase/wsman_repo.go +++ b/redfish/internal/usecase/wsman_repo.go @@ -7,6 +7,7 @@ import ( "fmt" "reflect" "strconv" + "strings" amtBoot "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/boot" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/setupandconfiguration" @@ -49,10 +50,20 @@ const ( redirectionWebSocketURI = "/relay/webrelay.ashx" controlModeACM = "ACM" controlModeCCM = "CCM" + userConsentKVM = "kvm" + userConsentAll = "all" userConsentNotRequired = "NotRequired" + userConsentRequested = "Requested" + userConsentGranted = "Granted" + userConsentDenied = "Denied" + userConsentTimeout = "Timeout" statusPendingConsent = "PendingConsent" statusError = "Error" + // Forward-compatible raw OptInState values for statuses not yet exposed by go-wsman constants. + optInStateDeniedRaw = 5 + optInStateTimeoutRaw = 6 + // Health state constants. healthStateOK = "OK" healthStateWarning = "Warning" @@ -95,6 +106,9 @@ const ( // Maximum items to process in arrays to prevent hangs. maxArrayItems = 10 + + // KVM consent code constraints. + consentCodeDigits = 6 ) var ( @@ -109,6 +123,9 @@ var ( // ErrUnsupportedBootTarget is returned when an unsupported boot target is requested. ErrUnsupportedBootTarget = errors.New("unsupported boot target") + + // ErrInvalidConsentCode is returned when the KVM consent code is not a six-digit numeric value. + ErrInvalidConsentCode = errors.New("invalid consent code: must be six-digit numeric value") ) // CIMObjectType represents different types of CIM objects. @@ -931,16 +948,16 @@ func (r *WsmanComputerSystemRepo) buildGraphicalConsole(useTLS bool, featuresV2 } } - kvmStatus := determineKVMStatus(featuresV2.EnableKVM, featuresV2.KVMAvailable, featuresV2.UserConsent, featuresV2.OptInState) + kvmStatus := determineKVMStatus(featuresV2.EnableKVM, featuresV2.KVMAvailable, featuresV2.UserConsent, featuresV2.OptInState, controlMode) + userConsentStatus := determineKVMUserConsentStatus(featuresV2.UserConsent, featuresV2.OptInState, controlMode) // Build OEM extensions with Intel AMT status oemExt := &redfishv1.ComputerSystemHostGraphicalConsoleOEM{ Intel: &redfishv1.ComputerSystemHostGraphicalConsoleIntel{ AMT: &redfishv1.ComputerSystemHostGraphicalConsoleAMT{ - ControlMode: controlMode, - KVMStatus: kvmStatus, - // Planned follow-up: query CIM_KVMRedirectionSAP and IPS_OptInService for actual user consent status. - UserConsentStatus: userConsentNotRequired, + ControlMode: controlMode, + KVMStatus: kvmStatus, + UserConsentStatus: userConsentStatus, }, }, } @@ -955,12 +972,7 @@ func (r *WsmanComputerSystemRepo) buildGraphicalConsole(useTLS bool, featuresV2 // determineKVMStatus returns Intel AMT KVMStatus using KVM availability, enablement, // and user consent runtime state from IPS_OptInService. -func determineKVMStatus(enableKVM, kvmAvailable bool, userConsent string, optInState int) string { - const ( - userConsentKVM = "kvm" - userConsentAll = "all" - ) - +func determineKVMStatus(enableKVM, kvmAvailable bool, userConsent string, optInState int, controlMode string) string { if !kvmAvailable { return StateDisabled } @@ -969,14 +981,14 @@ func determineKVMStatus(enableKVM, kvmAvailable bool, userConsent string, optInS return StateDisabled } - consentRequired := userConsent == userConsentKVM || userConsent == userConsentAll - if consentRequired { - switch optin.OptInState(optInState) { - case optin.InSession: + consentRequired := isKVMConsentRequired(userConsent, controlMode) + if consentRequired || optInState != int(optin.NotStarted) { + switch optInState { + case int(optin.InSession): return kvmStatusActive - case optin.NotStarted, optin.Requested, optin.Displayed: + case int(optin.NotStarted), int(optin.Requested), int(optin.Displayed): return statusPendingConsent - case optin.Received: + case int(optin.Received): return StateEnabled default: return statusError @@ -986,6 +998,37 @@ func determineKVMStatus(enableKVM, kvmAvailable bool, userConsent string, optInS return StateEnabled } +func determineKVMUserConsentStatus(userConsent string, optInState int, controlMode string) string { + consentRequired := isKVMConsentRequired(userConsent, controlMode) + if consentRequired || optInState != int(optin.NotStarted) { + switch optInState { + case int(optin.NotStarted), int(optin.Requested), int(optin.Displayed): + return userConsentRequested + case int(optin.Received), int(optin.InSession): + return userConsentGranted + case optInStateDeniedRaw: + return userConsentDenied + case optInStateTimeoutRaw: + return userConsentTimeout + default: + return userConsentRequested + } + } + + return userConsentNotRequired +} + +func isKVMConsentRequired(userConsent, controlMode string) bool { + consentMode := strings.ToLower(strings.TrimSpace(userConsent)) + normalizedControlMode := strings.TrimSpace(controlMode) + + if strings.EqualFold(normalizedControlMode, controlModeCCM) { + return true + } + + return consentMode == userConsentKVM || consentMode == userConsentAll +} + func (r *WsmanComputerSystemRepo) buildSerialConsole(systemID string, featuresV2 dtov2.Features, controlMode string) *redfishv1.ComputerSystemHostSerialConsole { serviceEnabled := featuresV2.EnableSOL maxConcurrentSessions := int64(1) @@ -1124,6 +1167,78 @@ func (r *WsmanComputerSystemRepo) UpdateSerialConsoleServiceEnabled(ctx context. return err } +// RequestKVMConsent starts a user consent request on the target system. +func (r *WsmanComputerSystemRepo) RequestKVMConsent(ctx context.Context, systemID string) error { + resp, err := r.usecase.GetUserConsentCode(ctx, systemID) + if r.isDeviceNotFoundError(err) { + return ErrSystemNotFound + } + + if err != nil { + return err + } + + if resp.Body.ReturnValue != 0 { + return &ConsentFailedError{Operation: consentOperationRequest, ReturnValue: resp.Body.ReturnValue} + } + + return nil +} + +// SubmitKVMConsentCode submits the six-digit user consent code for KVM. +func (r *WsmanComputerSystemRepo) SubmitKVMConsentCode(ctx context.Context, systemID, consentCode string) error { + if !isSixDigitNumeric(consentCode) { + return ErrInvalidConsentCode + } + + resp, err := r.usecase.SendConsentCode(ctx, dto.UserConsentCode{ConsentCode: consentCode}, systemID) + if r.isDeviceNotFoundError(err) { + return ErrSystemNotFound + } + + if err != nil { + return err + } + + if resp.Body.ReturnValue != 0 { + return &ConsentFailedError{Operation: consentOperationSubmit, ReturnValue: resp.Body.ReturnValue} + } + + return nil +} + +func isSixDigitNumeric(code string) bool { + if len(code) != consentCodeDigits { + return false + } + + for i := 0; i < len(code); i++ { + if code[i] < '0' || code[i] > '9' { + return false + } + } + + return true +} + +// CancelKVMConsent cancels a pending user consent request. +func (r *WsmanComputerSystemRepo) CancelKVMConsent(ctx context.Context, systemID string) error { + resp, err := r.usecase.CancelUserConsent(ctx, systemID) + if r.isDeviceNotFoundError(err) { + return ErrSystemNotFound + } + + if err != nil { + return err + } + + if resp.Body.ReturnValue != 0 { + return &ConsentFailedError{Operation: consentOperationCancel, ReturnValue: resp.Body.ReturnValue} + } + + return nil +} + // UpdatePowerState sends a power action command to the specified system via WSMAN. func (r *WsmanComputerSystemRepo) UpdatePowerState(ctx context.Context, systemID string, resetType redfishv1.PowerState) error { // Get the current power state for logging and validation diff --git a/redfish/internal/usecase/wsman_repo_test.go b/redfish/internal/usecase/wsman_repo_test.go index 2e7265cf..f5db4799 100644 --- a/redfish/internal/usecase/wsman_repo_test.go +++ b/redfish/internal/usecase/wsman_repo_test.go @@ -51,6 +51,37 @@ func TestMapProvisioningModeToControlMode(t *testing.T) { } } +func TestRequestKVMConsentRepo_ReturnValueFailure(t *testing.T) { + t.Parallel() + + device := &entity.Device{GUID: "system-1", TenantID: "tenant-1"} + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + repoMock := mocks.NewMockDeviceManagementRepository(ctrl) + wsmanMock := mocks.NewMockWSMAN(ctrl) + wsmanMock.EXPECT().Worker().Return().AnyTimes() + + management := mocks.NewMockManagement(ctrl) + + uc := devices.New(repoMock, wsmanMock, mocks.NewMockRedirection(ctrl), logger.New("error"), mocks.MockCrypto{}) + repo := &WsmanComputerSystemRepo{usecase: uc, log: logger.New("error")} + + repoMock.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + wsmanMock.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(management, nil) + management.EXPECT().GetUserConsentCode().Return(optin.Response{ + Body: optin.Body{StartOptInResponse: optin.StartOptIn_OUTPUT{ReturnValue: 5}}, + }, nil) + + err := repo.RequestKVMConsent(context.Background(), device.GUID) + + var consentErr *ConsentFailedError + if !errors.As(err, &consentErr) { + t.Fatalf("RequestKVMConsent() error type = %T, want *ConsentFailedError", err) + } +} + func TestMapControlModeFromVersion(t *testing.T) { t.Parallel() @@ -90,6 +121,37 @@ func TestMapControlModeFromVersion(t *testing.T) { } } +func TestSubmitKVMConsentCodeRepo_ReturnValueFailure(t *testing.T) { + t.Parallel() + + device := &entity.Device{GUID: "system-1", TenantID: "tenant-1"} + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + repoMock := mocks.NewMockDeviceManagementRepository(ctrl) + wsmanMock := mocks.NewMockWSMAN(ctrl) + wsmanMock.EXPECT().Worker().Return().AnyTimes() + + management := mocks.NewMockManagement(ctrl) + + uc := devices.New(repoMock, wsmanMock, mocks.NewMockRedirection(ctrl), logger.New("error"), mocks.MockCrypto{}) + repo := &WsmanComputerSystemRepo{usecase: uc, log: logger.New("error")} + + repoMock.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + wsmanMock.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(management, nil) + management.EXPECT().SendConsentCode(123456).Return(optin.Response{ + Body: optin.Body{SendOptInCodeResponse: optin.SendOptInCode_OUTPUT{ReturnValue: 7}}, + }, nil) + + err := repo.SubmitKVMConsentCode(context.Background(), device.GUID, "123456") + + var consentErr *ConsentFailedError + if !errors.As(err, &consentErr) { + t.Fatalf("SubmitKVMConsentCode() error type = %T, want *ConsentFailedError", err) + } +} + func TestGetAMTControlMode(t *testing.T) { t.Parallel() @@ -154,6 +216,37 @@ func TestGetAMTControlMode(t *testing.T) { } } +func TestCancelKVMConsentRepo_ReturnValueFailure(t *testing.T) { + t.Parallel() + + device := &entity.Device{GUID: "system-1", TenantID: "tenant-1"} + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + repoMock := mocks.NewMockDeviceManagementRepository(ctrl) + wsmanMock := mocks.NewMockWSMAN(ctrl) + wsmanMock.EXPECT().Worker().Return().AnyTimes() + + management := mocks.NewMockManagement(ctrl) + + uc := devices.New(repoMock, wsmanMock, mocks.NewMockRedirection(ctrl), logger.New("error"), mocks.MockCrypto{}) + repo := &WsmanComputerSystemRepo{usecase: uc, log: logger.New("error")} + + repoMock.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + wsmanMock.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(management, nil) + management.EXPECT().CancelUserConsentRequest().Return(optin.Response{ + Body: optin.Body{CancelOptInResponse: optin.CancelOptIn_OUTPUT{ReturnValue: 9}}, + }, nil) + + err := repo.CancelKVMConsent(context.Background(), device.GUID) + + var consentErr *ConsentFailedError + if !errors.As(err, &consentErr) { + t.Fatalf("CancelKVMConsent() error type = %T, want *ConsentFailedError", err) + } +} + func TestDetermineKVMStatus(t *testing.T) { t.Parallel() @@ -163,6 +256,7 @@ func TestDetermineKVMStatus(t *testing.T) { kvmAvailable bool userConsent string optInState int + controlMode string want string }{ { @@ -171,6 +265,7 @@ func TestDetermineKVMStatus(t *testing.T) { kvmAvailable: false, userConsent: "kvm", optInState: int(optin.InSession), + controlMode: controlModeACM, want: StateDisabled, }, { @@ -179,6 +274,7 @@ func TestDetermineKVMStatus(t *testing.T) { kvmAvailable: true, userConsent: "kvm", optInState: int(optin.InSession), + controlMode: controlModeACM, want: StateDisabled, }, { @@ -187,6 +283,7 @@ func TestDetermineKVMStatus(t *testing.T) { kvmAvailable: true, userConsent: "kvm", optInState: int(optin.InSession), + controlMode: controlModeACM, want: "Active", }, { @@ -195,6 +292,7 @@ func TestDetermineKVMStatus(t *testing.T) { kvmAvailable: true, userConsent: "all", optInState: int(optin.Requested), + controlMode: controlModeACM, want: "PendingConsent", }, { @@ -203,6 +301,7 @@ func TestDetermineKVMStatus(t *testing.T) { kvmAvailable: true, userConsent: "all", optInState: int(optin.Received), + controlMode: controlModeACM, want: StateEnabled, }, { @@ -211,6 +310,7 @@ func TestDetermineKVMStatus(t *testing.T) { kvmAvailable: true, userConsent: "kvm", optInState: 999, + controlMode: controlModeACM, want: "Error", }, { @@ -219,8 +319,27 @@ func TestDetermineKVMStatus(t *testing.T) { kvmAvailable: true, userConsent: "none", optInState: int(optin.NotStarted), + controlMode: controlModeACM, want: StateEnabled, }, + { + name: "pending when consent flow is requested even if policy is none", + enableKVM: true, + kvmAvailable: true, + userConsent: "none", + optInState: int(optin.Requested), + controlMode: controlModeACM, + want: statusPendingConsent, + }, + { + name: "CCM requires consent even when configured none", + enableKVM: true, + kvmAvailable: true, + userConsent: "none", + optInState: int(optin.Requested), + controlMode: controlModeCCM, + want: statusPendingConsent, + }, } for _, tt := range tests { @@ -229,7 +348,7 @@ func TestDetermineKVMStatus(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - got := determineKVMStatus(tt.enableKVM, tt.kvmAvailable, tt.userConsent, tt.optInState) + got := determineKVMStatus(tt.enableKVM, tt.kvmAvailable, tt.userConsent, tt.optInState, tt.controlMode) if got != tt.want { t.Fatalf("determineKVMStatus() = %q, want %q", got, tt.want) } @@ -237,7 +356,7 @@ func TestDetermineKVMStatus(t *testing.T) { } } -func assertGraphicalConsoleOEM(t *testing.T, got *redfishv1.ComputerSystemHostGraphicalConsole, wantKVMStatus string) { +func assertGraphicalConsoleOEM(t *testing.T, got *redfishv1.ComputerSystemHostGraphicalConsole, wantKVMStatus, wantUserConsentStatus string) { t.Helper() if got.OEM == nil || got.OEM.Intel == nil || got.OEM.Intel.AMT == nil { @@ -254,12 +373,12 @@ func assertGraphicalConsoleOEM(t *testing.T, got *redfishv1.ComputerSystemHostGr t.Errorf("ControlMode = %q, want %q", amt.ControlMode, "ACM") } - if amt.UserConsentStatus != "NotRequired" { - t.Errorf("UserConsentStatus = %q, want %q", amt.UserConsentStatus, "NotRequired") + if amt.UserConsentStatus != wantUserConsentStatus { + t.Errorf("UserConsentStatus = %q, want %q", amt.UserConsentStatus, wantUserConsentStatus) } } -func assertGraphicalConsole(t *testing.T, got *redfishv1.ComputerSystemHostGraphicalConsole, wantEnabled bool, wantConnTypes []string, wantPort int64, wantKVMStatus string) { +func assertGraphicalConsole(t *testing.T, got *redfishv1.ComputerSystemHostGraphicalConsole, wantEnabled bool, wantConnTypes []string, wantPort int64, wantKVMStatus, wantUserConsentStatus string) { t.Helper() if got == nil { @@ -290,7 +409,7 @@ func assertGraphicalConsole(t *testing.T, got *redfishv1.ComputerSystemHostGraph } } - assertGraphicalConsoleOEM(t, got, wantKVMStatus) + assertGraphicalConsoleOEM(t, got, wantKVMStatus, wantUserConsentStatus) } func TestBuildGraphicalConsole(t *testing.T) { @@ -307,6 +426,7 @@ func TestBuildGraphicalConsole(t *testing.T) { wantConnTypes []string wantPort int64 wantKVMStatus string + wantConsent string }{ { name: "KVM not available - no connect types and no port", @@ -316,6 +436,7 @@ func TestBuildGraphicalConsole(t *testing.T) { wantConnTypes: nil, wantPort: 0, wantKVMStatus: StateDisabled, + wantConsent: userConsentNotRequired, }, { name: "KVM available non-TLS port", @@ -325,6 +446,7 @@ func TestBuildGraphicalConsole(t *testing.T) { wantConnTypes: kvmIP, wantPort: 16994, wantKVMStatus: StateEnabled, + wantConsent: userConsentNotRequired, }, { name: "KVM available TLS port", @@ -334,6 +456,7 @@ func TestBuildGraphicalConsole(t *testing.T) { wantConnTypes: kvmIP, wantPort: 16995, wantKVMStatus: StateEnabled, + wantConsent: userConsentNotRequired, }, { name: "KVM disabled", @@ -343,6 +466,7 @@ func TestBuildGraphicalConsole(t *testing.T) { wantConnTypes: kvmIP, wantPort: 16994, wantKVMStatus: StateDisabled, + wantConsent: userConsentNotRequired, }, { name: "consent required and in session - active", @@ -352,6 +476,27 @@ func TestBuildGraphicalConsole(t *testing.T) { wantConnTypes: kvmIP, wantPort: 16994, wantKVMStatus: kvmStatusActive, + wantConsent: userConsentGranted, + }, + { + name: "consent requested maps to Requested", + useTLS: false, + features: dtov2.Features{EnableKVM: true, KVMAvailable: true, UserConsent: "kvm", OptInState: int(optin.Requested)}, + wantEnabled: true, + wantConnTypes: kvmIP, + wantPort: 16994, + wantKVMStatus: statusPendingConsent, + wantConsent: userConsentRequested, + }, + { + name: "consent flow requested with none policy is pending", + useTLS: false, + features: dtov2.Features{EnableKVM: true, KVMAvailable: true, UserConsent: "none", OptInState: int(optin.Requested)}, + wantEnabled: true, + wantConnTypes: kvmIP, + wantPort: 16994, + wantKVMStatus: statusPendingConsent, + wantConsent: userConsentRequested, }, } @@ -362,7 +507,48 @@ func TestBuildGraphicalConsole(t *testing.T) { t.Parallel() got := repo.buildGraphicalConsole(tt.useTLS, tt.features, controlModeACM) - assertGraphicalConsole(t, got, tt.wantEnabled, tt.wantConnTypes, tt.wantPort, tt.wantKVMStatus) + assertGraphicalConsole(t, got, tt.wantEnabled, tt.wantConnTypes, tt.wantPort, tt.wantKVMStatus, tt.wantConsent) + }) + } +} + +func TestDetermineKVMUserConsentStatus(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + userConsent string + optInState int + controlMode string + want string + }{ + {name: "kvm requested status", userConsent: "kvm", optInState: int(optin.Requested), want: userConsentRequested}, + {name: "all requested status", userConsent: "all", optInState: int(optin.Displayed), want: userConsentRequested}, + {name: "kvm uppercase requested status", userConsent: "KVM", optInState: int(optin.NotStarted), want: userConsentRequested}, + {name: "all with spaces requested status", userConsent: " all ", optInState: int(optin.NotStarted), want: userConsentRequested}, + {name: "received maps to granted", userConsent: "kvm", optInState: int(optin.Received), want: userConsentGranted}, + {name: "in-session maps to granted", userConsent: "kvm", optInState: int(optin.InSession), want: userConsentGranted}, + {name: "raw denied state maps to denied", userConsent: "kvm", optInState: optInStateDeniedRaw, want: userConsentDenied}, + {name: "raw timeout state maps to timeout", userConsent: "kvm", optInState: optInStateTimeoutRaw, want: userConsentTimeout}, + {name: "unknown required state falls back to requested", userConsent: "kvm", optInState: 999, want: userConsentRequested}, + {name: "none not required", userConsent: "none", want: userConsentNotRequired}, + {name: "none with requested flow maps to requested", userConsent: "none", optInState: int(optin.Requested), want: userConsentRequested}, + {name: "none with received flow maps to granted", userConsent: "none", optInState: int(optin.Received), want: userConsentGranted}, + {name: "empty not required", userConsent: "", want: userConsentNotRequired}, + {name: "CCM requires requested when none configured", userConsent: "none", optInState: int(optin.NotStarted), controlMode: controlModeCCM, want: userConsentRequested}, + {name: "CCM with received maps to granted", userConsent: "none", optInState: int(optin.Received), controlMode: controlModeCCM, want: userConsentGranted}, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := determineKVMUserConsentStatus(tt.userConsent, tt.optInState, tt.controlMode) + if got != tt.want { + t.Fatalf("determineKVMUserConsentStatus() = %q, want %q", got, tt.want) + } }) } } @@ -896,6 +1082,240 @@ func TestUpdateSerialConsoleServiceEnabledRepo(t *testing.T) { } } +func TestRequestKVMConsentRepo(t *testing.T) { + t.Parallel() + + errDeviceNotFound := errors.New(ErrMsgDeviceNotFound) + errAMT := errors.New("amt refused") + device := &entity.Device{GUID: "system-1", TenantID: "tenant-1"} + + tests := []struct { + name string + setup func(*mocks.MockDeviceManagementRepository, *mocks.MockWSMAN, *mocks.MockManagement) + wantErr error + }{ + { + name: "success", + setup: func(repoMock *mocks.MockDeviceManagementRepository, wsmanMock *mocks.MockWSMAN, management *mocks.MockManagement) { + repoMock.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + wsmanMock.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(management, nil) + management.EXPECT().GetUserConsentCode().Return(optin.Response{}, nil) + }, + }, + { + name: "device not found", + setup: func(repoMock *mocks.MockDeviceManagementRepository, _ *mocks.MockWSMAN, _ *mocks.MockManagement) { + repoMock.EXPECT().GetByID(context.Background(), device.GUID, "").Return(nil, errDeviceNotFound) + }, + wantErr: ErrSystemNotFound, + }, + { + name: "wsman error", + setup: func(repoMock *mocks.MockDeviceManagementRepository, wsmanMock *mocks.MockWSMAN, management *mocks.MockManagement) { + repoMock.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + wsmanMock.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(management, errAMT) + }, + wantErr: errAMT, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + repoMock := mocks.NewMockDeviceManagementRepository(ctrl) + wsmanMock := mocks.NewMockWSMAN(ctrl) + wsmanMock.EXPECT().Worker().Return().AnyTimes() + + management := mocks.NewMockManagement(ctrl) + + uc := devices.New(repoMock, wsmanMock, mocks.NewMockRedirection(ctrl), logger.New("error"), mocks.MockCrypto{}) + repo := &WsmanComputerSystemRepo{usecase: uc, log: logger.New("error")} + + tt.setup(repoMock, wsmanMock, management) + + err := repo.RequestKVMConsent(context.Background(), device.GUID) + if !errors.Is(err, tt.wantErr) { + t.Fatalf("RequestKVMConsent() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestSubmitKVMConsentCodeRepo(t *testing.T) { + t.Parallel() + + errDeviceNotFound := errors.New(ErrMsgDeviceNotFound) + errAMT := errors.New("amt refused") + device := &entity.Device{GUID: "system-1", TenantID: "tenant-1"} + + tests := []struct { + name string + setup func(*mocks.MockDeviceManagementRepository, *mocks.MockWSMAN, *mocks.MockManagement) + wantErr error + code string + }{ + { + name: "success", + code: "123456", + setup: func(repoMock *mocks.MockDeviceManagementRepository, wsmanMock *mocks.MockWSMAN, management *mocks.MockManagement) { + repoMock.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + wsmanMock.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(management, nil) + management.EXPECT().SendConsentCode(123456).Return(optin.Response{}, nil) + }, + }, + { + name: "device not found", + code: "123456", + setup: func(repoMock *mocks.MockDeviceManagementRepository, _ *mocks.MockWSMAN, _ *mocks.MockManagement) { + repoMock.EXPECT().GetByID(context.Background(), device.GUID, "").Return(nil, errDeviceNotFound) + }, + wantErr: ErrSystemNotFound, + }, + { + name: "wsman error", + code: "123456", + setup: func(repoMock *mocks.MockDeviceManagementRepository, wsmanMock *mocks.MockWSMAN, management *mocks.MockManagement) { + repoMock.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + wsmanMock.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(management, errAMT) + }, + wantErr: errAMT, + }, + { + name: "invalid consent code", + code: "12ab", + setup: func(_ *mocks.MockDeviceManagementRepository, _ *mocks.MockWSMAN, _ *mocks.MockManagement) { + }, + wantErr: ErrInvalidConsentCode, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + repoMock := mocks.NewMockDeviceManagementRepository(ctrl) + wsmanMock := mocks.NewMockWSMAN(ctrl) + wsmanMock.EXPECT().Worker().Return().AnyTimes() + + management := mocks.NewMockManagement(ctrl) + + uc := devices.New(repoMock, wsmanMock, mocks.NewMockRedirection(ctrl), logger.New("error"), mocks.MockCrypto{}) + repo := &WsmanComputerSystemRepo{usecase: uc, log: logger.New("error")} + + tt.setup(repoMock, wsmanMock, management) + + err := repo.SubmitKVMConsentCode(context.Background(), device.GUID, tt.code) + if !errors.Is(err, tt.wantErr) { + t.Fatalf("SubmitKVMConsentCode() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestCancelKVMConsentRepo(t *testing.T) { + t.Parallel() + + errDeviceNotFound := errors.New(ErrMsgDeviceNotFound) + errAMT := errors.New("amt refused") + device := &entity.Device{GUID: "system-1", TenantID: "tenant-1"} + + tests := []struct { + name string + setup func(*mocks.MockDeviceManagementRepository, *mocks.MockWSMAN, *mocks.MockManagement) + wantErr error + }{ + { + name: "success", + setup: func(repoMock *mocks.MockDeviceManagementRepository, wsmanMock *mocks.MockWSMAN, management *mocks.MockManagement) { + repoMock.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + wsmanMock.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(management, nil) + management.EXPECT().CancelUserConsentRequest().Return(optin.Response{}, nil) + }, + }, + { + name: "device not found", + setup: func(repoMock *mocks.MockDeviceManagementRepository, _ *mocks.MockWSMAN, _ *mocks.MockManagement) { + repoMock.EXPECT().GetByID(context.Background(), device.GUID, "").Return(nil, errDeviceNotFound) + }, + wantErr: ErrSystemNotFound, + }, + { + name: "wsman error", + setup: func(repoMock *mocks.MockDeviceManagementRepository, wsmanMock *mocks.MockWSMAN, management *mocks.MockManagement) { + repoMock.EXPECT().GetByID(context.Background(), device.GUID, "").Return(device, nil) + wsmanMock.EXPECT().SetupWsmanClient(gomock.Any(), gomock.Any(), false, true).Return(management, errAMT) + }, + wantErr: errAMT, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + repoMock := mocks.NewMockDeviceManagementRepository(ctrl) + wsmanMock := mocks.NewMockWSMAN(ctrl) + wsmanMock.EXPECT().Worker().Return().AnyTimes() + + management := mocks.NewMockManagement(ctrl) + + uc := devices.New(repoMock, wsmanMock, mocks.NewMockRedirection(ctrl), logger.New("error"), mocks.MockCrypto{}) + repo := &WsmanComputerSystemRepo{usecase: uc, log: logger.New("error")} + + tt.setup(repoMock, wsmanMock, management) + + err := repo.CancelKVMConsent(context.Background(), device.GUID) + if !errors.Is(err, tt.wantErr) { + t.Fatalf("CancelKVMConsent() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestIsSixDigitNumeric(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + code string + want bool + }{ + {name: "valid six digits", code: "123456", want: true}, + {name: "too short", code: "12345", want: false}, + {name: "too long", code: "1234567", want: false}, + {name: "contains non digit", code: "12a456", want: false}, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := isSixDigitNumeric(tt.code) + if got != tt.want { + t.Fatalf("isSixDigitNumeric(%q) = %v, want %v", tt.code, got, tt.want) + } + }) + } +} + func assertSerialConsoleOEM(t *testing.T, got *redfishv1.ComputerSystemHostSerialConsole, wantSOLStatus string) { t.Helper()