Skip to content

Commit 3fb92dc

Browse files
committed
feat(devices): add remote platform erase support
1 parent 92f460f commit 3fb92dc

26 files changed

Lines changed: 1698 additions & 129 deletions

codecov.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ coverage:
44
range: 25...100
55
# Specify files or directories to ignore
66
ignore:
7-
- "internal/usecase/devices/wsman/*"
7+
- "internal/usecase/devices/wsman/*"
8+
- "internal/mocks/*"
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package v1
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/gin-gonic/gin"
7+
8+
"github.com/device-management-toolkit/console/internal/entity/dto/v1"
9+
)
10+
11+
func (r *deviceManagementRoutes) getBootCapabilities(c *gin.Context) {
12+
guid := c.Param("guid")
13+
14+
capabilities, err := r.d.GetBootCapabilities(c.Request.Context(), guid)
15+
if err != nil {
16+
r.l.Error(err, "http - v1 - getBootCapabilities")
17+
ErrorResponse(c, err)
18+
19+
return
20+
}
21+
22+
c.JSON(http.StatusOK, capabilities)
23+
}
24+
25+
func (r *deviceManagementRoutes) setRPEEnabled(c *gin.Context) {
26+
guid := c.Param("guid")
27+
28+
var req dto.RPERequest
29+
if err := c.ShouldBindJSON(&req); err != nil {
30+
ErrorResponse(c, err)
31+
32+
return
33+
}
34+
35+
if err := r.d.SetRPEEnabled(c.Request.Context(), guid, req.Enabled); err != nil {
36+
r.l.Error(err, "http - v1 - setRPEEnabled")
37+
ErrorResponse(c, err)
38+
39+
return
40+
}
41+
42+
c.JSON(http.StatusOK, nil)
43+
}
44+
45+
func (r *deviceManagementRoutes) sendRemoteErase(c *gin.Context) {
46+
guid := c.Param("guid")
47+
48+
var req dto.RemoteEraseRequest
49+
if err := c.ShouldBindJSON(&req); err != nil {
50+
ErrorResponse(c, err)
51+
52+
return
53+
}
54+
55+
if err := r.d.SendRemoteErase(c.Request.Context(), guid, req.EraseMask); err != nil {
56+
r.l.Error(err, "http - v1 - sendRemoteErase")
57+
ErrorResponse(c, err)
58+
59+
return
60+
}
61+
62+
c.JSON(http.StatusOK, nil)
63+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package v1
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"net/http"
8+
"net/http/httptest"
9+
"testing"
10+
11+
"github.com/stretchr/testify/require"
12+
13+
"github.com/device-management-toolkit/console/internal/entity/dto/v1"
14+
"github.com/device-management-toolkit/console/internal/mocks"
15+
)
16+
17+
func TestGetBootCapabilities(t *testing.T) {
18+
t.Parallel()
19+
20+
tests := []struct {
21+
name string
22+
mock func(m *mocks.MockDeviceManagementFeature)
23+
expectedCode int
24+
response interface{}
25+
}{
26+
{
27+
name: "getBootCapabilities - successful retrieval",
28+
mock: func(m *mocks.MockDeviceManagementFeature) {
29+
m.EXPECT().GetBootCapabilities(context.Background(), "valid-guid").
30+
Return(dto.BootCapabilities{IDER: true, SOL: true}, nil)
31+
},
32+
expectedCode: http.StatusOK,
33+
response: dto.BootCapabilities{IDER: true, SOL: true},
34+
},
35+
{
36+
name: "getBootCapabilities - service failure",
37+
mock: func(m *mocks.MockDeviceManagementFeature) {
38+
m.EXPECT().GetBootCapabilities(context.Background(), "valid-guid").
39+
Return(dto.BootCapabilities{}, ErrGeneral)
40+
},
41+
expectedCode: http.StatusInternalServerError,
42+
response: nil,
43+
},
44+
}
45+
46+
for _, tc := range tests {
47+
tc := tc
48+
49+
t.Run(tc.name, func(t *testing.T) {
50+
t.Parallel()
51+
52+
deviceManagement, engine := deviceManagementTest(t)
53+
tc.mock(deviceManagement)
54+
55+
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/api/v1/amt/boot/capabilities/valid-guid", http.NoBody)
56+
require.NoError(t, err)
57+
58+
w := httptest.NewRecorder()
59+
engine.ServeHTTP(w, req)
60+
61+
require.Equal(t, tc.expectedCode, w.Code)
62+
63+
if tc.expectedCode == http.StatusOK {
64+
jsonBytes, _ := json.Marshal(tc.response)
65+
require.Equal(t, string(jsonBytes), w.Body.String())
66+
}
67+
})
68+
}
69+
}
70+
71+
func TestSetRPEEnabled(t *testing.T) {
72+
t.Parallel()
73+
74+
tests := []struct {
75+
name string
76+
requestBody interface{}
77+
mock func(m *mocks.MockDeviceManagementFeature)
78+
expectedCode int
79+
}{
80+
{
81+
name: "setRPEEnabled - successful (enabled=true)",
82+
requestBody: dto.RPERequest{Enabled: true},
83+
mock: func(m *mocks.MockDeviceManagementFeature) {
84+
m.EXPECT().SetRPEEnabled(context.Background(), "valid-guid", true).
85+
Return(nil)
86+
},
87+
expectedCode: http.StatusOK,
88+
},
89+
{
90+
name: "setRPEEnabled - successful (enabled=false)",
91+
requestBody: dto.RPERequest{Enabled: false},
92+
mock: func(m *mocks.MockDeviceManagementFeature) {
93+
m.EXPECT().SetRPEEnabled(context.Background(), "valid-guid", false).
94+
Return(nil)
95+
},
96+
expectedCode: http.StatusOK,
97+
},
98+
{
99+
name: "setRPEEnabled - invalid JSON payload",
100+
requestBody: "invalid-json",
101+
mock: func(_ *mocks.MockDeviceManagementFeature) {
102+
},
103+
expectedCode: http.StatusInternalServerError,
104+
},
105+
{
106+
name: "setRPEEnabled - service failure",
107+
requestBody: dto.RPERequest{Enabled: true},
108+
mock: func(m *mocks.MockDeviceManagementFeature) {
109+
m.EXPECT().SetRPEEnabled(context.Background(), "valid-guid", true).
110+
Return(ErrGeneral)
111+
},
112+
expectedCode: http.StatusInternalServerError,
113+
},
114+
}
115+
116+
for _, tc := range tests {
117+
tc := tc
118+
119+
t.Run(tc.name, func(t *testing.T) {
120+
t.Parallel()
121+
122+
deviceManagement, engine := deviceManagementTest(t)
123+
tc.mock(deviceManagement)
124+
125+
reqBody, _ := json.Marshal(tc.requestBody)
126+
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "/api/v1/amt/boot/rpe/valid-guid", bytes.NewBuffer(reqBody))
127+
require.NoError(t, err)
128+
129+
w := httptest.NewRecorder()
130+
engine.ServeHTTP(w, req)
131+
132+
require.Equal(t, tc.expectedCode, w.Code)
133+
})
134+
}
135+
}
136+
137+
func TestSendRemoteErase(t *testing.T) {
138+
t.Parallel()
139+
140+
tests := []struct {
141+
name string
142+
requestBody interface{}
143+
mock func(m *mocks.MockDeviceManagementFeature)
144+
expectedCode int
145+
}{
146+
{
147+
name: "sendRemoteErase - successful",
148+
requestBody: dto.RemoteEraseRequest{EraseMask: 3},
149+
mock: func(m *mocks.MockDeviceManagementFeature) {
150+
m.EXPECT().SendRemoteErase(context.Background(), "valid-guid", 3).
151+
Return(nil)
152+
},
153+
expectedCode: http.StatusOK,
154+
},
155+
{
156+
name: "sendRemoteErase - invalid JSON payload",
157+
requestBody: "invalid-json",
158+
mock: func(_ *mocks.MockDeviceManagementFeature) {
159+
},
160+
expectedCode: http.StatusInternalServerError,
161+
},
162+
{
163+
name: "sendRemoteErase - service failure",
164+
requestBody: dto.RemoteEraseRequest{EraseMask: 1},
165+
mock: func(m *mocks.MockDeviceManagementFeature) {
166+
m.EXPECT().SendRemoteErase(context.Background(), "valid-guid", 1).
167+
Return(ErrGeneral)
168+
},
169+
expectedCode: http.StatusInternalServerError,
170+
},
171+
}
172+
173+
for _, tc := range tests {
174+
tc := tc
175+
176+
t.Run(tc.name, func(t *testing.T) {
177+
t.Parallel()
178+
179+
deviceManagement, engine := deviceManagementTest(t)
180+
tc.mock(deviceManagement)
181+
182+
reqBody, _ := json.Marshal(tc.requestBody)
183+
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "/api/v1/amt/remoteErase/valid-guid", bytes.NewBuffer(reqBody))
184+
require.NoError(t, err)
185+
186+
w := httptest.NewRecorder()
187+
engine.ServeHTTP(w, req)
188+
189+
require.Equal(t, tc.expectedCode, w.Code)
190+
})
191+
}
192+
}

