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
@@ -2078,6 +2078,60 @@ func TestConvertToReanimator_MemoryAndPSURoundTripSurvivesReimport(t *testing.T)
}
}
// TestConvertToReanimator_StorageVendorDeviceIDSurvivesRoundTrip covers the
// hardware.storage[].vendor_id/device_id contract fields (v2.13).
func TestConvertToReanimator_StorageVendorDeviceIDSurvivesRoundTrip(t *testing.T) {
original := &models.AnalysisResult{
Filename: "test.zip",
CollectedAt: time.Date(2026, 8, 11, 0, 0, 0, 0, time.UTC),
Hardware: &models.HardwareConfig{
BoardInfo: models.BoardInfo{
Manufacturer: "Dell Inc.",
ProductName: "PowerEdge R7715",
SerialNumber: "1TVFYL4",
},
Storage: []models.Storage{
{
Slot: "OB01",
Type: "NVMe",
Model: "INTEL SSDPF2KX076T1",
VendorID: 32902,
DeviceID: 62305,
SerialNumber: "BTAX41900GF87P6DGN",
Present: true,
},
},
},
}
exported, err := ConvertToReanimator(original)
if err != nil {
t.Fatalf("ConvertToReanimator() failed: %v", err)
}
if len(exported.Hardware.Storage) != 1 {
t.Fatalf("expected 1 exported storage entry, got %d", len(exported.Hardware.Storage))
}
if exported.Hardware.Storage[0].VendorID != 32902 || exported.Hardware.Storage[0].DeviceID != 62305 {
t.Fatalf("expected exported storage vendor_id/device_id, got %+v", exported.Hardware.Storage[0])
}
raw, err := json.Marshal(exported)
if err != nil {
t.Fatalf("marshal exported: %v", err)
}
var reimported models.AnalysisResult
if err := json.Unmarshal(raw, &reimported); err != nil {
t.Fatalf("unmarshal into AnalysisResult: %v", err)
}
reconverted, err := ConvertToReanimator(&reimported)
if err != nil {
t.Fatalf("ConvertToReanimator() on reimported result failed: %v", err)
}
if len(reconverted.Hardware.Storage) != 1 || reconverted.Hardware.Storage[0].VendorID != 32902 || reconverted.Hardware.Storage[0].DeviceID != 62305 {
t.Fatalf("storage vendor_id/device_id did not survive reanimator round trip, got %+v", reconverted.Hardware.Storage)
}
}
// TestConvertToReanimator_ExportsLicenses covers the hardware.licenses contract
// section (v2.12): system-level licenses (no component_ref) and component-scoped
// licenses, skipping records without a name and records the source marked absent.