-
Notifications
You must be signed in to change notification settings - Fork 13
ADR devices export api
GitHub Issue: device-management-toolkit/console#1076
Parent: Console Discovery Dashboard & Device Data Export #1093
Status: Proposed
Introduce a new Console API endpoint that exports all device data (including deviceInfo) in JSON format. This enables fleet operators to extract a complete snapshot of managed and discovered devices for reporting, auditing, and tooling integration.
Endpoint: GET /api/v1/devices/export
Three options were evaluated. Option C is the chosen approach.
- Option A: Legacy Flat Model (Current DB Shape)
- Option B: Nested Subsystem Model (Initial Draft)
- Option C: Refined Nested Model with Platform Split (Chosen)
{
"guid": "143e4567-e89b-12d3-a456-426614174000",
"hostname": "lab-pc-01",
"friendlyName": "Lab PC Alpha",
"tags": ["campus-lab", "shared-device"],
"tenantId": "",
"dnsSuffix": "corp.example.com",
"connectionStatus": true,
"lastConnected": "2026-07-07T05:30:00Z",
"lastSeen": "2026-07-07T06:00:00Z",
"lastDisconnected": null,
"deviceInfo": {
"amtEnabledInBIOS": true,
"currentMode": "Admin",
"discovered": true,
"fwVersion": "16.1.32",
"fwBuild": "3400",
"fwSku": "16392",
"features": "AMT Pro Corporate",
"lmsInstalled": true,
"lmsVersion": "2410.5.0.0",
"meInterfaceVersion": "16.1.25.2124",
"tlsMode": "TLS 1.2",
"ipAddress": "10.0.0.12",
"osName": "linux",
"osVersion": "6.8.0-51-generic",
"osDistro": "Ubuntu 24.04 LTS",
"osIpAddress": "10.49.76.163",
"cpuModel": "Intel(R) Core(TM) Ultra 7 165H",
"dhcpEnabled": true,
"ethernetAdapterCount": 2,
"monitorConnected": true,
"ieee8021xEnabled": false,
"certHashes": ["a1b2c3", "d4e5f6"],
"upid": {
"csmeId": "4A45A39C5ED9462082510000",
"oemId": "",
"oemPlatformIdType": "Not Set (0)"
},
"lastUpdated": "2026-05-21T00:00:00Z"
}
}Decision for Option A: All AMT, OS, and peripheral fields are mixed into a single flat blob. As more peripherals are added (e.g. BMC, future hardware interfaces), the flat list grows unmanageably. Consumers of the export have no easy way to understand which fields belong to which subsystem without reading documentation.
{
"guid": "143e4567-e89b-12d3-a456-426614174000",
"hostname": "lab-pc-01",
"friendlyName": "Lab PC Alpha",
"tags": ["campus-lab", "shared-device"],
"dnsSuffix": "corp.example.com",
"deviceInfo": {
"currentMode": "Admin",
"amt": {
"amtEnabledInBIOS": true,
"fwVersion": "16.1.32",
"fwBuild": "3400",
"fwSku": "16392",
"features": "AMT Pro Corporate",
"tlsMode": "TLS 1.2",
"lmsInstalled": true,
"lmsVersion": "2410.5.0.0",
"meInterfaceVersion": "16.1.25.2124",
"dhcpEnabled": true,
"certHashes": ["a1b2c3", "d4e5f6"],
"upid": {
"csmeId": "4A45A39C5ED9462082510000",
"oemId": "",
"oemPlatformIdType": "Not Set (0)"
},
"network": {
"wired": {
"ipAddress": "10.0.0.12",
"dhcp": true,
"static": false
},
"wireless": {
"ipAddress": "192.168.1.20"
}
}
},
"os": {
"name": "linux",
"version": "6.8.0-51-generic",
"distro": "Ubuntu 24.04 LTS",
"cpu": "Intel(R) Core(TM) Ultra 7 165H",
"monitorConnected": true,
"ethernetAdapterCount": 2,
"ieee8021xEnabled": false,
"network": {
"wired": [
{
"name": "eth0",
"ipAddress": "10.49.76.163",
"macAddress": "AA:BB:CC:DD:EE:FF"
},
{
"name": "eth1",
"ipAddress": "10.49.76.164",
"macAddress": "AA:BB:CC:DD:EE:00"
}
],
"wireless": {
"ipAddress": "10.0.2.25"
}
}
},
"bmc": {
"vendor": "string",
"model": "string",
"firmwareVersion": "string"
}
}
}Fields are grouped by subsystem (
amt,os,bmc). Adding new peripheral data tomorrow (e.g. BMC firmware version, future HW interfaces) means adding a new top-level section underdeviceInfo— no consumer needs to guess which fields belong where. Network details are scoped to the subsystem that reported them (amt.networkvsos.network).
Decision for Option B: Better than a flat blob, but this needs slight modifications. Things like CPU model and physical network adapters don't really belong under
os, they're platform info. Also, calling the sectionamtis misleading since it also covers ISM devices. We took this structure and cleaned it up in Option C.
Decision: Option C is chosen. This is the cleaned-up version of Option B.
amtis renamed tomeso it works for both AMT and ISM devices. Detailed interface/IP data now lives underos.network, whileplatform.adaptersprovides a adapter-name summary (wiredandwireless). Each section has a clear purpose, and adding support for new hardware down the line just means adding a new key — nothing else needs to change.
The export organizes device information into logical subsystems for clarity and extensibility:
graph TB
Export["Device Export Response"]
Export --> Metadata["Metadata<br/>exportedAt, filtersApplied<br/>swVersion"]
Export --> Summary["Summary Statistics<br/>totalCount"]
Export --> Data["Data Array<br/>Individual Device Records"]
Data --> Device["Device Record<br/>guid, hostname, friendlyName<br/>tags"]
Device --> DeviceInfo["deviceInfo Object"]
DeviceInfo --> ME["Management Engine - me<br/>AMT/ISM firmware, TLS config<br/>network settings, certificates, UPID"]
DeviceInfo --> OS["Operating System - os<br/>OS name, version, distro<br/>LMS Info, monitors, 802.1X<br/>network interfaces and IPs"]
DeviceInfo --> Platform["Platform Details - platform<br/>CPU model, adapter count<br/>adapter-name summary"]
DeviceInfo --> BMC["Baseboard Mgmt - bmc<br/>High-level summary"]
ME --> MENetwork["ME Network<br/>wired IP, DHCP, wireless IP"]
OS --> OSNetwork["OS Network<br/>wired interfaces with IPs and MACs<br/>wireless IP"]
Platform --> PlatformAdapters["Platform Adapter Summary<br/>wired adapter name<br/>wireless adapter name"]
{
"metadata": {
"exportedAt": "2026-07-22T16:45:00Z",
"filtersApplied": {
"status": "all",
"tags": ["vPro", "lab"]
},
"swVersion": "console v1.38.1"
},
"summary": {
"totalCount": 2
},
"data": [
{
"guid": "143e4567-e89b-12d3-a456-426614174000",
"...": "..."
"deviceInfo": {
"...": "..."
}
},
{
"guid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"...": "..."
"deviceInfo": {
"...": "..."
}
}
]
}{
"guid": "143e4567-e89b-12d3-a456-426614174000",
"hostname": "lab-pc-01",
"friendlyName": "Lab PC Alpha",
"tags": ["campus-lab", "shared-device"],
"tenantId": "tenant1",
"firstDiscovered": "2025-06-15T10:30:00Z",
"lastSynced": "2026-07-20T14:22:15Z",
"lastUpdated": "2026-07-20T14:22:15Z",
"deviceInfo": {
"me": {
"dnsSuffix": "corp.example.com",
"currentMode": "Admin",
"mebxEnabledInBIOS": true,
"fwVersion": "16.1.32",
"fwBuild": "3400",
"fwSku": "16392",
"features": "AMT Pro Corporate",
"tlsMode": "TLS 1.2",
"dhcpEnabled": true,
"certHashes": ["a1b2c3xxx", "d4e5f6xxx"],
"upid": {
"csmeId": "4A45A39C5ED9462082510000",
"oemId": "",
"oemPlatformIdType": "Not Set (0)"
},
"network": {
"wired": {
"ipAddress": "10.0.0.12",
"dhcpEnabled": true,
"dhcpMode": "active",
"linkStatus": "up",
"macAddress": "AA:BB:CC:DD:EE:10"
},
"wireless": {
"ipAddress": "192.168.1.20",
"dhcpEnabled": true,
"dhcpMode": "passive",
"linkStatus": "up",
"macAddress": "AA:BB:CC:DD:EE:20"
}
}
},
"os": {
"dnsSuffix": "vpro.demo.com",
"name": "linux",
"version": "6.8.0-51-generic",
"distro": "Ubuntu 24.04 LTS",
"lmsInstalled": true,
"lmsVersion": "2410.5.0.0",
"meInterfaceVersion": "16.1.25.2124",
"monitorConnected": true,
"ieee8021xEnabled": false,
"network": {
"wired": [
{
"name": "eth0",
"ipAddress": "10.49.76.163",
"dhcpEnabled": true,
"linkStatus": "up",
"macAddress": "AA:BB:CC:DD:EE:FF"
},
{
"name": "eth1",
"ipAddress": "10.49.76.164",
"dhcpEnabled": false,
"linkStatus": "up",
"macAddress": "AA:BB:CC:DD:EE:00"
}
],
"wireless": {
"name": "wlan0",
"ipAddress": "10.0.2.25",
"dhcpEnabled": true,
"linkStatus": "up",
"macAddress": "AA:BB:CC:DD:EE:30"
}
}
},
"platform": {
"cpu": "Intel(R) Core(TM) Ultra 7 165H",
"ethernetAdapterCount": 2,
"adapters": {
"wired": "Intel Corporation Ethernet Controller I226-LM (rev 04)",
"wireless": "Intel Corporation Meteor Lake PCH CNVi WiFi (rev 20)"
}
},
"bmc": null
}
}Note on BMC fields: Currently restricted to high-level summary (vendor, model, firmware version). Future versions will expand to include sensors, event logs, firmware upgrade capabilities, and detailed health status.
For devices without Management Engine capabilities (e.g., standard Intel systems, non-vPro workstations, or non-Intel platforms), the me field is null:
{
"guid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"hostname": "standard-pc-05",
"friendlyName": "Standard Workstation",
"tags": ["standard-fleet"],
"tenantId": "tenant1",
"firstDiscovered": "2025-08-10T09:15:00Z",
"lastSynced": "2026-07-15T11:45:30Z",
"lastUpdated": "2026-07-15T11:45:30Z",
"deviceInfo": {
"me": null,
"os": {
"dnsSuffix": "corp.example.com",
"name": "linux",
"version": "6.8.0-51-generic",
"distro": "Ubuntu 24.04 LTS",
"lmsInstalled": true,
"lmsVersion": "2410.5.0.0",
"meInterfaceVersion": "16.1.25.2124",
"monitorConnected": true,
"ieee8021xEnabled": false,
"network": {
"wired": [
{
"name": "eth0",
"ipAddress": "10.49.76.163",
"dhcpEnabled": true,
"linkStatus": "up",
"macAddress": "AA:BB:CC:DD:EE:FF"
},
{
"name": "eth1",
"ipAddress": "10.49.76.164",
"dhcpEnabled": false,
"linkStatus": "up",
"macAddress": "AA:BB:CC:DD:EE:00"
}
],
"wireless": {
"name": "wlan0",
"ipAddress": "0.0.0.0",
"dhcpEnabled": true,
"linkStatus": "down",
"macAddress": "00:00:00:00:00:00"
}
}
},
"platform": {
"cpu": "Intel(R) Core(TM) Ultra 7 165H",
"ethernetAdapterCount": 2,
"adapters": {
"wired": "Intel Corporation 82540EM Gigabit Ethernet Controller (rev 03)",
"wireless": "Realtek RTL8821CE 802.11ac PCIe Wireless Network Adapter"
}
},
"bmc": null
}
}The Option C schema is the target shape. However, several fields it defines are not currently collected/populated by rpc-go (rpc amtinfo --sync) and therefore do not exist in the stored deviceInfo today. The list below was derived by comparing the Option C schema against the live GET /api/v1/devices payload from the database.
| Group | Schema field | Status | Notes |
|---|---|---|---|
| record | lastUpdated |
Missing | Only firstDiscovered and lastSynced exist today. Needs a writer to stamp last-update time. |
me |
tlsMode |
Missing | Not collected. DB only exposes top-level useTLS / allowSelfSigned, not a resolved TLS mode string. |
me |
network.wired (ipAddress, dhcpEnabled, dhcpMode, linkStatus, macAddress) |
Missing | No ME wired network object today; only a single flat ipAddress. Requires extended ME interface collection. |
me |
network.wireless (ipAddress, dhcpEnabled, dhcpMode, linkStatus, macAddress) |
Missing | No ME wireless network data collected at all. |
os |
dnsSuffix |
Missing | Only a single top-level dnsSuffix exists; a distinct OS-reported DNS suffix is not collected. |
os |
ieee8021xEnabled |
Missing | Not collected by rpc-go. |
os |
network.wired[] (name, per-interface ipAddress, dhcpEnabled, linkStatus, macAddress) |
Missing | DB stores only a single osIpAddress string — no per-adapter list, names, MACs, or link status. Requires extended OS telemetry. |
os |
network.wireless (name, ipAddress, dhcpEnabled, linkStatus, macAddress) |
Missing | No OS wireless interface data collected. |
platform |
adapters.wired (adapter name) |
Missing | Adapter model/name string not collected; only ethernetAdapterCount exists. |
platform |
adapters.wireless (adapter name) |
Missing | Wireless adapter model/name string not collected. |
GET /api/v1/devices/export
Authorization: Bearer <token>
Accept: application/json
Note: Filtering is not implemented in the initial release but is planned for a future iteration.
| Parameter | Type | Default | Description |
|---|---|---|---|
tags |
string | (none) | Comma-separated list; returns devices matching ANY tag |
status |
string | all | Values: connected, disconnected, all
|
discovered |
boolean | N/A | If true, returns only discovered devices |
Endpoint: GET /api/v1/devices/export
Authentication: Bearer JWT token from POST /api/v1/authorize
Headers:
| Header | Value | Required | Notes |
|---|---|---|---|
Authorization |
Bearer <jwt_token>
|
Yes | Signed JWT; expiration enforced per auth.jwtExpiration
|
Query Example:
GET /api/v1/devices/export?tags=vPro,production&status=connected
Response (Status 200 - JSON):
Conforms to the JSON Schema defined in Option C. Returns an object with metadata, summary, and data array.
Error Responses:
-
401 Unauthorized— Missing or invalid JWT token -
400 Bad Request— Invalid query parameter (e.g., malformed tag list) -
403 Forbidden— Authenticated user lacks export permissions -
500 Internal Server Error— Database or export generation failure
The proposed JSON example includes me.network.wired, me.network.wireless, platform.adapters.wired[], and platform.adapters.wireless. These fields do not currently exist in DeviceInfo.
- Does this require extending rpc amtinfo --sync to collect network adapter details?
- Does this require a DB schema update (new fields in the deviceInfo JSON blob)?
Sudhir:
- For Management Engine (me) network: wired/wireless IP addresses come from existing AMT fields already collected by
rpc amtinfo --sync. These are part of the standard ME interface query. - For Operating System (os) network: detailed adapter and IP information requires extended OS telemetry collection. This may require:
- Enhancement to
rpc amtinfo --syncto collect detailed OS network adapter list - DB schema update to store the expanded OS network details in
deviceInfo
- Enhancement to
The proposed export JSON uses a nested structure (me.{}, os.{}, platform.{}, bmc.{}), but the DB today stores deviceInfo as a flat JSON blob.
- Do we need to update the DB schema to match the nested structure, or do we keep the flat blob and transform it at the API layer on export?
- If we keep the flat DB schema and only reshape on export: any future change to the DB field names or structure will silently break the export API. Who is responsible for keeping the mapping in sync?
- If we update the DB to store the nested structure: this is a breaking migration — all existing deviceInfo rows need to be migrated, and every writer (rpc amtinfo --sync, Console API) needs to be updated.
Sudhir:
- Strategy: Keep the database schema flat (minimal breaking changes), and perform the nested transformation at the API layer (in the export endpoint).
- Responsibility: The export API handler is responsible for mapping flat DB fields → nested JSON response. A mapping helper will document the flat→nested transformation.
-
Future Migration Path: If Console adopts nested
deviceInfoat the DB level, the export API will need to evolve accordingly. This will be driven by a separate ADR when the time comes.
Should the response include metadata fields such as:
-
exportedAt— timestamp when export was generated -
exportedBy— authenticated user ID -
exportFormat— schema version for versioning -
filterApplied— which filters were active for this export
Sudhir:
- Yes, these metadata fields are included in the response schema.
-
exportedAtandswVersionare required for auditing and version traceability. -
filtersAppliedis optional to support scenarios where the export runs without filter context. - This enables downstream tooling to understand when the export occurred, which software version produced it, and what filters were applied.
Should the export include aggregated counts?
- Total devices
- activatedCount vs. not activated count
- discoveredCount
- vProCount vs. ISMCount
Sudhir:
-
Current: Include only
totalCountfor simplicity. This is the most critical metric and unblocks the feature. -
Future enhancements (Phase 2+): Add
activatedCount,discoveredCount, and distribution breakdowns (byOperatingSystem,byTlsMode) once operational dashboards require them.
Do not reuse the existing Device DTO. It contains sensitive fields such as mpsPassword and mebxPassword. Please use a dedicated export DTO with an explicit allowlist to ensure sensitive data is never exported.
Shradha:
Agreed, the existing Device entity will not be reused here. The problem is it carries mpsPassword, mebxPassword, and other credential fields that have no business appearing in an export file. A dedicated DeviceExportDto with an explicit allowlist is the right approach.
The allowlist covers exactly what Option C defines: guid, hostname, friendlyName, tags, tenantId, firstDiscovered, lastSynced, lastUpdated, and the nested deviceInfo sub-objects. Nothing else gets through. The standing rule: any new field added to the device model has to be explicitly opted into DeviceExportDto. It is never included by default.
6. Why was tenant ID removed? It was included in Option A but is missing in Option C. Please clarify whether the export is tenant-scoped and how tenant isolation will be enforced.
Shradha:
tenantId was accidentally dropped during the Option B → Option C cleanup. It has been added back to the schema.
On scoping: the export is tenant-scoped. Each call returns only the devices that belong to the caller's tenant, resolved from the tenantId claim in the JWT. Tenant isolation is enforced server-side in the repository query (a WHERE tenant_id = ? predicate for SQL, and the equivalent filter for MongoDB).
SQL and MongoDB should return the same fields and follow the same export schema so consumers get a consistent experience regardless of the underlying datastore.
Shradha:
Schema shaping should happen in exactly one place: the export handler. Both the SQL and MongoDB repositories just return the raw stored record, they don't shape anything. A flatToNested mapping function inside the export handler is responsible for converting that raw data into the final DeviceExportDto. This way, consumers always get the same JSON structure regardless of which database is running underneath.
Existing device APIs support pagination, but the export endpoint does not. If the intent is to export the full fleet, the ADR should explicitly justify this decision and define limits, large-export handling, and failure behavior.
Shradha:
Pagination was intentionally left out because this endpoint serves a different purpose than the regular device list API. When an operator hits /export, they want a complete snapshot of their tenant's fleet. Pagination would mean making multiple calls, stitching pages back together, and hoping nothing changed between requests which might add complexity.
That said, returning an unbounded dataset is not safe either, so we will put guardrails in place: a hard cap on the number of records returned, a request timeout, and a response header that tells the client how many records are coming. If the query times out or the database throws an error mid-way, the entire response is discarded and a 503 is returned so that we never send a partial export file.
9. Have you considered server-side audit logging for exports (who exported, source IP/server, device count, timestamp, etc.)?
I know metadata is planned in the JSON output, but server-side logs provide a more persistent audit trail. This may be worth discussing during the demo.
Shradha:
Yes, this is worth adding. The plan is to log each export call to the same structured logger Console already uses. Here is what gets captured per call:
| Field | Source |
|---|---|
timestamp |
Server clock (UTC) |
userId |
sub claim from the JWT |
tenantId |
tenantId claim from the JWT |
sourceIp |
X-Forwarded-For / RemoteAddr (proxy-aware) |
serverHostname |
os.hostname() or pod name in Kubernetes |
filtersApplied |
Parsed query parameters |
deviceCount |
Number of records included in the export |
durationMs |
Time taken to generate the export |
Failed attempts like auth failures, over-limit rejections, timeouts etc. are also logged with an outcome field so they are easy to distinguish. None of this ends up in the HTTP response; it stays server-side only.