export: align reanimator and enrich redfish metrics
This commit is contained in:
@@ -66,104 +66,15 @@ func (e *Exporter) ExportCSV(w io.Writer) error {
|
||||
}
|
||||
}
|
||||
|
||||
// CPUs
|
||||
for _, cpu := range e.result.Hardware.CPUs {
|
||||
if !hasUsableSerial(cpu.SerialNumber) {
|
||||
seenCanonical := make(map[string]struct{})
|
||||
for _, dev := range canonicalDevicesForExport(e.result.Hardware) {
|
||||
if !hasUsableSerial(dev.SerialNumber) {
|
||||
continue
|
||||
}
|
||||
if err := writer.Write([]string{
|
||||
cpu.Model,
|
||||
strings.TrimSpace(cpu.SerialNumber),
|
||||
"",
|
||||
"CPU",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Memory
|
||||
for _, mem := range e.result.Hardware.Memory {
|
||||
if !hasUsableSerial(mem.SerialNumber) {
|
||||
continue
|
||||
}
|
||||
location := mem.Location
|
||||
if location == "" {
|
||||
location = mem.Slot
|
||||
}
|
||||
if err := writer.Write([]string{
|
||||
mem.PartNumber,
|
||||
strings.TrimSpace(mem.SerialNumber),
|
||||
mem.Manufacturer,
|
||||
location,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Storage
|
||||
for _, stor := range e.result.Hardware.Storage {
|
||||
if !hasUsableSerial(stor.SerialNumber) {
|
||||
continue
|
||||
}
|
||||
if err := writer.Write([]string{
|
||||
stor.Model,
|
||||
strings.TrimSpace(stor.SerialNumber),
|
||||
stor.Manufacturer,
|
||||
stor.Slot,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// GPUs
|
||||
for _, gpu := range e.result.Hardware.GPUs {
|
||||
if !hasUsableSerial(gpu.SerialNumber) {
|
||||
continue
|
||||
}
|
||||
component := gpu.Model
|
||||
if component == "" {
|
||||
component = "GPU"
|
||||
}
|
||||
if err := writer.Write([]string{
|
||||
component,
|
||||
strings.TrimSpace(gpu.SerialNumber),
|
||||
gpu.Manufacturer,
|
||||
gpu.Slot,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// PCIe devices
|
||||
for _, pcie := range e.result.Hardware.PCIeDevices {
|
||||
if !hasUsableSerial(pcie.SerialNumber) {
|
||||
continue
|
||||
}
|
||||
if err := writer.Write([]string{
|
||||
pcie.DeviceClass,
|
||||
strings.TrimSpace(pcie.SerialNumber),
|
||||
pcie.Manufacturer,
|
||||
pcie.Slot,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Network adapters
|
||||
for _, nic := range e.result.Hardware.NetworkAdapters {
|
||||
if !hasUsableSerial(nic.SerialNumber) {
|
||||
continue
|
||||
}
|
||||
location := nic.Location
|
||||
if location == "" {
|
||||
location = nic.Slot
|
||||
}
|
||||
if err := writer.Write([]string{
|
||||
nic.Model,
|
||||
strings.TrimSpace(nic.SerialNumber),
|
||||
nic.Vendor,
|
||||
location,
|
||||
}); err != nil {
|
||||
serial := strings.TrimSpace(dev.SerialNumber)
|
||||
seenCanonical[serial] = struct{}{}
|
||||
component, manufacturer, location := csvFieldsFromCanonicalDevice(dev)
|
||||
if err := writer.Write([]string{component, serial, manufacturer, location}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -173,26 +84,15 @@ func (e *Exporter) ExportCSV(w io.Writer) error {
|
||||
if !hasUsableSerial(nic.SerialNumber) {
|
||||
continue
|
||||
}
|
||||
if err := writer.Write([]string{
|
||||
nic.Model,
|
||||
strings.TrimSpace(nic.SerialNumber),
|
||||
"",
|
||||
"Network",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Power supplies
|
||||
for _, psu := range e.result.Hardware.PowerSupply {
|
||||
if !hasUsableSerial(psu.SerialNumber) {
|
||||
serial := strings.TrimSpace(nic.SerialNumber)
|
||||
if _, ok := seenCanonical[serial]; ok {
|
||||
continue
|
||||
}
|
||||
if err := writer.Write([]string{
|
||||
psu.Model,
|
||||
strings.TrimSpace(psu.SerialNumber),
|
||||
psu.Vendor,
|
||||
psu.Slot,
|
||||
nic.Model,
|
||||
serial,
|
||||
"",
|
||||
"Network",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -221,3 +121,52 @@ func hasUsableSerial(serial string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func csvFieldsFromCanonicalDevice(dev models.HardwareDevice) (component, manufacturer, location string) {
|
||||
component = firstNonEmptyString(
|
||||
dev.Model,
|
||||
dev.PartNumber,
|
||||
dev.DeviceClass,
|
||||
dev.Kind,
|
||||
)
|
||||
manufacturer = firstNonEmptyString(dev.Manufacturer, inferCSVVendor(dev))
|
||||
location = firstNonEmptyString(dev.Location, dev.Slot, dev.BDF, dev.Kind)
|
||||
|
||||
switch dev.Kind {
|
||||
case models.DeviceKindCPU:
|
||||
if component == "" {
|
||||
component = "CPU"
|
||||
}
|
||||
if location == "" {
|
||||
location = "CPU"
|
||||
}
|
||||
case models.DeviceKindMemory:
|
||||
component = firstNonEmptyString(dev.PartNumber, dev.Model, "Memory")
|
||||
case models.DeviceKindPCIe, models.DeviceKindGPU, models.DeviceKindNetwork:
|
||||
if location == "" {
|
||||
location = firstNonEmptyString(dev.Slot, dev.BDF, "PCIe")
|
||||
}
|
||||
case models.DeviceKindPSU:
|
||||
component = firstNonEmptyString(dev.Model, "Power Supply")
|
||||
}
|
||||
|
||||
return component, manufacturer, location
|
||||
}
|
||||
|
||||
func inferCSVVendor(dev models.HardwareDevice) string {
|
||||
switch dev.Kind {
|
||||
case models.DeviceKindCPU:
|
||||
return ""
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func firstNonEmptyString(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -210,7 +210,7 @@ func TestConvertCPUs(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
result := convertCPUs(cpus, "2026-02-10T15:30:00Z")
|
||||
result := convertCPUs(cpus, "2026-02-10T15:30:00Z", "BOARD-001")
|
||||
|
||||
if len(result) != 2 {
|
||||
t.Fatalf("expected 2 CPUs, got %d", len(result))
|
||||
@@ -227,6 +227,9 @@ func TestConvertCPUs(t *testing.T) {
|
||||
if result[0].Status != "Unknown" {
|
||||
t.Errorf("expected Unknown status, got %q", result[0].Status)
|
||||
}
|
||||
if result[0].SerialNumber != "BOARD-001-CPU-0" {
|
||||
t.Errorf("expected generated CPU serial, got %q", result[0].SerialNumber)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertMemory(t *testing.T) {
|
||||
@@ -247,17 +250,13 @@ func TestConvertMemory(t *testing.T) {
|
||||
|
||||
result := convertMemory(memory, "2026-02-10T15:30:00Z")
|
||||
|
||||
if len(result) != 2 {
|
||||
t.Fatalf("expected 2 memory modules, got %d", len(result))
|
||||
if len(result) != 1 {
|
||||
t.Fatalf("expected 1 populated memory module, got %d", len(result))
|
||||
}
|
||||
|
||||
if result[0].Status != "OK" {
|
||||
t.Errorf("expected OK status for first module, got %q", result[0].Status)
|
||||
}
|
||||
|
||||
if result[1].Status != "Empty" {
|
||||
t.Errorf("expected Empty status for second module, got %q", result[1].Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertStorage(t *testing.T) {
|
||||
@@ -289,6 +288,48 @@ func TestConvertStorage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertStorage_RemainingEndurance(t *testing.T) {
|
||||
pct100 := 100
|
||||
pct3 := 3
|
||||
storage := []models.Storage{
|
||||
{
|
||||
Slot: "0",
|
||||
Model: "HFS480G3H2X069N",
|
||||
SerialNumber: "ESEAN5254I030B26B",
|
||||
Present: true,
|
||||
RemainingEndurancePct: &pct100,
|
||||
},
|
||||
{
|
||||
Slot: "1",
|
||||
Model: "HFS480G3H2X069N",
|
||||
SerialNumber: "ESEAN5254I030B26C",
|
||||
Present: true,
|
||||
// no endurance data
|
||||
},
|
||||
{
|
||||
Slot: "2",
|
||||
Model: "HFS480G3H2X069N",
|
||||
SerialNumber: "ESEAN5254I030B26D",
|
||||
Present: true,
|
||||
RemainingEndurancePct: &pct3,
|
||||
},
|
||||
}
|
||||
|
||||
result := convertStorage(storage, "2026-03-15T00:00:00Z")
|
||||
if len(result) != 3 {
|
||||
t.Fatalf("expected 3 results, got %d", len(result))
|
||||
}
|
||||
if result[0].RemainingEndurancePct == nil || *result[0].RemainingEndurancePct != 100 {
|
||||
t.Errorf("slot 0: expected remaining_endurance_pct=100, got %v", result[0].RemainingEndurancePct)
|
||||
}
|
||||
if result[1].RemainingEndurancePct != nil {
|
||||
t.Errorf("slot 1: expected remaining_endurance_pct absent, got %v", *result[1].RemainingEndurancePct)
|
||||
}
|
||||
if result[2].RemainingEndurancePct == nil || *result[2].RemainingEndurancePct != 3 {
|
||||
t.Errorf("slot 2: expected remaining_endurance_pct=3, got %v", result[2].RemainingEndurancePct)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertPCIeDevices(t *testing.T) {
|
||||
hw := &models.HardwareConfig{
|
||||
PCIeDevices: []models.PCIeDevice{
|
||||
@@ -329,16 +370,16 @@ func TestConvertPCIeDevices(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z")
|
||||
result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z", "BOARD-001")
|
||||
|
||||
// Should have: 2 PCIe devices + 1 GPU + 1 NIC = 4 total
|
||||
if len(result) != 4 {
|
||||
t.Fatalf("expected 4 PCIe devices total, got %d", len(result))
|
||||
}
|
||||
|
||||
// Check that serial is empty for second PCIe device (no auto-generation)
|
||||
if result[1].SerialNumber != "" {
|
||||
t.Errorf("expected empty serial for missing device serial, got %q", result[1].SerialNumber)
|
||||
// Check that serial is generated for second PCIe device
|
||||
if result[1].SerialNumber != "BOARD-001-PCIE-PCIeCard2" {
|
||||
t.Errorf("expected generated serial for missing device serial, got %q", result[1].SerialNumber)
|
||||
}
|
||||
|
||||
// Check GPU was included
|
||||
@@ -346,8 +387,8 @@ func TestConvertPCIeDevices(t *testing.T) {
|
||||
for _, dev := range result {
|
||||
if dev.SerialNumber == "GPU-001" {
|
||||
foundGPU = true
|
||||
if dev.DeviceClass != "DisplayController" {
|
||||
t.Errorf("expected GPU device_class DisplayController, got %q", dev.DeviceClass)
|
||||
if dev.DeviceClass != "VideoController" {
|
||||
t.Errorf("expected GPU device_class VideoController, got %q", dev.DeviceClass)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -375,14 +416,14 @@ func TestConvertPCIeDevices_NVSwitchWithoutSerialRemainsEmpty(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z")
|
||||
result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z", "BOARD-001")
|
||||
|
||||
if len(result) != 1 {
|
||||
t.Fatalf("expected 1 PCIe device, got %d", len(result))
|
||||
}
|
||||
|
||||
if result[0].SerialNumber != "" {
|
||||
t.Fatalf("expected empty NVSwitch serial, got %q", result[0].SerialNumber)
|
||||
if result[0].SerialNumber != "BOARD-001-PCIE-NVSWITCH1" {
|
||||
t.Fatalf("expected generated NVSwitch serial, got %q", result[0].SerialNumber)
|
||||
}
|
||||
if result[0].Firmware != "96.10.6D.00.01" {
|
||||
t.Fatalf("expected NVSwitch firmware 96.10.6D.00.01, got %q", result[0].Firmware)
|
||||
@@ -408,12 +449,12 @@ func TestConvertPCIeDevices_SkipsDisplayControllerDuplicates(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z")
|
||||
result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z", "BOARD-001")
|
||||
if len(result) != 1 {
|
||||
t.Fatalf("expected only dedicated GPU record without duplicate display PCIe, got %d", len(result))
|
||||
}
|
||||
if result[0].DeviceClass != "DisplayController" {
|
||||
t.Fatalf("expected GPU record with DisplayController class, got %q", result[0].DeviceClass)
|
||||
if result[0].DeviceClass != "VideoController" {
|
||||
t.Fatalf("expected GPU record with VideoController class, got %q", result[0].DeviceClass)
|
||||
}
|
||||
if result[0].Status != "OK" {
|
||||
t.Fatalf("expected GPU status OK, got %q", result[0].Status)
|
||||
@@ -441,7 +482,7 @@ func TestConvertPCIeDevices_MapsGPUStatusHistory(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z")
|
||||
result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z", "BOARD-001")
|
||||
if len(result) != 1 {
|
||||
t.Fatalf("expected 1 converted GPU, got %d", len(result))
|
||||
}
|
||||
@@ -452,9 +493,6 @@ func TestConvertPCIeDevices_MapsGPUStatusHistory(t *testing.T) {
|
||||
if result[0].StatusHistory[0].ChangedAt != "2026-01-12T15:05:18Z" {
|
||||
t.Fatalf("unexpected history changed_at: %q", result[0].StatusHistory[0].ChangedAt)
|
||||
}
|
||||
if result[0].StatusAtCollect == nil || result[0].StatusAtCollect.At != "2026-02-10T15:30:00Z" {
|
||||
t.Fatalf("expected status_at_collection to be populated from collected_at")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertPowerSupplies(t *testing.T) {
|
||||
@@ -518,8 +556,8 @@ func TestSourceTypeOmittedWhenInvalidOrEmpty(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("marshal failed: %v", err)
|
||||
}
|
||||
if strings.Contains(string(payload), `"source_type"`) {
|
||||
t.Fatalf("expected source_type to be omitted for invalid value, got %s", string(payload))
|
||||
if !strings.Contains(string(payload), `"source_type":"logfile"`) {
|
||||
t.Fatalf("expected archive source_type to map to logfile, got %s", string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -688,9 +726,6 @@ func TestConvertToReanimator_StatusFallbackUsesCollectedAt(t *testing.T) {
|
||||
if got.StatusCheckedAt != wantTs {
|
||||
t.Fatalf("expected status_checked_at=%q, got %q", wantTs, got.StatusCheckedAt)
|
||||
}
|
||||
if got.StatusAtCollect == nil || got.StatusAtCollect.At != wantTs {
|
||||
t.Fatalf("expected status_at_collection.at=%q, got %#v", wantTs, got.StatusAtCollect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_FirmwareExcludesDeviceBoundEntries(t *testing.T) {
|
||||
@@ -698,6 +733,9 @@ func TestConvertToReanimator_FirmwareExcludesDeviceBoundEntries(t *testing.T) {
|
||||
Filename: "fw-filter-test.json",
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
||||
CPUs: []models.CPU{
|
||||
{Socket: 0, Model: "Intel Xeon Gold"},
|
||||
},
|
||||
Firmware: []models.FirmwareInfo{
|
||||
{DeviceName: "BIOS", Version: "1.0.0"},
|
||||
{DeviceName: "BMC", Version: "2.0.0"},
|
||||
@@ -735,6 +773,58 @@ func TestConvertToReanimator_FirmwareExcludesDeviceBoundEntries(t *testing.T) {
|
||||
if _, exists := got["NVSwitch NVSWITCH0 (965-25612-0002-000)"]; exists {
|
||||
t.Fatalf("expected NVSwitch firmware to be excluded from hardware.firmware")
|
||||
}
|
||||
if len(out.Hardware.CPUs) != 1 {
|
||||
t.Fatalf("expected 1 CPU entry, got %d", len(out.Hardware.CPUs))
|
||||
}
|
||||
if out.Hardware.CPUs[0].Firmware != "0x2b000643" {
|
||||
t.Fatalf("expected CPU firmware field to carry microcode, got %q", out.Hardware.CPUs[0].Firmware)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConvertToReanimator_FirmwareExcludesDellFQDDEntries verifies that Dell TSR
|
||||
// SoftwareIdentity firmware entries whose Description contains a device-bound FQDD
|
||||
// (InfiniBand.Slot.*, RAID.SL.*, etc.) are filtered from hardware.firmware.
|
||||
//
|
||||
// Regression guard: PowerEdge R6625 (8VS2LG4) — "Mellanox Network Adapter" (FQDD
|
||||
// InfiniBand.Slot.1-1) and "PERC H755 Front" (FQDD RAID.SL.3-1) leaked into
|
||||
// hardware.firmware. (2026-03-15)
|
||||
func TestConvertToReanimator_FirmwareExcludesDellFQDDEntries(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Filename: "dell-fw-filter-test.json",
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "8VS2LG4"},
|
||||
Firmware: []models.FirmwareInfo{
|
||||
// system-level — must be kept
|
||||
{DeviceName: "BIOS", Version: "1.15.3", Description: "system bios"},
|
||||
{DeviceName: "iDRAC", Version: "7.20.80.50", Description: "idrac card"},
|
||||
{DeviceName: "Lifecycle Controller", Version: "7.20.80.50", Description: "idrac lifecycle"},
|
||||
// device-bound via FQDD — must be filtered
|
||||
{DeviceName: "Mellanox Network Adapter", Version: "20.39.35.60", Description: "InfiniBand.Slot.1-1"},
|
||||
{DeviceName: "PERC H755 Front", Version: "52.30.0-6115", Description: "RAID.SL.3-1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ConvertToReanimator(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
||||
}
|
||||
|
||||
got := make(map[string]string, len(out.Hardware.Firmware))
|
||||
for _, fw := range out.Hardware.Firmware {
|
||||
got[fw.DeviceName] = fw.Version
|
||||
}
|
||||
|
||||
for _, keep := range []string{"BIOS", "iDRAC", "Lifecycle Controller"} {
|
||||
if _, ok := got[keep]; !ok {
|
||||
t.Errorf("expected %q in hardware.firmware, but it was missing", keep)
|
||||
}
|
||||
}
|
||||
for _, drop := range []string{"Mellanox Network Adapter", "PERC H755 Front"} {
|
||||
if _, ok := got[drop]; ok {
|
||||
t.Errorf("%q must not appear in hardware.firmware (device-bound FQDD)", drop)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_UsesCanonicalDevices(t *testing.T) {
|
||||
@@ -774,9 +864,65 @@ func TestConvertToReanimator_UsesCanonicalDevices(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_BindsDeviceVitals(t *testing.T) {
|
||||
func TestConvertToReanimator_MergesCanonicalAndLegacyDevices(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Filename: "merged.json",
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
||||
Devices: []models.HardwareDevice{
|
||||
{
|
||||
Kind: models.DeviceKindPCIe,
|
||||
Slot: "PCIe 3",
|
||||
Model: "RAID Controller",
|
||||
DeviceClass: "raid_controller",
|
||||
Status: "ok",
|
||||
},
|
||||
},
|
||||
CPUs: []models.CPU{
|
||||
{Socket: 0, Model: "Xeon Platinum", SerialNumber: "CPU-001"},
|
||||
},
|
||||
Memory: []models.MemoryDIMM{
|
||||
{Slot: "DIMM0", Location: "DIMM0", Present: true, SizeMB: 32768, Type: "DDR5", SerialNumber: "MEM-001"},
|
||||
},
|
||||
Storage: []models.Storage{
|
||||
{Slot: "U.2-1", Type: "NVMe", Model: "Drive1", SerialNumber: "SSD-001", Present: true},
|
||||
},
|
||||
PowerSupply: []models.PSU{
|
||||
{Slot: "PSU0", Model: "PSU", SerialNumber: "PSU-001", Present: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ConvertToReanimator(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
||||
}
|
||||
if len(out.Hardware.CPUs) != 1 {
|
||||
t.Fatalf("expected cpu from legacy inventory to survive canonical merge, got %d", len(out.Hardware.CPUs))
|
||||
}
|
||||
if len(out.Hardware.Memory) != 1 {
|
||||
t.Fatalf("expected memory from legacy inventory to survive canonical merge, got %d", len(out.Hardware.Memory))
|
||||
}
|
||||
if len(out.Hardware.Storage) != 1 {
|
||||
t.Fatalf("expected storage from legacy inventory to survive canonical merge, got %d", len(out.Hardware.Storage))
|
||||
}
|
||||
if len(out.Hardware.PowerSupplies) != 1 {
|
||||
t.Fatalf("expected psu from legacy inventory to survive canonical merge, got %d", len(out.Hardware.PowerSupplies))
|
||||
}
|
||||
if len(out.Hardware.PCIeDevices) != 1 {
|
||||
t.Fatalf("expected supplemental canonical pcie device to remain present, got %d", len(out.Hardware.PCIeDevices))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_ExportsSensorsAndPSUTelemetry(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Filename: "vitals.json",
|
||||
Sensors: []models.SensorReading{
|
||||
{Name: "FAN1", Type: "fan", Value: 4200, Unit: "RPM", Status: "OK"},
|
||||
{Name: "12V Rail", Type: "voltage", Value: 12.1, Unit: "V", Status: "OK"},
|
||||
{Name: "CPU0 Temp", Type: "temperature", Value: 71, Unit: "C", Status: "Warning"},
|
||||
{Name: "Humidity", Type: "humidity", Value: 38.5, Unit: "%", Status: "OK"},
|
||||
},
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
||||
Devices: []models.HardwareDevice{
|
||||
@@ -786,11 +932,6 @@ func TestConvertToReanimator_BindsDeviceVitals(t *testing.T) {
|
||||
Model: "B200 180GB HBM3e",
|
||||
SerialNumber: "GPU-001",
|
||||
BDF: "0000:17:00.0",
|
||||
Details: map[string]any{
|
||||
"temperature": 71,
|
||||
"power": 350,
|
||||
"voltage": 12.2,
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: models.DeviceKindPSU,
|
||||
@@ -815,26 +956,38 @@ func TestConvertToReanimator_BindsDeviceVitals(t *testing.T) {
|
||||
t.Fatalf("expected one pcie device, got %d", len(out.Hardware.PCIeDevices))
|
||||
}
|
||||
pcie := out.Hardware.PCIeDevices[0]
|
||||
if pcie.TemperatureC != 71 {
|
||||
t.Fatalf("expected GPU temperature 71C, got %d", pcie.TemperatureC)
|
||||
}
|
||||
if pcie.PowerW != 350 {
|
||||
t.Fatalf("expected GPU power 350W, got %d", pcie.PowerW)
|
||||
}
|
||||
if pcie.VoltageV != 12.2 {
|
||||
t.Fatalf("expected device voltage 12.2V, got %.2f", pcie.VoltageV)
|
||||
if pcie.TemperatureC != 0 {
|
||||
t.Fatalf("expected canonical GPU telemetry to stay off the component unless sourced from details/gpu path, got %.2f", pcie.TemperatureC)
|
||||
}
|
||||
|
||||
if len(out.Hardware.PowerSupplies) != 1 {
|
||||
t.Fatalf("expected one PSU, got %d", len(out.Hardware.PowerSupplies))
|
||||
}
|
||||
psu := out.Hardware.PowerSupplies[0]
|
||||
if psu.InputPowerW != 1400 {
|
||||
t.Fatalf("expected PSU input power 1400W, got %.2f", psu.InputPowerW)
|
||||
}
|
||||
if psu.TemperatureC != 44 {
|
||||
t.Fatalf("expected PSU temperature 44C, got %d", psu.TemperatureC)
|
||||
t.Fatalf("expected PSU temperature 44C, got %.2f", psu.TemperatureC)
|
||||
}
|
||||
if out.Hardware.Sensors == nil {
|
||||
t.Fatalf("expected sensors section")
|
||||
}
|
||||
if len(out.Hardware.Sensors.Fans) != 1 || out.Hardware.Sensors.Fans[0].RPM != 4200 {
|
||||
t.Fatalf("expected fan sensor export, got %#v", out.Hardware.Sensors.Fans)
|
||||
}
|
||||
if len(out.Hardware.Sensors.Power) != 1 || out.Hardware.Sensors.Power[0].VoltageV != 12.1 {
|
||||
t.Fatalf("expected power sensor export, got %#v", out.Hardware.Sensors.Power)
|
||||
}
|
||||
if len(out.Hardware.Sensors.Temperatures) != 1 || out.Hardware.Sensors.Temperatures[0].Celsius != 71 {
|
||||
t.Fatalf("expected temperature sensor export, got %#v", out.Hardware.Sensors.Temperatures)
|
||||
}
|
||||
if len(out.Hardware.Sensors.Other) != 1 || out.Hardware.Sensors.Other[0].Unit != "%" {
|
||||
t.Fatalf("expected other sensor export, got %#v", out.Hardware.Sensors.Other)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_PreservesVitalsAcrossCanonicalDedup(t *testing.T) {
|
||||
func TestConvertToReanimator_PreservesCanonicalDedupWithoutDeviceVitals(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Filename: "dedup-vitals.json",
|
||||
Hardware: &models.HardwareConfig{
|
||||
@@ -872,11 +1025,283 @@ func TestConvertToReanimator_PreservesVitalsAcrossCanonicalDedup(t *testing.T) {
|
||||
t.Fatalf("expected deduped one pcie entry, got %d", len(out.Hardware.PCIeDevices))
|
||||
}
|
||||
got := out.Hardware.PCIeDevices[0]
|
||||
if got.DeviceClass != "VideoController" {
|
||||
t.Fatalf("expected GPU to export as VideoController, got %q", got.DeviceClass)
|
||||
}
|
||||
if got.TemperatureC != 67 {
|
||||
t.Fatalf("expected deduped GPU temperature 67C, got %d", got.TemperatureC)
|
||||
t.Fatalf("expected deduped GPU temperature 67C, got %.2f", got.TemperatureC)
|
||||
}
|
||||
if got.PowerW != 330 {
|
||||
t.Fatalf("expected deduped GPU power 330W, got %d", got.PowerW)
|
||||
t.Fatalf("expected deduped GPU power 330W, got %.2f", got.PowerW)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_DedupesLooseCanonicalNICAndPCIeEntries(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Filename: "loose-dedup.json",
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
||||
PCIeDevices: []models.PCIeDevice{
|
||||
{
|
||||
Slot: "Slot 4",
|
||||
DeviceClass: "NetworkController",
|
||||
VendorID: 0x15b3,
|
||||
DeviceID: 0x1021,
|
||||
Manufacturer: "Mellanox",
|
||||
PartNumber: "MCX623106AC-CDAT",
|
||||
},
|
||||
},
|
||||
NetworkAdapters: []models.NetworkAdapter{
|
||||
{
|
||||
Slot: "Slot 4",
|
||||
Model: "ConnectX-6",
|
||||
VendorID: 0x15b3,
|
||||
DeviceID: 0x1021,
|
||||
Vendor: "Mellanox",
|
||||
MACAddresses: []string{"00:11:22:33:44:55"},
|
||||
PortCount: 2,
|
||||
Present: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ConvertToReanimator(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
||||
}
|
||||
if len(out.Hardware.PCIeDevices) != 1 {
|
||||
t.Fatalf("expected one merged loose-key pcie entry, got %d", len(out.Hardware.PCIeDevices))
|
||||
}
|
||||
got := out.Hardware.PCIeDevices[0]
|
||||
if got.Model == "" {
|
||||
t.Fatalf("expected merged pcie entry to retain a model, got empty")
|
||||
}
|
||||
if len(got.MACAddresses) != 1 || got.MACAddresses[0] != "00:11:22:33:44:55" {
|
||||
t.Fatalf("expected MACs from NIC side after loose merge, got %#v", got.MACAddresses)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_ExportsContractV24Telemetry(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Filename: "contract-v24.json",
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
||||
Devices: []models.HardwareDevice{
|
||||
{
|
||||
Kind: models.DeviceKindCPU,
|
||||
Slot: "CPU0",
|
||||
Model: "INTEL(R) XEON(R) GOLD 6530",
|
||||
Details: map[string]any{
|
||||
"socket": 0,
|
||||
"temperature_c": 61.5,
|
||||
"power_w": 182.0,
|
||||
"throttled": false,
|
||||
"correctable_error_count": int64(4),
|
||||
"uncorrectable_error_count": int64(1),
|
||||
"life_remaining_pct": 98.5,
|
||||
"life_used_pct": 1.5,
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: models.DeviceKindMemory,
|
||||
Slot: "DIMM_A1",
|
||||
SerialNumber: "MEM-001",
|
||||
Present: boolPtr(true),
|
||||
SizeMB: 32768,
|
||||
Type: "DDR5",
|
||||
Details: map[string]any{
|
||||
"temperature_c": 43.0,
|
||||
"correctable_ecc_error_count": int64(2),
|
||||
"uncorrectable_ecc_error_count": int64(0),
|
||||
"life_remaining_pct": 99.0,
|
||||
"spare_blocks_remaining_pct": 97.0,
|
||||
"performance_degraded": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: models.DeviceKindStorage,
|
||||
Slot: "U.2-1",
|
||||
SerialNumber: "SSD-001",
|
||||
Model: "PM9A3",
|
||||
Present: boolPtr(true),
|
||||
Details: map[string]any{
|
||||
"temperature_c": 38.5,
|
||||
"power_on_hours": int64(12450),
|
||||
"unsafe_shutdowns": int64(3),
|
||||
"written_bytes": int64(9876543210),
|
||||
"life_remaining_pct": 91.0,
|
||||
"available_spare_pct": 88.0,
|
||||
"offline_uncorrectable": int64(0),
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: models.DeviceKindPCIe,
|
||||
Slot: "PCIeCard2",
|
||||
SerialNumber: "NIC-001",
|
||||
DeviceClass: "EthernetController",
|
||||
NUMANode: 1,
|
||||
Details: map[string]any{
|
||||
"temperature_c": 48.5,
|
||||
"power_w": 18.2,
|
||||
"life_remaining_pct": 95.0,
|
||||
"ecc_corrected_total": int64(12),
|
||||
"battery_health_pct": 87.0,
|
||||
"sfp_temperature_c": 36.2,
|
||||
"sfp_tx_power_dbm": -1.8,
|
||||
"sfp_rx_power_dbm": -2.1,
|
||||
"sfp_bias_ma": 5.5,
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: models.DeviceKindPSU,
|
||||
Slot: "PSU0",
|
||||
SerialNumber: "PSU-001",
|
||||
Present: boolPtr(true),
|
||||
Details: map[string]any{
|
||||
"life_remaining_pct": 97.0,
|
||||
"life_used_pct": 3.0,
|
||||
},
|
||||
TemperatureC: 39,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ConvertToReanimator(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
||||
}
|
||||
|
||||
if got := out.Hardware.CPUs[0]; got.TemperatureC != 61.5 || got.PowerW != 182.0 || got.Throttled == nil || *got.Throttled {
|
||||
t.Fatalf("unexpected CPU telemetry: %#v", got)
|
||||
}
|
||||
if got := out.Hardware.Memory[0]; got.TemperatureC != 43.0 || got.CorrectableECCErrorCount != 2 || got.PerformanceDegraded == nil || *got.PerformanceDegraded {
|
||||
t.Fatalf("unexpected memory telemetry: %#v", got)
|
||||
}
|
||||
if got := out.Hardware.Storage[0]; got.TemperatureC != 38.5 || got.PowerOnHours != 12450 || got.LifeRemainingPct != 91.0 {
|
||||
t.Fatalf("unexpected storage telemetry: %#v", got)
|
||||
}
|
||||
if got := out.Hardware.PCIeDevices[0]; got.NUMANode != 1 || got.TemperatureC != 48.5 || got.PowerW != 18.2 || got.SFPTemperatureC != 36.2 {
|
||||
t.Fatalf("unexpected PCIe telemetry: %#v", got)
|
||||
}
|
||||
if got := out.Hardware.PowerSupplies[0]; got.TemperatureC != 39 || got.LifeRemainingPct != 97.0 || got.LifeUsedPct != 3.0 {
|
||||
t.Fatalf("unexpected PSU telemetry: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_PreservesLegacyStorageAndPSUDetails(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Filename: "legacy-details.json",
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
||||
Storage: []models.Storage{
|
||||
{
|
||||
Slot: "Drive0",
|
||||
Type: "NVMe",
|
||||
Model: "NVMe SSD",
|
||||
SerialNumber: "SSD-001",
|
||||
Present: true,
|
||||
Details: map[string]any{
|
||||
"temperature_c": 38.5,
|
||||
"power_on_hours": int64(12450),
|
||||
"life_remaining_pct": 91.0,
|
||||
},
|
||||
},
|
||||
},
|
||||
PowerSupply: []models.PSU{
|
||||
{
|
||||
Slot: "PSU0",
|
||||
Model: "PSU",
|
||||
SerialNumber: "PSU-001",
|
||||
Present: true,
|
||||
Details: map[string]any{
|
||||
"temperature_c": 41.0,
|
||||
"life_remaining_pct": 96.0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ConvertToReanimator(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
||||
}
|
||||
if got := out.Hardware.Storage[0]; got.TemperatureC != 38.5 || got.PowerOnHours != 12450 || got.LifeRemainingPct != 91.0 {
|
||||
t.Fatalf("expected storage details from legacy model to survive canonical conversion, got %+v", got)
|
||||
}
|
||||
if got := out.Hardware.PowerSupplies[0]; got.TemperatureC != 41.0 || got.LifeRemainingPct != 96.0 {
|
||||
t.Fatalf("expected psu details from legacy model to survive canonical conversion, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_PreservesLegacyPCIeAndNICDetails(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Filename: "legacy-pcie-details.json",
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
||||
PCIeDevices: []models.PCIeDevice{
|
||||
{
|
||||
Slot: "PCIe 1",
|
||||
BDF: "0000:17:00.0",
|
||||
VendorID: 0x10de,
|
||||
DeviceID: 0x2331,
|
||||
PartNumber: "H100",
|
||||
Manufacturer: "NVIDIA",
|
||||
SerialNumber: "GPU-001",
|
||||
Details: map[string]any{
|
||||
"temperature_c": 48.5,
|
||||
"power_w": 315.0,
|
||||
"ecc_corrected_total": int64(12),
|
||||
"battery_health_pct": 87.0,
|
||||
},
|
||||
},
|
||||
},
|
||||
NetworkAdapters: []models.NetworkAdapter{
|
||||
{
|
||||
Slot: "Slot 4",
|
||||
BDF: "0000:18:00.0",
|
||||
VendorID: 0x15b3,
|
||||
DeviceID: 0x1021,
|
||||
Model: "ConnectX-6",
|
||||
SerialNumber: "NIC-001",
|
||||
Present: true,
|
||||
Details: map[string]any{
|
||||
"sfp_temperature_c": 34.0,
|
||||
"sfp_tx_power_dbm": -1.8,
|
||||
"sfp_rx_power_dbm": -2.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ConvertToReanimator(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
||||
}
|
||||
if len(out.Hardware.PCIeDevices) != 2 {
|
||||
t.Fatalf("expected two pcie-class devices, got %d", len(out.Hardware.PCIeDevices))
|
||||
}
|
||||
foundGPU := false
|
||||
foundNIC := false
|
||||
for _, dev := range out.Hardware.PCIeDevices {
|
||||
switch dev.SerialNumber {
|
||||
case "GPU-001":
|
||||
foundGPU = true
|
||||
if dev.TemperatureC != 48.5 || dev.PowerW != 315.0 || dev.ECCCorrectedTotal != 12 || dev.BatteryHealthPct != 87.0 {
|
||||
t.Fatalf("expected GPU telemetry preserved, got %+v", dev)
|
||||
}
|
||||
case "NIC-001":
|
||||
foundNIC = true
|
||||
if dev.SFPTemperatureC != 34.0 || dev.SFPTXPowerDBm != -1.8 || dev.SFPRXPowerDBm != -2.1 {
|
||||
t.Fatalf("expected NIC sfp telemetry preserved, got %+v", dev)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundGPU || !foundNIC {
|
||||
t.Fatalf("expected both gpu and nic pcie-class exports, got %+v", out.Hardware.PCIeDevices)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -932,3 +1357,42 @@ func TestIsDeviceBoundFirmwareName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsDeviceBoundFirmwareFQDD verifies that Dell TSR SoftwareIdentity FQDD strings
|
||||
// (stored in FirmwareInfo.Description) correctly identify device-bound entries.
|
||||
//
|
||||
// Regression guard: "InfiniBand.Slot.1-1" (Mellanox ConnectX-6) and "RAID.SL.3-1"
|
||||
// (PERC H755 Front) were not filtered because only "raid.backplane." was listed and
|
||||
// "infiniband." was absent. Both firmware entries leaked into hardware.firmware on
|
||||
// PowerEdge R6625 (8VS2LG4). (2026-03-15)
|
||||
func TestIsDeviceBoundFirmwareFQDD(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
want bool
|
||||
}{
|
||||
// Dell TSR SoftwareIdentity FQDDs — device-bound, must be excluded
|
||||
{"InfiniBand.Slot.1-1", true}, // Mellanox ConnectX-6
|
||||
{"InfiniBand.Slot.2-1", true}, // any InfiniBand slot
|
||||
{"RAID.SL.3-1", true}, // PERC H755 Front
|
||||
{"RAID.Integrated.1-1", true}, // embedded RAID controller
|
||||
{"RAID.Backplane.Firmware.0", true}, // backplane (previously covered)
|
||||
{"NIC.Integrated.1-1-1", true}, // embedded NIC
|
||||
{"NIC.Slot.1-1-1", true}, // slotted NIC
|
||||
{"PSU.Slot.1", true}, // PSU
|
||||
{"Disk.Bay.0:Enclosure.Internal.0-1:RAID.SL.3-1", true},
|
||||
{"GPU.Slot.1-1", true},
|
||||
{"FC.Slot.1-1", true}, // Fibre Channel HBA
|
||||
// System-level descriptions — must NOT be excluded
|
||||
{"system bios", false},
|
||||
{"idrac lifecycle", false},
|
||||
{"idrac card", false},
|
||||
{"storage controller", false}, // legacy description before fqdd fix
|
||||
{"", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := isDeviceBoundFirmwareFQDD(tc.desc)
|
||||
if got != tc.want {
|
||||
t.Errorf("isDeviceBoundFirmwareFQDD(%q) = %v, want %v", tc.desc, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ type ReanimatorHardware struct {
|
||||
Storage []ReanimatorStorage `json:"storage,omitempty"`
|
||||
PCIeDevices []ReanimatorPCIe `json:"pcie_devices,omitempty"`
|
||||
PowerSupplies []ReanimatorPSU `json:"power_supplies,omitempty"`
|
||||
Sensors *ReanimatorSensors `json:"sensors,omitempty"`
|
||||
}
|
||||
|
||||
// ReanimatorBoard represents motherboard/server information
|
||||
@@ -36,11 +37,6 @@ type ReanimatorFirmware struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type ReanimatorStatusAtCollection struct {
|
||||
Status string `json:"status"`
|
||||
At string `json:"at"`
|
||||
}
|
||||
|
||||
type ReanimatorStatusHistoryEntry struct {
|
||||
Status string `json:"status"`
|
||||
ChangedAt string `json:"changed_at"`
|
||||
@@ -49,90 +45,136 @@ type ReanimatorStatusHistoryEntry struct {
|
||||
|
||||
// ReanimatorCPU represents processor information
|
||||
type ReanimatorCPU struct {
|
||||
Socket int `json:"socket"`
|
||||
Model string `json:"model"`
|
||||
Cores int `json:"cores,omitempty"`
|
||||
Threads int `json:"threads,omitempty"`
|
||||
FrequencyMHz int `json:"frequency_mhz,omitempty"`
|
||||
MaxFrequencyMHz int `json:"max_frequency_mhz,omitempty"`
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
StatusCheckedAt string `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt string `json:"status_changed_at,omitempty"`
|
||||
StatusAtCollect *ReanimatorStatusAtCollection `json:"status_at_collection,omitempty"`
|
||||
StatusHistory []ReanimatorStatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
Socket int `json:"socket"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Cores int `json:"cores,omitempty"`
|
||||
Threads int `json:"threads,omitempty"`
|
||||
FrequencyMHz int `json:"frequency_mhz,omitempty"`
|
||||
MaxFrequencyMHz int `json:"max_frequency_mhz,omitempty"`
|
||||
TemperatureC float64 `json:"temperature_c,omitempty"`
|
||||
PowerW float64 `json:"power_w,omitempty"`
|
||||
Throttled *bool `json:"throttled,omitempty"`
|
||||
CorrectableErrorCount int64 `json:"correctable_error_count,omitempty"`
|
||||
UncorrectableErrorCount int64 `json:"uncorrectable_error_count,omitempty"`
|
||||
LifeRemainingPct float64 `json:"life_remaining_pct,omitempty"`
|
||||
LifeUsedPct float64 `json:"life_used_pct,omitempty"`
|
||||
SerialNumber string `json:"serial_number,omitempty"`
|
||||
Firmware string `json:"firmware,omitempty"`
|
||||
Present *bool `json:"present,omitempty"`
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
StatusCheckedAt string `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt string `json:"status_changed_at,omitempty"`
|
||||
StatusHistory []ReanimatorStatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
}
|
||||
|
||||
// ReanimatorMemory represents a memory module (DIMM)
|
||||
type ReanimatorMemory struct {
|
||||
Slot string `json:"slot"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Present bool `json:"present"`
|
||||
SizeMB int `json:"size_mb,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
MaxSpeedMHz int `json:"max_speed_mhz,omitempty"`
|
||||
CurrentSpeedMHz int `json:"current_speed_mhz,omitempty"`
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
SerialNumber string `json:"serial_number,omitempty"`
|
||||
PartNumber string `json:"part_number,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
StatusCheckedAt string `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt string `json:"status_changed_at,omitempty"`
|
||||
StatusAtCollect *ReanimatorStatusAtCollection `json:"status_at_collection,omitempty"`
|
||||
StatusHistory []ReanimatorStatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
Slot string `json:"slot"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Present *bool `json:"present,omitempty"`
|
||||
SizeMB int `json:"size_mb,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
MaxSpeedMHz int `json:"max_speed_mhz,omitempty"`
|
||||
CurrentSpeedMHz int `json:"current_speed_mhz,omitempty"`
|
||||
TemperatureC float64 `json:"temperature_c,omitempty"`
|
||||
CorrectableECCErrorCount int64 `json:"correctable_ecc_error_count,omitempty"`
|
||||
UncorrectableECCErrorCount int64 `json:"uncorrectable_ecc_error_count,omitempty"`
|
||||
LifeRemainingPct float64 `json:"life_remaining_pct,omitempty"`
|
||||
LifeUsedPct float64 `json:"life_used_pct,omitempty"`
|
||||
SpareBlocksRemainingPct float64 `json:"spare_blocks_remaining_pct,omitempty"`
|
||||
PerformanceDegraded *bool `json:"performance_degraded,omitempty"`
|
||||
DataLossDetected *bool `json:"data_loss_detected,omitempty"`
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
SerialNumber string `json:"serial_number,omitempty"`
|
||||
PartNumber string `json:"part_number,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
StatusCheckedAt string `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt string `json:"status_changed_at,omitempty"`
|
||||
StatusHistory []ReanimatorStatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
}
|
||||
|
||||
// ReanimatorStorage represents a storage device
|
||||
type ReanimatorStorage struct {
|
||||
Slot string `json:"slot"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Model string `json:"model"`
|
||||
SizeGB int `json:"size_gb,omitempty"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
Firmware string `json:"firmware,omitempty"`
|
||||
Interface string `json:"interface,omitempty"`
|
||||
Present bool `json:"present"`
|
||||
Status string `json:"status,omitempty"`
|
||||
StatusCheckedAt string `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt string `json:"status_changed_at,omitempty"`
|
||||
StatusAtCollect *ReanimatorStatusAtCollection `json:"status_at_collection,omitempty"`
|
||||
StatusHistory []ReanimatorStatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
Slot string `json:"slot"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Model string `json:"model"`
|
||||
SizeGB int `json:"size_gb,omitempty"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
Firmware string `json:"firmware,omitempty"`
|
||||
Interface string `json:"interface,omitempty"`
|
||||
Present *bool `json:"present,omitempty"`
|
||||
TemperatureC float64 `json:"temperature_c,omitempty"`
|
||||
PowerOnHours int64 `json:"power_on_hours,omitempty"`
|
||||
PowerCycles int64 `json:"power_cycles,omitempty"`
|
||||
UnsafeShutdowns int64 `json:"unsafe_shutdowns,omitempty"`
|
||||
MediaErrors int64 `json:"media_errors,omitempty"`
|
||||
ErrorLogEntries int64 `json:"error_log_entries,omitempty"`
|
||||
WrittenBytes int64 `json:"written_bytes,omitempty"`
|
||||
ReadBytes int64 `json:"read_bytes,omitempty"`
|
||||
LifeUsedPct float64 `json:"life_used_pct,omitempty"`
|
||||
RemainingEndurancePct *int `json:"remaining_endurance_pct,omitempty"`
|
||||
LifeRemainingPct float64 `json:"life_remaining_pct,omitempty"`
|
||||
AvailableSparePct float64 `json:"available_spare_pct,omitempty"`
|
||||
ReallocatedSectors int64 `json:"reallocated_sectors,omitempty"`
|
||||
CurrentPendingSectors int64 `json:"current_pending_sectors,omitempty"`
|
||||
OfflineUncorrectable int64 `json:"offline_uncorrectable,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
StatusCheckedAt string `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt string `json:"status_changed_at,omitempty"`
|
||||
StatusHistory []ReanimatorStatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
}
|
||||
|
||||
// ReanimatorPCIe represents a PCIe device
|
||||
type ReanimatorPCIe struct {
|
||||
Slot string `json:"slot"`
|
||||
VendorID int `json:"vendor_id,omitempty"`
|
||||
DeviceID int `json:"device_id,omitempty"`
|
||||
BDF string `json:"bdf,omitempty"`
|
||||
DeviceClass string `json:"device_class,omitempty"`
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
LinkWidth int `json:"link_width,omitempty"`
|
||||
LinkSpeed string `json:"link_speed,omitempty"`
|
||||
MaxLinkWidth int `json:"max_link_width,omitempty"`
|
||||
MaxLinkSpeed string `json:"max_link_speed,omitempty"`
|
||||
SerialNumber string `json:"serial_number,omitempty"`
|
||||
Firmware string `json:"firmware,omitempty"`
|
||||
TemperatureC int `json:"temperature_c,omitempty"`
|
||||
PowerW int `json:"power_w,omitempty"`
|
||||
VoltageV float64 `json:"voltage_v,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
StatusCheckedAt string `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt string `json:"status_changed_at,omitempty"`
|
||||
StatusAtCollect *ReanimatorStatusAtCollection `json:"status_at_collection,omitempty"`
|
||||
StatusHistory []ReanimatorStatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
Slot string `json:"slot"`
|
||||
VendorID int `json:"vendor_id,omitempty"`
|
||||
DeviceID int `json:"device_id,omitempty"`
|
||||
NUMANode int `json:"numa_node,omitempty"`
|
||||
TemperatureC float64 `json:"temperature_c,omitempty"`
|
||||
PowerW float64 `json:"power_w,omitempty"`
|
||||
LifeRemainingPct float64 `json:"life_remaining_pct,omitempty"`
|
||||
LifeUsedPct float64 `json:"life_used_pct,omitempty"`
|
||||
ECCCorrectedTotal int64 `json:"ecc_corrected_total,omitempty"`
|
||||
ECCUncorrectedTotal int64 `json:"ecc_uncorrected_total,omitempty"`
|
||||
HWSlowdown *bool `json:"hw_slowdown,omitempty"`
|
||||
BatteryChargePct float64 `json:"battery_charge_pct,omitempty"`
|
||||
BatteryHealthPct float64 `json:"battery_health_pct,omitempty"`
|
||||
BatteryTemperatureC float64 `json:"battery_temperature_c,omitempty"`
|
||||
BatteryVoltageV float64 `json:"battery_voltage_v,omitempty"`
|
||||
BatteryReplaceRequired *bool `json:"battery_replace_required,omitempty"`
|
||||
SFPTemperatureC float64 `json:"sfp_temperature_c,omitempty"`
|
||||
SFPTXPowerDBm float64 `json:"sfp_tx_power_dbm,omitempty"`
|
||||
SFPRXPowerDBm float64 `json:"sfp_rx_power_dbm,omitempty"`
|
||||
SFPVoltageV float64 `json:"sfp_voltage_v,omitempty"`
|
||||
SFPBiasMA float64 `json:"sfp_bias_ma,omitempty"`
|
||||
BDF string `json:"bdf,omitempty"`
|
||||
DeviceClass string `json:"device_class,omitempty"`
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
LinkWidth int `json:"link_width,omitempty"`
|
||||
LinkSpeed string `json:"link_speed,omitempty"`
|
||||
MaxLinkWidth int `json:"max_link_width,omitempty"`
|
||||
MaxLinkSpeed string `json:"max_link_speed,omitempty"`
|
||||
MACAddresses []string `json:"mac_addresses,omitempty"`
|
||||
Present *bool `json:"present,omitempty"`
|
||||
SerialNumber string `json:"serial_number,omitempty"`
|
||||
Firmware string `json:"firmware,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
StatusCheckedAt string `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt string `json:"status_changed_at,omitempty"`
|
||||
StatusHistory []ReanimatorStatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
}
|
||||
|
||||
// ReanimatorPSU represents a power supply unit
|
||||
type ReanimatorPSU struct {
|
||||
Slot string `json:"slot"`
|
||||
Present bool `json:"present"`
|
||||
Present *bool `json:"present,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Vendor string `json:"vendor,omitempty"`
|
||||
WattageW int `json:"wattage_w,omitempty"`
|
||||
@@ -141,13 +183,54 @@ type ReanimatorPSU struct {
|
||||
Firmware string `json:"firmware,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
InputType string `json:"input_type,omitempty"`
|
||||
InputPowerW int `json:"input_power_w,omitempty"`
|
||||
OutputPowerW int `json:"output_power_w,omitempty"`
|
||||
InputPowerW float64 `json:"input_power_w,omitempty"`
|
||||
OutputPowerW float64 `json:"output_power_w,omitempty"`
|
||||
InputVoltage float64 `json:"input_voltage,omitempty"`
|
||||
TemperatureC int `json:"temperature_c,omitempty"`
|
||||
TemperatureC float64 `json:"temperature_c,omitempty"`
|
||||
LifeRemainingPct float64 `json:"life_remaining_pct,omitempty"`
|
||||
LifeUsedPct float64 `json:"life_used_pct,omitempty"`
|
||||
StatusCheckedAt string `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt string `json:"status_changed_at,omitempty"`
|
||||
StatusAtCollect *ReanimatorStatusAtCollection `json:"status_at_collection,omitempty"`
|
||||
StatusHistory []ReanimatorStatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
}
|
||||
|
||||
type ReanimatorSensors struct {
|
||||
Fans []ReanimatorFanSensor `json:"fans,omitempty"`
|
||||
Power []ReanimatorPowerSensor `json:"power,omitempty"`
|
||||
Temperatures []ReanimatorTemperatureSensor `json:"temperatures,omitempty"`
|
||||
Other []ReanimatorOtherSensor `json:"other,omitempty"`
|
||||
}
|
||||
|
||||
type ReanimatorFanSensor struct {
|
||||
Name string `json:"name"`
|
||||
Location string `json:"location,omitempty"`
|
||||
RPM int `json:"rpm,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
type ReanimatorPowerSensor struct {
|
||||
Name string `json:"name"`
|
||||
Location string `json:"location,omitempty"`
|
||||
VoltageV float64 `json:"voltage_v,omitempty"`
|
||||
CurrentA float64 `json:"current_a,omitempty"`
|
||||
PowerW float64 `json:"power_w,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
type ReanimatorTemperatureSensor struct {
|
||||
Name string `json:"name"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Celsius float64 `json:"celsius,omitempty"`
|
||||
ThresholdWarningCelsius float64 `json:"threshold_warning_celsius,omitempty"`
|
||||
ThresholdCriticalCelsius float64 `json:"threshold_critical_celsius,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
type ReanimatorOtherSensor struct {
|
||||
Name string `json:"name"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Value float64 `json:"value,omitempty"`
|
||||
Unit string `json:"unit,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user