Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fe982f3
feat(devices): add Remote Platform Erase (RPE) support
nmgaston Apr 22, 2026
f418e9c
refactor: remove RPE capabilities from features and API
nmgaston May 4, 2026
fd17eae
Merge branch 'main' into remotePlatformErase
nmgaston May 4, 2026
72b55d3
build: fix build issues
nmgaston May 6, 2026
9e0257e
Merge branch 'main' into remotePlatformErase
nmgaston May 7, 2026
49d999c
build: fix merge
nmgaston May 7, 2026
e81eee3
Merge remote-tracking branch 'origin/main' into remotePlatformErase
nmgaston May 29, 2026
ff1ae92
Merge branch 'main' into remotePlatformErase
nmgaston Jun 10, 2026
8bd758f
feat: add pyrite password for wiping encrypted SSD
nmgaston Jun 10, 2026
1bbb54d
Merge branch 'main' into remotePlatformErase
nmgaston Jun 15, 2026
09b45fb
Merge branch 'main' into remotePlatformErase
nmgaston Jun 24, 2026
c091630
Merge branch 'main' into remotePlatformErase
nmgaston Jun 24, 2026
e1a316d
fix: merge issues
nmgaston Jun 24, 2026
4435b9a
Merge branch 'main' into remotePlatformErase
nmgaston Jun 24, 2026
5b715b8
Merge branch 'main' into remotePlatformErase
nmgaston Jun 25, 2026
a26356e
feat: check current power state before restart to know if ON=2 or RES…
nmgaston Jun 30, 2026
c25170f
Merge branch 'main' into remotePlatformErase
nmgaston Jun 30, 2026
25c62da
feat: check current power state before restart to know if ON=2 or RES…
nmgaston Jun 30, 2026
f49203a
fix: should not reboot if mask=0 for RPE
nmgaston Jul 15, 2026
746081c
fix: Review comments
nmgaston Jul 24, 2026
7d0779c
Merge branch 'main' into remotePlatformErase
nmgaston Jul 29, 2026
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
3 changes: 2 additions & 1 deletion internal/controller/httpapi/ui_noui.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ package httpapi
import (
"net/http"

"github.com/gin-gonic/gin"

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

func HasUI() bool { return false }
Expand Down
43 changes: 43 additions & 0 deletions internal/controller/httpapi/v1/boot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package v1

import (
"net/http"

"github.com/gin-gonic/gin"

"github.com/device-management-toolkit/console/internal/entity/dto/v1"
)

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

capabilities, err := r.d.GetRemoteEraseCapabilities(c.Request.Context(), guid)
if err != nil {
r.l.Error(err, "http - v1 - getRemoteEraseCapabilities")
ErrorResponse(c, err)

return
}

c.JSON(http.StatusOK, capabilities)
}

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

var req dto.RemoteEraseRequest
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, err)

return
}

if err := r.d.SetRemoteEraseOptions(c.Request.Context(), guid, req); err != nil {
r.l.Error(err, "http - v1 - setRemoteEraseOptions")
ErrorResponse(c, err)

return
}

c.JSON(http.StatusOK, nil)
}
147 changes: 147 additions & 0 deletions internal/controller/httpapi/v1/boot_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package v1

import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/require"

"github.com/device-management-toolkit/console/internal/entity/dto/v1"
"github.com/device-management-toolkit/console/internal/mocks"
)

