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
@@ -175,6 +175,8 @@ func buildDevicesFromLegacy(hw *models.HardwareConfig) []models.HardwareDevice {
Kind: models.DeviceKindStorage,
Slot: stor.Slot,
Model: stor.Model,
VendorID: stor.VendorID,
DeviceID: stor.DeviceID,
Manufacturer: stor.Manufacturer,
RemainingEndurancePct: stor.RemainingEndurancePct,
SerialNumber: stor.SerialNumber,
@@ -771,6 +773,8 @@ func convertStorageFromDevices(devices []models.HardwareDevice, collectedAt stri
Slot: d.Slot,
Type: d.Type,
Model: d.Model,
VendorID: d.VendorID,
DeviceID: d.DeviceID,
SizeGB: d.SizeGB,
SerialNumber: d.SerialNumber,
Manufacturer: d.Manufacturer,
@@ -1452,6 +1456,8 @@ func convertStorage(storage []models.Storage, collectedAt string) []ReanimatorSt
Slot: stor.Slot,
Type: stor.Type,
Model: stor.Model,
VendorID: stor.VendorID,
DeviceID: stor.DeviceID,
SizeGB: stor.SizeGB,
SerialNumber: stor.SerialNumber,
Manufacturer: stor.Manufacturer,
@@ -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.
+2
View File
@@ -118,6 +118,8 @@ type ReanimatorStorage struct {
Slot string `json:"slot"`
Type string `json:"type,omitempty"`
Model string `json:"model"`
VendorID int `json:"vendor_id,omitempty"`
DeviceID int `json:"device_id,omitempty"`
SizeGB int `json:"size_gb,omitempty"`
SerialNumber string `json:"serial_number"`
Manufacturer string `json:"manufacturer,omitempty"`