internal/controller/httpapi/v1/devicemanagement.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ func NewAmtRoutes(handler *gin.RouterGroup, d devices.Feature, amt amtexplorer.F
3030
h.POST("alarmOccurrences/:guid", r.createAlarmOccurrences)
3131
h.DELETE("alarmOccurrences/:guid", r.deleteAlarmOccurrences)
3232

33+
h.GET("boot/capabilities/:guid", r.getBootCapabilities)
34+
h.POST("boot/rpe/:guid", r.setRPEEnabled)
35+
h.POST("remoteErase/:guid", r.sendRemoteErase)
3336
h.GET("hardwareInfo/:guid", r.getHardwareInfo)
3437
h.GET("diskInfo/:guid", r.getDiskInfo)
3538
h.GET("power/state/:guid", r.getPowerState)

internal/controller/httpapi/v1/devicemanagement_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ func TestDeviceManagement(t *testing.T) {
133133
OCR: false,
134134
OptInState: 0,
135135
Redirection: false,
136-
RemoteErase: false,
136+
RemoteEraseEnabled: false,
137137
UserConsent: "",
138138
WinREBootSupported: false,
139139
},

internal/controller/httpapi/v1/features.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ func (r *deviceManagementRoutes) getFeatures(c *gin.Context) {
4545
HTTPSBootSupported: features.HTTPSBootSupported,
4646
WinREBootSupported: features.WinREBootSupported,
4747
LocalPBABootSupported: features.LocalPBABootSupported,
48-
RemoteErase: features.RemoteErase,
48+
RemoteEraseEnabled: features.RemoteEraseEnabled,
49+
RemoteEraseSupported: features.RemoteEraseSupported,
50+
PlatformEraseCaps: features.PlatformEraseCaps,
4951
}
5052

5153
c.JSON(http.StatusOK, v1Features)

internal/controller/openapi/devicemanagement.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,20 @@ func (f *FuegoAdapter) registerPowerRoutes() {
172172
fuego.OptionDescription("Retrieve power capabilities for a device"),
173173
fuego.OptionPath("guid", "Device GUID"),
174174
)
175+
176+
fuego.Get(f.server, "/api/v1/admin/amt/boot/capabilities/{guid}", f.getBootCapabilities,
177+
fuego.OptionTags("Device Management"),
178+
fuego.OptionSummary("Get Boot Capabilities"),
179+
fuego.OptionDescription("Read AMT_BootCapabilities.PlatformErase to determine Remote Platform Erase (RPE) support in the BIOS"),
180+
fuego.OptionPath("guid", "Device GUID"),
181+
)
182+
183+
fuego.Post(f.server, "/api/v1/admin/amt/boot/rpe/{guid}", f.setRPEEnabled,
184+
fuego.OptionTags("Device Management"),
185+
fuego.OptionSummary("Set RPE Enabled"),
186+
fuego.OptionDescription("Enable or disable Remote Platform Erase (RPE) in Intel AMT via CIM_BootService.RequestStateChange. Requires administrative privileges and BIOS support."),
187+
fuego.OptionPath("guid", "Device GUID"),
188+
)
175189
}
176190