func TestGetRemoteEraseCapabilities(t *testing.T) {
t.Parallel()

tests := []struct {
name string
mock func(m *mocks.MockDeviceManagementFeature)
expectedCode int
response interface{}
}{
{
name: "getRemoteEraseCapabilities - successful retrieval",
mock: func(m *mocks.MockDeviceManagementFeature) {
m.EXPECT().GetRemoteEraseCapabilities(context.Background(), "valid-guid").
Return(dto.BootCapabilities{SecureEraseAllSSDs: true}, nil)
},
expectedCode: http.StatusOK,
response: dto.BootCapabilities{SecureEraseAllSSDs: true},
},
{
name: "getRemoteEraseCapabilities - service failure",
mock: func(m *mocks.MockDeviceManagementFeature) {
m.EXPECT().GetRemoteEraseCapabilities(context.Background(), "valid-guid").
Return(dto.BootCapabilities{}, ErrGeneral)
},
expectedCode: http.StatusInternalServerError,
response: nil,
},
}

for _, tc := range tests {
tc := tc

t.Run(tc.name, func(t *testing.T) {
t.Parallel()

deviceManagement, engine := deviceManagementTest(t)
tc.mock(deviceManagement)

req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/api/v1/amt/boot/remoteErase/valid-guid", http.NoBody)
require.NoError(t, err)

w := httptest.NewRecorder()
engine.ServeHTTP(w, req)

require.Equal(t, tc.expectedCode, w.Code)

if tc.expectedCode == http.StatusOK {
jsonBytes, _ := json.Marshal(tc.response)
require.Equal(t, string(jsonBytes), w.Body.String())
}
})
}
}

