Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions internal/controller/http/redfish/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package redfish

import (
"net/http"

"github.com/gin-gonic/gin"

"github.com/device-management-toolkit/console/pkg/logger"
)

// NewRoutes registers the Redfish Service Root handler.
// It responds to GET on the group base path (e.g., /redfish/v1).
func NewRoutes(r *gin.RouterGroup, l logger.Interface) {
// Service Root per DMTF Redfish spec. Keep minimal and compliant.
handler := func(c *gin.Context) {
// Minimal ServiceRoot payload. Additional links can be added later.
payload := map[string]any{
"@odata.type": "#ServiceRoot.v1_0_0.ServiceRoot",
"@odata.id": "/redfish/v1/",
"Id": "RootService",
"Name": "Redfish Service Root",
"RedfishVersion": "1.0.0",
"SessionService": map[string]any{
"@odata.id": "/redfish/v1/SessionService",
},
"Systems": map[string]any{
"@odata.id": "/redfish/v1/Systems",
},
"Links": map[string]any{
"Sessions": map[string]any{
"@odata.id": "/redfish/v1/SessionService/Sessions",
},
},
}

c.JSON(http.StatusOK, payload)
}

// Register for both the group root and explicit trailing slash.
r.GET("", handler)
r.GET("/", handler)

// $metadata endpoint (minimal OData metadata document)
r.GET("/$metadata", func(c *gin.Context) {
c.Header("Content-Type", "application/xml; charset=utf-8")
c.String(http.StatusOK, `<?xml version="1.0" encoding="UTF-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
<edmx:DataServices>
<Schema Namespace="Redfish" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityType Name="ServiceRoot">
<Key><PropertyRef Name="Id"/></Key>
<Property Name="Id" Type="Edm.String" Nullable="false"/>
<Property Name="Name" Type="Edm.String"/>
<Property Name="RedfishVersion" Type="Edm.String"/>
<NavigationProperty Name="SessionService" Type="Redfish.SessionService"/>
<NavigationProperty Name="Systems" Type="Collection(Redfish.ComputerSystem)"/>
</EntityType>
<EntityType Name="SessionService">
<Key><PropertyRef Name="Id"/></Key>
<Property Name="Id" Type="Edm.String" Nullable="false"/>
<Property Name="Name" Type="Edm.String"/>
<Property Name="ServiceEnabled" Type="Edm.Boolean"/>
<Property Name="SessionTimeout" Type="Edm.Int64"/>
<NavigationProperty Name="Sessions" Type="Collection(Redfish.Session)"/>
</EntityType>
<EntityType Name="Session">
<Key><PropertyRef Name="Id"/></Key>
<Property Name="Id" Type="Edm.String" Nullable="false"/>
<Property Name="Name" Type="Edm.String"/>
<Property Name="UserName" Type="Edm.String"/>
</EntityType>
<EntityType Name="ComputerSystem">
<Key><PropertyRef Name="Id"/></Key>
<Property Name="Id" Type="Edm.String" Nullable="false"/>
<Property Name="Name" Type="Edm.String"/>
<Property Name="PowerState" Type="Edm.String"/>
</EntityType>
<EntityContainer Name="Service">
<EntitySet Name="ServiceRoot" EntityType="Redfish.ServiceRoot"/>
<EntitySet Name="SessionService" EntityType="Redfish.SessionService"/>
<EntitySet Name="Sessions" EntityType="Redfish.Session"/>
<EntitySet Name="Systems" EntityType="Redfish.ComputerSystem"/>
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>`)
})

// SessionService endpoint
r.GET("/SessionService", func(c *gin.Context) {
payload := map[string]any{
"@odata.type": "#SessionService.v1_0_0.SessionService",
"@odata.id": "/redfish/v1/SessionService",
"Id": "SessionService",
"Name": "Redfish Session Service",
"ServiceEnabled": true,
"SessionTimeout": 30,
"Sessions": map[string]any{"@odata.id": "/redfish/v1/SessionService/Sessions"},
}

c.JSON(http.StatusOK, payload)
})

// Sessions collection endpoint (read-only, empty list for now)
r.GET("/SessionService/Sessions", func(c *gin.Context) {
payload := map[string]any{
"@odata.type": "#SessionCollection.SessionCollection",
"@odata.id": "/redfish/v1/SessionService/Sessions",
"Name": "Session Collection",
"[email protected]": 0,
"Members": []any{},
}

c.JSON(http.StatusOK, payload)
})

l.Info("Registered Redfish Service Root at %s", r.BasePath())
}
158 changes: 158 additions & 0 deletions internal/controller/http/redfish/systems.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package redfish

import (
"net/http"

"github.com/gin-gonic/gin"

"github.com/device-management-toolkit/console/internal/usecase/devices"
"github.com/device-management-toolkit/console/pkg/logger"
)

// Lint constants
const (
maxSystemsList = 100
powerStateUnknown = "Unknown"
powerStateOn = "On"
powerStateOff = "Off"
resetTypeOn = "On"
resetTypeForceOff = "ForceOff"
resetTypeForceRestart = "ForceRestart"
resetTypePowerCycle = "PowerCycle"
actionPowerUp = 2
actionPowerCycle = 5
actionPowerDown = 8
actionReset = 10
// CIM PowerState enum values (Device.PowerState)
cimPowerOn = 2
cimPowerSleep = 3
cimPowerStandby = 4
cimPowerSoftOff = 7
cimPowerHardOff = 8
)

