diff --git a/codecov.yml b/codecov.yml index 8537facdc..e076cf098 100644 --- a/codecov.yml +++ b/codecov.yml @@ -4,4 +4,5 @@ coverage: range: 25...100 # Specify files or directories to ignore ignore: -- "internal/usecase/devices/wsman/*" \ No newline at end of file +- "internal/usecase/devices/wsman/*" +- "internal/mocks/*" \ No newline at end of file diff --git a/go.mod b/go.mod index d21f25378..8672e0ba2 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,6 @@ module github.com/device-management-toolkit/console go 1.25.0 -// replace github.com/device-management-toolkit/go-wsman-messages/v2 => ../go-wsman-messages - require ( github.com/Masterminds/squirrel v1.5.4 github.com/coreos/go-oidc/v3 v3.17.0 diff --git a/internal/controller/httpapi/v1/boot.go b/internal/controller/httpapi/v1/boot.go new file mode 100644 index 000000000..cb0c97f15 --- /dev/null +++ b/internal/controller/httpapi/v1/boot.go @@ -0,0 +1,63 @@ +package v1 + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/device-management-toolkit/console/internal/entity/dto/v1" +) + +func (r *deviceManagementRoutes) getBootCapabilities(c *gin.Context) { + guid := c.Param("guid") + + capabilities, err := r.d.GetBootCapabilities(c.Request.Context(), guid) + if err != nil { + r.l.Error(err, "http - v1 - getBootCapabilities") + ErrorResponse(c, err) + + return + } + + c.JSON(http.StatusOK, capabilities) +} + +func (r *deviceManagementRoutes) setRPEEnabled(c *gin.Context) { + guid := c.Param("guid") + + var req dto.RPERequest + if err := c.ShouldBindJSON(&req); err != nil { + ErrorResponse(c, err) + + return + } + + if err := r.d.SetRPEEnabled(c.Request.Context(), guid, req.Enabled); err != nil { + r.l.Error(err, "http - v1 - setRPEEnabled") + ErrorResponse(c, err) + + return + } + + c.JSON(http.StatusOK, nil) +} + +func (r *deviceManagementRoutes) sendRemoteErase(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.SendRemoteErase(c.Request.Context(), guid, req.EraseMask); err != nil { + r.l.Error(err, "http - v1 - sendRemoteErase") + ErrorResponse(c, err) + + return + } + + c.JSON(http.StatusOK, nil) +} diff --git a/internal/controller/httpapi/v1/boot_test.go b/internal/controller/httpapi/v1/boot_test.go new file mode 100644 index 000000000..2a0c52d95 --- /dev/null +++ b/internal/controller/httpapi/v1/boot_test.go @@ -0,0 +1,192 @@ +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 TestGetBootCapabilities(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + mock func(m *mocks.MockDeviceManagementFeature) + expectedCode int + response interface{} + }{ + { + name: "getBootCapabilities - successful retrieval", + mock: func(m *mocks.MockDeviceManagementFeature) { + m.EXPECT().GetBootCapabilities(context.Background(), "valid-guid"). + Return(dto.BootCapabilities{IDER: true, SOL: true}, nil) + }, + expectedCode: http.StatusOK, + response: dto.BootCapabilities{IDER: true, SOL: true}, + }, + { + name: "getBootCapabilities - service failure", + mock: func(m *mocks.MockDeviceManagementFeature) { + m.EXPECT().GetBootCapabilities(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/capabilities/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 TestSetRPEEnabled(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + requestBody interface{} + mock func(m *mocks.MockDeviceManagementFeature) + expectedCode int + }{ + { + name: "setRPEEnabled - successful (enabled=true)", + requestBody: dto.RPERequest{Enabled: true}, + mock: func(m *mocks.MockDeviceManagementFeature) { + m.EXPECT().SetRPEEnabled(context.Background(), "valid-guid", true). + Return(nil) + }, + expectedCode: http.StatusOK, + }, + { + name: "setRPEEnabled - successful (enabled=false)", + requestBody: dto.RPERequest{Enabled: false}, + mock: func(m *mocks.MockDeviceManagementFeature) { + m.EXPECT().SetRPEEnabled(context.Background(), "valid-guid", false). + Return(nil) + }, + expectedCode: http.StatusOK, + }, + { + name: "setRPEEnabled - invalid JSON payload", + requestBody: "invalid-json", + mock: func(_ *mocks.MockDeviceManagementFeature) { + }, + expectedCode: http.StatusInternalServerError, + }, + { + name: "setRPEEnabled - service failure", + requestBody: dto.RPERequest{Enabled: true}, + mock: func(m *mocks.MockDeviceManagementFeature) { + m.EXPECT().SetRPEEnabled(context.Background(), "valid-guid", 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/rpe/valid-guid", bytes.NewBuffer(reqBody)) + require.NoError(t, err) + + w := httptest.NewRecorder() + engine.ServeHTTP(w, req) + + require.Equal(t, tc.expectedCode, w.Code) + }) + } +} + +func TestSendRemoteErase(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + requestBody interface{} + mock func(m *mocks.MockDeviceManagementFeature) + expectedCode int + }{ + { + name: "sendRemoteErase - successful", + requestBody: dto.RemoteEraseRequest{EraseMask: 3}, + mock: func(m *mocks.MockDeviceManagementFeature) { + m.EXPECT().SendRemoteErase(context.Background(), "valid-guid", 3). + Return(nil) + }, + expectedCode: http.StatusOK, + }, + { + name: "sendRemoteErase - invalid JSON payload", + requestBody: "invalid-json", + mock: func(_ *mocks.MockDeviceManagementFeature) { + }, + expectedCode: http.StatusInternalServerError, + }, + { + name: "sendRemoteErase - service failure", + requestBody: dto.RemoteEraseRequest{EraseMask: 1}, + mock: func(m *mocks.MockDeviceManagementFeature) { + m.EXPECT().SendRemoteErase(context.Background(), "valid-guid", 1). + 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/remoteErase/valid-guid", bytes.NewBuffer(reqBody)) + require.NoError(t, err) + + w := httptest.NewRecorder() + engine.ServeHTTP(w, req) + + require.Equal(t, tc.expectedCode, w.Code) + }) + } +} diff --git a/internal/controller/httpapi/v1/devicemanagement.go b/internal/controller/httpapi/v1/devicemanagement.go index 3b1347865..b8ee6aba1 100644 --- a/internal/controller/httpapi/v1/devicemanagement.go +++ b/internal/controller/httpapi/v1/devicemanagement.go @@ -30,6 +30,9 @@ func NewAmtRoutes(handler *gin.RouterGroup, d devices.Feature, amt amtexplorer.F h.POST("alarmOccurrences/:guid", r.createAlarmOccurrences) h.DELETE("alarmOccurrences/:guid", r.deleteAlarmOccurrences) + h.GET("boot/capabilities/:guid", r.getBootCapabilities) + h.POST("boot/rpe/:guid", r.setRPEEnabled) + h.POST("remoteErase/:guid", r.sendRemoteErase) h.GET("hardwareInfo/:guid", r.getHardwareInfo) h.GET("diskInfo/:guid", r.getDiskInfo) h.GET("power/state/:guid", r.getPowerState) diff --git a/internal/controller/httpapi/v1/devicemanagement_test.go b/internal/controller/httpapi/v1/devicemanagement_test.go index ee0741e80..cfa8f828a 100644 --- a/internal/controller/httpapi/v1/devicemanagement_test.go +++ b/internal/controller/httpapi/v1/devicemanagement_test.go @@ -133,7 +133,7 @@ func TestDeviceManagement(t *testing.T) { OCR: false, OptInState: 0, Redirection: false, - RemoteErase: false, + RPE: false, UserConsent: "", WinREBootSupported: false, }, diff --git a/internal/controller/httpapi/v1/features.go b/internal/controller/httpapi/v1/features.go index 5c95394a3..27f571ecf 100644 --- a/internal/controller/httpapi/v1/features.go +++ b/internal/controller/httpapi/v1/features.go @@ -45,7 +45,13 @@ 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, + RPECaps: features.RPECaps, + RPESecureErase: features.RPESecureErase, + RPETPMClear: features.RPETPMClear, + RPEClearBIOSNVM: features.RPEClearBIOSNVM, + RPEBIOSReload: features.RPEBIOSReload, } c.JSON(http.StatusOK, v1Features) diff --git a/internal/controller/openapi/devicemanagement.go b/internal/controller/openapi/devicemanagement.go index 57b629cff..a1afc4ed6 100644 --- a/internal/controller/openapi/devicemanagement.go +++ b/internal/controller/openapi/devicemanagement.go @@ -172,6 +172,20 @@ func (f *FuegoAdapter) registerPowerRoutes() { fuego.OptionDescription("Retrieve power capabilities for a device"), fuego.OptionPath("guid", "Device GUID"), ) + + fuego.Get(f.server, "/api/v1/admin/amt/boot/capabilities/{guid}", f.getBootCapabilities, + fuego.OptionTags("Device Management"), + fuego.OptionSummary("Get Boot Capabilities"), + fuego.OptionDescription("Read AMT_BootCapabilities.PlatformErase to determine Remote Platform Erase (RPE) support in the BIOS"), + fuego.OptionPath("guid", "Device GUID"), + ) + + fuego.Post(f.server, "/api/v1/admin/amt/boot/rpe/{guid}", f.setRPEEnabled, + fuego.OptionTags("Device Management"), + fuego.OptionSummary("Set RPE Enabled"), + fuego.OptionDescription("Enable or disable Remote Platform Erase (RPE) in Intel AMT via CIM_BootService.RequestStateChange. Requires administrative privileges and BIOS support."), + fuego.OptionPath("guid", "Device GUID"), + ) } func (f *FuegoAdapter) registerLogsAndAlarmRoutes() { @@ -359,6 +373,14 @@ func (f *FuegoAdapter) getPowerCapabilities(_ fuego.ContextNoBody) (dto.PowerCap return dto.PowerCapabilities{}, nil } +func (f *FuegoAdapter) getBootCapabilities(_ fuego.ContextNoBody) (dto.BootCapabilities, error) { + return dto.BootCapabilities{}, nil +} + +func (f *FuegoAdapter) setRPEEnabled(_ fuego.ContextWithBody[dto.RPERequest]) (any, error) { + return nil, nil +} + func (f *FuegoAdapter) getAlarmOccurrences(_ fuego.ContextNoBody) ([]dto.AlarmClockOccurrence, error) { return []dto.AlarmClockOccurrence{}, nil } diff --git a/internal/controller/openapi/devicemanagement_test.go b/internal/controller/openapi/devicemanagement_test.go new file mode 100644 index 000000000..3696180b9 --- /dev/null +++ b/internal/controller/openapi/devicemanagement_test.go @@ -0,0 +1,59 @@ +package openapi + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + dto "github.com/device-management-toolkit/console/internal/entity/dto/v1" + "github.com/device-management-toolkit/console/internal/usecase" + "github.com/device-management-toolkit/console/pkg/logger" +) + +func newTestAdapter() *FuegoAdapter { + log := logger.New("error") + + return NewFuegoAdapter(usecase.Usecases{}, log) +} + +func TestGetBootCapabilities(t *testing.T) { + t.Parallel() + + f := newTestAdapter() + + result, err := f.getBootCapabilities(nil) + + require.NoError(t, err) + require.Equal(t, dto.BootCapabilities{}, result) +} + +func TestSetRPEEnabled(t *testing.T) { + t.Parallel() + + f := newTestAdapter() + + result, err := f.setRPEEnabled(nil) + + require.NoError(t, err) + require.Nil(t, result) +} + +func TestRegisterPowerRoutes_IncludesBootEndpoints(t *testing.T) { + t.Parallel() + + f := newTestAdapter() + f.RegisterDeviceManagementRoutes() + + specBytes, err := f.GetOpenAPISpec() + require.NoError(t, err) + + var spec map[string]interface{} + require.NoError(t, json.Unmarshal(specBytes, &spec)) + + paths, ok := spec["paths"].(map[string]interface{}) + require.True(t, ok) + + require.Contains(t, paths, "/api/v1/admin/amt/boot/capabilities/{guid}", "boot capabilities route should be registered") + require.Contains(t, paths, "/api/v1/admin/amt/boot/rpe/{guid}", "set RPE enabled route should be registered") +} diff --git a/internal/controller/ws/v1/interface.go b/internal/controller/ws/v1/interface.go index abcebe431..029f1447c 100644 --- a/internal/controller/ws/v1/interface.go +++ b/internal/controller/ws/v1/interface.go @@ -64,6 +64,8 @@ type Feature interface { GetDeviceCertificate(c context.Context, guid string) (dto.Certificate, error) AddCertificate(c context.Context, guid string, certInfo dto.CertInfo) (string, error) DeleteCertificate(c context.Context, guid, instanceID string) error + GetBootCapabilities(ctx context.Context, guid string) (dto.BootCapabilities, error) + SetRPEEnabled(ctx context.Context, guid string, enabled bool) error GetBootSourceSetting(ctx context.Context, guid string) ([]dto.BootSources, error) // KVM Screen Settings GetKVMScreenSettings(c context.Context, guid string) (dto.KVMScreenSettings, error) diff --git a/internal/entity/dto/v1/bootcapabilities.go b/internal/entity/dto/v1/bootcapabilities.go new file mode 100644 index 000000000..3b8b93c96 --- /dev/null +++ b/internal/entity/dto/v1/bootcapabilities.go @@ -0,0 +1,40 @@ +package dto + +type RPERequest struct { + Enabled bool `json:"enabled"` +} + +type RemoteEraseRequest struct { + EraseMask int `json:"eraseMask"` +} + +type BootCapabilities struct { + IDER bool `json:"IDER,omitempty"` + SOL bool `json:"SOL,omitempty"` + BIOSReflash bool `json:"BIOSReflash,omitempty"` + BIOSSetup bool `json:"BIOSSetup,omitempty"` + BIOSPause bool `json:"BIOSPause,omitempty"` + ForcePXEBoot bool `json:"ForcePXEBoot,omitempty"` + ForceHardDriveBoot bool `json:"ForceHardDriveBoot,omitempty"` + ForceHardDriveSafeModeBoot bool `json:"ForceHardDriveSafeModeBoot,omitempty"` + ForceDiagnosticBoot bool `json:"ForceDiagnosticBoot,omitempty"` + ForceCDorDVDBoot bool `json:"ForceCDorDVDBoot,omitempty"` + VerbosityScreenBlank bool `json:"VerbosityScreenBlank,omitempty"` + PowerButtonLock bool `json:"PowerButtonLock,omitempty"` + ResetButtonLock bool `json:"ResetButtonLock,omitempty"` + KeyboardLock bool `json:"KeyboardLock,omitempty"` + SleepButtonLock bool `json:"SleepButtonLock,omitempty"` + UserPasswordBypass bool `json:"UserPasswordBypass,omitempty"` + ForcedProgressEvents bool `json:"ForcedProgressEvents,omitempty"` + VerbosityVerbose bool `json:"VerbosityVerbose,omitempty"` + VerbosityQuiet bool `json:"VerbosityQuiet,omitempty"` + ConfigurationDataReset bool `json:"ConfigurationDataReset,omitempty"` + BIOSSecureBoot bool `json:"BIOSSecureBoot,omitempty"` + SecureErase bool `json:"SecureErase,omitempty"` + ForceWinREBoot bool `json:"ForceWinREBoot,omitempty"` + ForceUEFILocalPBABoot bool `json:"ForceUEFILocalPBABoot,omitempty"` + ForceUEFIHTTPSBoot bool `json:"ForceUEFIHTTPSBoot,omitempty"` + AMTSecureBootControl bool `json:"AMTSecureBootControl,omitempty"` + UEFIWiFiCoExistenceAndProfileShare bool `json:"UEFIWiFiCoExistenceAndProfileShare,omitempty"` + PlatformErase int `json:"PlatformErase,omitempty"` +} diff --git a/internal/entity/dto/v1/features.go b/internal/entity/dto/v1/features.go index 426a07746..de79321c0 100644 --- a/internal/entity/dto/v1/features.go +++ b/internal/entity/dto/v1/features.go @@ -23,7 +23,13 @@ type Features struct { HTTPSBootSupported bool `json:"httpsBootSupported" example:"true"` WinREBootSupported bool `json:"winREBootSupported" example:"true"` LocalPBABootSupported bool `json:"localPBABootSupported" example:"true"` - RemoteErase bool `json:"remoteErase" example:"true"` + RPE bool `json:"rpe" example:"true"` + RPESupported bool `json:"rpeSupported" example:"true"` + RPECaps int `json:"rpeCaps,omitempty" example:"15"` + RPESecureErase bool `json:"rpeSecureErase,omitempty" example:"false"` + RPETPMClear bool `json:"rpeTPMClear,omitempty" example:"false"` + RPEClearBIOSNVM bool `json:"rpeClearBIOSNVM,omitempty" example:"false"` + RPEBIOSReload bool `json:"rpeBIOSReload,omitempty" example:"false"` } type FeaturesRequest struct { @@ -32,5 +38,5 @@ type FeaturesRequest struct { EnableIDER bool `json:"enableIDER" example:"true"` EnableKVM bool `json:"enableKVM" example:"true"` OCR bool `json:"ocr" example:"true"` - RemoteErase bool `json:"remoteErase" example:"true"` + RPE bool `json:"rpe" example:"true"` } diff --git a/internal/entity/dto/v1/getfeatures.go b/internal/entity/dto/v1/getfeatures.go index d62dd5698..b9185e41d 100644 --- a/internal/entity/dto/v1/getfeatures.go +++ b/internal/entity/dto/v1/getfeatures.go @@ -12,5 +12,11 @@ type GetFeaturesResponse struct { HTTPSBootSupported bool `json:"httpsBootSupported" binding:"required" example:"false"` WinREBootSupported bool `json:"winREBootSupported" binding:"required" example:"false"` LocalPBABootSupported bool `json:"localPBABootSupported" binding:"required" example:"false"` - RemoteErase bool `json:"remoteErase" binding:"required" example:"false"` + RPE bool `json:"rpe" binding:"required" example:"false"` + RPESupported bool `json:"rpeSupported" example:"false"` + RPECaps int `json:"rpeCaps" example:"0"` + RPESecureErase bool `json:"rpeSecureErase" example:"false"` + RPETPMClear bool `json:"rpeTPMClear" example:"false"` + RPEClearBIOSNVM bool `json:"rpeClearBIOSNVM" example:"false"` + RPEBIOSReload bool `json:"rpeBIOSReload" example:"false"` } diff --git a/internal/entity/dto/v2/features.go b/internal/entity/dto/v2/features.go index 8152ec245..8d5235b21 100644 --- a/internal/entity/dto/v2/features.go +++ b/internal/entity/dto/v2/features.go @@ -20,9 +20,15 @@ type Features struct { Redirection bool `json:"redirection" example:"true"` OptInState int `json:"optInState" example:"0"` KVMAvailable bool `json:"kvmAvailable" example:"true"` - OCR bool `json:"httpBoot" example:"true"` + OCR bool `json:"ocr" example:"true"` HTTPSBootSupported bool `json:"httpBootSupported,omitempty" example:"true"` WinREBootSupported bool `json:"winREBootSupported,omitempty" example:"true"` LocalPBABootSupported bool `json:"localPBABootSupported,omitempty" example:"true"` - RemoteErase bool `json:"remoteErase" example:"true"` + RPE bool `json:"rpe" example:"true"` + RPESupported bool `json:"rpeSupported" example:"true"` + RPECaps int `json:"rpeCaps,omitempty" example:"15"` + RPESecureErase bool `json:"rpeSecureErase,omitempty" example:"false"` + RPETPMClear bool `json:"rpeTPMClear,omitempty" example:"false"` + RPEClearBIOSNVM bool `json:"rpeClearBIOSNVM,omitempty" example:"false"` + RPEBIOSReload bool `json:"rpeBIOSReload,omitempty" example:"false"` } diff --git a/internal/mocks/devicemanagement_mocks.go b/internal/mocks/devicemanagement_mocks.go index 11a564515..cef998284 100644 --- a/internal/mocks/devicemanagement_mocks.go +++ b/internal/mocks/devicemanagement_mocks.go @@ -594,6 +594,21 @@ func (mr *MockDeviceManagementFeatureMockRecorder) GetAuditLog(ctx, startIndex, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuditLog", reflect.TypeOf((*MockDeviceManagementFeature)(nil).GetAuditLog), ctx, startIndex, guid) } +// GetBootCapabilities mocks base method. +func (m *MockDeviceManagementFeature) GetBootCapabilities(ctx context.Context, guid string) (dto.BootCapabilities, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBootCapabilities", ctx, guid) + ret0, _ := ret[0].(dto.BootCapabilities) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBootCapabilities indicates an expected call of GetBootCapabilities. +func (mr *MockDeviceManagementFeatureMockRecorder) GetBootCapabilities(ctx, guid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBootCapabilities", reflect.TypeOf((*MockDeviceManagementFeature)(nil).GetBootCapabilities), ctx, guid) +} + // GetBootSourceSetting mocks base method. func (m *MockDeviceManagementFeature) GetBootSourceSetting(c context.Context, guid string) ([]dto.BootSources, error) { m.ctrl.T.Helper() @@ -790,21 +805,6 @@ func (mr *MockDeviceManagementFeatureMockRecorder) GetHardwareInfo(ctx, guid any return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHardwareInfo", reflect.TypeOf((*MockDeviceManagementFeature)(nil).GetHardwareInfo), ctx, guid) } -// SetLinkPreference mocks base method. -func (m *MockDeviceManagementFeature) SetLinkPreference(c context.Context, guid string, req dto.LinkPreferenceRequest) (dto.LinkPreferenceResponse, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetLinkPreference", c, guid, req) - ret0, _ := ret[0].(dto.LinkPreferenceResponse) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// SetLinkPreference indicates an expected call of SetLinkPreference. -func (mr *MockDeviceManagementFeatureMockRecorder) SetLinkPreference(c, guid, req any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLinkPreference", reflect.TypeOf((*MockDeviceManagementFeature)(nil).SetLinkPreference), c, guid, req) -} - // GetKVMScreenSettings mocks base method. func (m *MockDeviceManagementFeature) GetKVMScreenSettings(c context.Context, guid string) (dto.KVMScreenSettings, error) { m.ctrl.T.Helper() @@ -1016,6 +1016,49 @@ func (mr *MockDeviceManagementFeatureMockRecorder) SetKVMScreenSettings(c, guid, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetKVMScreenSettings", reflect.TypeOf((*MockDeviceManagementFeature)(nil).SetKVMScreenSettings), c, guid, req) } +// SetLinkPreference mocks base method. +func (m *MockDeviceManagementFeature) SetLinkPreference(c context.Context, guid string, req dto.LinkPreferenceRequest) (dto.LinkPreferenceResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetLinkPreference", c, guid, req) + ret0, _ := ret[0].(dto.LinkPreferenceResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SetLinkPreference indicates an expected call of SetLinkPreference. +func (mr *MockDeviceManagementFeatureMockRecorder) SetLinkPreference(c, guid, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLinkPreference", reflect.TypeOf((*MockDeviceManagementFeature)(nil).SetLinkPreference), c, guid, req) +} + +// SetRPEEnabled mocks base method. +func (m *MockDeviceManagementFeature) SetRPEEnabled(ctx context.Context, guid string, enabled bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetRPEEnabled", ctx, guid, enabled) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetRPEEnabled indicates an expected call of SetRPEEnabled. +func (mr *MockDeviceManagementFeatureMockRecorder) SetRPEEnabled(ctx, guid, enabled any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRPEEnabled", reflect.TypeOf((*MockDeviceManagementFeature)(nil).SetRPEEnabled), ctx, guid, enabled) +} + +// SendRemoteErase mocks base method. +func (m *MockDeviceManagementFeature) SendRemoteErase(ctx context.Context, guid string, eraseMask int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendRemoteErase", ctx, guid, eraseMask) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendRemoteErase indicates an expected call of SendRemoteErase. +func (mr *MockDeviceManagementFeatureMockRecorder) SendRemoteErase(ctx, guid, eraseMask any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendRemoteErase", reflect.TypeOf((*MockDeviceManagementFeature)(nil).SendRemoteErase), ctx, guid, eraseMask) +} + // Update mocks base method. func (m *MockDeviceManagementFeature) Update(ctx context.Context, d *dto.Device) (*dto.Device, error) { m.ctrl.T.Helper() diff --git a/internal/mocks/wsman_mocks.go b/internal/mocks/wsman_mocks.go index 647bc8635..354c2f1bd 100644 --- a/internal/mocks/wsman_mocks.go +++ b/internal/mocks/wsman_mocks.go @@ -239,6 +239,21 @@ func (mr *MockManagementMockRecorder) GetAuditLog(startIndex any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuditLog", reflect.TypeOf((*MockManagement)(nil).GetAuditLog), startIndex) } +// GetBootCapabilities mocks base method. +func (m *MockManagement) GetBootCapabilities() (boot.BootCapabilitiesResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBootCapabilities") + ret0, _ := ret[0].(boot.BootCapabilitiesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBootCapabilities indicates an expected call of GetBootCapabilities. +func (mr *MockManagementMockRecorder) GetBootCapabilities() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBootCapabilities", reflect.TypeOf((*MockManagement)(nil).GetBootCapabilities)) +} + // GetBootData mocks base method. func (m *MockManagement) GetBootData() (boot.BootSettingDataResponse, error) { m.ctrl.T.Helper() @@ -690,6 +705,34 @@ func (mr *MockManagementMockRecorder) SetBootData(data any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBootData", reflect.TypeOf((*MockManagement)(nil).SetBootData), data) } +// SetRPEEnabled mocks base method. +func (m *MockManagement) SetRPEEnabled(enabled bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetRPEEnabled", enabled) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetRPEEnabled indicates an expected call of SetRPEEnabled. +func (mr *MockManagementMockRecorder) SetRPEEnabled(enabled any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRPEEnabled", reflect.TypeOf((*MockManagement)(nil).SetRPEEnabled), enabled) +} + +// SendRemoteErase mocks base method. +func (m *MockManagement) SendRemoteErase(eraseMask int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendRemoteErase", eraseMask) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendRemoteErase indicates an expected call of SendRemoteErase. +func (mr *MockManagementMockRecorder) SendRemoteErase(eraseMask any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendRemoteErase", reflect.TypeOf((*MockManagement)(nil).SendRemoteErase), eraseMask) +} + // SetIPSKVMRedirectionSettingData mocks base method. func (m *MockManagement) SetIPSKVMRedirectionSettingData(data *kvmredirection.KVMRedirectionSettingsRequest) (kvmredirection.Response, error) { m.ctrl.T.Helper() diff --git a/internal/mocks/wsv1_mocks.go b/internal/mocks/wsv1_mocks.go index 3ca7c33e3..ccc312afa 100644 --- a/internal/mocks/wsv1_mocks.go +++ b/internal/mocks/wsv1_mocks.go @@ -270,6 +270,49 @@ func (mr *MockFeatureMockRecorder) GetAuditLog(ctx, startIndex, guid any) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuditLog", reflect.TypeOf((*MockFeature)(nil).GetAuditLog), ctx, startIndex, guid) } +// GetBootCapabilities mocks base method. +func (m *MockFeature) GetBootCapabilities(ctx context.Context, guid string) (dto.BootCapabilities, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBootCapabilities", ctx, guid) + ret0, _ := ret[0].(dto.BootCapabilities) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBootCapabilities indicates an expected call of GetBootCapabilities. +func (mr *MockFeatureMockRecorder) GetBootCapabilities(ctx, guid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBootCapabilities", reflect.TypeOf((*MockFeature)(nil).GetBootCapabilities), ctx, guid) +} + +// SetRPEEnabled mocks base method. +func (m *MockFeature) SetRPEEnabled(ctx context.Context, guid string, enabled bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetRPEEnabled", ctx, guid, enabled) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetRPEEnabled indicates an expected call of SetRPEEnabled. +func (mr *MockFeatureMockRecorder) SetRPEEnabled(ctx, guid, enabled any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRPEEnabled", reflect.TypeOf((*MockFeature)(nil).SetRPEEnabled), ctx, guid, enabled) +} + +// SendRemoteErase mocks base method. +func (m *MockFeature) SendRemoteErase(ctx context.Context, guid string, eraseMask int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendRemoteErase", ctx, guid, eraseMask) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendRemoteErase indicates an expected call of SendRemoteErase. +func (mr *MockFeatureMockRecorder) SendRemoteErase(ctx, guid, eraseMask any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendRemoteErase", reflect.TypeOf((*MockFeature)(nil).SendRemoteErase), ctx, guid, eraseMask) +} + // GetBootSourceSetting mocks base method. func (m *MockFeature) GetBootSourceSetting(ctx context.Context, guid string) ([]dto.BootSources, error) { m.ctrl.T.Helper() diff --git a/internal/usecase/devices/boot.go b/internal/usecase/devices/boot.go new file mode 100644 index 000000000..ec1e91a3f --- /dev/null +++ b/internal/usecase/devices/boot.go @@ -0,0 +1,135 @@ +package devices + +import ( + "context" + "errors" + + "github.com/device-management-toolkit/console/internal/entity/dto/v1" + deviceManagement "github.com/device-management-toolkit/console/internal/usecase/devices/wsman" + "github.com/device-management-toolkit/console/pkg/consoleerrors" +) + +func (uc *UseCase) GetBootCapabilities(c context.Context, guid string) (dto.BootCapabilities, error) { + item, err := uc.repo.GetByID(c, guid, "") + if err != nil { + return dto.BootCapabilities{}, err + } + + if item == nil || item.GUID == "" { + return dto.BootCapabilities{}, ErrNotFound + } + + device, err := uc.device.SetupWsmanClient(*item, false, true) + if err != nil { + return dto.BootCapabilities{}, err + } + + capabilities, err := device.GetBootCapabilities() + if err != nil { + return dto.BootCapabilities{}, err + } + + uc.log.Debug("GetBootCapabilities: PlatformErase capability", "guid", guid, "PlatformErase", capabilities.PlatformErase, "supported", capabilities.PlatformErase != 0) + + return dto.BootCapabilities{ + IDER: capabilities.IDER, + SOL: capabilities.SOL, + BIOSReflash: capabilities.BIOSReflash, + BIOSSetup: capabilities.BIOSSetup, + BIOSPause: capabilities.BIOSPause, + ForcePXEBoot: capabilities.ForcePXEBoot, + ForceHardDriveBoot: capabilities.ForceHardDriveBoot, + ForceHardDriveSafeModeBoot: capabilities.ForceHardDriveSafeModeBoot, + ForceDiagnosticBoot: capabilities.ForceDiagnosticBoot, + ForceCDorDVDBoot: capabilities.ForceCDorDVDBoot, + VerbosityScreenBlank: capabilities.VerbosityScreenBlank, + PowerButtonLock: capabilities.PowerButtonLock, + ResetButtonLock: capabilities.ResetButtonLock, + KeyboardLock: capabilities.KeyboardLock, + SleepButtonLock: capabilities.SleepButtonLock, + UserPasswordBypass: capabilities.UserPasswordBypass, + ForcedProgressEvents: capabilities.ForcedProgressEvents, + VerbosityVerbose: capabilities.VerbosityVerbose, + VerbosityQuiet: capabilities.VerbosityQuiet, + ConfigurationDataReset: capabilities.ConfigurationDataReset, + BIOSSecureBoot: capabilities.BIOSSecureBoot, + SecureErase: capabilities.SecureErase, + ForceWinREBoot: capabilities.ForceWinREBoot, + ForceUEFILocalPBABoot: capabilities.ForceUEFILocalPBABoot, + ForceUEFIHTTPSBoot: capabilities.ForceUEFIHTTPSBoot, + AMTSecureBootControl: capabilities.AMTSecureBootControl, + UEFIWiFiCoExistenceAndProfileShare: capabilities.UEFIWiFiCoExistenceAndProfileShare, + PlatformErase: capabilities.PlatformErase, + }, nil +} + +func (uc *UseCase) SetRPEEnabled(c context.Context, guid string, enabled bool) error { + item, err := uc.repo.GetByID(c, guid, "") + if err != nil { + return err + } + + if item == nil || item.GUID == "" { + return ErrNotFound + } + + device, err := uc.device.SetupWsmanClient(*item, false, true) + if err != nil { + return err + } + + capabilities, err := device.GetBootCapabilities() + if err != nil { + return err + } + + if capabilities.PlatformErase == 0 { + return ValidationError{}.Wrap("SetRPEEnabled", "check boot capabilities", "device does not support Remote Platform Erase") + } + + return device.SetRPEEnabled(enabled) +} + +func (uc *UseCase) SendRemoteErase(c context.Context, guid string, eraseMask int) error { + item, err := uc.repo.GetByID(c, guid, "") + if err != nil { + return err + } + + if item == nil || item.GUID == "" { + return ErrNotFound + } + + device, err := uc.device.SetupWsmanClient(*item, false, true) + if err != nil { + return err + } + + capabilities, err := device.GetBootCapabilities() + if err != nil { + return err + } + + if capabilities.PlatformErase == 0 { + return ValidationError{}.Wrap("SendRemoteErase", "check boot capabilities", "device does not support Remote Platform Erase") + } + + uc.log.Debug("SendRemoteErase", + "guid", guid, + "eraseMask", eraseMask, + "secureErase", eraseMask&0x01 != 0, + "ecStorage", eraseMask&0x02 != 0, + "storageDrives", eraseMask&0x04 != 0, + "meRegion", eraseMask&0x08 != 0, + ) + + if err := device.SendRemoteErase(eraseMask); err != nil { + if errors.Is(err, deviceManagement.ErrRPENotEnabled) { + return NotSupportedError{Console: consoleerrors.CreateConsoleError("Remote Platform Erase is not enabled by the BIOS on this device")} + } + + return err + } + + return nil +} diff --git a/internal/usecase/devices/boot_test.go b/internal/usecase/devices/boot_test.go new file mode 100644 index 000000000..873303cbb --- /dev/null +++ b/internal/usecase/devices/boot_test.go @@ -0,0 +1,555 @@ +package devices_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + gomock "go.uber.org/mock/gomock" + + "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/boot" + + "github.com/device-management-toolkit/console/internal/entity" + "github.com/device-management-toolkit/console/internal/entity/dto/v1" + "github.com/device-management-toolkit/console/internal/mocks" + devices "github.com/device-management-toolkit/console/internal/usecase/devices" +) + +func TestGetBootCapabilities(t *testing.T) { + t.Parallel() + + device := &entity.Device{ + GUID: "device-guid-123", + TenantID: "tenant-id-456", + } + + fullCapabilities := boot.BootCapabilitiesResponse{ + IDER: true, + SOL: true, + BIOSReflash: true, + BIOSSetup: false, + BIOSPause: false, + ForcePXEBoot: true, + ForceHardDriveBoot: false, + ForceHardDriveSafeModeBoot: false, + ForceDiagnosticBoot: false, + ForceCDorDVDBoot: false, + VerbosityScreenBlank: false, + PowerButtonLock: false, + ResetButtonLock: false, + KeyboardLock: false, + SleepButtonLock: false, + UserPasswordBypass: false, + ForcedProgressEvents: false, + VerbosityVerbose: false, + VerbosityQuiet: false, + ConfigurationDataReset: false, + BIOSSecureBoot: false, + SecureErase: false, + ForceWinREBoot: false, + ForceUEFILocalPBABoot: false, + ForceUEFIHTTPSBoot: true, + AMTSecureBootControl: false, + UEFIWiFiCoExistenceAndProfileShare: false, + PlatformErase: 1, + } + + expectedDTO := dto.BootCapabilities{ + IDER: true, + SOL: true, + BIOSReflash: true, + ForcePXEBoot: true, + ForceUEFIHTTPSBoot: true, + PlatformErase: 1, + } + + tests := []struct { + name string + manMock func(*mocks.MockWSMAN, *mocks.MockManagement) + repoMock func(*mocks.MockDeviceManagementRepository) + res dto.BootCapabilities + err error + }{ + { + name: "success", + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(fullCapabilities, nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + res: expectedDTO, + err: nil, + }, + { + name: "GetByID returns error", + manMock: func(_ *mocks.MockWSMAN, _ *mocks.MockManagement) {}, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(nil, ErrGeneral) + }, + res: dto.BootCapabilities{}, + err: ErrGeneral, + }, + { + name: "GetByID returns nil device", + manMock: func(_ *mocks.MockWSMAN, _ *mocks.MockManagement) {}, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(nil, nil) + }, + res: dto.BootCapabilities{}, + err: devices.ErrNotFound, + }, + { + name: "GetByID returns device with empty GUID", + manMock: func(_ *mocks.MockWSMAN, _ *mocks.MockManagement) {}, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(&entity.Device{GUID: "", TenantID: "tenant-id-456"}, nil) + }, + res: dto.BootCapabilities{}, + err: devices.ErrNotFound, + }, + { + name: "SetupWsmanClient returns error", + manMock: func(man *mocks.MockWSMAN, _ *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(nil, ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + res: dto.BootCapabilities{}, + err: ErrGeneral, + }, + { + name: "GetBootCapabilities wsman call returns error", + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{}, ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + res: dto.BootCapabilities{}, + err: ErrGeneral, + }, + } + + for _, tc := range tests { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + useCase, wsmanMock, management, repo := initInfoTest(t) + tc.manMock(wsmanMock, management) + tc.repoMock(repo) + + res, err := useCase.GetBootCapabilities(context.Background(), device.GUID) + require.Equal(t, tc.err, err) + require.Equal(t, tc.res, res) + }) + } +} + +func TestSetRPEEnabled(t *testing.T) { + t.Parallel() + + device := &entity.Device{ + GUID: "device-guid-123", + TenantID: "tenant-id-456", + } + + tests := []struct { + name string + enabled bool + manMock func(*mocks.MockWSMAN, *mocks.MockManagement) + repoMock func(*mocks.MockDeviceManagementRepository) + err error + }{ + { + name: "success - enable RPE on supported device", + enabled: true, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 1}, nil) + man2.EXPECT(). + SetRPEEnabled(true). + Return(nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: nil, + }, + { + name: "success - disable RPE on supported device", + enabled: false, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 1}, nil) + man2.EXPECT(). + SetRPEEnabled(false). + Return(nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: nil, + }, + { + name: "device does not support RPE - PlatformErase is 0", + enabled: true, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 0}, nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: devices.ValidationError{}.Wrap("SetRPEEnabled", "check boot capabilities", "device does not support Remote Platform Erase"), + }, + { + name: "GetByID returns error", + enabled: true, + manMock: func(_ *mocks.MockWSMAN, _ *mocks.MockManagement) {}, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(nil, ErrGeneral) + }, + err: ErrGeneral, + }, + { + name: "GetByID returns nil device", + enabled: true, + manMock: func(_ *mocks.MockWSMAN, _ *mocks.MockManagement) {}, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(nil, nil) + }, + err: devices.ErrNotFound, + }, + { + name: "GetByID returns device with empty GUID", + enabled: true, + manMock: func(_ *mocks.MockWSMAN, _ *mocks.MockManagement) {}, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(&entity.Device{GUID: "", TenantID: "tenant-id-456"}, nil) + }, + err: devices.ErrNotFound, + }, + { + name: "SetupWsmanClient returns error", + enabled: true, + manMock: func(man *mocks.MockWSMAN, _ *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(nil, ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: ErrGeneral, + }, + { + name: "GetBootCapabilities returns error", + enabled: true, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{}, ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: ErrGeneral, + }, + { + name: "SetRPEEnabled wsman call returns error", + enabled: true, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 1}, nil) + man2.EXPECT(). + SetRPEEnabled(true). + Return(ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: ErrGeneral, + }, + } + + for _, tc := range tests { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + useCase, wsmanMock, management, repo := initInfoTest(t) + tc.manMock(wsmanMock, management) + tc.repoMock(repo) + + err := useCase.SetRPEEnabled(context.Background(), device.GUID, tc.enabled) + require.Equal(t, tc.err, err) + }) + } +} + +func TestSendRemoteErase(t *testing.T) { + t.Parallel() + + device := &entity.Device{ + GUID: "device-guid-123", + TenantID: "tenant-id-456", + } + + tests := []struct { + name string + eraseMask int + manMock func(*mocks.MockWSMAN, *mocks.MockManagement) + repoMock func(*mocks.MockDeviceManagementRepository) + err error + }{ + { + name: "success - eraseMask 0 erases all", + eraseMask: 0, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 3}, nil) + man2.EXPECT(). + SendRemoteErase(0). + Return(nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: nil, + }, + { + name: "success - specific supported eraseMask", + eraseMask: 2, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 3}, nil) + man2.EXPECT(). + SendRemoteErase(2). + Return(nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: nil, + }, + { + name: "device does not support RPE - PlatformErase is 0", + eraseMask: 0, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 0}, nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: devices.ValidationError{}.Wrap("SendRemoteErase", "check boot capabilities", "device does not support Remote Platform Erase"), + }, + { + name: "eraseMask with PlatformErase nonzero succeeds regardless of specific bits", + eraseMask: 4, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 3}, nil) + man2.EXPECT(). + SendRemoteErase(4). + Return(nil) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: nil, + }, + { + name: "GetByID returns error", + eraseMask: 0, + manMock: func(_ *mocks.MockWSMAN, _ *mocks.MockManagement) {}, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(nil, ErrGeneral) + }, + err: ErrGeneral, + }, + { + name: "GetByID returns nil device", + eraseMask: 0, + manMock: func(_ *mocks.MockWSMAN, _ *mocks.MockManagement) {}, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(nil, nil) + }, + err: devices.ErrNotFound, + }, + { + name: "GetByID returns device with empty GUID", + eraseMask: 0, + manMock: func(_ *mocks.MockWSMAN, _ *mocks.MockManagement) {}, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(&entity.Device{GUID: "", TenantID: "tenant-id-456"}, nil) + }, + err: devices.ErrNotFound, + }, + { + name: "SetupWsmanClient returns error", + eraseMask: 0, + manMock: func(man *mocks.MockWSMAN, _ *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(nil, ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: ErrGeneral, + }, + { + name: "GetBootCapabilities returns error", + eraseMask: 0, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{}, ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: ErrGeneral, + }, + { + name: "SendRemoteErase wsman call returns error", + eraseMask: 0, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 3}, nil) + man2.EXPECT(). + SendRemoteErase(0). + Return(ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + err: ErrGeneral, + }, + } + + for _, tc := range tests { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + useCase, wsmanMock, management, repo := initInfoTest(t) + tc.manMock(wsmanMock, management) + tc.repoMock(repo) + + err := useCase.SendRemoteErase(context.Background(), device.GUID, tc.eraseMask) + require.Equal(t, tc.err, err) + }) + } +} diff --git a/internal/usecase/devices/features.go b/internal/usecase/devices/features.go index ba572058f..54d4e0e85 100644 --- a/internal/usecase/devices/features.go +++ b/internal/usecase/devices/features.go @@ -3,6 +3,7 @@ package devices import ( "context" "errors" + "log" "strings" "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/amterror" @@ -20,12 +21,12 @@ import ( var ErrOCRNotSupportedUseCase = NotSupportedError{Console: consoleerrors.CreateConsoleError("One Click Recovery Unsupported")} -// AMT BootService EnabledState constants. +// AMT BootService EnabledState constants (CIM_BootService.RequestStateChange ValueMap). const ( - // EnabledState values. - enabledStateEnabled = 32769 - enabledStateEnabledButOffline = 32771 - enabledStateDisabled = 32768 + enabledStateOCRAndRPEDisabled = 32768 // OCR disabled, RPE disabled + enabledStateOCREnabled = 32769 // OCR enabled, RPE disabled + enabledStateRPEEnabled = 32770 // OCR disabled, RPE enabled + enabledStateOCRAndRPEEnabled = 32771 // OCR enabled, RPE enabled ) const ( @@ -33,7 +34,7 @@ const ( targetsPBAWinREInstanceID = "Intel(r) AMT: Force OCR UEFI Boot Option" ) -type OCRData struct { +type BootConfiguration struct { bootService cimBoot.BootService bootSourceSettings []cimBoot.BootSourceSetting capabilities boot.BootCapabilitiesResponse @@ -87,41 +88,45 @@ func (uc *UseCase) GetFeatures(c context.Context, guid string) (settingsResults settingsResults.KVMAvailable = settingsResultsV2.KVMAvailable // Get boot service related settings - err = getOneClickRecoverySettings(&settingsResultsV2, device) + err = getBootConfigurationSettings(&settingsResultsV2, device) if err != nil { return dto.Features{}, dtov2.Features{}, err } settingsResults.OCR = settingsResultsV2.OCR + settingsResults.RPE = settingsResultsV2.RPE settingsResults.HTTPSBootSupported = settingsResultsV2.HTTPSBootSupported settingsResults.WinREBootSupported = settingsResultsV2.WinREBootSupported settingsResults.LocalPBABootSupported = settingsResultsV2.LocalPBABootSupported + settingsResults.RPESupported = settingsResultsV2.RPESupported + settingsResults.RPECaps = settingsResultsV2.RPECaps + settingsResults.RPESecureErase = settingsResultsV2.RPESecureErase + settingsResults.RPETPMClear = settingsResultsV2.RPETPMClear + settingsResults.RPEClearBIOSNVM = settingsResultsV2.RPEClearBIOSNVM + settingsResults.RPEBIOSReload = settingsResultsV2.RPEBIOSReload + + uc.log.Debug("GetFeatures: RemoteErase (PlatformErase) support", "guid", guid, "RPESupported", settingsResultsV2.RPESupported, "RPE", settingsResultsV2.RPE) return settingsResults, settingsResultsV2, nil } -func getOCRData(device wsman.Management) (OCRData, error) { - bootService, err := device.GetBootService() - if err != nil { - return OCRData{}, err - } +func getBootConfiguration(device wsman.Management) (BootConfiguration, error) { + // These are non-fatal: if unavailable, OCR/RPE state and boot settings default to zero values + bootService, _ := device.GetBootService() - bootSourceSettings, err := device.GetCIMBootSourceSetting() - if err != nil { - return OCRData{}, err - } + var bootSourceSettings cimBoot.Response + + bootSourceSettings, _ = device.GetCIMBootSourceSetting() + // These are fatal: capabilities and boot data are required for correct operation capabilities, err := device.GetPowerCapabilities() if err != nil { - return OCRData{}, err + return BootConfiguration{}, err } - bootData, err := device.GetBootData() - if err != nil { - return OCRData{}, err - } + bootData, _ := device.GetBootData() - return OCRData{ + return BootConfiguration{ bootService: bootService, bootSourceSettings: bootSourceSettings.Body.PullResponse.BootSourceSettingItems, capabilities: capabilities, @@ -129,6 +134,46 @@ func getOCRData(device wsman.Management) (OCRData, error) { }, nil } +func getBootConfigurationSettings(settingsResultsV2 *dtov2.Features, device wsman.Management) error { + bootConfig, err := getBootConfiguration(device) + if err != nil { + return err + } + + isOCR := bootConfig.bootService.EnabledState == enabledStateOCREnabled || bootConfig.bootService.EnabledState == enabledStateOCRAndRPEEnabled + isRPE := bootConfig.bootService.EnabledState == enabledStateRPEEnabled || bootConfig.bootService.EnabledState == enabledStateOCRAndRPEEnabled + + result := FindBootSettingInstances(bootConfig.bootSourceSettings) + + // AMT_BootSettingData.UEFIHTTPSBootEnabled is read-only. AMT_BootCapabilities instance is read-only. + // So, these cannot be updated + settingsResultsV2.OCR = isOCR + settingsResultsV2.RPE = isRPE + settingsResultsV2.HTTPSBootSupported = result.IsHTTPSBootExists && bootConfig.capabilities.ForceUEFIHTTPSBoot && bootConfig.bootData.UEFIHTTPSBootEnabled + settingsResultsV2.WinREBootSupported = result.IsWinREExists && bootConfig.bootData.WinREBootEnabled && bootConfig.capabilities.ForceWinREBoot + settingsResultsV2.LocalPBABootSupported = result.IsPBAExists && bootConfig.bootData.UEFILocalPBABootEnabled && bootConfig.capabilities.ForceUEFILocalPBABoot + settingsResultsV2.RPESupported = bootConfig.capabilities.PlatformErase&0x01 != 0 // Bit 0: RPE overall support + settingsResultsV2.RPECaps = bootConfig.capabilities.PlatformErase + settingsResultsV2.RPESecureErase = bootConfig.capabilities.PlatformErase&0x04 != 0 // Bit 2: Secure Erase All SSDs + settingsResultsV2.RPETPMClear = bootConfig.capabilities.PlatformErase&0x40 != 0 // Bit 6: TPM Clear + settingsResultsV2.RPEClearBIOSNVM = bootConfig.capabilities.PlatformErase&0x2000000 != 0 // Bit 25: Clear BIOS NVM Variables + settingsResultsV2.RPEBIOSReload = bootConfig.capabilities.PlatformErase&0x4000000 != 0 // Bit 26: BIOS Reload of Golden Configuration + + log.Printf("getBootConfigurationSettings: RPE caps from BIOS PlatformErase=0x%08x (rpeSupport=%v secureEraseSSDs=%v verifyStorageErase=%v sanitizationReport=%v tpmClear=%v oemCustom=%v clearBIOSNVM=%v biosReloadGolden=%v)", + bootConfig.capabilities.PlatformErase, + bootConfig.capabilities.PlatformErase&0x01 != 0, // Bit 0: RPE overall support + bootConfig.capabilities.PlatformErase&0x04 != 0, // Bit 2: Secure Erase All SSDs + bootConfig.capabilities.PlatformErase&0x08 != 0, // Bit 3: Verify Storage Erase (CSME 18.0+) + bootConfig.capabilities.PlatformErase&0x10 != 0, // Bit 4: Generate sanitization report (CSME 18.0+) + bootConfig.capabilities.PlatformErase&0x40 != 0, // Bit 6: TPM Clear + bootConfig.capabilities.PlatformErase&0x10000 != 0, // Bit 16: OEM Custom Action (Raptor Lake CSME 16.1+) + bootConfig.capabilities.PlatformErase&0x2000000 != 0, // Bit 25: Clear BIOS NVM Variables (Raptor Lake CSME 16.1+) + bootConfig.capabilities.PlatformErase&0x4000000 != 0, // Bit 26: BIOS Reload of Golden Configuration + ) + + return nil +} + func FindBootSettingInstances(bootSourceSettings []cimBoot.BootSourceSetting) dtov2.BootSettings { result := dtov2.BootSettings{} @@ -156,32 +201,6 @@ func FindBootSettingInstances(bootSourceSettings []cimBoot.BootSourceSetting) dt return result } -func getOneClickRecoverySettings(settingsResultsV2 *dtov2.Features, device wsman.Management) error { - ocrData, err := getOCRData(device) - if err != nil { - return err - } - - isOCR := ocrData.bootService.EnabledState == enabledStateEnabled || ocrData.bootService.EnabledState == enabledStateEnabledButOffline - - result := FindBootSettingInstances(ocrData.bootSourceSettings) - - // AMT_BootSettingData.UEFIHTTPSBootEnabled is read-only. AMT_BootCapabilities instance is read-only. - // So, these cannot be updated - isHTTPSBootSupported := result.IsHTTPSBootExists && ocrData.capabilities.ForceUEFIHTTPSBoot && ocrData.bootData.UEFIHTTPSBootEnabled - - isWinREBootSupported := result.IsWinREExists && ocrData.bootData.WinREBootEnabled && ocrData.capabilities.ForceWinREBoot - - isLocalPBABootSupported := result.IsPBAExists && ocrData.bootData.UEFILocalPBABootEnabled && ocrData.capabilities.ForceUEFILocalPBABoot - - settingsResultsV2.OCR = isOCR - settingsResultsV2.HTTPSBootSupported = isHTTPSBootSupported - settingsResultsV2.WinREBootSupported = isWinREBootSupported - settingsResultsV2.LocalPBABootSupported = isLocalPBABootSupported - - return nil -} - func (uc *UseCase) SetFeatures(c context.Context, guid string, features dto.Features) (settingsResults dto.Features, settingsResultsV2 dtov2.Features, err error) { item, err := uc.repo.GetByID(c, guid, "") if err != nil { @@ -235,20 +254,40 @@ func (uc *UseCase) SetFeatures(c context.Context, guid string, features dto.Feat settingsResults.UserConsent = features.UserConsent settingsResultsV2.UserConsent = features.UserConsent - // Configure OCR settings + // RPE: must run before BootServiceStateChange (OCR state change blocks the PUT) + if err = setRPE(features.RPE, &settingsResultsV2, device); err != nil { + return settingsResults, settingsResultsV2, err + } + + // Remote Platform Erase (RPE) support and capabilities may be affected by the state change, so we need to get the settings again to return the correct values. + settingsResults.RPE = settingsResultsV2.RPE + settingsResults.RPESupported = settingsResultsV2.RPESupported + settingsResults.RPECaps = settingsResultsV2.RPECaps + settingsResults.RPESecureErase = settingsResultsV2.RPESecureErase + settingsResults.RPETPMClear = settingsResultsV2.RPETPMClear + settingsResults.RPEClearBIOSNVM = settingsResultsV2.RPEClearBIOSNVM + settingsResults.RPEBIOSReload = settingsResultsV2.RPEBIOSReload + + // Configure OCR/RPE boot service state + // 32768 = both disabled, 32769 = OCR only, 32770 = RPE only, 32771 = both enabled requestedState := 0 - if features.OCR { - requestedState = enabledStateEnabled - } else { - requestedState = enabledStateDisabled + switch { + case features.OCR && features.RPE: + requestedState = enabledStateOCRAndRPEEnabled + case features.OCR: + requestedState = enabledStateOCREnabled + case features.RPE: + requestedState = enabledStateRPEEnabled + default: + requestedState = enabledStateOCRAndRPEDisabled } _, err = device.BootServiceStateChange(requestedState) if err == nil { // Get OCR settings - err = getOneClickRecoverySettings(&settingsResultsV2, device) + err = getBootConfigurationSettings(&settingsResultsV2, device) if err != nil { - return dto.Features{}, dtov2.Features{}, err + return settingsResults, settingsResultsV2, nil } settingsResults.OCR = settingsResultsV2.OCR @@ -262,6 +301,34 @@ func (uc *UseCase) SetFeatures(c context.Context, guid string, features dto.Feat return settingsResults, settingsResultsV2, nil } +func setRPE(enableRemoteErase bool, settingsResultsV2 *dtov2.Features, device wsman.Management) error { + bootCapabilities, err := device.GetBootCapabilities() + if err != nil { + return err + } + + settingsResultsV2.RPE = enableRemoteErase + settingsResultsV2.RPESupported = bootCapabilities.PlatformErase&0x01 != 0 // Bit 0: RPE overall support + settingsResultsV2.RPECaps = bootCapabilities.PlatformErase + settingsResultsV2.RPESecureErase = bootCapabilities.PlatformErase&0x04 != 0 + settingsResultsV2.RPETPMClear = bootCapabilities.PlatformErase&0x40 != 0 + settingsResultsV2.RPEClearBIOSNVM = bootCapabilities.PlatformErase&0x2000000 != 0 + settingsResultsV2.RPEBIOSReload = bootCapabilities.PlatformErase&0x4000000 != 0 + + // Latch PlatformErase=true in AMT_BootSettingData while the boot service is idle. + // This must happen before setOCRFeatures calls BootServiceStateChange, which prevents the PUT. + // Non-fatal: the PUT may fail on some firmware versions. + if enableRemoteErase { + if err := device.SetRPEEnabled(true); err != nil { + log.Printf("setRPE: SetRPEEnabled failed (non-fatal): %v", err) + } else { + log.Printf("setRPE: SetRPEEnabled succeeded - PlatformErase latched") + } + } + + return nil +} + func handleAMTKVMError(err error, results *dtov2.Features) bool { amtErr := &amterror.AMTError{} if errors.As(err, &amtErr) { diff --git a/internal/usecase/devices/features_test.go b/internal/usecase/devices/features_test.go index 3c2143259..37a052752 100644 --- a/internal/usecase/devices/features_test.go +++ b/internal/usecase/devices/features_test.go @@ -316,7 +316,7 @@ func TestGetFeatures(t *testing.T) { HTTPSBootSupported: true, WinREBootSupported: false, LocalPBABootSupported: false, - RemoteErase: false, + RPE: false, }, resV2: dtov2.Features{ UserConsent: "kvm", @@ -570,18 +570,27 @@ func TestGetFeatures(t *testing.T) { }, }, }, nil) + man2.EXPECT(). + GetPowerCapabilities(). + Return(boot.BootCapabilitiesResponse{}, nil) man2.EXPECT(). GetBootService(). Return(cimBoot.BootService{}, ErrGeneral) + man2.EXPECT(). + GetCIMBootSourceSetting(). + Return(cimBoot.Response{}, nil) + man2.EXPECT(). + GetBootData(). + Return(boot.BootSettingDataResponse{}, nil) }, repoMock: func(repo *mocks.MockDeviceManagementRepository) { repo.EXPECT(). GetByID(context.Background(), device.GUID, ""). Return(device, nil) }, - res: dto.Features{}, - resV2: dtov2.Features{}, - err: ErrGeneral, + res: featureSetNoOCR, + resV2: featureSetV2NoOCR, + err: nil, }, { name: "GetFeatures fails on boot service", @@ -619,18 +628,27 @@ func TestGetFeatures(t *testing.T) { }, }, }, nil) + man2.EXPECT(). + GetPowerCapabilities(). + Return(boot.BootCapabilitiesResponse{}, nil) man2.EXPECT(). GetBootService(). Return(cimBoot.BootService{}, ErrGeneral) + man2.EXPECT(). + GetCIMBootSourceSetting(). + Return(cimBoot.Response{}, nil) + man2.EXPECT(). + GetBootData(). + Return(boot.BootSettingDataResponse{}, nil) }, repoMock: func(repo *mocks.MockDeviceManagementRepository) { repo.EXPECT(). GetByID(context.Background(), device.GUID, ""). Return(device, nil) }, - res: dto.Features{}, - resV2: dtov2.Features{}, - err: ErrGeneral, + res: featureSetNoOCR, + resV2: featureSetV2NoOCR, + err: nil, }, { name: "GetFeatures fails on boot source setting", @@ -668,6 +686,9 @@ func TestGetFeatures(t *testing.T) { }, }, }, nil) + man2.EXPECT(). + GetPowerCapabilities(). + Return(boot.BootCapabilitiesResponse{}, nil) man2.EXPECT(). GetBootService(). Return(cimBoot.BootService{ @@ -676,15 +697,39 @@ func TestGetFeatures(t *testing.T) { man2.EXPECT(). GetCIMBootSourceSetting(). Return(cimBoot.Response{}, ErrGeneral) + man2.EXPECT(). + GetPowerCapabilities(). + Return(boot.BootCapabilitiesResponse{}, nil) + man2.EXPECT(). + GetBootData(). + Return(boot.BootSettingDataResponse{}, nil) }, repoMock: func(repo *mocks.MockDeviceManagementRepository) { repo.EXPECT(). GetByID(context.Background(), device.GUID, ""). Return(device, nil) }, - res: dto.Features{}, - resV2: dtov2.Features{}, - err: ErrGeneral, + res: dto.Features{ + UserConsent: "kvm", + EnableSOL: true, + EnableIDER: true, + EnableKVM: true, + Redirection: true, + KVMAvailable: true, + OptInState: 1, + OCR: true, // GetBootService succeeded with EnabledState=32769 + }, + resV2: dtov2.Features{ + UserConsent: "kvm", + EnableSOL: true, + EnableIDER: true, + EnableKVM: true, + Redirection: true, + KVMAvailable: true, + OptInState: 1, + OCR: true, + }, + err: nil, }, { name: "GetFeatures fails on power capabilities", @@ -746,6 +791,7 @@ func TestGetFeatures(t *testing.T) { man2.EXPECT(). GetPowerCapabilities(). Return(boot.BootCapabilitiesResponse{}, ErrGeneral) + // GetBootData is not called because GetPowerCapabilities is fatal }, repoMock: func(repo *mocks.MockDeviceManagementRepository) { repo.EXPECT(). @@ -829,9 +875,27 @@ func TestGetFeatures(t *testing.T) { GetByID(context.Background(), device.GUID, ""). Return(device, nil) }, - res: dto.Features{}, - resV2: dtov2.Features{}, - err: ErrGeneral, + res: dto.Features{ + UserConsent: "kvm", + EnableSOL: true, + EnableIDER: true, + EnableKVM: true, + Redirection: true, + KVMAvailable: true, + OptInState: 1, + OCR: true, // GetBootService succeeded with EnabledState=32769 + }, + resV2: dtov2.Features{ + UserConsent: "kvm", + EnableSOL: true, + EnableIDER: true, + EnableKVM: true, + Redirection: true, + KVMAvailable: true, + OptInState: 1, + OCR: true, + }, + err: nil, }, { name: "GetFeatures on ISM", @@ -986,7 +1050,8 @@ func TestGetFeatures(t *testing.T) { Redirection: true, KVMAvailable: true, OptInState: 1, - OCR: true, // Should be true for EnabledState 32771 + OCR: true, // EnabledState 32771 = OCR+RPE both enabled + RPE: true, // EnabledState 32771 = OCR+RPE both enabled HTTPSBootSupported: true, WinREBootSupported: false, LocalPBABootSupported: false, @@ -1000,6 +1065,7 @@ func TestGetFeatures(t *testing.T) { KVMAvailable: true, OptInState: 1, OCR: true, + RPE: true, HTTPSBootSupported: true, WinREBootSupported: false, LocalPBABootSupported: false, @@ -1142,18 +1208,27 @@ func TestGetFeatures(t *testing.T) { }, }, }, nil) + man2.EXPECT(). + GetPowerCapabilities(). + Return(boot.BootCapabilitiesResponse{}, nil) man2.EXPECT(). GetBootService(). Return(cimBoot.BootService{}, ErrGeneral) + man2.EXPECT(). + GetCIMBootSourceSetting(). + Return(cimBoot.Response{}, nil) + man2.EXPECT(). + GetBootData(). + Return(boot.BootSettingDataResponse{}, nil) }, repoMock: func(repo *mocks.MockDeviceManagementRepository) { repo.EXPECT(). GetByID(context.Background(), device.GUID, ""). Return(device, nil) }, - res: dto.Features{}, - resV2: dtov2.Features{}, - err: ErrGeneral, + res: featureSetNoOCR, + resV2: featureSetV2NoOCR, + err: nil, }, } @@ -1216,7 +1291,7 @@ func TestSetFeatures(t *testing.T) { HTTPSBootSupported: true, WinREBootSupported: false, LocalPBABootSupported: false, - RemoteErase: false, + RPE: false, } featureSetDisabledOCRResult := dto.Features{ @@ -1229,7 +1304,7 @@ func TestSetFeatures(t *testing.T) { HTTPSBootSupported: true, WinREBootSupported: false, LocalPBABootSupported: false, - RemoteErase: false, + RPE: false, } featureSetV2DisabledOCR := dtov2.Features{ @@ -1243,7 +1318,16 @@ func TestSetFeatures(t *testing.T) { HTTPSBootSupported: true, WinREBootSupported: false, LocalPBABootSupported: false, - RemoteErase: false, + RPE: false, + } + + featureSetWithRPE := dto.Features{ + UserConsent: "kvm", + EnableSOL: true, + EnableIDER: true, + EnableKVM: true, + OCR: true, + RPE: true, } failGetByIDResult := dto.Features{} @@ -1331,6 +1415,9 @@ func TestSetFeatures(t *testing.T) { OptInState: 0, }). Return(nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{}, nil) man2.EXPECT(). BootServiceStateChange(32769). // OCR enabled Return(cimBoot.BootService{}, nil) @@ -1385,7 +1472,7 @@ func TestSetFeatures(t *testing.T) { HTTPSBootSupported: true, WinREBootSupported: false, LocalPBABootSupported: false, - RemoteErase: false, + RPE: false, }, resV2: featureSetV2, err: nil, @@ -1442,6 +1529,9 @@ func TestSetFeatures(t *testing.T) { OptInState: 0, }). Return(nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{}, nil) man2.EXPECT(). BootServiceStateChange(32768). Return(cimBoot.BootService{}, nil) @@ -1596,6 +1686,9 @@ func TestSetFeatures(t *testing.T) { OptInState: 0, }). Return(nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{}, nil) man2.EXPECT(). BootServiceStateChange(32769). Return(cimBoot.BootService{}, nil) @@ -1650,7 +1743,7 @@ func TestSetFeatures(t *testing.T) { HTTPSBootSupported: true, WinREBootSupported: false, LocalPBABootSupported: false, - RemoteErase: false, + RPE: false, }, resV2: dtov2.Features{ UserConsent: "kvm", @@ -1813,6 +1906,9 @@ func TestSetFeatures(t *testing.T) { OptInState: 0, }). Return(nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{}, nil) man2.EXPECT(). BootServiceStateChange(32769). Return(cimBoot.BootService{}, ErrGeneral) @@ -1891,21 +1987,46 @@ func TestSetFeatures(t *testing.T) { OptInState: 0, }). Return(nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{}, nil) man2.EXPECT(). BootServiceStateChange(32769). Return(cimBoot.BootService{}, nil) + man2.EXPECT(). + GetPowerCapabilities(). + Return(boot.BootCapabilitiesResponse{}, nil) man2.EXPECT(). GetBootService(). Return(cimBoot.BootService{}, ErrGeneral) + man2.EXPECT(). + GetCIMBootSourceSetting(). + Return(cimBoot.Response{}, nil) + man2.EXPECT(). + GetBootData(). + Return(boot.BootSettingDataResponse{}, nil) }, repoMock: func(repo *mocks.MockDeviceManagementRepository) { repo.EXPECT(). GetByID(context.Background(), device.GUID, ""). Return(device, nil) }, - res: dto.Features{}, - resV2: dtov2.Features{}, - err: ErrGeneral, + res: dto.Features{ + UserConsent: "kvm", + EnableSOL: true, + EnableIDER: true, + EnableKVM: true, + Redirection: true, + }, + resV2: dtov2.Features{ + UserConsent: "kvm", + EnableSOL: true, + EnableIDER: true, + EnableKVM: true, + Redirection: true, + KVMAvailable: true, + }, + err: nil, }, { name: "SetFeatures fails on setRedirectionService - SetAMTRedirectionService error", @@ -2028,6 +2149,177 @@ func TestSetFeatures(t *testing.T) { }, err: ErrGeneral, }, + { + name: "setRPE fails on GetBootCapabilities error", + action: 0, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + RequestAMTRedirectionServiceStateChange(true, true). + Return(redirection.EnableIDERAndSOL, 1, nil) + man2.EXPECT(). + SetKVMRedirection(true). + Return(1, nil) + man2.EXPECT(). + GetAMTRedirectionService(). + Return(redirection.Response{ + Body: redirection.Body{ + GetAndPutResponse: redirection.RedirectionResponse{ + EnabledState: 32771, + ListenerEnabled: true, + }, + }, + }, nil) + man2.EXPECT(). + SetAMTRedirectionService(&redirection.RedirectionRequest{ + EnabledState: redirection.EnabledState(redirection.EnableIDERAndSOL), + ListenerEnabled: true, + }). + Return(redirection.Response{ + Body: redirection.Body{ + GetAndPutResponse: redirection.RedirectionResponse{ + EnabledState: 32771, + ListenerEnabled: true, + }, + }, + }, nil) + man2.EXPECT(). + GetIPSOptInService(). + Return(optin.Response{ + Body: optin.Body{ + GetAndPutResponse: optin.OptInServiceResponse{ + OptInRequired: 1, + OptInState: 0, + }, + }, + }, nil) + man2.EXPECT(). + SetIPSOptInService(optin.OptInServiceRequest{ + OptInRequired: 1, + OptInState: 0, + }). + Return(nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{}, ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + res: dto.Features{ + EnableSOL: true, + EnableIDER: true, + EnableKVM: true, + Redirection: true, + }, + resV2: dtov2.Features{ + EnableSOL: true, + EnableIDER: true, + EnableKVM: true, + Redirection: true, + KVMAvailable: true, + }, + err: ErrGeneral, + }, + { + name: "setRPE succeeds with PlatformErase supported and enabled", + action: 0, + manMock: func(man *mocks.MockWSMAN, man2 *mocks.MockManagement) { + man.EXPECT(). + SetupWsmanClient(gomock.Any(), false, true). + Return(man2, nil) + man2.EXPECT(). + RequestAMTRedirectionServiceStateChange(true, true). + Return(redirection.EnableIDERAndSOL, 1, nil) + man2.EXPECT(). + SetKVMRedirection(true). + Return(1, nil) + man2.EXPECT(). + GetAMTRedirectionService(). + Return(redirection.Response{ + Body: redirection.Body{ + GetAndPutResponse: redirection.RedirectionResponse{ + EnabledState: 32771, + ListenerEnabled: true, + }, + }, + }, nil) + man2.EXPECT(). + SetAMTRedirectionService(&redirection.RedirectionRequest{ + EnabledState: redirection.EnabledState(redirection.EnableIDERAndSOL), + ListenerEnabled: true, + }). + Return(redirection.Response{ + Body: redirection.Body{ + GetAndPutResponse: redirection.RedirectionResponse{ + EnabledState: 32771, + ListenerEnabled: true, + }, + }, + }, nil) + man2.EXPECT(). + GetIPSOptInService(). + Return(optin.Response{ + Body: optin.Body{ + GetAndPutResponse: optin.OptInServiceResponse{ + OptInRequired: 1, + OptInState: 0, + }, + }, + }, nil) + man2.EXPECT(). + SetIPSOptInService(optin.OptInServiceRequest{ + OptInRequired: 1, + OptInState: 0, + }). + Return(nil) + man2.EXPECT(). + GetBootCapabilities(). + Return(boot.BootCapabilitiesResponse{PlatformErase: 3}, nil) + man2.EXPECT(). + SetRPEEnabled(true). + Return(nil) + // BootServiceStateChange fails (non-fatal), so getOneClickRecoverySettings + // is skipped and the RPE fields set by setRPE are preserved. + // OCR=true + RPE=true → state 32771 + man2.EXPECT(). + BootServiceStateChange(32771). + Return(cimBoot.BootService{}, ErrGeneral) + }, + repoMock: func(repo *mocks.MockDeviceManagementRepository) { + repo.EXPECT(). + GetByID(context.Background(), device.GUID, ""). + Return(device, nil) + }, + res: dto.Features{ + UserConsent: "kvm", + EnableSOL: true, + EnableIDER: true, + EnableKVM: true, + Redirection: true, + OCR: false, + RPE: true, + RPESupported: true, + RPECaps: 3, + }, + resV2: dtov2.Features{ + UserConsent: "kvm", + EnableSOL: true, + EnableIDER: true, + EnableKVM: true, + Redirection: true, + KVMAvailable: true, + OCR: false, + RPE: true, + RPESupported: true, + RPECaps: 3, + }, + err: nil, + }, } for _, tc := range tests { @@ -2042,11 +2334,15 @@ func TestSetFeatures(t *testing.T) { tc.repoMock(repo) - // Use the appropriate input for the OCR disabled test + // Use the appropriate input for tests that require non-default features var inputFeatures dto.Features - if tc.name == "success with OCR disabled" { + + switch tc.name { + case "success with OCR disabled": inputFeatures = featureSetDisabledOCR - } else { + case "setRPE succeeds with PlatformErase supported and enabled": + inputFeatures = featureSetWithRPE + default: inputFeatures = featureSet } diff --git a/internal/usecase/devices/interfaces.go b/internal/usecase/devices/interfaces.go index 1fe76e625..2ba88afc1 100644 --- a/internal/usecase/devices/interfaces.go +++ b/internal/usecase/devices/interfaces.go @@ -70,6 +70,9 @@ type ( GetHardwareInfo(ctx context.Context, guid string) (dto.HardwareInfo, error) GetPowerState(ctx context.Context, guid string) (dto.PowerState, error) GetPowerCapabilities(ctx context.Context, guid string) (dto.PowerCapabilities, error) + GetBootCapabilities(ctx context.Context, guid string) (dto.BootCapabilities, error) + SetRPEEnabled(ctx context.Context, guid string, enabled bool) error + SendRemoteErase(ctx context.Context, guid string, eraseMask int) error GetGeneralSettings(ctx context.Context, guid string) (dto.GeneralSettings, error) CancelUserConsent(ctx context.Context, guid string) (dto.UserConsentMessage, error) GetUserConsentCode(ctx context.Context, guid string) (dto.UserConsentMessage, error) diff --git a/internal/usecase/devices/wsman/interfaces.go b/internal/usecase/devices/wsman/interfaces.go index 14848ec7b..538ca18e6 100644 --- a/internal/usecase/devices/wsman/interfaces.go +++ b/internal/usecase/devices/wsman/interfaces.go @@ -46,6 +46,7 @@ type Management interface { GetIPSPowerManagementService() (ipspower.PowerManagementService, error) RequestOSPowerSavingStateChange(osPowerSavingState ipspower.OSPowerSavingState) (ipspower.PowerActionResponse, error) GetPowerCapabilities() (boot.BootCapabilitiesResponse, error) + GetBootCapabilities() (boot.BootCapabilitiesResponse, error) GetGeneralSettings() (interface{}, error) CancelUserConsentRequest() (optin.Response, error) GetUserConsentCode() (optin.Response, error) @@ -72,4 +73,6 @@ type Management interface { SetIPSKVMRedirectionSettingData(data *kvmredirection.KVMRedirectionSettingsRequest) (kvmredirection.Response, error) DeleteCertificate(instanceID string) error SetLinkPreference(linkPreference, timeout uint32) (int, error) + SetRPEEnabled(enabled bool) error + SendRemoteErase(eraseMask int) error } diff --git a/internal/usecase/devices/wsman/message.go b/internal/usecase/devices/wsman/message.go index 709b26ed6..6c4f29cfd 100644 --- a/internal/usecase/devices/wsman/message.go +++ b/internal/usecase/devices/wsman/message.go @@ -2,7 +2,11 @@ package wsman import ( gotls "crypto/tls" + "encoding/base64" + "encoding/binary" "errors" + "fmt" + "log" "net" "sync" "time" @@ -69,6 +73,8 @@ const ( maxReadRecords = 390 ) +var ErrRPENotEnabled = errors.New("Remote Platform Erase is not enabled by the BIOS on this device") + var ( connections = make(map[string]*ConnectionEntry) connectionsMu sync.RWMutex @@ -628,6 +634,117 @@ func (c *ConnectionEntry) GetPowerCapabilities() (boot.BootCapabilitiesResponse, return response.Body.BootCapabilitiesGetResponse, nil } +func (c *ConnectionEntry) GetBootCapabilities() (boot.BootCapabilitiesResponse, error) { + response, err := c.WsmanMessages.AMT.BootCapabilities.Get() + if err != nil { + return boot.BootCapabilitiesResponse{}, err + } + + return response.Body.BootCapabilitiesGetResponse, nil +} + +func (c *ConnectionEntry) SetRPEEnabled(enabled bool) error { + bootData, err := c.WsmanMessages.AMT.BootSettingData.Get() + if err != nil { + return err + } + + current := bootData.Body.BootSettingDataGetResponse + + _, err = c.WsmanMessages.AMT.BootSettingData.Put(boot.BootSettingDataRequest{ + ElementName: current.ElementName, + InstanceID: current.InstanceID, + OwningEntity: current.OwningEntity, + PlatformErase: enabled, + }) + + return err +} + +func (c *ConnectionEntry) SendRemoteErase(eraseMask int) error { + // Step 1 (AMT docs): Read AMT_BootSettingData to check RPEEnabled and current state. + bootData, err := c.WsmanMessages.AMT.BootSettingData.Get() + if err != nil { + return fmt.Errorf("BootSettingData.Get: %w", err) + } + + current := bootData.Body.BootSettingDataGetResponse + + log.Printf("SendRemoteErase: step 1 GET XMLInput=%s", bootData.XMLInput) + log.Printf("SendRemoteErase: step 1 GET XMLOutput=%s", bootData.XMLOutput) + log.Printf("SendRemoteErase: step 1 state RPEEnabled=%v PlatformErase=%v BIOSLastStatus=%v eraseMask=0x%08x", + current.RPEEnabled, current.PlatformErase, current.BIOSLastStatus, eraseMask) + + if !current.RPEEnabled { + return ErrRPENotEnabled + } + + // Step 1a (AMT docs): Enable RPE mode in the boot service (32770 = OCR disabled, RPE enabled). + // This is required when the boot service is in OCR mode (32769) from a prior SetFeatures call. + const rpeEnabledState = 32770 + rscResp, rscErr := c.WsmanMessages.CIM.BootService.RequestStateChange(rpeEnabledState) + log.Printf("SendRemoteErase: step 1a RequestStateChange(%d) XMLInput=%s XMLOutput=%s err=%v", + rpeEnabledState, rscResp.XMLInput, rscResp.XMLOutput, rscErr) + + // Build TLV for the device bitmask (RPE ParameterTypeID=1, 4 bytes, mandatory). + // Format per Intel AMT spec: [vendor:2 LE][typeID:2 LE][length:4 LE][value:4 LE] + const rpeTLVLen = 12 + + tlvBuf := make([]byte, rpeTLVLen) + binary.LittleEndian.PutUint16(tlvBuf[0:], 0x8086) // Intel vendor prefix + binary.LittleEndian.PutUint16(tlvBuf[2:], 1) // ParameterTypeID = 1 (device bitmask) + binary.LittleEndian.PutUint32(tlvBuf[4:], 4) // value length = 4 bytes + binary.LittleEndian.PutUint32(tlvBuf[8:], uint32(eraseMask)) // erase target bitmask + + encodedParams := base64.StdEncoding.EncodeToString(tlvBuf) + log.Printf("SendRemoteErase: step 2 TLV eraseMask=0x%08x encoded=%s", eraseMask, encodedParams) + + // Step 2 (AMT docs): PUT PlatformErase=true with UefiBootParametersArray specifying targets. + putReq := boot.BootSettingDataRequest{ + ElementName: current.ElementName, + InstanceID: current.InstanceID, + OwningEntity: current.OwningEntity, + PlatformErase: true, + UefiBootParametersArray: encodedParams, + UefiBootNumberOfParams: 1, + } + + putResp, putErr := c.WsmanMessages.AMT.BootSettingData.Put(putReq) + log.Printf("SendRemoteErase: step 2 PUT XMLInput=%s", putResp.XMLInput) + log.Printf("SendRemoteErase: step 2 PUT XMLOutput=%s", putResp.XMLOutput) + log.Printf("SendRemoteErase: step 2 PUT err=%v", putErr) + + if putErr != nil { + // Verify whether PlatformErase latched from SetFeatures despite PUT failure. + verifyData, verifyErr := c.WsmanMessages.AMT.BootSettingData.Get() + if verifyErr == nil { + v := verifyData.Body.BootSettingDataGetResponse + log.Printf("SendRemoteErase: step 2 verify PlatformErase=%v BIOSLastStatus=%v", v.PlatformErase, v.BIOSLastStatus) + + if !v.PlatformErase { + return fmt.Errorf("SendRemoteErase: PlatformErase did not latch (PUT err=%v); aborting to avoid reboot with erase disabled", putErr) + } + + log.Printf("SendRemoteErase: step 2 PUT failed but PlatformErase already latched by SetFeatures - continuing without mask") + } else { + return fmt.Errorf("SendRemoteErase: PUT failed (%v) and verify GET failed (%v); aborting", putErr, verifyErr) + } + } + + // Step 4 (AMT docs): Activate the RPE boot config role so it executes on next restart. + _, err = c.WsmanMessages.CIM.BootService.SetBootConfigRole("Intel(r) AMT: Boot Configuration 0", 1) + log.Printf("SendRemoteErase: step 4 SetBootConfigRole(1) err=%v", err) + + // Step 5 (AMT docs): Restart the platform. + if _, err = c.WsmanMessages.CIM.PowerManagementService.RequestPowerStateChange(power.MasterBusReset); err != nil { + return fmt.Errorf("RequestPowerStateChange: %w", err) + } + + log.Println("SendRemoteErase: step 5 restart triggered") + + return nil +} + func (c *ConnectionEntry) GetGeneralSettings() (interface{}, error) { response, err := c.WsmanMessages.AMT.GeneralSettings.Get() if err != nil { diff --git a/internal/usecase/profiles/usecase.go b/internal/usecase/profiles/usecase.go index 81d20f00a..a8de30103 100644 --- a/internal/usecase/profiles/usecase.go +++ b/internal/usecase/profiles/usecase.go @@ -326,7 +326,7 @@ func (uc *UseCase) BuildConfigurationObject(profileName string, data *entity.Pro Wired: config.Wired{ DHCPEnabled: data.DHCPEnabled, IPSyncEnabled: data.IPSyncEnabled, - SharedStaticIP: !data.DHCPEnabled, + SharedStaticIP: false, }, Wireless: config.Wireless{ WiFiSyncEnabled: data.LocalWiFiSyncEnabled, diff --git a/internal/usecase/profiles/usecase_test.go b/internal/usecase/profiles/usecase_test.go index 3bb442b8b..428667602 100644 --- a/internal/usecase/profiles/usecase_test.go +++ b/internal/usecase/profiles/usecase_test.go @@ -942,78 +942,6 @@ func TestBuildConfigurationObject(t *testing.T) { }, }, }, - { - name: "static IP mode sets SharedStaticIP to true", - profile: &entity.Profile{ - ProfileName: "test-profile-static", - Tags: "static", - DHCPEnabled: false, - IPSyncEnabled: true, - Activation: "acmactivate", - AMTPassword: "testAMTPassword", - MEBXPassword: "testMEBXPassword", - TLSMode: 0, - KVMEnabled: true, - SOLEnabled: false, - IDEREnabled: false, - UserConsent: "All", - }, - domain: &entity.Domain{ - ProvisioningCert: "testCert", - ProvisioningCertPassword: "testCertPwd", - }, - wifi: []config.WirelessProfile{}, - expected: config.Configuration{ - Name: "test-profile-static", - Tags: []string{"static"}, - Configuration: config.RemoteManagement{ - GeneralSettings: config.GeneralSettings{ - SharedFQDN: false, - NetworkInterfaceEnabled: 0, - PingResponseEnabled: false, - }, - Network: config.Network{ - Wired: config.Wired{ - DHCPEnabled: false, - IPSyncEnabled: true, - SharedStaticIP: true, - }, - Wireless: config.Wireless{ - Profiles: []config.WirelessProfile{}, - }, - }, - Redirection: config.Redirection{ - Enabled: true, - Services: config.Services{ - KVM: true, - SOL: false, - IDER: false, - }, - UserConsent: "All", - }, - TLS: config.TLS{ - MutualAuthentication: false, - Enabled: false, - AllowNonTLS: false, - }, - EnterpriseAssistant: config.EnterpriseAssistant{ - URL: "http://test.com:8080", - Username: "username", - Password: "password", - }, - AMTSpecific: config.AMTSpecific{ - ControlMode: "acmactivate", - AdminPassword: "testAMTPassword", - MEBXPassword: "testMEBXPassword", - ProvisioningCert: "testCert", - ProvisioningCertPwd: "testCertPwd", - CIRA: config.CIRA{ - EnvironmentDetection: []string{}, - }, - }, - }, - }, - }, } for _, tc := range tests { diff --git a/internal/usecase/sqldb/device_test.go b/internal/usecase/sqldb/device_test.go index 1f31250ee..aae8602e7 100644 --- a/internal/usecase/sqldb/device_test.go +++ b/internal/usecase/sqldb/device_test.go @@ -17,6 +17,24 @@ import ( "github.com/device-management-toolkit/console/pkg/db" ) +const BuilderErrorTestName = "Builder error" + +// alwaysErrFormat is a squirrel PlaceholderFormat that always fails ReplacePlaceholders, +// causing builder.ToSql() to return an error. +type alwaysErrFormat struct{} + +func (alwaysErrFormat) ReplacePlaceholders(_ string) (string, error) { + return "", errors.New("placeholder error") +} + +func createSQLConfigWithBuilderError(dbConn *sql.DB) *db.SQL { + return &db.SQL{ + Builder: squirrel.StatementBuilder.PlaceholderFormat(alwaysErrFormat{}), + Pool: dbConn, + IsEmbedded: true, + } +} + var ( crthash = "certhash" Certhash = &crthash @@ -1179,6 +1197,14 @@ func TestDeviceRepo_UpdateConnectionStatus(t *testing.T) { err: &sqldb.DatabaseError{}, verify: func(_ *testing.T, _ *sql.DB) {}, }, + { + name: BuilderErrorTestName, + setup: func(_ *sql.DB) {}, + guid: "guid1", + status: true, + err: &sqldb.DatabaseError{}, + verify: func(_ *testing.T, _ *sql.DB) {}, + }, } for _, tc := range tests { @@ -1191,7 +1217,12 @@ func TestDeviceRepo_UpdateConnectionStatus(t *testing.T) { tc.setup(dbConn) - sqlConfig := CreateSQLConfig(dbConn, tc.name == QueryExecutionErrorTestName) + var sqlConfig *db.SQL + if tc.name == BuilderErrorTestName { + sqlConfig = createSQLConfigWithBuilderError(dbConn) + } else { + sqlConfig = CreateSQLConfig(dbConn, tc.name == QueryExecutionErrorTestName) + } mockLog := mocks.NewMockLogger(nil) repo := sqldb.NewDeviceRepo(sqlConfig, mockLog) @@ -1258,6 +1289,13 @@ func TestDeviceRepo_UpdateLastSeen(t *testing.T) { err: &sqldb.DatabaseError{}, verify: func(_ *testing.T, _ *sql.DB) {}, }, + { + name: BuilderErrorTestName, + setup: func(_ *sql.DB) {}, + guid: "guid1", + err: &sqldb.DatabaseError{}, + verify: func(_ *testing.T, _ *sql.DB) {}, + }, } for _, tc := range tests { @@ -1270,7 +1308,12 @@ func TestDeviceRepo_UpdateLastSeen(t *testing.T) { tc.setup(dbConn) - sqlConfig := CreateSQLConfig(dbConn, tc.name == QueryExecutionErrorTestName) + var sqlConfig *db.SQL + if tc.name == BuilderErrorTestName { + sqlConfig = createSQLConfigWithBuilderError(dbConn) + } else { + sqlConfig = CreateSQLConfig(dbConn, tc.name == QueryExecutionErrorTestName) + } mockLog := mocks.NewMockLogger(nil) repo := sqldb.NewDeviceRepo(sqlConfig, mockLog) diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 227b5fb06..0c6d381ab 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -114,6 +114,6 @@ func (l *logger) log(e *zerolog.Event, m string, args ...any) { if len(args) == 0 { e.Msg(m) } else { - e.Msgf(m, args...) + e.Fields(args).Msg(m) } }