func TestSetRemoteEraseOptions(t *testing.T) {
t.Parallel()

tests := []struct {
name string
requestBody interface{}
mock func(m *mocks.MockDeviceManagementFeature)
expectedCode int
}{
{
name: "setRemoteEraseOptions - successful",
requestBody: dto.RemoteEraseRequest{SecureEraseAllSSDs: true, TPMClear: true},
mock: func(m *mocks.MockDeviceManagementFeature) {
m.EXPECT().SetRemoteEraseOptions(context.Background(), "valid-guid", dto.RemoteEraseRequest{SecureEraseAllSSDs: true, TPMClear: true}).
Return(nil)
},
expectedCode: http.StatusOK,
},
{
name: "setRemoteEraseOptions - forwards full payload",
requestBody: dto.RemoteEraseRequest{
SecureEraseAllSSDs: true,
TPMClear: true,
RestoreBIOSToEOM: true,
UnconfigureCSME: true,
SSDPassword: "s3cr3t",
},
mock: func(m *mocks.MockDeviceManagementFeature) {
m.EXPECT().SetRemoteEraseOptions(context.Background(), "valid-guid", dto.RemoteEraseRequest{
SecureEraseAllSSDs: true,
TPMClear: true,
RestoreBIOSToEOM: true,
UnconfigureCSME: true,
SSDPassword: "s3cr3t",
}).
Return(nil)
},
expectedCode: http.StatusOK,
},
{
name: "setRemoteEraseOptions - invalid JSON payload",
requestBody: "invalid-json",
mock: func(_ *mocks.MockDeviceManagementFeature) {
},
expectedCode: http.StatusInternalServerError,
},
{
name: "setRemoteEraseOptions - service failure",
requestBody: dto.RemoteEraseRequest{UnconfigureCSME: true},
mock: func(m *mocks.MockDeviceManagementFeature) {
m.EXPECT().SetRemoteEraseOptions(context.Background(), "valid-guid", dto.RemoteEraseRequest{UnconfigureCSME: true}).
Return(ErrGeneral)
},
expectedCode: http.StatusInternalServerError,
},
}

for _, tc := range tests {
tc := tc

t.Run(tc.name, func(t *testing.T) {
t.Parallel()

deviceManagement, engine := deviceManagementTest(t)
tc.mock(deviceManagement)

reqBody, _ := json.Marshal(tc.requestBody)
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "/api/v1/amt/boot/remoteErase/valid-guid", bytes.NewBuffer(reqBody))
require.NoError(t, err)

w := httptest.NewRecorder()
engine.ServeHTTP(w, req)

require.Equal(t, tc.expectedCode, w.Code)
})
}
}
127 changes: 71 additions & 56 deletions internal/controller/httpapi/v1/devicemanagement.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,60 +20,75 @@ func NewAmtRoutes(handler *gin.RouterGroup, d devices.Feature, amt amtexplorer.F
r := &deviceManagementRoutes{d, amt, e, l}

h := handler.Group("/amt")
{
h.GET("version/:guid", r.getVersion)

h.GET("features/:guid", r.getFeatures)
h.POST("features/:guid", r.setFeatures)

h.GET("alarmOccurrences/:guid", r.getAlarmOccurrences)
h.POST("alarmOccurrences/:guid", r.createAlarmOccurrences)
h.DELETE("alarmOccurrences/:guid", r.deleteAlarmOccurrences)

h.GET("hardwareInfo/:guid", r.getHardwareInfo)
h.GET("diskInfo/:guid", r.getDiskInfo)
h.GET("power/state/:guid", r.getPowerState)
h.POST("power/action/:guid", r.powerAction)
h.POST("power/bootOptions/:guid", r.setBootOptions)
h.POST("power/bootoptions/:guid", r.setBootOptions)
h.GET("power/bootSources/:guid", r.getBootSources)
h.GET("power/capabilities/:guid", r.getPowerCapabilities)

h.GET("log/audit/:guid", r.getAuditLog)
h.GET("log/audit/:guid/download", r.downloadAuditLog)
h.GET("log/event/:guid", r.getEventLog)
h.GET("log/event/:guid/download", r.downloadEventLog)
h.GET("generalSettings/:guid", r.getGeneralSettings)

h.GET("userConsentCode/cancel/:guid", r.cancelUserConsentCode)
h.GET("userConsentCode/:guid", r.getUserConsentCode)
h.POST("userConsentCode/:guid", r.sendConsentCode)

h.GET("networkSettings/:guid", r.getNetworkSettings)
h.GET("networkSettings/wired/:guid", r.getWiredNetworkSettings)
h.PATCH("networkSettings/wired/:guid", r.patchWiredNetworkSettings)
h.GET("networkSettings/wireless/state/:guid", r.getWirelessState)
h.POST("networkSettings/wireless/state/:guid", r.requestWirelessStateChange)
h.GET("networkSettings/wireless/profileSync/:guid", r.getWirelessProfileSync)
h.POST("networkSettings/wireless/profileSync/:guid", r.setWirelessProfileSync)
h.GET("networkSettings/wireless/profile/:guid", r.getWirelessProfiles)
h.POST("networkSettings/wireless/profile/:guid", r.addWirelessProfile)
h.PATCH("networkSettings/wireless/profile/:guid", r.updateWirelessProfile)
h.DELETE("networkSettings/wireless/profile/:guid/:profileName", r.deleteWirelessProfile)

h.GET("explorer", r.getCallList)
h.GET("explorer/:guid/:call", r.executeCall)
h.GET("tls/:guid", r.getTLSSettingData)

h.GET("certificates/:guid", r.getCertificates)
h.POST("certificates/:guid", r.addCertificate)
h.DELETE("certificates/:guid/:instanceId", r.deleteCertificate)

// KVM display settings
h.GET("kvm/displays/:guid", r.getKVMDisplays)
h.PUT("kvm/displays/:guid", r.setKVMDisplays)

// Network link preference
h.POST("network/linkPreference/:guid", r.setLinkPreference)
}
r.registerCoreRoutes(h)
r.registerPowerAndLogRoutes(h)
r.registerNetworkRoutes(h)
r.registerExplorerAndCertificateRoutes(h)
r.registerKVMAndLinkRoutes(h)
}

func (r *deviceManagementRoutes) registerCoreRoutes(h *gin.RouterGroup) {
h.GET("version/:guid", r.getVersion)

h.GET("features/:guid", r.getFeatures)
h.POST("features/:guid", r.setFeatures)

h.GET("alarmOccurrences/:guid", r.getAlarmOccurrences)
h.POST("alarmOccurrences/:guid", r.createAlarmOccurrences)
h.DELETE("alarmOccurrences/:guid", r.deleteAlarmOccurrences)

h.GET("boot/remoteErase/:guid", r.getRemoteEraseCapabilities)
h.POST("boot/remoteErase/:guid", r.setRemoteEraseOptions)
h.GET("hardwareInfo/:guid", r.getHardwareInfo)
h.GET("diskInfo/:guid", r.getDiskInfo)
h.GET("generalSettings/:guid", r.getGeneralSettings)

h.GET("userConsentCode/cancel/:guid", r.cancelUserConsentCode)
h.GET("userConsentCode/:guid", r.getUserConsentCode)
h.POST("userConsentCode/:guid", r.sendConsentCode)
}

