diff --git a/config/config.yml b/config/config.yml index 721b5e8b..e11a668c 100644 --- a/config/config.yml +++ b/config/config.yml @@ -23,7 +23,7 @@ ea: username: "" password: "" auth: - disabled: false + disabled: true adminUsername: standalone adminPassword: G@ppm0ym jwtKey: your_secret_jwt_key diff --git a/internal/controller/http/redfish/v1/constants.go b/internal/controller/http/redfish/v1/constants.go new file mode 100644 index 00000000..35c9a112 --- /dev/null +++ b/internal/controller/http/redfish/v1/constants.go @@ -0,0 +1,138 @@ +/********************************************************************* + * Copyright (c) Intel Corporation 2025 + * SPDX-License-Identifier: Apache-2.0 + **********************************************************************/ + +// Package v1 implements Redfish API v1 constants and shared values. +package v1 + +// Task state constants for Redfish Task responses +const ( + // TaskStateCompleted indicates the task has completed successfully + TaskStateCompleted = "Completed" + // TaskStateException indicates the task encountered an error + TaskStateException = "Exception" + // TaskStateRunning indicates the task is currently running + TaskStateRunning = "Running" + // TaskStatePending indicates the task is pending execution + TaskStatePending = "Pending" +) + +// Task status constants for Redfish Task responses +const ( + // TaskStatusOK indicates successful completion + TaskStatusOK = "OK" + // TaskStatusCritical indicates a critical error occurred + TaskStatusCritical = "Critical" + // TaskStatusWarning indicates a warning condition + TaskStatusWarning = "Warning" +) + +// Redfish Base Message Registry constants - used across multiple files +const ( + BaseSuccessMessageID = "Base.1.11.0.Success" + BaseErrorMessageID = "Base.1.11.0.GeneralError" + BaseMalformedJSONID = "Base.1.11.0.MalformedJSON" + BasePropertyMissingID = "Base.1.11.0.PropertyMissing" + BasePropertyValueNotInListID = "Base.1.11.0.PropertyValueNotInList" + BaseResourceNotFoundID = "Base.1.11.0.ResourceNotFound" + BaseOperationNotAllowedID = "Base.1.11.0.OperationNotAllowed" + BaseActionNotSupportedID = "Base.1.11.0.ActionNotSupported" + BaseNoValidSessionID = "Base.1.11.0.NoValidSession" + BaseInsufficientPrivilegeID = "Base.1.11.0.InsufficientPrivilege" + BaseNotAcceptableID = "Base.1.11.0.NotAcceptable" +) + +// HTTP Header constants for Redfish compliance +const ( + ContentTypeJSON = "application/json; charset=utf-8" + ContentTypeHeaderName = "Content-Type" + ODataVersionValue = "4.0" + ODataVersionHeader = "OData-Version" + CacheControlValue = "no-cache" + CacheControlHeader = "Cache-Control" + XFrameOptionsHeader = "X-Frame-Options" + XFrameOptionsValue = "DENY" + CSPHeader = "Content-Security-Policy" + CSPValue = "default-src 'self'" +) + +// Additional HTTP constants +const ( + MediaTypeWildcard = "*/*" + HeaderAccept = "Accept" +) + +// Redfish Service Information +const ( + RedfishVersion = "1.11.0" + ServiceRootID = "RootService" + ServiceRootName = "Redfish Root Service" + ServiceProduct = "Device Management Toolkit Console" + ServiceVendor = "Intel Corporation" + DefaultServiceUUID = "550e8400-e29b-41d4-a716-446655440000" +) + +// Common Redfish Schema Types - used across multiple files +const ( + SchemaServiceRoot = "#ServiceRoot.v1_11_0.ServiceRoot" + SchemaSessionService = "#SessionService.v1_0_0.SessionService" + SchemaSessionCollection = "#SessionCollection.SessionCollection" + SchemaComputerSystem = "#ComputerSystem.v1_0_0.ComputerSystem" + SchemaComputerSystemCollection = "#ComputerSystemCollection.ComputerSystemCollection" + SchemaSoftwareInventory = "#SoftwareInventory.v1_3_0.SoftwareInventory" + SchemaSoftwareInventoryCollection = "#SoftwareInventoryCollection.SoftwareInventoryCollection" + SchemaTask = "#Task.v1_6_0.Task" + SchemaIntelOEM = "#Intel.v1_0_0.Intel" +) + +// Common Redfish API Paths - used across multiple files +const ( + PathRedfishRoot = "/redfish/v1/" + PathSystems = PathRedfishRoot + "Systems" + PathSessionService = PathRedfishRoot + "SessionService" + PathSessionServiceSessions = PathSessionService + "/Sessions" + PathTaskService = PathRedfishRoot + "TaskService/Tasks" + PathMetadata = PathRedfishRoot + "$metadata" +) + +// Common Redfish API Path patterns - for building dynamic paths +const ( + // System-specific paths + PathSystemInstance = PathSystems + "/" // /redfish/v1/Systems/ + PathSystemActions = "/Actions/ComputerSystem.Reset" // Appended to system instance + PathSystemFirmware = "/FirmwareInventory" // Appended to system instance + PathSystemFirmwareItem = PathSystemFirmware + "/" // /FirmwareInventory/ +) + +// BuildSystemPath builds a path to a specific system: /redfish/v1/Systems/{systemID} +func BuildSystemPath(systemID string) string { + return PathSystemInstance + systemID +} + +// BuildSystemFirmwarePath builds a path to system firmware inventory: /redfish/v1/Systems/{systemID}/FirmwareInventory +func BuildSystemFirmwarePath(systemID string) string { + return PathSystemInstance + systemID + PathSystemFirmware +} + +// BuildSystemFirmwareItemPath builds a path to a specific firmware item: /redfish/v1/Systems/{systemID}/FirmwareInventory/{itemID} +func BuildSystemFirmwareItemPath(systemID, itemID string) string { + return PathSystemInstance + systemID + PathSystemFirmwareItem + itemID +} + +// OData Context paths for metadata +const ( + ODataContextTask = PathMetadata + "#Task.Task" + ODataContextSoftwareInventory = PathMetadata + "#SoftwareInventory.SoftwareInventory" + ODataContextSoftwareInventoryCollection = PathMetadata + "#SoftwareInventoryCollection.SoftwareInventoryCollection" +) + +// Cache control values +const ( + CacheMaxAge5Min = "max-age=300" // 5 minutes cache +) + +// Common value constants +const ( + UnknownValue = "Unknown" // Default value for unknown/missing information +) diff --git a/internal/controller/http/redfish/v1/errors.go b/internal/controller/http/redfish/v1/errors.go index 52501661..592d1769 100644 --- a/internal/controller/http/redfish/v1/errors.go +++ b/internal/controller/http/redfish/v1/errors.go @@ -17,21 +17,6 @@ import ( "github.com/device-management-toolkit/console/config" ) -// Redfish Base Message Registry v1.11.0 Message IDs -const ( - BaseSuccessMessageID = "Base.1.11.0.Success" - BaseErrorMessageID = "Base.1.11.0.GeneralError" - BaseMalformedJSONID = "Base.1.11.0.MalformedJSON" - BasePropertyMissingID = "Base.1.11.0.PropertyMissing" - BasePropertyValueNotInListID = "Base.1.11.0.PropertyValueNotInList" - BaseResourceNotFoundID = "Base.1.11.0.ResourceNotFound" - BaseOperationNotAllowedID = "Base.1.11.0.OperationNotAllowed" - BaseActionNotSupportedID = "Base.1.11.0.ActionNotSupported" - BaseNoValidSessionID = "Base.1.11.0.NoValidSession" - BaseInsufficientPrivilegeID = "Base.1.11.0.InsufficientPrivilege" - BaseNotAcceptableID = "Base.1.11.0.NotAcceptable" -) - // redfishError creates a standard Redfish error response structure func redfishError(messageID, message, severity, resolution string, messageArgs []string) map[string]any { extendedInfo := map[string]any{ @@ -57,11 +42,11 @@ func redfishError(messageID, message, severity, resolution string, messageArgs [ // SetRedfishHeaders sets standard Redfish-compliant HTTP headers func SetRedfishHeaders(c *gin.Context) { - c.Header("Content-Type", "application/json; charset=utf-8") - c.Header("OData-Version", "4.0") - c.Header("Cache-Control", "no-cache") - c.Header("X-Frame-Options", "DENY") - c.Header("Content-Security-Policy", "default-src 'self'") + c.Header(ContentTypeHeaderName, ContentTypeJSON) + c.Header(ODataVersionHeader, ODataVersionValue) + c.Header(CacheControlHeader, CacheControlValue) + c.Header(XFrameOptionsHeader, XFrameOptionsValue) + c.Header(CSPHeader, CSPValue) } // redfishErrorResponse sends a Redfish error response with proper headers @@ -75,7 +60,7 @@ func MalformedJSONError(c *gin.Context) { redfishErrorResponse(c, http.StatusBadRequest, BaseMalformedJSONID, "The request body submitted was malformed JSON and could not be parsed by the receiving service.", - "Critical", + TaskStatusCritical, "Ensure that the request body is valid JSON and resubmit the request.", nil) } @@ -85,7 +70,7 @@ func PropertyMissingError(c *gin.Context, propertyName string) { redfishErrorResponse(c, http.StatusBadRequest, BasePropertyMissingID, fmt.Sprintf("The property %s is a required property and must be included in the request.", propertyName), - "Warning", + TaskStatusWarning, "Ensure that the property is in the request body and has a valid value and resubmit the request.", []string{propertyName}) } @@ -95,7 +80,7 @@ func PropertyValueNotInListError(c *gin.Context, value, propertyName string) { redfishErrorResponse(c, http.StatusBadRequest, BasePropertyValueNotInListID, fmt.Sprintf("The value '%s' for the property %s is not in the list of acceptable values.", value, propertyName), - "Warning", + TaskStatusWarning, "Choose a value from the enumeration list that the implementation can support and resubmit the request if the operation failed.", []string{value, propertyName}) } @@ -105,7 +90,7 @@ func ResourceNotFoundError(c *gin.Context, resourceType, resourceID string) { redfishErrorResponse(c, http.StatusNotFound, BaseResourceNotFoundID, fmt.Sprintf("The requested resource of type %s named '%s' was not found.", resourceType, resourceID), - "Critical", + TaskStatusCritical, "Provide a valid resource identifier and resubmit the request.", []string{resourceType, resourceID}) } @@ -115,7 +100,7 @@ func OperationNotAllowedError(c *gin.Context) { redfishErrorResponse(c, http.StatusConflict, BaseOperationNotAllowedID, "The operation was not successful because the resource is in a state that does not allow this operation.", - "Critical", + TaskStatusCritical, "The operation was not successful because the resource is in a state that does not allow this operation.", nil) } @@ -128,7 +113,7 @@ func MethodNotAllowedError(c *gin.Context, action, allowedMethods string) { redfishErrorResponse(c, http.StatusMethodNotAllowed, BaseActionNotSupportedID, fmt.Sprintf("The action %s is not supported by the resource.", action), - "Critical", + TaskStatusCritical, "The action supplied cannot be resubmitted to the implementation. Perhaps the action was invalid, the wrong resource was the target or the implementation documentation may be of assistance.", []string{action}) } @@ -141,7 +126,7 @@ func HTTPMethodNotAllowedError(c *gin.Context, method, resourceType, allowedMeth redfishErrorResponse(c, http.StatusMethodNotAllowed, BaseOperationNotAllowedID, fmt.Sprintf("The HTTP method %s is not allowed on this resource.", method), - "Critical", + TaskStatusCritical, fmt.Sprintf("The operation is not allowed. The %s method is not supported for %s resources. Use one of the allowed methods: %s.", method, resourceType, allowedMethods), []string{method, resourceType}) } @@ -151,7 +136,7 @@ func NoValidSessionError(c *gin.Context) { redfishErrorResponse(c, http.StatusUnauthorized, BaseNoValidSessionID, "There is no valid session established with the implementation.", - "Critical", + TaskStatusCritical, "Establish a valid session before attempting any operations.", nil) } @@ -161,7 +146,7 @@ func InsufficientPrivilegeError(c *gin.Context) { redfishErrorResponse(c, http.StatusForbidden, BaseInsufficientPrivilegeID, "There are insufficient privileges for the account or credentials associated with the current session to perform the requested operation.", - "Critical", + TaskStatusCritical, "Either abandon the operation or change the associated access rights and resubmit the request if the operation failed for authorization reasons.", nil) } @@ -171,7 +156,7 @@ func NotAcceptableError(c *gin.Context, requestedType string) { redfishErrorResponse(c, http.StatusNotAcceptable, BaseNotAcceptableID, fmt.Sprintf("The requested media type '%s' is not acceptable. This service only supports 'application/json'.", requestedType), - "Warning", + TaskStatusWarning, "Resubmit the request with a supported media type in the Accept header.", []string{requestedType}) } @@ -221,7 +206,7 @@ func GeneralError(c *gin.Context) { redfishErrorResponse(c, http.StatusInternalServerError, BaseErrorMessageID, "A general error has occurred. See ExtendedInfo for more information.", - "Critical", + TaskStatusCritical, "None.", nil) } @@ -231,7 +216,7 @@ func BadGatewayError(c *gin.Context) { redfishErrorResponse(c, http.StatusBadGateway, BaseErrorMessageID, "The upstream service or managed device is unavailable or unreachable.", - "Critical", + TaskStatusCritical, "Verify network connectivity to the managed device and ensure the device is powered on and accessible.", nil) } @@ -242,7 +227,7 @@ func ServiceUnavailableError(c *gin.Context) { redfishErrorResponse(c, http.StatusBadGateway, BaseErrorMessageID, "The upstream service or managed device is unavailable or unreachable.", - "Critical", + TaskStatusCritical, "Verify network connectivity to the managed device and ensure the device is powered on and accessible.", nil) } @@ -253,7 +238,7 @@ func ServiceTemporarilyUnavailableError(c *gin.Context) { redfishErrorResponse(c, http.StatusServiceUnavailable, BaseErrorMessageID, "The service is temporarily unavailable due to overloading or maintenance. Please retry the request after some time.", - "Critical", + TaskStatusCritical, "Wait for the specified retry period and resubmit the request.", nil) } diff --git a/internal/controller/http/redfish/v1/firmware.go b/internal/controller/http/redfish/v1/firmware.go index 69b1e160..0c17d44a 100644 --- a/internal/controller/http/redfish/v1/firmware.go +++ b/internal/controller/http/redfish/v1/firmware.go @@ -18,7 +18,6 @@ import ( // Firmware-related constants const ( // Common string constants - unknownValue = "Unknown" biosID = "BIOS" sleepDurationMs = 100 systemManufacturer = "System Manufacturer" @@ -79,30 +78,30 @@ func NewFirmwareRoutes(systems *gin.RouterGroup, d devices.Feature, l logger.Int // Register method-not-allowed handlers for FirmwareInventory collection systems.POST(":id/FirmwareInventory", func(c *gin.Context) { - HTTPMethodNotAllowedError(c, "POST", "SoftwareInventoryCollection", "GET") + HTTPMethodNotAllowedError(c, http.MethodPost, "SoftwareInventoryCollection", http.MethodGet) }) systems.PUT(":id/FirmwareInventory", func(c *gin.Context) { - HTTPMethodNotAllowedError(c, "PUT", "SoftwareInventoryCollection", "GET") + HTTPMethodNotAllowedError(c, http.MethodPut, "SoftwareInventoryCollection", http.MethodGet) }) systems.PATCH(":id/FirmwareInventory", func(c *gin.Context) { - HTTPMethodNotAllowedError(c, "PATCH", "SoftwareInventoryCollection", "GET") + HTTPMethodNotAllowedError(c, http.MethodPatch, "SoftwareInventoryCollection", http.MethodGet) }) systems.DELETE(":id/FirmwareInventory", func(c *gin.Context) { - HTTPMethodNotAllowedError(c, "DELETE", "SoftwareInventoryCollection", "GET") + HTTPMethodNotAllowedError(c, http.MethodDelete, "SoftwareInventoryCollection", http.MethodGet) }) // Register method-not-allowed handlers for FirmwareInventory instances systems.POST(":id/FirmwareInventory/:firmwareId", func(c *gin.Context) { - HTTPMethodNotAllowedError(c, "POST", "SoftwareInventory", "GET") + HTTPMethodNotAllowedError(c, http.MethodPost, "SoftwareInventory", http.MethodGet) }) systems.PUT(":id/FirmwareInventory/:firmwareId", func(c *gin.Context) { - HTTPMethodNotAllowedError(c, "PUT", "SoftwareInventory", "GET") + HTTPMethodNotAllowedError(c, http.MethodPut, "SoftwareInventory", http.MethodGet) }) systems.PATCH(":id/FirmwareInventory/:firmwareId", func(c *gin.Context) { - HTTPMethodNotAllowedError(c, "PATCH", "SoftwareInventory", "GET") + HTTPMethodNotAllowedError(c, http.MethodPatch, "SoftwareInventory", http.MethodGet) }) systems.DELETE(":id/FirmwareInventory/:firmwareId", func(c *gin.Context) { - HTTPMethodNotAllowedError(c, "DELETE", "SoftwareInventory", "GET") + HTTPMethodNotAllowedError(c, http.MethodDelete, "SoftwareInventory", http.MethodGet) }) l.Info("Registered Redfish FirmwareInventory routes under %s", systems.BasePath()) @@ -123,8 +122,8 @@ func parseBIOSInfo(hwInfo interface{}) (version, versionString, manufacturer, re // extractBIOSDetails handles the complex parsing logic for BIOS information func extractBIOSDetails(hwInfo interface{}) (version, versionString, manufacturer, releaseDate string) { // Set defaults - version = unknownValue - versionString = unknownValue + version = UnknownValue + versionString = UnknownValue manufacturer = systemManufacturer releaseDate = time.Now().UTC().Format("2006-01-02") // Fallback to current date if not found @@ -142,8 +141,8 @@ func extractBIOSDetails(hwInfo interface{}) (version, versionString, manufacture // parseFromMap extracts BIOS info from map structure func parseFromMap(hwInfoMap map[string]interface{}) (version, versionString, manufacturer, releaseDate string) { - version = unknownValue - versionString = unknownValue + version = UnknownValue + versionString = UnknownValue manufacturer = systemManufacturer releaseDate = time.Now().UTC().Format("2006-01-02") @@ -169,8 +168,8 @@ func parseResponse(response interface{}, _, _, _, _ string) (version, versionStr // parseFromResponseMap extracts BIOS info from response map func parseFromResponseMap(responseMap map[string]interface{}) (version, versionString, manufacturer, releaseDate string) { - version = unknownValue - versionString = unknownValue + version = UnknownValue + versionString = UnknownValue manufacturer = systemManufacturer releaseDate = time.Now().UTC().Format("2006-01-02") @@ -193,7 +192,7 @@ func parseFromResponseMap(responseMap map[string]interface{}) (version, versionS if releaseDateObj, exists := responseMap["ReleaseDate"]; exists { releaseDate = extractReleaseDateFromMap(releaseDateObj, version, releaseDate) - if version != unknownValue { + if version != UnknownValue { versionString = fmt.Sprintf("%s (Released: %s)", version, releaseDate) } } @@ -229,8 +228,8 @@ func extractReleaseDateFromMap(releaseDateObj interface{}, _, defaultReleaseDate // parseFromStruct handles response as a struct using reflection func parseFromStruct(response interface{}) (version, versionString, manufacturer, releaseDate string) { - version = unknownValue - versionString = unknownValue + version = UnknownValue + versionString = UnknownValue manufacturer = systemManufacturer releaseDate = time.Now().UTC().Format("2006-01-02") @@ -249,8 +248,8 @@ func parseFromStruct(response interface{}) (version, versionString, manufacturer // extractFieldsFromStruct processes struct fields to extract BIOS information func extractFieldsFromStruct(responseValue reflect.Value) (version, versionString, manufacturer, releaseDate string) { - version = unknownValue - versionString = unknownValue + version = UnknownValue + versionString = UnknownValue manufacturer = systemManufacturer releaseDate = time.Now().UTC().Format("2006-01-02") @@ -268,7 +267,7 @@ func extractFieldsFromStruct(responseValue reflect.Value) (version, versionStrin manufacturer = extractManufacturerFromField(fieldValue) case "ReleaseDate": releaseDate = extractReleaseDateFromStruct(fieldValue, "", releaseDate) - if version != unknownValue { + if version != UnknownValue { versionString = fmt.Sprintf("%s (Released: %s)", version, releaseDate) } } @@ -285,7 +284,7 @@ func extractVersionFromField(fieldValue reflect.Value) (version, versionString s } } - return unknownValue, unknownValue + return UnknownValue, UnknownValue } // extractManufacturerFromField extracts manufacturer from a struct field @@ -372,7 +371,7 @@ func getFirmwareInventoryCollectionHandler(d devices.Feature, l logger.Interface // Set ETag header for HTTP caching c.Header("ETag", collection.ODataEtag) - c.Header("Cache-Control", "max-age=300") // Cache for 5 minutes + c.Header("Cache-Control", CacheMaxAge5Min) // Cache for 5 minutes c.JSON(http.StatusOK, collection) } @@ -399,9 +398,9 @@ func buildFirmwareCollection(d devices.Feature, l logger.Interface, c *gin.Conte // Build firmware inventory collection from AMT version data collection := FirmwareInventoryCollection{ - ODataContext: "/redfish/v1/$metadata#SoftwareInventoryCollection.SoftwareInventoryCollection", - ODataID: "/redfish/v1/Systems/" + systemID + "/FirmwareInventory", - ODataType: "#SoftwareInventoryCollection.SoftwareInventoryCollection", + ODataContext: ODataContextSoftwareInventoryCollection, + ODataID: BuildSystemFirmwarePath(systemID), + ODataType: SchemaSoftwareInventoryCollection, ID: "FirmwareInventory", Name: "Firmware Inventory Collection", Description: "Collection of firmware inventory for this system", @@ -443,25 +442,25 @@ func addFirmwareMembers(collection *FirmwareInventoryCollection, systemID string // Add AMT firmware components as inventory items if amt := getStringField(v, "AMT"); amt != "" { collection.Members = append(collection.Members, FirmwareInventoryMember{ - ODataID: "/redfish/v1/Systems/" + systemID + "/FirmwareInventory/AMT", + ODataID: BuildSystemFirmwareItemPath(systemID, "AMT"), }) } if flash := getStringField(v, "Flash"); flash != "" { collection.Members = append(collection.Members, FirmwareInventoryMember{ - ODataID: "/redfish/v1/Systems/" + systemID + "/FirmwareInventory/Flash", + ODataID: BuildSystemFirmwareItemPath(systemID, "Flash"), }) } if netstack := getStringField(v, "Netstack"); netstack != "" { collection.Members = append(collection.Members, FirmwareInventoryMember{ - ODataID: "/redfish/v1/Systems/" + systemID + "/FirmwareInventory/Netstack", + ODataID: BuildSystemFirmwareItemPath(systemID, "Netstack"), }) } if amtApps := getStringField(v, "AMTApps"); amtApps != "" { collection.Members = append(collection.Members, FirmwareInventoryMember{ - ODataID: "/redfish/v1/Systems/" + systemID + "/FirmwareInventory/AMTApps", + ODataID: BuildSystemFirmwareItemPath(systemID, "AMTApps"), }) } } @@ -479,7 +478,7 @@ func getStringField(v reflect.Value, fieldName string) string { // addBIOSMember adds BIOS firmware member to the collection func addBIOSMember(collection *FirmwareInventoryCollection, systemID string) { collection.Members = append(collection.Members, FirmwareInventoryMember{ - ODataID: "/redfish/v1/Systems/" + systemID + "/FirmwareInventory/BIOS", + ODataID: BuildSystemFirmwareItemPath(systemID, "BIOS"), }) } @@ -571,7 +570,7 @@ func sendFirmwareResponse(c *gin.Context, firmware *FirmwareInventory) { c.Header("ETag", firmware.ODataEtag) } - c.Header("Cache-Control", "max-age=300") // Cache for 5 minutes + c.Header("Cache-Control", CacheMaxAge5Min) // Cache for 5 minutes c.JSON(http.StatusOK, firmware) } @@ -589,22 +588,22 @@ func createAMTFirmware(systemID string, versionInfo interface{}) *FirmwareInvent } return &FirmwareInventory{ - ODataContext: "/redfish/v1/$metadata#SoftwareInventory.SoftwareInventory", + ODataContext: ODataContextSoftwareInventory, ODataID: "/redfish/v1/Systems/" + systemID + "/FirmwareInventory/AMT", - ODataType: "#SoftwareInventory.v1_3_0.SoftwareInventory", + ODataType: SchemaSoftwareInventory, ODataEtag: generateETag(fmt.Sprintf("AMT-%s-%s", systemID, amt)), ID: "AMT", Name: "Intel Active Management Technology", Description: "Intel AMT Firmware", Version: amt, VersionString: amt, - Manufacturer: "Intel Corporation", + Manufacturer: ServiceVendor, ReleaseDate: time.Now().UTC().Format("2006-01-02"), // Current date as placeholder SoftwareID: "AMT-" + systemID, Updateable: false, // AMT firmware updates typically require special procedures Status: Status{ State: "Enabled", - Health: "OK", + Health: TaskStatusOK, }, Oem: createAMTOemSection(versionInfo, systemID), } @@ -623,22 +622,22 @@ func createFlashFirmware(systemID string, versionInfo interface{}) *FirmwareInve } return &FirmwareInventory{ - ODataContext: "/redfish/v1/$metadata#SoftwareInventory.SoftwareInventory", + ODataContext: ODataContextSoftwareInventory, ODataID: "/redfish/v1/Systems/" + systemID + "/FirmwareInventory/Flash", - ODataType: "#SoftwareInventory.v1_3_0.SoftwareInventory", + ODataType: SchemaSoftwareInventory, ODataEtag: generateETag(fmt.Sprintf("Flash-%s-%s", systemID, flash)), ID: "Flash", Name: "AMT Flash Firmware", Description: "AMT Flash Memory Firmware", Version: flash, VersionString: flash, - Manufacturer: "Intel Corporation", + Manufacturer: ServiceVendor, ReleaseDate: time.Now().UTC().Format("2006-01-02"), SoftwareID: "Flash-" + systemID, Updateable: false, Status: Status{ State: "Enabled", - Health: "OK", + Health: TaskStatusOK, }, Oem: map[string]interface{}{ "Intel": map[string]interface{}{ @@ -664,22 +663,22 @@ func createNetstackFirmware(systemID string, versionInfo interface{}) *FirmwareI } return &FirmwareInventory{ - ODataContext: "/redfish/v1/$metadata#SoftwareInventory.SoftwareInventory", + ODataContext: ODataContextSoftwareInventory, ODataID: "/redfish/v1/Systems/" + systemID + "/FirmwareInventory/Netstack", - ODataType: "#SoftwareInventory.v1_3_0.SoftwareInventory", + ODataType: SchemaSoftwareInventory, ODataEtag: generateETag(fmt.Sprintf("Netstack-%s-%s", systemID, netstack)), ID: "Netstack", Name: "AMT Network Stack", Description: "AMT Network Stack Firmware", Version: netstack, VersionString: netstack, - Manufacturer: "Intel Corporation", + Manufacturer: ServiceVendor, ReleaseDate: time.Now().UTC().Format("2006-01-02"), SoftwareID: "Netstack-" + systemID, Updateable: false, Status: Status{ State: "Enabled", - Health: "OK", + Health: TaskStatusOK, }, Oem: map[string]interface{}{ "Intel": map[string]interface{}{ @@ -705,22 +704,22 @@ func createAMTAppsFirmware(systemID string, versionInfo interface{}) *FirmwareIn } return &FirmwareInventory{ - ODataContext: "/redfish/v1/$metadata#SoftwareInventory.SoftwareInventory", + ODataContext: ODataContextSoftwareInventory, ODataID: "/redfish/v1/Systems/" + systemID + "/FirmwareInventory/AMTApps", - ODataType: "#SoftwareInventory.v1_3_0.SoftwareInventory", + ODataType: SchemaSoftwareInventory, ODataEtag: generateETag(fmt.Sprintf("AMTApps-%s-%s", systemID, amtApps)), ID: "AMTApps", Name: "AMT Applications", Description: "AMT Applications Firmware", Version: amtApps, VersionString: amtApps, - Manufacturer: "Intel Corporation", + Manufacturer: ServiceVendor, ReleaseDate: time.Now().UTC().Format("2006-01-02"), SoftwareID: "AMTApps-" + systemID, Updateable: false, Status: Status{ State: "Enabled", - Health: "OK", + Health: TaskStatusOK, }, Oem: map[string]interface{}{ "Intel": map[string]interface{}{ @@ -750,9 +749,9 @@ func createBIOSFirmware(systemID string, hwInfo interface{}, l logger.Interface) l.Info("BIOS case: parsed version=%s, manufacturer=%s, releaseDate=%s", version, manufacturer, releaseDate) return &FirmwareInventory{ - ODataContext: "/redfish/v1/$metadata#SoftwareInventory.SoftwareInventory", + ODataContext: ODataContextSoftwareInventory, ODataID: "/redfish/v1/Systems/" + systemID + "/FirmwareInventory/BIOS", - ODataType: "#SoftwareInventory.v1_3_0.SoftwareInventory", + ODataType: SchemaSoftwareInventory, ODataEtag: generateETag(fmt.Sprintf("BIOS-%s-%s", systemID, version)), ID: "BIOS", Name: "System BIOS/UEFI", @@ -765,7 +764,7 @@ func createBIOSFirmware(systemID string, hwInfo interface{}, l logger.Interface) Updateable: false, // BIOS updates typically require special procedures Status: Status{ State: "Enabled", - Health: "OK", + Health: TaskStatusOK, }, Oem: map[string]interface{}{ "Intel": map[string]interface{}{ diff --git a/internal/controller/http/redfish/v1/root.go b/internal/controller/http/redfish/v1/root.go index c3d66aea..316ca0eb 100644 --- a/internal/controller/http/redfish/v1/root.go +++ b/internal/controller/http/redfish/v1/root.go @@ -13,6 +13,7 @@ import ( "strings" "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" "github.com/device-management-toolkit/console/config" "github.com/device-management-toolkit/console/pkg/logger" @@ -35,7 +36,7 @@ func generateServiceUUID() string { _, err := rand.Read(uuid) if err != nil { // Fallback to a static UUID if random generation fails - return "550e8400-e29b-41d4-a716-446655440000" + return DefaultServiceUUID } // Set version (4) and variant bits @@ -109,9 +110,9 @@ func serviceRootHandler(c *gin.Context) { SetRedfishHeaders(c) // Validate Accept header (406 Not Acceptable) - acceptHeader := c.GetHeader("Accept") - if acceptHeader != "" && acceptHeader != "*/*" && acceptHeader != "application/json" && - !strings.Contains(acceptHeader, "application/json") && !strings.Contains(acceptHeader, "*/*") { + acceptHeader := c.GetHeader(HeaderAccept) + if acceptHeader != "" && acceptHeader != MediaTypeWildcard && acceptHeader != binding.MIMEJSON && + !strings.Contains(acceptHeader, binding.MIMEJSON) && !strings.Contains(acceptHeader, MediaTypeWildcard) { NotAcceptableError(c, acceptHeader) return @@ -171,21 +172,21 @@ func serviceRootHandler(c *gin.Context) { } payload := map[string]any{ - "@odata.type": "#ServiceRoot.v1_11_0.ServiceRoot", - "@odata.id": "/redfish/v1/", - "Id": "RootService", - "Name": "Redfish Root Service", - "RedfishVersion": "1.11.0", + "@odata.type": SchemaServiceRoot, + "@odata.id": PathRedfishRoot, + "Id": ServiceRootID, + "Name": ServiceRootName, + "RedfishVersion": RedfishVersion, "UUID": serviceUUID, - "Systems": map[string]any{"@odata.id": "/redfish/v1/Systems"}, - "SessionService": map[string]any{"@odata.id": "/redfish/v1/SessionService"}, + "Systems": map[string]any{"@odata.id": PathSystems}, + "SessionService": map[string]any{"@odata.id": PathSessionService}, // Mandatory Links property with Sessions reference "Links": map[string]any{ - "Sessions": map[string]any{"@odata.id": "/redfish/v1/SessionService/Sessions"}, + "Sessions": map[string]any{"@odata.id": PathSessionServiceSessions}, }, // Optional but recommended properties (supported in v1_11_0) - "Product": "Device Management Toolkit Console", - "Vendor": "Intel Corporation", + "Product": ServiceProduct, + "Vendor": ServiceVendor, } c.JSON(http.StatusOK, payload) @@ -194,32 +195,32 @@ func serviceRootHandler(c *gin.Context) { // registerServiceRootMethodHandlers registers unsupported method handlers for ServiceRoot func registerServiceRootMethodHandlers(r *gin.RouterGroup) { r.POST("/", func(c *gin.Context) { - HTTPMethodNotAllowedError(c, "POST", "ServiceRoot", "GET") + HTTPMethodNotAllowedError(c, http.MethodPost, "ServiceRoot", http.MethodGet) }) r.PUT("/", func(c *gin.Context) { - HTTPMethodNotAllowedError(c, "PUT", "ServiceRoot", "GET") + HTTPMethodNotAllowedError(c, http.MethodPut, "ServiceRoot", http.MethodGet) }) r.PATCH("/", func(c *gin.Context) { - HTTPMethodNotAllowedError(c, "PATCH", "ServiceRoot", "GET") + HTTPMethodNotAllowedError(c, http.MethodPatch, "ServiceRoot", http.MethodGet) }) r.DELETE("/", func(c *gin.Context) { - HTTPMethodNotAllowedError(c, "DELETE", "ServiceRoot", "GET") + HTTPMethodNotAllowedError(c, http.MethodDelete, "ServiceRoot", http.MethodGet) }) } // registerSystemsMethodHandlers registers unsupported method handlers for Systems collection func registerSystemsMethodHandlers(r *gin.RouterGroup) { r.POST("/Systems", func(c *gin.Context) { - HTTPMethodNotAllowedError(c, "POST", "ComputerSystemCollection", "GET") + HTTPMethodNotAllowedError(c, http.MethodPost, "ComputerSystemCollection", http.MethodGet) }) r.PUT("/Systems", func(c *gin.Context) { - HTTPMethodNotAllowedError(c, "PUT", "ComputerSystemCollection", "GET") + HTTPMethodNotAllowedError(c, http.MethodPut, "ComputerSystemCollection", http.MethodGet) }) r.PATCH("/Systems", func(c *gin.Context) { - HTTPMethodNotAllowedError(c, "PATCH", "ComputerSystemCollection", "GET") + HTTPMethodNotAllowedError(c, http.MethodPatch, "ComputerSystemCollection", http.MethodGet) }) r.DELETE("/Systems", func(c *gin.Context) { - HTTPMethodNotAllowedError(c, "DELETE", "ComputerSystemCollection", "GET") + HTTPMethodNotAllowedError(c, http.MethodDelete, "ComputerSystemCollection", http.MethodGet) }) } @@ -230,16 +231,16 @@ func registerSessionServiceRoutes(r *gin.RouterGroup) { // Handle unsupported methods on SessionService with proper 405 responses r.POST("/SessionService", func(c *gin.Context) { - HTTPMethodNotAllowedError(c, "POST", "SessionService", "GET") + HTTPMethodNotAllowedError(c, http.MethodPost, "SessionService", http.MethodGet) }) r.PUT("/SessionService", func(c *gin.Context) { - HTTPMethodNotAllowedError(c, "PUT", "SessionService", "GET") + HTTPMethodNotAllowedError(c, http.MethodPut, "SessionService", http.MethodGet) }) r.PATCH("/SessionService", func(c *gin.Context) { - HTTPMethodNotAllowedError(c, "PATCH", "SessionService", "GET") + HTTPMethodNotAllowedError(c, http.MethodPatch, "SessionService", http.MethodGet) }) r.DELETE("/SessionService", func(c *gin.Context) { - HTTPMethodNotAllowedError(c, "DELETE", "SessionService", "GET") + HTTPMethodNotAllowedError(c, http.MethodDelete, "SessionService", http.MethodGet) }) // Sessions collection endpoint (read-only, empty list for now) @@ -263,13 +264,13 @@ func sessionServiceHandler(c *gin.Context) { SetRedfishHeaders(c) payload := map[string]any{ - "@odata.type": "#SessionService.v1_0_0.SessionService", - "@odata.id": "/redfish/v1/SessionService", + "@odata.type": SchemaSessionService, + "@odata.id": PathSessionService, "Id": "SessionService", "Name": "Redfish Session Service", "ServiceEnabled": true, "SessionTimeout": 30, - "Sessions": map[string]any{"@odata.id": "/redfish/v1/SessionService/Sessions"}, + "Sessions": map[string]any{"@odata.id": PathSessionServiceSessions}, } c.JSON(http.StatusOK, payload) @@ -281,8 +282,8 @@ func sessionsCollectionHandler(c *gin.Context) { SetRedfishHeaders(c) payload := map[string]any{ - "@odata.type": "#SessionCollection.SessionCollection", - "@odata.id": "/redfish/v1/SessionService/Sessions", + "@odata.type": SchemaSessionCollection, + "@odata.id": PathSessionServiceSessions, "Name": "Session Collection", "Members@odata.count": 0, "Members": []any{}, diff --git a/internal/controller/http/redfish/v1/system.go b/internal/controller/http/redfish/v1/system.go index b5c37b55..e326889d 100644 --- a/internal/controller/http/redfish/v1/system.go +++ b/internal/controller/http/redfish/v1/system.go @@ -7,10 +7,18 @@ package v1 import ( + "crypto/rand" + "fmt" + "math/big" "net/http" + "strings" + "time" "github.com/gin-gonic/gin" + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/power" + + "github.com/device-management-toolkit/console/config" "github.com/device-management-toolkit/console/internal/usecase/devices" "github.com/device-management-toolkit/console/pkg/logger" ) @@ -35,21 +43,38 @@ const ( cimPowerStandby = 4 cimPowerSoftOff = 7 cimPowerHardOff = 8 + // Task ID generation constants + taskIDRandomRange = 900000 // Range for random task ID generation + taskIDTimestampMod = 1000000 // Modulo for timestamp-based fallback ID + taskIDBaseOffset = 100000 // Base offset for all task IDs ) -// NewSystemsRoutes registers minimal Redfish ComputerSystem routes. +// NewSystemsRoutes registers Redfish v1 ComputerSystem routes. // It exposes: -// - GET /redfish/v1/Systems -// - GET /redfish/v1/Systems/:id -// - POST /redfish/v1/Systems/:id/Actions/ComputerSystem.Reset -// - GET /redfish/v1/Systems/:id/FirmwareInventory -// - GET /redfish/v1/Systems/:id/FirmwareInventory/:firmwareId +// - GET /redfish/v1/Systems (collection) +// - GET /redfish/v1/Systems/:id (individual system) +// - POST /redfish/v1/Systems/:id/Actions/ComputerSystem.Reset (reset action) +// - GET/PUT/PATCH/DELETE /redfish/v1/Systems/:id/Actions/ComputerSystem.Reset (405 Method Not Allowed) +// - GET /redfish/v1/Systems/:id/FirmwareInventory (individual system) +// - GET /redfish/v1/Systems/:id/FirmwareInventory/:firmwareId (individual system/firmware type) // The :id is expected to be the device GUID and will be mapped directly to SendPowerAction. -func NewSystemsRoutes(r *gin.RouterGroup, d devices.Feature, l logger.Interface) { +func NewSystemsRoutes(r *gin.RouterGroup, d devices.Feature, cfg *config.Config, l logger.Interface) { systems := r.Group("/Systems") + + // Apply Redfish-compliant authentication if auth is enabled + if !cfg.Disabled { + systems.Use(RedfishJWTAuthMiddleware(cfg)) + } + systems.GET("", getSystemsCollectionHandler(d, l)) systems.GET(":id", getSystemInstanceHandler(d, l)) + + // ComputerSystem.Reset Action - only POST is allowed systems.POST(":id/Actions/ComputerSystem.Reset", postSystemResetHandler(d, l)) + systems.GET(":id/Actions/ComputerSystem.Reset", methodNotAllowedHandler()) + systems.PUT(":id/Actions/ComputerSystem.Reset", methodNotAllowedHandler()) + systems.PATCH(":id/Actions/ComputerSystem.Reset", methodNotAllowedHandler()) + systems.DELETE(":id/Actions/ComputerSystem.Reset", methodNotAllowedHandler()) // Add firmware inventory routes NewFirmwareRoutes(systems, d, l) @@ -61,8 +86,16 @@ func getSystemsCollectionHandler(d devices.Feature, l logger.Interface) gin.Hand return func(c *gin.Context) { items, err := d.Get(c.Request.Context(), maxSystemsList, 0, "") if err != nil { - l.Error(err, "http - redfish - Systems collection") - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + l.Error(err, "http - redfish v1 - Systems collection") + + switch { + case isServiceTemporarilyUnavailable(err): + ServiceTemporarilyUnavailableError(c) + case isUpstreamCommunicationError(err): + ServiceUnavailableError(c) + default: + GeneralError(c) + } return } @@ -75,13 +108,13 @@ func getSystemsCollectionHandler(d devices.Feature, l logger.Interface) gin.Hand } members = append(members, map[string]any{ - "@odata.id": "/redfish/v1/Systems/" + it.GUID, + "@odata.id": PathSystemInstance + it.GUID, }) } payload := map[string]any{ - "@odata.type": "#ComputerSystemCollection.ComputerSystemCollection", - "@odata.id": "/redfish/v1/Systems", + "@odata.type": SchemaComputerSystemCollection, + "@odata.id": PathSystems, "Name": "Computer System Collection", "Members@odata.count": len(members), "Members": members, @@ -96,7 +129,7 @@ func getSystemInstanceHandler(d devices.Feature, l logger.Interface) gin.Handler powerState := powerStateUnknown if ps, err := d.GetPowerState(c.Request.Context(), id); err != nil { - l.Warn("redfish - Systems instance: failed to get power state for %s: %v", id, err) + l.Warn("redfish v1 - Systems instance: failed to get power state for %s: %v", id, err) } else { switch ps.PowerState { // CIM PowerState values case actionPowerUp: // 2 (On) @@ -111,14 +144,14 @@ func getSystemInstanceHandler(d devices.Feature, l logger.Interface) gin.Handler } payload := map[string]any{ - "@odata.type": "#ComputerSystem.v1_0_0.ComputerSystem", - "@odata.id": "/redfish/v1/Systems/" + id, + "@odata.type": SchemaComputerSystem, + "@odata.id": PathSystemInstance + id, "Id": id, "Name": "Computer System " + id, "PowerState": powerState, "Actions": map[string]any{ "#ComputerSystem.Reset": map[string]any{ - "target": "/redfish/v1/Systems/" + id + "/Actions/ComputerSystem.Reset", + "target": PathSystemInstance + id + PathSystemActions, "ResetType@Redfish.AllowableValues": []string{resetTypeOn, resetTypeForceOff, resetTypeForceRestart, resetTypePowerCycle}, }, }, @@ -127,44 +160,265 @@ func getSystemInstanceHandler(d devices.Feature, l logger.Interface) gin.Handler } } +// methodNotAllowedHandler returns a handler that responds with 405 Method Not Allowed for ComputerSystem.Reset action +func methodNotAllowedHandler() gin.HandlerFunc { + return func(c *gin.Context) { + MethodNotAllowedError(c, "ComputerSystem.Reset", http.MethodPost) + } +} + +// parseResetRequest parses and validates the reset request body +func parseResetRequest(c *gin.Context) (int, bool) { + var body struct { + ResetType string `json:"ResetType"` + } + + if err := c.ShouldBindJSON(&body); err != nil { + MalformedJSONError(c) + + return 0, false + } + + // Check if ResetType is provided (required property) + if body.ResetType == "" { + PropertyMissingError(c, "ResetType") + + return 0, false + } + + // Map reset type to action + var action int + + switch body.ResetType { + case resetTypeOn: + action = actionPowerUp + case resetTypeForceOff: + action = actionPowerDown + case resetTypeForceRestart: + action = actionReset + case resetTypePowerCycle: + action = actionPowerCycle + default: + PropertyValueNotInListError(c, body.ResetType, "ResetType") + + return 0, false + } + + return action, true +} + +// checkPowerStateConflict checks if the requested action conflicts with current power state. +// Returns true if a conflict is detected, false otherwise. +func checkPowerStateConflict(c *gin.Context, d devices.Feature, l logger.Interface, id string, action int) bool { + currentPowerState, err := d.GetPowerState(c.Request.Context(), id) + if err != nil { + // Log the warning and continue with the action anyway + l.Warn("redfish v1 - Systems instance: failed to get power state for %s: %v", id, err) + + return false + } + + // Map CIM power states to determine if action would result in no change + isCurrentlyOn := (currentPowerState.PowerState == cimPowerOn) + isCurrentlyOff := (currentPowerState.PowerState == cimPowerSoftOff || currentPowerState.PowerState == cimPowerHardOff) + + switch action { + case actionPowerUp: // Power On + return isCurrentlyOn + case actionPowerDown: // Power Off + return isCurrentlyOff + } + + return false +} + +// handlePowerStateConflict sends an appropriate error response for power state conflicts +func handlePowerStateConflict(c *gin.Context) { + OperationNotAllowedError(c) +} + +// handleResetError handles different types of errors from power action +func handleResetError(c *gin.Context, l logger.Interface, err error, id string) { + l.Error(err, "http - redfish v1 - ComputerSystem.Reset") + + // Check error type and respond appropriately + switch { + case strings.Contains(strings.ToLower(err.Error()), "not found") || + strings.Contains(err.Error(), "DevicesUseCase"): + ResourceNotFoundError(c, "ComputerSystem", id) + case isServiceTemporarilyUnavailable(err): + // 503 Service Unavailable for temporary service overload/maintenance + ServiceTemporarilyUnavailableError(c) + case isUpstreamCommunicationError(err): + // 502 Bad Gateway for upstream device communication failures + ServiceUnavailableError(c) + default: + // 500 Internal Server Error for other failures + GeneralError(c) + } +} + +// generateTaskResponse generates a secure task ID and returns the task response +func generateTaskResponse(c *gin.Context, res power.PowerActionResponse) { + // Generate a task ID for this reset operation using crypto/rand + taskID := generateSecureTaskID() + + // Determine task state based on the result + taskState, taskStatus, messageID, message := determineTaskResult(res) + + // Return Redfish-compliant Task response + taskResponse := map[string]any{ + "@odata.context": ODataContextTask, + "@odata.id": PathTaskService + "/" + taskID, + "@odata.type": SchemaTask, + "Id": taskID, + "Name": "System Reset Task", + "TaskState": taskState, + "TaskStatus": taskStatus, + "StartTime": time.Now().UTC().Format(time.RFC3339), + "EndTime": time.Now().UTC().Format(time.RFC3339), + "Messages": []map[string]any{ + { + "MessageId": messageID, + "Message": message, + "Severity": taskStatus, + }, + }, + } + + // Set Redfish-compliant headers + SetRedfishHeaders(c) + c.JSON(http.StatusOK, taskResponse) +} + +// generateSecureTaskID generates a secure task ID using crypto/rand +func generateSecureTaskID() string { + randomNum, err := rand.Int(rand.Reader, big.NewInt(taskIDRandomRange)) + if err != nil { + // Fallback to timestamp-based ID if random generation fails + return fmt.Sprintf("%d", time.Now().UnixNano()%taskIDTimestampMod+taskIDBaseOffset) + } + + return fmt.Sprintf("%d", randomNum.Int64()+taskIDBaseOffset) +} + +// determineTaskResult determines task state and status based on power action result +func determineTaskResult(res power.PowerActionResponse) (taskState, taskStatus, messageID, message string) { + taskState = TaskStateCompleted + taskStatus = TaskStatusOK + messageID = BaseSuccessMessageID + message = "The request completed successfully." + + // Check if the operation was successful based on ReturnValue + if int(res.ReturnValue) != 0 { + taskState = TaskStateException + taskStatus = TaskStatusCritical + messageID = BaseErrorMessageID + message = "A general error has occurred." + } + + return taskState, taskStatus, messageID, message +} + func postSystemResetHandler(d devices.Feature, l logger.Interface) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") - var body struct { - ResetType string `json:"ResetType"` - } - if err := c.ShouldBindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - + // Parse and validate request body + action, ok := parseResetRequest(c) + if !ok { return } - var action int - - switch body.ResetType { - case resetTypeOn: - action = actionPowerUp - case resetTypeForceOff: - action = actionPowerDown - case resetTypeForceRestart: - action = actionReset - case resetTypePowerCycle: - action = actionPowerCycle - default: - c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported ResetType"}) + // Check for power state conflicts + if conflictDetected := checkPowerStateConflict(c, d, l, id, action); conflictDetected { + handlePowerStateConflict(c) return } + // Execute the power action res, err := d.SendPowerAction(c.Request.Context(), id, action) if err != nil { - l.Error(err, "http - redfish - ComputerSystem.Reset") - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + handleResetError(c, l, err, id) return } - c.JSON(http.StatusOK, res) + // Generate and return task response + generateTaskResponse(c, res) } } + +// isUpstreamCommunicationError determines if an error is due to upstream device communication failure +func isUpstreamCommunicationError(err error) bool { + if err == nil { + return false + } + + errMsg := strings.ToLower(err.Error()) + + // Check for common upstream communication error patterns + upstreamErrors := []string{ + "connection refused", + "connection timeout", + "timeout", + "network unreachable", + "no route to host", + "connection reset", + "wsman", // WSMAN-specific errors + "amt", // AMT-specific errors + "unauthorized", // AMT authentication failures + "certificate", // TLS certificate issues + "ssl", // SSL/TLS errors + "tls", // TLS errors + "dial tcp", // TCP connection errors + "i/o timeout", // I/O timeout errors + "connection aborted", + "host unreachable", + } + + for _, pattern := range upstreamErrors { + if strings.Contains(errMsg, pattern) { + return true + } + } + + return false +} + +// isServiceTemporarilyUnavailable determines if the service should return 503 due to overload or maintenance +func isServiceTemporarilyUnavailable(err error) bool { + if err == nil { + return false + } + + errMsg := strings.ToLower(err.Error()) + + // Check for temporary service unavailability patterns + serviceUnavailableErrors := []string{ + "too many connections", + "connection pool exhausted", + "database pool full", + "service overloaded", + "maintenance mode", + "rate limit exceeded", + "too many requests", + "resource exhausted", + "service unavailable", + "temporarily unavailable", + "max connections reached", + "server overloaded", + "capacity exceeded", + "throttled", + "circuit breaker", + } + + for _, pattern := range serviceUnavailableErrors { + if strings.Contains(errMsg, pattern) { + return true + } + } + + return false +} diff --git a/internal/controller/http/redfish/v1/system_test.go b/internal/controller/http/redfish/v1/system_test.go index 131b6202..fdf07b59 100644 --- a/internal/controller/http/redfish/v1/system_test.go +++ b/internal/controller/http/redfish/v1/system_test.go @@ -22,6 +22,7 @@ import ( "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/cim/power" + "github.com/device-management-toolkit/console/config" dto "github.com/device-management-toolkit/console/internal/entity/dto/v1" dtov2 "github.com/device-management-toolkit/console/internal/entity/dto/v2" "github.com/device-management-toolkit/console/internal/mocks" @@ -30,9 +31,9 @@ import ( const ( testSystemGUID = "test-system-guid-123" testInvalidGUID = "invalid-system-guid" - systemsBasePath = "/redfish/v1/Systems" - systemsInstanceURL = systemsBasePath + "/" + testSystemGUID - resetActionURL = systemsInstanceURL + "/Actions/ComputerSystem.Reset" + systemsBasePath = PathSystems + systemsInstanceURL = PathSystemInstance + testSystemGUID + resetActionURL = systemsInstanceURL + PathSystemActions ) func TestNewSystemsRoutes(t *testing.T) { @@ -46,6 +47,7 @@ func TestNewSystemsRoutes(t *testing.T) { mockFeature := mocks.NewMockDeviceManagementFeature(ctrl) mockLogger := mocks.NewMockLogger(ctrl) + mockConfig := &config.Config{} // Expect logging calls for route registration mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).Times(2) // Systems + Firmware routes @@ -55,7 +57,7 @@ func TestNewSystemsRoutes(t *testing.T) { // Test route registration redfishGroup := router.Group("/redfish/v1") - NewSystemsRoutes(redfishGroup, mockFeature, mockLogger) + NewSystemsRoutes(redfishGroup, mockFeature, mockConfig, mockLogger) // Verify routes exist by testing them routes := router.Routes() @@ -91,7 +93,7 @@ func TestNewSystemsRoutes(t *testing.T) { // This will panic due to firmware routes accessing nil logger // Testing that routes can be set up, but will fail on actual usage require.Panics(t, func() { - NewSystemsRoutes(redfishGroup, nil, nil) + NewSystemsRoutes(redfishGroup, nil, nil, nil) }) }) } @@ -218,7 +220,7 @@ func TestGetSystemsCollectionHandler(t *testing.T) { expectedStatus: http.StatusInternalServerError, validateResponse: func(t *testing.T, body string) { t.Helper() - assert.Contains(t, body, "backend connection failed") + assert.Contains(t, body, "GeneralError") }, }, } @@ -456,6 +458,11 @@ func TestPostSystemResetHandler(t *testing.T) { systemID: testSystemGUID, requestBody: `{"ResetType": "On"}`, setupMocks: func(mockFeature *mocks.MockDeviceManagementFeature, mockLogger *mocks.MockLogger) { + // Mock GetPowerState call (for conflict checking) + mockFeature.EXPECT(). + GetPowerState(gomock.Any(), testSystemGUID). + Return(dto.PowerState{PowerState: 8}, nil) // Power off state + expectedResult := power.PowerActionResponse{ ReturnValue: power.ReturnValue(0), } @@ -468,7 +475,8 @@ func TestPostSystemResetHandler(t *testing.T) { expectedStatus: http.StatusOK, validateResponse: func(t *testing.T, body string) { t.Helper() - assert.Contains(t, body, "ReturnValue") + assert.Contains(t, body, "TaskState") + assert.Contains(t, body, "Completed") }, }, { @@ -476,6 +484,11 @@ func TestPostSystemResetHandler(t *testing.T) { systemID: testSystemGUID, requestBody: `{"ResetType": "ForceOff"}`, setupMocks: func(mockFeature *mocks.MockDeviceManagementFeature, _ *mocks.MockLogger) { + // Mock GetPowerState call (for conflict checking) + mockFeature.EXPECT(). + GetPowerState(gomock.Any(), testSystemGUID). + Return(dto.PowerState{PowerState: 2}, nil) // Power on state + expectedResult := power.PowerActionResponse{ ReturnValue: power.ReturnValue(0), } @@ -486,7 +499,8 @@ func TestPostSystemResetHandler(t *testing.T) { expectedStatus: http.StatusOK, validateResponse: func(t *testing.T, body string) { t.Helper() - assert.Contains(t, body, "ReturnValue") + assert.Contains(t, body, "TaskState") + assert.Contains(t, body, "Completed") }, }, { @@ -494,6 +508,11 @@ func TestPostSystemResetHandler(t *testing.T) { systemID: testSystemGUID, requestBody: `{"ResetType": "ForceRestart"}`, setupMocks: func(mockFeature *mocks.MockDeviceManagementFeature, _ *mocks.MockLogger) { + // Mock GetPowerState call (for conflict checking) + mockFeature.EXPECT(). + GetPowerState(gomock.Any(), testSystemGUID). + Return(dto.PowerState{PowerState: 2}, nil) // Power on state + expectedResult := power.PowerActionResponse{ ReturnValue: power.ReturnValue(0), } @@ -504,7 +523,8 @@ func TestPostSystemResetHandler(t *testing.T) { expectedStatus: http.StatusOK, validateResponse: func(t *testing.T, body string) { t.Helper() - assert.Contains(t, body, "ReturnValue") + assert.Contains(t, body, "TaskState") + assert.Contains(t, body, "Completed") }, }, { @@ -512,6 +532,11 @@ func TestPostSystemResetHandler(t *testing.T) { systemID: testSystemGUID, requestBody: `{"ResetType": "PowerCycle"}`, setupMocks: func(mockFeature *mocks.MockDeviceManagementFeature, _ *mocks.MockLogger) { + // Mock GetPowerState call (for conflict checking) + mockFeature.EXPECT(). + GetPowerState(gomock.Any(), testSystemGUID). + Return(dto.PowerState{PowerState: 2}, nil) // Power on state + expectedResult := power.PowerActionResponse{ ReturnValue: power.ReturnValue(0), } @@ -522,7 +547,8 @@ func TestPostSystemResetHandler(t *testing.T) { expectedStatus: http.StatusOK, validateResponse: func(t *testing.T, body string) { t.Helper() - assert.Contains(t, body, "ReturnValue") + assert.Contains(t, body, "TaskState") + assert.Contains(t, body, "Completed") }, }, { @@ -535,7 +561,7 @@ func TestPostSystemResetHandler(t *testing.T) { expectedStatus: http.StatusBadRequest, validateResponse: func(t *testing.T, body string) { t.Helper() - assert.Contains(t, body, "unsupported ResetType") + assert.Contains(t, body, "PropertyValueNotInList") }, }, { @@ -561,7 +587,7 @@ func TestPostSystemResetHandler(t *testing.T) { expectedStatus: http.StatusBadRequest, validateResponse: func(t *testing.T, body string) { t.Helper() - assert.Contains(t, body, "unsupported ResetType") + assert.Contains(t, body, "PropertyMissing") }, }, { @@ -569,16 +595,21 @@ func TestPostSystemResetHandler(t *testing.T) { systemID: testSystemGUID, requestBody: `{"ResetType": "On"}`, setupMocks: func(mockFeature *mocks.MockDeviceManagementFeature, mockLogger *mocks.MockLogger) { + // Mock GetPowerState call (for conflict checking) + mockFeature.EXPECT(). + GetPowerState(gomock.Any(), testSystemGUID). + Return(dto.PowerState{PowerState: 8}, nil) // Power off state + mockFeature.EXPECT(). SendPowerAction(gomock.Any(), testSystemGUID, actionPowerUp). Return(power.PowerActionResponse{}, fmt.Errorf("system not found")) mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).Times(1) }, - expectedStatus: http.StatusInternalServerError, + expectedStatus: http.StatusNotFound, validateResponse: func(t *testing.T, body string) { t.Helper() - assert.Contains(t, body, "system not found") + assert.Contains(t, body, "ResourceNotFound") }, }, } @@ -748,6 +779,23 @@ func TestResetTypeMapping(t *testing.T) { mockFeature := mocks.NewMockDeviceManagementFeature(ctrl) mockLogger := mocks.NewMockLogger(ctrl) + // Mock GetPowerState call (for conflict checking) + // Use power state that won't conflict with the specific action being tested + var powerState int + + switch tt.expectedCIMAction { + case actionPowerUp: // 2 - if testing PowerUp, mock as powered off + powerState = 8 // Power off + case actionPowerDown: // 8 - if testing PowerDown, mock as powered on + powerState = 2 // Power on + default: // For other actions, use power on state + powerState = 2 // Power on + } + + mockFeature.EXPECT(). + GetPowerState(gomock.Any(), testSystemGUID). + Return(dto.PowerState{PowerState: powerState}, nil) + expectedResult := power.PowerActionResponse{ ReturnValue: power.ReturnValue(0), } @@ -813,7 +861,10 @@ func TestSystemsIntegrationWithFirmware(t *testing.T) { // Setup complete systems routes including firmware redfishGroup := router.Group("/redfish/v1") - NewSystemsRoutes(redfishGroup, mockFeature, mockLogger) + mockConfig := &config.Config{ + Auth: config.Auth{Disabled: true}, // Disable auth for testing + } + NewSystemsRoutes(redfishGroup, mockFeature, mockConfig, mockLogger) // Test that firmware inventory endpoint is accessible via systems routes w := httptest.NewRecorder() @@ -823,6 +874,7 @@ func TestSystemsIntegrationWithFirmware(t *testing.T) { "/redfish/v1/Systems/"+testSystemGUID+"/FirmwareInventory", http.NoBody, ) + req.Header.Set("Accept", "application/json") router.ServeHTTP(w, req) @@ -838,52 +890,6 @@ func TestSystemsIntegrationWithFirmware(t *testing.T) { }) } -func TestConstants(t *testing.T) { - t.Parallel() - - t.Run("power state constants", func(t *testing.T) { - t.Parallel() - - assert.Equal(t, "Unknown", powerStateUnknown) - assert.Equal(t, "On", powerStateOn) - assert.Equal(t, "Off", powerStateOff) - }) - - t.Run("reset type constants", func(t *testing.T) { - t.Parallel() - - assert.Equal(t, "On", resetTypeOn) - assert.Equal(t, "ForceOff", resetTypeForceOff) - assert.Equal(t, "ForceRestart", resetTypeForceRestart) - assert.Equal(t, "PowerCycle", resetTypePowerCycle) - }) - - t.Run("action constants", func(t *testing.T) { - t.Parallel() - - assert.Equal(t, 2, actionPowerUp) - assert.Equal(t, 5, actionPowerCycle) - assert.Equal(t, 8, actionPowerDown) - assert.Equal(t, 10, actionReset) - }) - - t.Run("CIM power state constants", func(t *testing.T) { - t.Parallel() - - assert.Equal(t, 2, cimPowerOn) - assert.Equal(t, 3, cimPowerSleep) - assert.Equal(t, 4, cimPowerStandby) - assert.Equal(t, 7, cimPowerSoftOff) - assert.Equal(t, 8, cimPowerHardOff) - }) - - t.Run("limits constants", func(t *testing.T) { - t.Parallel() - - assert.Equal(t, 100, maxSystemsList) - }) -} - func TestSystemResponseStructure(t *testing.T) { t.Parallel() diff --git a/internal/controller/http/router.go b/internal/controller/http/router.go index db47766b..4ffecc6c 100644 --- a/internal/controller/http/router.go +++ b/internal/controller/http/router.go @@ -118,7 +118,7 @@ func NewRouter(handler *gin.Engine, l logger.Interface, t usecase.Usecases, cfg redfish := handler.Group("/redfish/v1") { redfishv1.NewServiceRootRoutes(redfish, cfg, l) - redfishv1.NewSystemsRoutes(redfish, t.Devices, l) + redfishv1.NewSystemsRoutes(redfish, t.Devices, cfg, l) } // Catch-all route to serve index.html for any route not matched above to be handled by Angular diff --git a/main b/main new file mode 100755 index 00000000..faa7ef48 Binary files /dev/null and b/main differ