feat(exporter): collect and export storage.vendor_id/device_id (contract v2.13)

Add PCI Vendor ID / Device ID to hardware.storage[] per the updated Reanimator
ingest contract, mirroring the existing pcie_devices[] fields. Populated for
Redfish-collected NVMe drives (live + TSR replay, via linked PCIeFunctions)
and for Inspur (from asset.json's own PcieInfo[], joined by PcieSlot). Also
fixes canonicalDevicesForExport dropping the fields when converting Storage
into the canonical HardwareDevice list used by the actual export path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-08-11 16:04:54 +03:00
co-authored by Claude Sonnet 5
parent 063587958e
commit 399eca5f49
11 changed files with 221 additions and 4 deletions
+19
View File
@@ -162,6 +162,23 @@ func ParseAssetJSON(content []byte, pcieSlotDeviceNames map[int]string, pcieSlot
}
}
// Build a PcieSlot -> (VendorId, DeviceId) map from asset.json's own PcieInfo section,
// used below to enrich NVMe HddInfo entries (BMC does not populate vendor/device IDs
// directly on HddInfo).
pcieSlotVendorID := make(map[int]int, len(asset.PcieInfo))
pcieSlotDeviceID := make(map[int]int, len(asset.PcieInfo))
for _, pcie := range asset.PcieInfo {
if pcie.PcieSlot <= 0 {
continue
}
if pcie.VendorId > 0 {
pcieSlotVendorID[pcie.PcieSlot] = pcie.VendorId
}
if pcie.DeviceId > 0 {
pcieSlotDeviceID[pcie.PcieSlot] = pcie.DeviceId
}
}
// Parse storage info
for _, hdd := range asset.HddInfo {
slot := normalizeAssetHDDSlot(hdd.LocationString, hdd.Location, hdd.DiskInterfaceType)
@@ -207,6 +224,8 @@ func ParseAssetJSON(content []byte, pcieSlotDeviceNames map[int]string, pcieSlot
Slot: slot,
Type: storageType,
Model: modelName,
VendorID: pcieSlotVendorID[hdd.PcieSlot],
DeviceID: pcieSlotDeviceID[hdd.PcieSlot],
SizeGB: hdd.Capacity,
SerialNumber: serial,
Manufacturer: manufacturer,
@@ -44,6 +44,45 @@ func TestParseAssetJSON_HddSlotFallbackAndPresence(t *testing.T) {
}
}
func TestParseAssetJSON_HddEnrichedWithPcieVendorDeviceID(t *testing.T) {
content := []byte(`{
"HddInfo": [
{
"PresentBitmap": [1],
"SerialNumber": "NVME-SN-1",
"Manufacturer": "",
"ModelName": "",
"FirmwareVersion": "",
"Capacity": 7680,
"Location": 1,
"DiskInterfaceType": 5,
"MediaType": 1,
"LocationString": "OB01",
"PcieSlot": 12
}
],
"PcieInfo": [
{
"VendorId": 32902,
"DeviceId": 62305,
"PcieSlot": 12,
"LocString": "OB01"
}
]
}`)
hw, err := ParseAssetJSON(content, nil, nil)
if err != nil {
t.Fatalf("ParseAssetJSON failed: %v", err)
}
if len(hw.Storage) != 1 {
t.Fatalf("expected 1 storage entry, got %d", len(hw.Storage))
}
if hw.Storage[0].VendorID != 32902 || hw.Storage[0].DeviceID != 62305 {
t.Fatalf("expected vendor_id/device_id enriched from PcieInfo, got vendor=%d device=%d", hw.Storage[0].VendorID, hw.Storage[0].DeviceID)
}
}
func TestParseDiskBackplaneInfo_PopulatesOnlyMissingPresentDrives(t *testing.T) {
text := `RESTful diskbackplane info:
[