// NewSystemsRoutes registers minimal Redfish ComputerSystem routes.
// It exposes:
// - GET /redfish/v1/Systems
// - GET /redfish/v1/Systems/:id
// - POST /redfish/v1/Systems/:id/Actions/ComputerSystem.Reset
// 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) {
systems := r.Group("/Systems")
systems.GET("", getSystemsCollectionHandler(d, l))
systems.GET(":id", getSystemInstanceHandler(d, l))
systems.POST(":id/Actions/ComputerSystem.Reset", postSystemResetHandler(d, l))
l.Info("Registered Redfish Systems routes under %s", r.BasePath()+"/Systems")
}

func getSystemsCollectionHandler(d devices.Feature, l logger.Interface) gin.HandlerFunc {
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()})

return
}

members := make([]any, 0, len(items))
for i := range items { // avoid value copy
it := &items[i]
if it.GUID == "" {
continue
}

members = append(members, map[string]any{
"@odata.id": "/redfish/v1/Systems/" + it.GUID,
})
}

payload := map[string]any{
"@odata.type": "#ComputerSystemCollection.ComputerSystemCollection",
"@odata.id": "/redfish/v1/Systems",
"Name": "Computer System Collection",
"[email protected]": len(members),
"Members": members,
}
c.JSON(http.StatusOK, payload)
}
}

func getSystemInstanceHandler(d devices.Feature, l logger.Interface) gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
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)
} else {
switch ps.PowerState { // CIM PowerState values
case actionPowerUp: // 2 (On)
powerState = powerStateOn
case cimPowerSleep, cimPowerStandby: // Sleep/Standby -> treat as On
powerState = powerStateOn
case cimPowerSoftOff, cimPowerHardOff: // Soft Off / Hard Off
powerState = powerStateOff
default:
powerState = powerStateUnknown
}
}

payload := map[string]any{
"@odata.type": "#ComputerSystem.v1_0_0.ComputerSystem",
"@odata.id": "/redfish/v1/Systems/" + 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",
"[email protected]": []string{resetTypeOn, resetTypeForceOff, resetTypeForceRestart, resetTypePowerCycle},
},
},
}
c.JSON(http.StatusOK, payload)
}
}

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()})

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"})

return
}

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()})

return
}

c.JSON(http.StatusOK, res)
}
}
8 changes: 8 additions & 0 deletions internal/controller/http/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
ginSwagger "github.com/swaggo/gin-swagger"

"github.com/device-management-toolkit/console/config"
"github.com/device-management-toolkit/console/internal/controller/http/redfish"
v1 "github.com/device-management-toolkit/console/internal/controller/http/v1"
v2 "github.com/device-management-toolkit/console/internal/controller/http/v2"
"github.com/device-management-toolkit/console/internal/usecase"
Expand Down Expand Up @@ -99,6 +100,13 @@ func NewRouter(handler *gin.Engine, l logger.Interface, t usecase.Usecases, cfg
v1.NewAmtRoutes(h2, t.Devices, t.AMTExplorer, t.Exporter, l)
}

bluefish := protected.Group("/redfish/v1")
{
// Redfish Service Root and minimal services
redfish.NewRoutes(bluefish, l)
redfish.NewSystemsRoutes(bluefish, t.Devices, l)
}

h := protected.Group("/v1/admin")
{
v1.NewDomainRoutes(h, t.Domains, l)
Expand Down
2 changes: 2 additions & 0 deletions internal/controller/http/v1/devicemanagement.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ func NewAmtRoutes(handler *gin.RouterGroup, d devices.Feature, amt amtexplorer.F
h.GET("diskInfo/:guid", r.getDiskInfo)
h.GET("power/state/:guid", r.getPowerState)
h.POST("power/action/:guid", r.powerAction)
h.POST("power/on/:guid", r.powerOn)
h.POST("power/off/:guid", r.powerOff)
h.POST("power/bootOptions/:guid", r.setBootOptions)
h.POST("power/bootoptions/:guid", r.setBootOptions)
h.GET("power/capabilities/:guid", r.getPowerCapabilities)
Expand Down
38 changes: 38 additions & 0 deletions internal/controller/http/v1/power.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import (
"github.com/device-management-toolkit/console/internal/entity/dto/v1"
)

// Reuse the same action codes used in Redfish systems
const (
actionPowerUp = 2
actionPowerDown = 8
)

func (r *deviceManagementRoutes) getPowerState(c *gin.Context) {
guid := c.Param("guid")

Expand Down Expand Up @@ -57,6 +63,38 @@ func (r *deviceManagementRoutes) powerAction(c *gin.Context) {
c.JSON(http.StatusOK, response)
}

// powerOn maps POST /amt/power/on/:guid to SendPowerAction with action=2 (Power Up)
func (r *deviceManagementRoutes) powerOn(c *gin.Context) {
guid := c.Param("guid")

// Power Up
response, err := r.d.SendPowerAction(c.Request.Context(), guid, actionPowerUp)
if err != nil {
r.l.Error(err, "http - v1 - powerOn")
ErrorResponse(c, err)

return
}

c.JSON(http.StatusOK, response)
}

// powerOff maps POST /amt/power/off/:guid to SendPowerAction with action=8 (Power Down)
func (r *deviceManagementRoutes) powerOff(c *gin.Context) {
guid := c.Param("guid")

// Power Down
response, err := r.d.SendPowerAction(c.Request.Context(), guid, actionPowerDown)
if err != nil {
r.l.Error(err, "http - v1 - powerOff")
ErrorResponse(c, err)

return
}

c.JSON(http.StatusOK, response)
}

func (r *deviceManagementRoutes) setBootOptions(c *gin.Context) {
guid := c.Param("guid")

Expand Down
Loading