func (r *deviceManagementRoutes) registerPowerAndLogRoutes(h *gin.RouterGroup) {
h.GET("power/state/:guid", r.getPowerState)
h.POST("power/action/:guid", r.powerAction)
h.POST("power/bootOptions/:guid", r.setBootOptions)
h.POST("power/bootoptions/:guid", r.setBootOptions)
h.GET("power/bootSources/:guid", r.getBootSources)
h.GET("power/capabilities/:guid", r.getPowerCapabilities)

h.GET("log/audit/:guid", r.getAuditLog)
h.GET("log/audit/:guid/download", r.downloadAuditLog)
h.GET("log/event/:guid", r.getEventLog)
h.GET("log/event/:guid/download", r.downloadEventLog)
}

func (r *deviceManagementRoutes) registerNetworkRoutes(h *gin.RouterGroup) {
h.GET("networkSettings/:guid", r.getNetworkSettings)
h.GET("networkSettings/wired/:guid", r.getWiredNetworkSettings)
h.PATCH("networkSettings/wired/:guid", r.patchWiredNetworkSettings)
h.GET("networkSettings/wireless/state/:guid", r.getWirelessState)
h.POST("networkSettings/wireless/state/:guid", r.requestWirelessStateChange)
h.GET("networkSettings/wireless/profileSync/:guid", r.getWirelessProfileSync)
h.POST("networkSettings/wireless/profileSync/:guid", r.setWirelessProfileSync)
h.GET("networkSettings/wireless/profile/:guid", r.getWirelessProfiles)
h.POST("networkSettings/wireless/profile/:guid", r.addWirelessProfile)
h.PATCH("networkSettings/wireless/profile/:guid", r.updateWirelessProfile)
h.DELETE("networkSettings/wireless/profile/:guid/:profileName", r.deleteWirelessProfile)
}

func (r *deviceManagementRoutes) registerExplorerAndCertificateRoutes(h *gin.RouterGroup) {
h.GET("explorer", r.getCallList)
h.GET("explorer/:guid/:call", r.executeCall)
h.GET("tls/:guid", r.getTLSSettingData)

h.GET("certificates/:guid", r.getCertificates)
h.POST("certificates/:guid", r.addCertificate)
h.DELETE("certificates/:guid/:instanceId", r.deleteCertificate)
}

func (r *deviceManagementRoutes) registerKVMAndLinkRoutes(h *gin.RouterGroup) {
h.GET("kvm/displays/:guid", r.getKVMDisplays)
h.PUT("kvm/displays/:guid", r.setKVMDisplays)

h.POST("network/linkPreference/:guid", r.setLinkPreference)
}
2 changes: 1 addition & 1 deletion internal/controller/httpapi/v1/devicemanagement_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func TestDeviceManagement(t *testing.T) {
OCR: false,
OptInState: 0,
Redirection: false,
RemoteErase: false,
RPE: false,
UserConsent: "",
WinREBootSupported: false,
},
Expand Down
3 changes: 2 additions & 1 deletion internal/controller/httpapi/v1/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ func (r *deviceManagementRoutes) getFeatures(c *gin.Context) {
HTTPSBootSupported: features.HTTPSBootSupported,
WinREBootSupported: features.WinREBootSupported,
LocalPBABootSupported: features.LocalPBABootSupported,
RemoteErase: features.RemoteErase,
RPE: features.RPE,
RPESupported: features.RPESupported,
}

c.JSON(http.StatusOK, v1Features)
Expand Down
Loading
Loading