177191
func (f *FuegoAdapter) registerLogsAndAlarmRoutes() {
@@ -359,6 +373,14 @@ func (f *FuegoAdapter) getPowerCapabilities(_ fuego.ContextNoBody) (dto.PowerCap
359373
return dto.PowerCapabilities{}, nil
360374
}
361375

376+
func (f *FuegoAdapter) getBootCapabilities(_ fuego.ContextNoBody) (dto.BootCapabilities, error) {
377+
return dto.BootCapabilities{}, nil
378+
}
379+
380+
func (f *FuegoAdapter) setRPEEnabled(_ fuego.ContextWithBody[dto.RPERequest]) (any, error) {
381+
return nil, nil
382+
}
383+
362384
func (f *FuegoAdapter) getAlarmOccurrences(_ fuego.ContextNoBody) ([]dto.AlarmClockOccurrence, error) {
363385
return []dto.AlarmClockOccurrence{}, nil
364386
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package openapi
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
dto "github.com/device-management-toolkit/console/internal/entity/dto/v1"
10+
"github.com/device-management-toolkit/console/internal/usecase"
11+
"github.com/device-management-toolkit/console/pkg/logger"
12+
)
13+
14+
func newTestAdapter() *FuegoAdapter {
15+
log := logger.New("error")
16+
17+
return NewFuegoAdapter(usecase.Usecases{}, log)
18+
}
19+
20+
func TestGetBootCapabilities(t *testing.T) {
21+
t.Parallel()
22+
23+
f := newTestAdapter()
24+
25+
result, err := f.getBootCapabilities(nil)
26+
27+
require.NoError(t, err)
28+
require.Equal(t, dto.BootCapabilities{}, result)
29+
}
30+
31+
func TestSetRPEEnabled(t *testing.T) {
32+
t.Parallel()
33+
34+
f := newTestAdapter()
35+
36+
result, err := f.setRPEEnabled(nil)
37+
38+
require.NoError(t, err)
39+
require.Nil(t, result)
40+
}
41+
42+
func TestRegisterPowerRoutes_IncludesBootEndpoints(t *testing.T) {
43+
t.Parallel()
44+
45+
f := newTestAdapter()
46+
f.RegisterDeviceManagementRoutes()
47+
48+
specBytes, err := f.GetOpenAPISpec()
49+
require.NoError(t, err)
50+
51+
var spec map[string]interface{}
52+
require.NoError(t, json.Unmarshal(specBytes, &spec))
53+
54+
paths, ok := spec["paths"].(map[string]interface{})
55+
require.True(t, ok)
56+
57+
require.Contains(t, paths, "/api/v1/admin/amt/boot/capabilities/{guid}", "boot capabilities route should be registered")
58+
require.Contains(t, paths, "/api/v1/admin/amt/boot/rpe/{guid}", "set RPE enabled route should be registered")
59+
}

0 commit comments

Comments
 (0)