Hardware Discovery¶
Opt-in pass that enriches each Proxmox node's dcim.Device and per-NIC
dcim.Interface records with chassis and link facts that Proxmox VE does not
expose over its REST API. Facts are gathered over SSH from the node by running
dmidecode, ip -o link show, and ethtool.
This pass is disabled by default. With the flag off, zero SSH sockets are opened during a sync.
Architecture¶
proxbox-api (orchestrator — no paramiko import)
└── proxbox_api/services/hardware_discovery.py
├── is_enabled() settings flag check
├── fetch_credential(node_id) HTTPS+Bearer to netbox-proxbox
└── run_for_nodes(nb, nodes, *, bridge)
imports proxmox_sdk.ssh.RemoteSSHClient
imports proxmox_sdk.node.hardware.discover_node
sequential per-node loop, exception → SSE warning frame
proxmox-sdk (library — owns all SSH primitives + parsers)
├── proxmox_sdk.ssh.RemoteSSHClient
└── proxmox_sdk.node.hardware.{dmidecode,ethtool,facts,discover}
netbox-proxbox (NetBox plugin)
├── ProxboxPluginSettings.hardware_discovery_enabled
└── NodeSSHCredential model + REST endpoint
/api/plugins/proxbox/ssh-credentials/by-node/{node_id}/credentials/
The "no paramiko under proxbox_api/" invariant is pinned by
tests/test_hardware_discovery_no_paramiko_import.py, which AST-walks the
package and fails on any import of paramiko, asyncssh, fabric, etc.
Enabling the pass¶
- In NetBox → Plugins → Proxbox → Settings, toggle Hardware discovery enabled.
- For each node, create a
NodeSSHCredential(NetBox → Plugins → Proxbox → SSH Credentials). Configure: - username
- private key (ed25519 recommended) or password
- SHA-256 host-key fingerprint (no TOFU; the fingerprint must be captured beforehand and pinned)
sudo_required(defaults toTruefordmidecode)- On the Proxmox node, provision a dedicated
proxbox-discoveryuser with a sudoers entry restricted to/usr/sbin/dmidecode -t 1and/usr/sbin/dmidecode -t 3only. - Trigger a sync. After each node is upserted, the discovery pass runs sequentially for nodes that have a primary IP.
See netbox-proxbox/docs/configuration/hardware-discovery.md for the operator
walkthrough (key generation, fingerprint pinning UI, node-side authorized_keys
command= setup).
SSE frames¶
On success the orchestrator emits one hardware_discovery frame per node via
WebSocketSSEBridge.emit_hardware_discovery_progress():
{
"type": "hardware_discovery",
"node": "pve01",
"node_id": 42,
"chassis_serial": "ABCD1234",
"chassis_manufacturer": "Dell Inc.",
"chassis_product": "PowerEdge R740",
"nic_count": 4
}
On failure the orchestrator emits a generic item_progress frame with a
warning field. Warning codes:
| Warning | Cause |
|---|---|
hardware_discovery_no_primary_ip |
Node has no primary_ip4/primary_ip set in NetBox. |
hardware_discovery_no_credential |
No NodeSSHCredential exists for this node id. |
hardware_discovery_timeout |
SSH connect or exec timed out. |
hardware_discovery_auth_failed |
SSH authentication failed. |
host_key_mismatch |
The node's host key does not match the pinned fingerprint. Credentials are NOT sent. |
hardware_discovery_failed: <exc> |
Catch-all for any other exception. |
NetBox custom fields¶
Six custom fields are bootstrapped under the existing Proxmox group_name
by proxbox_api/routes/extras/__init__.py::create_custom_fields():
| Field | Object | Type |
|---|---|---|
hardware_chassis_serial |
dcim.device |
text |
hardware_chassis_manufacturer |
dcim.device |
text |
hardware_chassis_product |
dcim.device |
text |
nic_speed_gbps |
dcim.interface |
integer |
nic_duplex |
dcim.interface |
text |
nic_link |
dcim.interface |
boolean |
Writes go through the existing drift-detect PATCH path
(netbox_rest.rest_patch_async), so a second consecutive successful sync emits
zero extras.ObjectChange rows for these fields.
Security boundary¶
- Credentials live encrypted (Fernet) inside netbox-proxbox; plaintext only exists in the orchestrator's process memory for the duration of one SSH session.
- The credential REST endpoint requires a Bearer token matching
FastAPIEndpoint.token. RemoteSSHClientin proxmox-sdk enforces:- SHA-256 host-key fingerprint pinning (refuses connect on mismatch — no TOFU)
- argv-list-only
run()(no shell interpolation) - command allowlist (
["dmidecode", "ip", "ethtool"]is enforced by the orchestrator) - output cap, connect/exec timeouts
- log-redactor regexes to keep key material out of
caplog/logger - The orchestrator runs nodes sequentially per cluster so a stalled node cannot starve others.
Test surface¶
| Test | Pins |
|---|---|
tests/test_hardware_discovery_no_paramiko_import.py |
Static AST guard: no SSH library imports under proxbox_api/. |
tests/test_hardware_discovery_flag_off.py |
Flag off → zero RemoteSSHClient constructions. |
tests/test_hardware_discovery_orchestrator.py |
Sequential dispatch, success-frame shape, warning-code mapping for every failure class. |
tests/test_hardware_discovery_credential_fetch.py |
HTTPS+Bearer URL shape, 404 → MissingCredential, 5xx/malformed/non-dict → HardwareDiscoveryError, no secret leakage at DEBUG. |