diff --git a/integration-test/collections/console_mps_apis.postman_collection.json b/integration-test/collections/console_mps_apis.postman_collection.json index e7b29ebfc..ca0c10736 100644 --- a/integration-test/collections/console_mps_apis.postman_collection.json +++ b/integration-test/collections/console_mps_apis.postman_collection.json @@ -1148,7 +1148,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\r\n \"guid\": \"143e4567-e89b-12d3-a456-426614174000\",\r\n \"friendlyName\": \"friendlyName\",\r\n \"hostname\": \"hostname\",\r\n \"tags\": [],\r\n \"mpsusername\": \"admin\"\r\n}", + "raw": "{\r\n \"guid\": \"143e4567-e89b-12d3-a456-426614174000\",\r\n \"friendlyName\": \"friendlyName\",\r\n \"hostname\": \"hostname\",\r\n \"tags\": [],\r\n \"mpsusername\": \"admin\",\r\n \"deviceInfo\": {\r\n \"fwVersion\": \"16.1.30\",\r\n \"fwBuild\": \"3400\",\r\n \"fwSku\": \"11\",\r\n \"currentMode\": \"Admin\",\r\n \"features\": \"SOL,IDER,KVM\",\r\n \"ipAddress\": \"10.0.0.12\",\r\n \"lastUpdated\": \"2026-05-21T00:00:00Z\",\r\n \"tlsMode\": \"TLS 1.2\",\r\n \"upid\": {\r\n \"oemPlatformIdType\": \"Not Set (0)\",\r\n \"oemId\": \"\",\r\n \"csmeId\": \"4A45A39C5ED9462082510000\"\r\n },\r\n \"amtEnabledInBIOS\": true,\r\n \"meInterfaceVersion\": \"16.1.25.2124\",\r\n \"dhcpEnabled\": true,\r\n \"certHashes\": [\r\n \"a1b2c3\",\r\n \"d4e5f6\"\r\n ],\r\n \"lmsInstalled\": true,\r\n \"lmsVersion\": \"2410.5.0.0\",\r\n \"osName\": \"linux\",\r\n \"osVersion\": \"6.8.0-51-generic\",\r\n \"osDistro\": \"Ubuntu 24.04 LTS\",\r\n \"cpuModel\": \"Intel(R) Core(TM) Ultra 7 165H\",\r\n \"osIpAddress\": \"10.49.76.163\",\r\n \"ethernetAdapterCount\": 2,\r\n \"monitorConnected\": true,\r\n \"ieee8021xEnabled\": false\r\n }\r\n}", "options": { "raw": { "language": "json" @@ -1217,7 +1217,7 @@ "});\r", "pm.test(\"Expect an error with wrong device guid format\",function(){\r", " var jsonData = pm.response.json();\r", - " pm.expect(jsonData.error).to.eq(\"Error not found\")\r", + " pm.expect(jsonData.error).to.eq(\"device not found\")\r", "})" ], "type": "text/javascript", @@ -1307,7 +1307,7 @@ "});\r", "pm.test(\"Expect to return device info\",function(){\r", " var jsonData = pm.response.json();\r", - " pm.expect(jsonData.error).to.be.equal(\"Error not found\")\r", + " pm.expect(jsonData.error).to.be.equal(\"device not found\")\r", "})" ], "type": "text/javascript", @@ -1828,7 +1828,7 @@ "});\r", "pm.test(\"Expect an error for invalid guid\", function () {\r", " var jsonData = pm.response.json();\r", - " pm.expect(jsonData.error).to.eq(\"Error not found\")\r", + " pm.expect(jsonData.error).to.eq(\"device not found\")\r", "});" ], "type": "text/javascript", @@ -1927,7 +1927,7 @@ "});\r", "pm.test(\"Expect an error when there is no device\", function () {\r", " var jsonData = pm.response.json();\r", - " pm.expect(jsonData.error).to.eq(\"Error not found\")\r", + " pm.expect(jsonData.error).to.eq(\"device not found\")\r", "});" ], "type": "text/javascript", diff --git a/internal/controller/httpapi/v1/devices.go b/internal/controller/httpapi/v1/devices.go index 8b6b356d8..006d4f70a 100644 --- a/internal/controller/httpapi/v1/devices.go +++ b/internal/controller/httpapi/v1/devices.go @@ -207,6 +207,8 @@ func (dr *deviceRoutes) insert(c *gin.Context) { // Keys are lowercased so callers can match against setter maps regardless of // client casing (encoding/json unmarshals case-insensitively). +// Nested objects are flattened with dot notation (for example, +// "deviceinfo.fwversion") so PATCH handlers can deep-merge object fields. func providedJSONFields(c *gin.Context) (map[string]bool, error) { var raw map[string]json.RawMessage if err := c.ShouldBindBodyWithJSON(&raw); err != nil { @@ -214,13 +216,34 @@ func providedJSONFields(c *gin.Context) (map[string]bool, error) { } fields := make(map[string]bool, len(raw)) - for k := range raw { - fields[strings.ToLower(k)] = true + for k, v := range raw { + key := strings.ToLower(k) + fields[key] = true + collectNestedJSONFields(key, v, fields, 0) } return fields, nil } +const maxNestedJSONFieldDepth = 16 + +func collectNestedJSONFields(prefix string, raw json.RawMessage, fields map[string]bool, depth int) { + if depth >= maxNestedJSONFieldDepth { + return + } + + var obj map[string]json.RawMessage + if err := json.Unmarshal(raw, &obj); err != nil { + return + } + + for k, v := range obj { + path := prefix + "." + strings.ToLower(k) + fields[path] = true + collectNestedJSONFields(path, v, fields, depth+1) + } +} + func (dr *deviceRoutes) update(c *gin.Context) { var device dto.Device if err := c.ShouldBindBodyWithJSON(&device); err != nil { diff --git a/internal/controller/httpapi/v1/devices_test.go b/internal/controller/httpapi/v1/devices_test.go index 0d20315d3..49c49f430 100644 --- a/internal/controller/httpapi/v1/devices_test.go +++ b/internal/controller/httpapi/v1/devices_test.go @@ -442,6 +442,137 @@ func TestDevicesUpdatePartialPatchMixedCaseKeys(t *testing.T) { require.Equal(t, http.StatusOK, w.Code) } +func TestDevicesUpdatePartialPatchTracksDeviceInfoSubfields(t *testing.T) { + t.Parallel() + + guid := testDeviceGUID + + incoming := &dto.Device{ + GUID: guid, + DeviceInfo: &dto.DeviceInfo{ + FWVersion: "16.1.30", + }, + } + + expectedFields := map[string]bool{ + "guid": true, + "deviceinfo": true, + "deviceinfo.fwversion": true, + } + + devicesFeature, engine := devicesTest(t) + + devicesFeature.EXPECT(). + Update(context.Background(), incoming, expectedFields). + Return(incoming, nil) + + body := []byte(`{"guid":"` + guid + `","deviceInfo":{"fwVersion":"16.1.30"}}`) + + req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, "/api/v1/devices", bytes.NewBuffer(body)) + require.NoError(t, err) + + w := httptest.NewRecorder() + engine.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) +} + +func TestDevicesInsertAcceptsFullDeviceInfo(t *testing.T) { + t.Parallel() + + lmsInstalled := false + amtEnabledInBIOS := true + dhcpEnabled := true + ethernetAdapterCount := 2 + monitorConnected := true + ieee8021xEnabled := false + + incoming := &dto.Device{ + GUID: testDeviceGUID, + Hostname: "test-device", + DeviceInfo: &dto.DeviceInfo{ + FWVersion: "16.1.30", + FWBuild: "3400", + FWSku: "11", + CurrentMode: "Admin", + Features: "SOL,IDER,KVM", + IPAddress: "10.0.0.12", + LastUpdated: &timeNow, + TLSMode: "TLS 1.2", + UPID: map[string]json.RawMessage{ + "oemPlatformIdType": json.RawMessage(`"Not Set (0)"`), + "oemId": json.RawMessage(`""`), + "csmeId": json.RawMessage(`"4A45A39C5ED9462082510000"`), + }, + AMTEnabledInBIOS: &amtEnabledInBIOS, + MEInterfaceVersion: "16.1.25.2124", + DHCPEnabled: &dhcpEnabled, + CertHashes: []string{"a1b2c3", "d4e5f6"}, + LMSInstalled: &lmsInstalled, + LMSVersion: "2410.5.0.0", + OSName: "linux", + OSVersion: "6.8.0-51-generic", + OSDistro: "Ubuntu 24.04 LTS", + CPUModel: "Intel(R) Core(TM) Ultra 7 165H", + OSIPAddress: "10.49.76.163", + EthernetAdapterCount: ðernetAdapterCount, + MonitorConnected: &monitorConnected, + IEEE8021XEnabled: &ieee8021xEnabled, + }, + } + + devicesFeature, engine := devicesTest(t) + + devicesFeature.EXPECT(). + Insert(context.Background(), incoming). + Return(incoming, nil) + + body := []byte(`{ + "guid":"` + testDeviceGUID + `", + "hostname":"test-device", + "deviceInfo":{ + "fwVersion":"16.1.30", + "fwBuild":"3400", + "fwSku":"11", + "currentMode":"Admin", + "features":"SOL,IDER,KVM", + "ipAddress":"10.0.0.12", + "lastUpdated":"` + timeNow.Format(time.RFC3339Nano) + `", + "tlsMode":"TLS 1.2", + "upid":{ + "oemPlatformIdType":"Not Set (0)", + "oemId":"", + "csmeId":"4A45A39C5ED9462082510000" + }, + "amtEnabledInBIOS":true, + "meInterfaceVersion":"16.1.25.2124", + "dhcpEnabled":true, + "certHashes":["a1b2c3","d4e5f6"], + "lmsInstalled":false, + "lmsVersion":"2410.5.0.0", + "osName":"linux", + "osVersion":"6.8.0-51-generic", + "osDistro":"Ubuntu 24.04 LTS", + "cpuModel":"Intel(R) Core(TM) Ultra 7 165H", + "osIpAddress":"10.49.76.163", + "ethernetAdapterCount":2, + "monitorConnected":true, + "ieee8021xEnabled":false + } + }`) + + req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "/api/v1/devices", bytes.NewBuffer(body)) + require.NoError(t, err) + + w := httptest.NewRecorder() + engine.ServeHTTP(w, req) + + require.Equal(t, http.StatusCreated, w.Code) + + expected, _ := json.Marshal(incoming) + require.Equal(t, string(expected), w.Body.String()) +} + // TestLoginRedirection verifies the device redirection token endpoint func TestLoginRedirection(t *testing.T) { t.Parallel() diff --git a/internal/entity/dto/v1/device.go b/internal/entity/dto/v1/device.go index 538d9bcc6..27f51d10f 100644 --- a/internal/entity/dto/v1/device.go +++ b/internal/entity/dto/v1/device.go @@ -1,6 +1,7 @@ package dto import ( + "encoding/json" "time" ) @@ -37,14 +38,29 @@ type Device struct { } type DeviceInfo struct { - FWVersion string `json:"fwVersion"` - FWBuild string `json:"fwBuild"` - FWSku string `json:"fwSku"` - CurrentMode string `json:"currentMode"` - Features string `json:"features"` - IPAddress string `json:"ipAddress"` - LastUpdated time.Time `json:"lastUpdated"` - LMSInstalled *bool `json:"lmsInstalled,omitempty"` + FWVersion string `json:"fwVersion"` + FWBuild string `json:"fwBuild"` + FWSku string `json:"fwSku"` + CurrentMode string `json:"currentMode"` + Features string `json:"features"` + IPAddress string `json:"ipAddress"` + LastUpdated *time.Time `json:"lastUpdated,omitempty"` + LMSInstalled *bool `json:"lmsInstalled,omitempty"` + LMSVersion string `json:"lmsVersion,omitempty"` + TLSMode string `json:"tlsMode,omitempty"` + UPID map[string]json.RawMessage `json:"upid,omitempty"` + AMTEnabledInBIOS *bool `json:"amtEnabledInBIOS,omitempty"` + MEInterfaceVersion string `json:"meInterfaceVersion,omitempty"` + DHCPEnabled *bool `json:"dhcpEnabled,omitempty"` + CertHashes []string `json:"certHashes,omitempty"` + OSName string `json:"osName,omitempty"` + OSVersion string `json:"osVersion,omitempty"` + OSDistro string `json:"osDistro,omitempty"` + CPUModel string `json:"cpuModel,omitempty"` + OSIPAddress string `json:"osIpAddress,omitempty"` + EthernetAdapterCount *int `json:"ethernetAdapterCount,omitempty"` + MonitorConnected *bool `json:"monitorConnected,omitempty"` + IEEE8021XEnabled *bool `json:"ieee8021xEnabled,omitempty"` } type Explorer struct { diff --git a/internal/entity/dto/v1/device_test.go b/internal/entity/dto/v1/device_test.go new file mode 100644 index 000000000..b21073fdd --- /dev/null +++ b/internal/entity/dto/v1/device_test.go @@ -0,0 +1,64 @@ +package dto + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestDeviceInfoJSONRoundTrip(t *testing.T) { + t.Parallel() + + amtEnabled := true + dhcpEnabled := true + lmsInstalled := true + ethernetAdapterCount := 2 + monitorConnected := true + ieee8021xEnabled := false + lastUpdated := time.Date(2026, 5, 21, 0, 0, 0, 0, time.UTC) + + info := DeviceInfo{ + FWVersion: "16.1.30", + FWBuild: "3400", + FWSku: "11", + CurrentMode: "Admin", + Features: "SOL,IDER,KVM", + IPAddress: "10.0.0.12", + LastUpdated: &lastUpdated, + TLSMode: "TLS 1.2", + UPID: map[string]json.RawMessage{ + "oemPlatformIdType": json.RawMessage(`"Not Set (0)"`), + "oemId": json.RawMessage(`""`), + "csmeId": json.RawMessage(`"4A45A39C5ED94620"`), + }, + AMTEnabledInBIOS: &amtEnabled, + MEInterfaceVersion: "16.1.25.2124", + DHCPEnabled: &dhcpEnabled, + CertHashes: []string{"a1b2c3", "d4e5f6"}, + LMSInstalled: &lmsInstalled, + LMSVersion: "2410.5.0.0", + OSName: "linux", + OSVersion: "6.8.0-51-generic", + OSDistro: "Ubuntu 24.04 LTS", + CPUModel: "Intel(R) Core(TM) Ultra 7 165H", + OSIPAddress: "10.49.76.163", + EthernetAdapterCount: ðernetAdapterCount, + MonitorConnected: &monitorConnected, + IEEE8021XEnabled: &ieee8021xEnabled, + } + + encoded, err := json.Marshal(info) + require.NoError(t, err) + + var decoded DeviceInfo + require.NoError(t, json.Unmarshal(encoded, &decoded)) + + require.Equal(t, info.TLSMode, decoded.TLSMode) + require.Equal(t, info.MEInterfaceVersion, decoded.MEInterfaceVersion) + require.Equal(t, info.CertHashes, decoded.CertHashes) + require.Equal(t, info.LMSVersion, decoded.LMSVersion) + require.NotNil(t, decoded.LMSInstalled) + require.Equal(t, *info.LMSInstalled, *decoded.LMSInstalled) +} diff --git a/internal/usecase/devices/repo.go b/internal/usecase/devices/repo.go index 28e067d52..b050bc2fd 100644 --- a/internal/usecase/devices/repo.go +++ b/internal/usecase/devices/repo.go @@ -20,6 +20,8 @@ var ( ErrCancelled = dto.CanceledError{Console: ErrDeviceUseCase} ) +const deviceNotFoundMessage = "device not found" + // History - getting translate history from store. func (uc *UseCase) GetCount(ctx context.Context, tenantID string) (int, error) { count, err := uc.repo.GetCount(ctx, tenantID) @@ -83,7 +85,7 @@ func (uc *UseCase) GetByID(ctx context.Context, guid, tenantID string, includeSe } if data == nil || data.GUID == "" { - return nil, ErrNotFound + return nil, ErrNotFound.WrapWithMessage("GetByID", "uc.repo.GetByID", deviceNotFoundMessage) } d2, err := uc.entityToDTO(data) @@ -192,7 +194,7 @@ func (uc *UseCase) Delete(ctx context.Context, guid, tenantID string) error { } if !isSuccessful { - return ErrNotFound + return ErrNotFound.WrapWithMessage("Delete", "uc.repo.Delete", deviceNotFoundMessage) } return nil @@ -222,7 +224,7 @@ func (uc *UseCase) Update(ctx context.Context, d *dto.Device, fields map[string] } if !updated { - return nil, ErrNotFound.Wrap("Update", "uc.repo.Update", nil) + return nil, ErrNotFound.WrapWithMessage("Update", "uc.repo.Update", deviceNotFoundMessage) } updateDevice, err := uc.repo.GetByID(ctx, d1.GUID, d1.TenantID) diff --git a/internal/usecase/devices/repo_test.go b/internal/usecase/devices/repo_test.go index aba5caeac..c0695c7a9 100644 --- a/internal/usecase/devices/repo_test.go +++ b/internal/usecase/devices/repo_test.go @@ -2,6 +2,7 @@ package devices_test import ( "context" + "encoding/json" "testing" "github.com/stretchr/testify/require" @@ -10,6 +11,7 @@ import ( "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" + "github.com/device-management-toolkit/console/internal/repoerrors" "github.com/device-management-toolkit/console/internal/usecase/devices" "github.com/device-management-toolkit/console/pkg/logger" ) @@ -18,6 +20,10 @@ func ptr(s string) *string { return &s } +func boolPtr(v bool) *bool { + return &v +} + type testUsecase struct { name string guid string @@ -230,7 +236,10 @@ func TestGetByID(t *testing.T) { if tc.err != nil { require.Error(t, err) - require.Contains(t, err.Error(), tc.err.Error()) + + var notFoundErr repoerrors.NotFoundError + require.ErrorAs(t, err, ¬FoundErr) + require.Equal(t, "device not found", notFoundErr.Console.FriendlyMessage()) } else { require.NoError(t, err) require.Equal(t, tc.res, got) @@ -280,7 +289,10 @@ func TestDelete(t *testing.T) { if tc.err != nil { require.Error(t, err) - require.Equal(t, err.Error(), tc.err.Error()) + + var notFoundErr repoerrors.NotFoundError + require.ErrorAs(t, err, ¬FoundErr) + require.Equal(t, "device not found", notFoundErr.Console.FriendlyMessage()) } else { require.NoError(t, err) } @@ -740,6 +752,7 @@ func TestUpdatePartial(t *testing.T) { GUID: "device-guid-123", TenantID: "tenant-id-456", Hostname: "old-hostname", + DeviceInfo: `{"fwVersion":"11.8.50","fwBuild":"3400","ipAddress":"10.0.0.1","lmsInstalled":false}`, Tags: "lab,floor-2", MPSUsername: "admin", Username: "amtadmin", @@ -753,14 +766,30 @@ func TestUpdatePartial(t *testing.T) { GUID: "device-guid-123", TenantID: "tenant-id-456", Hostname: "new-hostname", + DeviceInfo: &dto.DeviceInfo{ + FWVersion: "16.1.30", + IPAddress: "10.0.0.55", + LMSInstalled: boolPtr(true), + }, + } + fields := map[string]bool{ + "guid": true, + "tenantId": true, + "hostname": true, + "deviceinfo": true, + "deviceinfo.fwversion": true, + "deviceinfo.ipaddress": true, + "deviceinfo.lmsinstalled": true, } - fields := map[string]bool{"guid": true, "tenantId": true, "hostname": true} // After merge + dtoToEntity (MockCrypto re-encrypts plaintext to "encrypted"): + // Only fields without omitempty tag are included in JSON for zero values. + expectedDeviceInfoJSON := `{"fwVersion":"16.1.30","fwBuild":"3400","fwSku":"","currentMode":"","features":"","ipAddress":"10.0.0.55","lmsInstalled":true}` expectedEntity := &entity.Device{ GUID: "device-guid-123", TenantID: "tenant-id-456", Hostname: "new-hostname", + DeviceInfo: expectedDeviceInfoJSON, Tags: "lab,floor-2", MPSUsername: "admin", Username: "amtadmin", @@ -770,9 +799,15 @@ func TestUpdatePartial(t *testing.T) { } expectedDTO := &dto.Device{ - GUID: "device-guid-123", - TenantID: "tenant-id-456", - Hostname: "new-hostname", + GUID: "device-guid-123", + TenantID: "tenant-id-456", + Hostname: "new-hostname", + DeviceInfo: &dto.DeviceInfo{ + FWVersion: "16.1.30", + FWBuild: "3400", + IPAddress: "10.0.0.55", + LMSInstalled: boolPtr(true), + }, Tags: []string{"lab", "floor-2"}, MPSUsername: "admin", Username: "amtadmin", @@ -785,17 +820,30 @@ func TestUpdatePartial(t *testing.T) { useCase, repo, management := devicesTest(t) - gomock.InOrder( - repo.EXPECT(). - GetByID(context.Background(), "device-guid-123", "tenant-id-456"). - Return(existing, nil), - repo.EXPECT(). - Update(context.Background(), expectedEntity). - Return(true, nil), - repo.EXPECT(). - GetByID(context.Background(), "device-guid-123", "tenant-id-456"). - Return(expectedEntity, nil), - ) + repo.EXPECT(). + GetByID(context.Background(), "device-guid-123", "tenant-id-456"). + Return(existing, nil) + repo.EXPECT(). + Update(context.Background(), gomock.Any()). + DoAndReturn(func(_ context.Context, actualEntity *entity.Device) (bool, error) { + require.NotNil(t, actualEntity) + require.JSONEq(t, expectedDeviceInfoJSON, actualEntity.DeviceInfo) + + expectedWithoutInfo := *expectedEntity + actualWithoutInfo := *actualEntity + expectedWithoutInfo.DeviceInfo = "" + actualWithoutInfo.DeviceInfo = "" + require.Equal(t, expectedWithoutInfo, actualWithoutInfo) + + var actualInfo dto.DeviceInfo + require.NoError(t, json.Unmarshal([]byte(actualEntity.DeviceInfo), &actualInfo)) + require.Equal(t, "3400", actualInfo.FWBuild) + + return true, nil + }) + repo.EXPECT(). + GetByID(context.Background(), "device-guid-123", "tenant-id-456"). + Return(expectedEntity, nil) management.EXPECT().DestroyWsmanClient(*expectedDTO) result, err := useCase.Update(context.Background(), incoming, fields) diff --git a/internal/usecase/devices/usecase.go b/internal/usecase/devices/usecase.go index 50eaad554..03f52c5d9 100644 --- a/internal/usecase/devices/usecase.go +++ b/internal/usecase/devices/usecase.go @@ -35,6 +35,9 @@ const ( // MinAMTVersion - minimum AMT version required for certain features in power capabilities. MinAMTVersion = 9 + + deviceInfoFieldKey = "deviceinfo" + deviceInfoFieldPrefix = deviceInfoFieldKey + "." ) // UseCase -. @@ -156,15 +159,81 @@ var deviceFieldSetters = map[string]func(dst, src *dto.Device){ "usetls": func(dst, src *dto.Device) { dst.UseTLS = src.UseTLS }, "allowselfsigned": func(dst, src *dto.Device) { dst.AllowSelfSigned = src.AllowSelfSigned }, "certhash": func(dst, src *dto.Device) { dst.CertHash = src.CertHash }, - "deviceinfo": func(dst, src *dto.Device) { dst.DeviceInfo = src.DeviceInfo }, + deviceInfoFieldKey: func(dst, src *dto.Device) { dst.DeviceInfo = src.DeviceInfo }, +} + +var deviceInfoFieldSetters = map[string]func(dst, src *dto.DeviceInfo){ + "fwversion": func(dst, src *dto.DeviceInfo) { dst.FWVersion = src.FWVersion }, + "fwbuild": func(dst, src *dto.DeviceInfo) { dst.FWBuild = src.FWBuild }, + "fwsku": func(dst, src *dto.DeviceInfo) { dst.FWSku = src.FWSku }, + "currentmode": func(dst, src *dto.DeviceInfo) { dst.CurrentMode = src.CurrentMode }, + "features": func(dst, src *dto.DeviceInfo) { dst.Features = src.Features }, + "ipaddress": func(dst, src *dto.DeviceInfo) { dst.IPAddress = src.IPAddress }, + "lastupdated": func(dst, src *dto.DeviceInfo) { dst.LastUpdated = src.LastUpdated }, + "tlsmode": func(dst, src *dto.DeviceInfo) { dst.TLSMode = src.TLSMode }, + "upid": func(dst, src *dto.DeviceInfo) { dst.UPID = src.UPID }, + "amtenabledinbios": func(dst, src *dto.DeviceInfo) { dst.AMTEnabledInBIOS = src.AMTEnabledInBIOS }, + "meinterfaceversion": func(dst, src *dto.DeviceInfo) { dst.MEInterfaceVersion = src.MEInterfaceVersion }, + "dhcpenabled": func(dst, src *dto.DeviceInfo) { dst.DHCPEnabled = src.DHCPEnabled }, + "certhashes": func(dst, src *dto.DeviceInfo) { dst.CertHashes = src.CertHashes }, + "lmsinstalled": func(dst, src *dto.DeviceInfo) { dst.LMSInstalled = src.LMSInstalled }, + "lmsversion": func(dst, src *dto.DeviceInfo) { dst.LMSVersion = src.LMSVersion }, + "osname": func(dst, src *dto.DeviceInfo) { dst.OSName = src.OSName }, + "osversion": func(dst, src *dto.DeviceInfo) { dst.OSVersion = src.OSVersion }, + "osdistro": func(dst, src *dto.DeviceInfo) { dst.OSDistro = src.OSDistro }, + "cpumodel": func(dst, src *dto.DeviceInfo) { dst.CPUModel = src.CPUModel }, + "osipaddress": func(dst, src *dto.DeviceInfo) { dst.OSIPAddress = src.OSIPAddress }, + "ethernetadaptercount": func(dst, src *dto.DeviceInfo) { dst.EthernetAdapterCount = src.EthernetAdapterCount }, + "monitorconnected": func(dst, src *dto.DeviceInfo) { dst.MonitorConnected = src.MonitorConnected }, + "ieee8021xenabled": func(dst, src *dto.DeviceInfo) { dst.IEEE8021XEnabled = src.IEEE8021XEnabled }, } func mergeDeviceFields(dst, src *dto.Device, fields map[string]bool) { for key := range fields { + if key == deviceInfoFieldKey || strings.HasPrefix(key, deviceInfoFieldPrefix) { + continue + } + if apply, ok := deviceFieldSetters[key]; ok { apply(dst, src) } } + + if fields[deviceInfoFieldKey] { + mergeDeviceInfo(dst, src, fields) + } +} + +func mergeDeviceInfo(dst, src *dto.Device, fields map[string]bool) { + if src.DeviceInfo == nil { + dst.DeviceInfo = nil + + return + } + + hasNestedFields := false + + if dst.DeviceInfo == nil { + dst.DeviceInfo = &dto.DeviceInfo{} + } + + for key := range fields { + if !strings.HasPrefix(key, deviceInfoFieldPrefix) { + continue + } + + hasNestedFields = true + + subfield := strings.TrimPrefix(key, deviceInfoFieldPrefix) + if apply, ok := deviceInfoFieldSetters[subfield]; ok { + apply(dst.DeviceInfo, src.DeviceInfo) + } + } + + if !hasNestedFields { + // Backward compatibility for callers that only send top-level field maps. + dst.DeviceInfo = src.DeviceInfo + } } // convert entity.Device to dto.Device.