export: align reanimator contract v2.7
This commit is contained in:
@@ -45,12 +45,13 @@ func ConvertToReanimator(result *models.AnalysisResult) (*ReanimatorExport, erro
|
||||
Hardware: ReanimatorHardware{
|
||||
Board: convertBoard(result.Hardware.BoardInfo),
|
||||
Firmware: dedupeFirmware(convertFirmware(result.Hardware.Firmware)),
|
||||
CPUs: convertCPUsFromDevices(devices, collectedAt, result.Hardware.BoardInfo.SerialNumber, buildCPUMicrocodeBySocket(result.Hardware.Firmware)),
|
||||
Memory: convertMemoryFromDevices(devices, collectedAt),
|
||||
Storage: convertStorageFromDevices(devices, collectedAt),
|
||||
PCIeDevices: convertPCIeFromDevices(devices, collectedAt, result.Hardware.BoardInfo.SerialNumber),
|
||||
PowerSupplies: convertPSUsFromDevices(devices, collectedAt),
|
||||
CPUs: dedupeCPUs(convertCPUsFromDevices(devices, collectedAt, result.Hardware.BoardInfo.SerialNumber, buildCPUMicrocodeBySocket(result.Hardware.Firmware))),
|
||||
Memory: dedupeMemory(convertMemoryFromDevices(devices, collectedAt)),
|
||||
Storage: dedupeStorage(convertStorageFromDevices(devices, collectedAt)),
|
||||
PCIeDevices: dedupePCIe(convertPCIeFromDevices(devices, collectedAt)),
|
||||
PowerSupplies: dedupePSUs(convertPSUsFromDevices(devices, collectedAt)),
|
||||
Sensors: convertSensors(result.Sensors),
|
||||
EventLogs: convertEventLogs(result.Events, collectedAt),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -91,6 +92,7 @@ func buildDevicesFromLegacy(hw *models.HardwareConfig) []models.HardwareDevice {
|
||||
return nil
|
||||
}
|
||||
all := make([]models.HardwareDevice, 0, len(hw.CPUs)+len(hw.Memory)+len(hw.Storage)+len(hw.PCIeDevices)+len(hw.GPUs)+len(hw.NetworkAdapters)+len(hw.PowerSupply))
|
||||
nvswitchFirmwareBySlot := buildNVSwitchFirmwareBySlot(hw.Firmware)
|
||||
appendDevice := func(d models.HardwareDevice) {
|
||||
all = append(all, d)
|
||||
}
|
||||
@@ -175,6 +177,16 @@ func buildDevicesFromLegacy(hw *models.HardwareConfig) []models.HardwareDevice {
|
||||
if pcieModel == "" {
|
||||
pcieModel = pcie.Description
|
||||
}
|
||||
details := mergeDetailMaps(nil, pcie.Details)
|
||||
pcieFirmware := stringFromDetailMap(details, "firmware")
|
||||
if pcieFirmware == "" && isNVSwitchPCIeDevice(pcie) {
|
||||
pcieFirmware = nvswitchFirmwareBySlot[normalizeNVSwitchSlotForLookup(pcie.Slot)]
|
||||
if pcieFirmware != "" {
|
||||
details = mergeDetailMaps(details, map[string]any{
|
||||
"firmware": pcieFirmware,
|
||||
})
|
||||
}
|
||||
}
|
||||
appendDevice(models.HardwareDevice{
|
||||
Kind: models.DeviceKindPCIe,
|
||||
Slot: pcie.Slot,
|
||||
@@ -197,7 +209,7 @@ func buildDevicesFromLegacy(hw *models.HardwareConfig) []models.HardwareDevice {
|
||||
StatusAtCollect: pcie.StatusAtCollect,
|
||||
StatusHistory: pcie.StatusHistory,
|
||||
ErrorDescription: pcie.ErrorDescription,
|
||||
Details: mergeDetailMaps(nil, pcie.Details),
|
||||
Details: details,
|
||||
})
|
||||
}
|
||||
for _, gpu := range hw.GPUs {
|
||||
@@ -353,6 +365,7 @@ func dedupeCanonicalDevices(items []models.HardwareDevice) []models.HardwareDevi
|
||||
|
||||
var unmatched []models.HardwareDevice
|
||||
for _, item := range noKey {
|
||||
mergeKind := canonicalMergeKind(item.Kind)
|
||||
identity := deviceIdentity(item)
|
||||
mfr := strings.ToLower(strings.TrimSpace(item.Manufacturer))
|
||||
if identity == "" {
|
||||
@@ -363,7 +376,8 @@ func dedupeCanonicalDevices(items []models.HardwareDevice) []models.HardwareDevi
|
||||
matchCount := 0
|
||||
for _, k := range order {
|
||||
existing := byKey[k].item
|
||||
if deviceIdentity(existing) == identity &&
|
||||
if canonicalMergeKind(existing.Kind) == mergeKind &&
|
||||
deviceIdentity(existing) == identity &&
|
||||
strings.ToLower(strings.TrimSpace(existing.Manufacturer)) == mfr {
|
||||
matchKey = k
|
||||
matchCount++
|
||||
@@ -507,32 +521,43 @@ func mergeDetailMaps(primary, secondary map[string]any) map[string]any {
|
||||
}
|
||||
|
||||
func canonicalKey(item models.HardwareDevice) string {
|
||||
kind := canonicalMergeKind(item.Kind)
|
||||
if sn := normalizedSerial(item.SerialNumber); sn != "" {
|
||||
return "sn:" + strings.ToLower(sn)
|
||||
return kind + "|sn:" + strings.ToLower(sn)
|
||||
}
|
||||
if bdf := strings.ToLower(strings.TrimSpace(item.BDF)); bdf != "" {
|
||||
return "bdf:" + bdf
|
||||
return kind + "|bdf:" + bdf
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func canonicalLooseKey(item models.HardwareDevice) string {
|
||||
kind := canonicalMergeKind(item.Kind)
|
||||
slot := strings.ToLower(strings.TrimSpace(item.Slot))
|
||||
model := strings.ToLower(strings.TrimSpace(item.Model))
|
||||
part := strings.ToLower(strings.TrimSpace(item.PartNumber))
|
||||
mfr := strings.ToLower(strings.TrimSpace(item.Manufacturer))
|
||||
if item.VendorID != 0 && item.DeviceID != 0 && slot != "" {
|
||||
return fmt.Sprintf("slotid:%s|%d|%d", slot, item.VendorID, item.DeviceID)
|
||||
return fmt.Sprintf("%s|slotid:%s|%d|%d", kind, slot, item.VendorID, item.DeviceID)
|
||||
}
|
||||
if slot != "" && model != "" && mfr != "" {
|
||||
return "slotmodel:" + slot + "|" + model + "|" + mfr
|
||||
return kind + "|slotmodel:" + slot + "|" + model + "|" + mfr
|
||||
}
|
||||
if slot != "" && part != "" && mfr != "" {
|
||||
return "slotpart:" + slot + "|" + part + "|" + mfr
|
||||
return kind + "|slotpart:" + slot + "|" + part + "|" + mfr
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func canonicalMergeKind(kind string) string {
|
||||
switch kind {
|
||||
case models.DeviceKindPCIe, models.DeviceKindGPU, models.DeviceKindNetwork:
|
||||
return "pcie-class"
|
||||
default:
|
||||
return strings.TrimSpace(kind)
|
||||
}
|
||||
}
|
||||
|
||||
func canonicalScore(item models.HardwareDevice) int {
|
||||
score := 0
|
||||
if normalizedSerial(item.SerialNumber) != "" {
|
||||
@@ -607,18 +632,19 @@ func convertCPUsFromDevices(devices []models.HardwareDevice, collectedAt, boardS
|
||||
UncorrectableErrorCount: int64FromDetailMap(d.Details, "uncorrectable_error_count"),
|
||||
LifeRemainingPct: floatFromDetailMap(d.Details, "life_remaining_pct"),
|
||||
LifeUsedPct: floatFromDetailMap(d.Details, "life_used_pct"),
|
||||
SerialNumber: generatedCPUSerial(d.SerialNumber, boardSerial, socket),
|
||||
Firmware: firstNonEmptyString(
|
||||
SerialNumber: strings.TrimSpace(d.SerialNumber),
|
||||
Firmware: firstNonEmptyString(
|
||||
stringFromDetailMap(d.Details, "microcode"),
|
||||
microcodeBySocket[socket],
|
||||
stringFromDetailMap(d.Details, "firmware"),
|
||||
),
|
||||
Manufacturer: inferCPUManufacturer(d.Model),
|
||||
Status: cpuStatus,
|
||||
StatusCheckedAt: meta.StatusCheckedAt,
|
||||
StatusChangedAt: meta.StatusChangedAt,
|
||||
StatusHistory: meta.StatusHistory,
|
||||
ErrorDescription: meta.ErrorDescription,
|
||||
Manufacturer: inferCPUManufacturer(d.Model),
|
||||
Status: cpuStatus,
|
||||
StatusCheckedAt: meta.StatusCheckedAt,
|
||||
StatusChangedAt: meta.StatusChangedAt,
|
||||
ManufacturedYearWeek: manufacturedYearWeekFromDetails(d.Details),
|
||||
StatusHistory: meta.StatusHistory,
|
||||
ErrorDescription: meta.ErrorDescription,
|
||||
})
|
||||
}
|
||||
return result
|
||||
@@ -657,6 +683,7 @@ func convertMemoryFromDevices(devices []models.HardwareDevice, collectedAt strin
|
||||
Status: status,
|
||||
StatusCheckedAt: meta.StatusCheckedAt,
|
||||
StatusChangedAt: meta.StatusChangedAt,
|
||||
ManufacturedYearWeek: manufacturedYearWeekFromDetails(d.Details),
|
||||
StatusHistory: meta.StatusHistory,
|
||||
ErrorDescription: meta.ErrorDescription,
|
||||
})
|
||||
@@ -670,6 +697,9 @@ func convertStorageFromDevices(devices []models.HardwareDevice, collectedAt stri
|
||||
if d.Kind != models.DeviceKindStorage {
|
||||
continue
|
||||
}
|
||||
if isVirtualExportStorageDevice(d) {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(d.SerialNumber) == "" {
|
||||
continue
|
||||
}
|
||||
@@ -709,6 +739,7 @@ func convertStorageFromDevices(devices []models.HardwareDevice, collectedAt stri
|
||||
Status: status,
|
||||
StatusCheckedAt: meta.StatusCheckedAt,
|
||||
StatusChangedAt: meta.StatusChangedAt,
|
||||
ManufacturedYearWeek: manufacturedYearWeekFromDetails(d.Details),
|
||||
StatusHistory: meta.StatusHistory,
|
||||
ErrorDescription: meta.ErrorDescription,
|
||||
})
|
||||
@@ -716,19 +747,25 @@ func convertStorageFromDevices(devices []models.HardwareDevice, collectedAt stri
|
||||
return result
|
||||
}
|
||||
|
||||
func convertPCIeFromDevices(devices []models.HardwareDevice, collectedAt, boardSerial string) []ReanimatorPCIe {
|
||||
func convertPCIeFromDevices(devices []models.HardwareDevice, collectedAt string) []ReanimatorPCIe {
|
||||
result := make([]ReanimatorPCIe, 0)
|
||||
for _, d := range devices {
|
||||
if d.Kind != models.DeviceKindPCIe && d.Kind != models.DeviceKindGPU && d.Kind != models.DeviceKindNetwork {
|
||||
continue
|
||||
}
|
||||
if isStorageEndpointPCIeDevice(d) {
|
||||
continue
|
||||
}
|
||||
if isPlaceholderPCIeExportDevice(d) {
|
||||
continue
|
||||
}
|
||||
if d.Present != nil && !*d.Present {
|
||||
continue
|
||||
}
|
||||
deviceClass := normalizePCIeDeviceClass(d)
|
||||
model := d.Model
|
||||
model := normalizePlaceholderDeviceModel(d.Model)
|
||||
if model == "" {
|
||||
model = d.PartNumber
|
||||
model = normalizePlaceholderDeviceModel(d.PartNumber)
|
||||
}
|
||||
// General rule: if model not found in source data but PCI IDs are known, resolve from pci.ids.
|
||||
if model == "" && d.VendorID != 0 && d.DeviceID != 0 {
|
||||
@@ -749,8 +786,9 @@ func convertPCIeFromDevices(devices []models.HardwareDevice, collectedAt, boardS
|
||||
)
|
||||
status := normalizeStatus(d.Status, false)
|
||||
meta := buildStatusMeta(status, d.StatusCheckedAt, d.StatusChangedAt, d.StatusHistory, d.ErrorDescription, collectedAt)
|
||||
slot := firstNonEmptyString(d.Slot, d.BDF)
|
||||
result = append(result, ReanimatorPCIe{
|
||||
Slot: d.Slot,
|
||||
Slot: slot,
|
||||
VendorID: d.VendorID,
|
||||
DeviceID: d.DeviceID,
|
||||
NUMANode: d.NUMANode,
|
||||
@@ -780,11 +818,12 @@ func convertPCIeFromDevices(devices []models.HardwareDevice, collectedAt, boardS
|
||||
MaxLinkWidth: d.MaxLinkWidth,
|
||||
MaxLinkSpeed: d.MaxLinkSpeed,
|
||||
MACAddresses: append([]string(nil), d.MACAddresses...),
|
||||
SerialNumber: generatedPCIeSerial(d.SerialNumber, boardSerial, d.Slot),
|
||||
Firmware: d.Firmware,
|
||||
SerialNumber: strings.TrimSpace(d.SerialNumber),
|
||||
Firmware: firstNonEmptyString(d.Firmware, stringFromDetailMap(d.Details, "firmware")),
|
||||
Status: status,
|
||||
StatusCheckedAt: meta.StatusCheckedAt,
|
||||
StatusChangedAt: meta.StatusChangedAt,
|
||||
ManufacturedYearWeek: manufacturedYearWeekFromDetails(d.Details),
|
||||
StatusHistory: meta.StatusHistory,
|
||||
ErrorDescription: meta.ErrorDescription,
|
||||
})
|
||||
@@ -792,6 +831,96 @@ func convertPCIeFromDevices(devices []models.HardwareDevice, collectedAt, boardS
|
||||
return result
|
||||
}
|
||||
|
||||
func isStorageEndpointPCIeDevice(d models.HardwareDevice) bool {
|
||||
if d.Kind != models.DeviceKindPCIe {
|
||||
return false
|
||||
}
|
||||
|
||||
class := strings.ToLower(strings.TrimSpace(d.DeviceClass))
|
||||
if !strings.Contains(class, "storage") && !strings.Contains(class, "nonvolatile") && !strings.Contains(class, "nvme") {
|
||||
return false
|
||||
}
|
||||
|
||||
joined := strings.ToLower(strings.TrimSpace(strings.Join([]string{
|
||||
d.Slot,
|
||||
d.Model,
|
||||
d.PartNumber,
|
||||
d.Manufacturer,
|
||||
stringFromDetailMap(d.Details, "description"),
|
||||
}, " ")))
|
||||
|
||||
if strings.Contains(joined, "raid") || strings.Contains(joined, "hba") || strings.Contains(joined, "controller") {
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.Contains(joined, "nvme") ||
|
||||
strings.Contains(joined, "ssd") ||
|
||||
strings.Contains(joined, "u.2") ||
|
||||
strings.Contains(joined, "e1.s") ||
|
||||
strings.Contains(joined, "e3.s") ||
|
||||
strings.Contains(joined, "disk") ||
|
||||
strings.Contains(joined, "drive")
|
||||
}
|
||||
|
||||
func isVirtualExportStorageDevice(d models.HardwareDevice) bool {
|
||||
if d.Kind != models.DeviceKindStorage {
|
||||
return false
|
||||
}
|
||||
mfr := strings.ToUpper(strings.TrimSpace(d.Manufacturer))
|
||||
model := strings.ToUpper(strings.TrimSpace(d.Model))
|
||||
slot := strings.ToUpper(strings.TrimSpace(d.Slot))
|
||||
if strings.Contains(mfr, "AMERICAN MEGATRENDS") || strings.Contains(mfr, "AMI") {
|
||||
joined := strings.Join([]string{mfr, model, slot}, " ")
|
||||
for _, marker := range []string{
|
||||
"VIRTUAL CDROM",
|
||||
"VIRTUAL CD/DVD",
|
||||
"VIRTUAL FLOPPY",
|
||||
"VIRTUAL FDD",
|
||||
"VIRTUAL MEDIA",
|
||||
"USB_DEVICE",
|
||||
} {
|
||||
if strings.Contains(joined, marker) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isPlaceholderPCIeExportDevice(d models.HardwareDevice) bool {
|
||||
if d.Kind != models.DeviceKindPCIe && d.Kind != models.DeviceKindNetwork {
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(d.BDF) != "" {
|
||||
return false
|
||||
}
|
||||
if d.VendorID != 0 || d.DeviceID != 0 {
|
||||
return false
|
||||
}
|
||||
if normalizedSerial(d.SerialNumber) != "" {
|
||||
return false
|
||||
}
|
||||
if len(d.MACAddresses) > 0 {
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(d.Firmware) != "" {
|
||||
return false
|
||||
}
|
||||
if d.LinkWidth != 0 || d.MaxLinkWidth != 0 || strings.TrimSpace(d.LinkSpeed) != "" || strings.TrimSpace(d.MaxLinkSpeed) != "" {
|
||||
return false
|
||||
}
|
||||
if hasMeaningfulExporterText(d.Model) || hasMeaningfulExporterText(d.PartNumber) || hasMeaningfulExporterText(d.Manufacturer) || hasMeaningfulExporterText(stringFromDetailMap(d.Details, "description")) {
|
||||
return false
|
||||
}
|
||||
|
||||
class := strings.ToLower(strings.TrimSpace(d.DeviceClass))
|
||||
if class != "" && class != "unknown" && class != "other" && class != "pcie device" && class != "network" && class != "network controller" && class != "networkcontroller" {
|
||||
return false
|
||||
}
|
||||
|
||||
return isNumericExporterSlot(d.Slot)
|
||||
}
|
||||
|
||||
func convertPSUsFromDevices(devices []models.HardwareDevice, collectedAt string) []ReanimatorPSU {
|
||||
result := make([]ReanimatorPSU, 0)
|
||||
for _, d := range devices {
|
||||
@@ -805,30 +934,66 @@ func convertPSUsFromDevices(devices []models.HardwareDevice, collectedAt string)
|
||||
status := normalizeStatus(d.Status, false)
|
||||
meta := buildStatusMeta(status, d.StatusCheckedAt, d.StatusChangedAt, d.StatusHistory, d.ErrorDescription, collectedAt)
|
||||
result = append(result, ReanimatorPSU{
|
||||
Slot: d.Slot,
|
||||
Model: d.Model,
|
||||
Vendor: d.Manufacturer,
|
||||
WattageW: d.WattageW,
|
||||
SerialNumber: d.SerialNumber,
|
||||
PartNumber: d.PartNumber,
|
||||
Firmware: d.Firmware,
|
||||
Status: status,
|
||||
InputType: d.InputType,
|
||||
InputPowerW: float64(d.InputPowerW),
|
||||
OutputPowerW: float64(d.OutputPowerW),
|
||||
InputVoltage: d.InputVoltage,
|
||||
TemperatureC: firstNonZeroFloat(float64(d.TemperatureC), floatFromDetailMap(d.Details, "temperature_c")),
|
||||
LifeRemainingPct: floatFromDetailMap(d.Details, "life_remaining_pct"),
|
||||
LifeUsedPct: floatFromDetailMap(d.Details, "life_used_pct"),
|
||||
StatusCheckedAt: meta.StatusCheckedAt,
|
||||
StatusChangedAt: meta.StatusChangedAt,
|
||||
StatusHistory: meta.StatusHistory,
|
||||
ErrorDescription: meta.ErrorDescription,
|
||||
Slot: d.Slot,
|
||||
Model: d.Model,
|
||||
Vendor: d.Manufacturer,
|
||||
WattageW: d.WattageW,
|
||||
SerialNumber: d.SerialNumber,
|
||||
PartNumber: d.PartNumber,
|
||||
Firmware: d.Firmware,
|
||||
Status: status,
|
||||
InputType: d.InputType,
|
||||
InputPowerW: float64(d.InputPowerW),
|
||||
OutputPowerW: float64(d.OutputPowerW),
|
||||
InputVoltage: d.InputVoltage,
|
||||
TemperatureC: firstNonZeroFloat(float64(d.TemperatureC), floatFromDetailMap(d.Details, "temperature_c")),
|
||||
LifeRemainingPct: floatFromDetailMap(d.Details, "life_remaining_pct"),
|
||||
LifeUsedPct: floatFromDetailMap(d.Details, "life_used_pct"),
|
||||
StatusCheckedAt: meta.StatusCheckedAt,
|
||||
StatusChangedAt: meta.StatusChangedAt,
|
||||
ManufacturedYearWeek: manufacturedYearWeekFromDetails(d.Details),
|
||||
StatusHistory: meta.StatusHistory,
|
||||
ErrorDescription: meta.ErrorDescription,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func convertEventLogs(events []models.Event, collectedAt string) []ReanimatorEventLog {
|
||||
if len(events) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]ReanimatorEventLog, 0, len(events))
|
||||
for _, event := range events {
|
||||
source := normalizeEventLogSource(event.Source)
|
||||
message := strings.TrimSpace(event.Description)
|
||||
if source == "" || message == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
item := ReanimatorEventLog{
|
||||
Source: source,
|
||||
EventTime: formatEventLogTime(event.Timestamp, collectedAt),
|
||||
Severity: normalizeEventLogSeverity(event.Severity),
|
||||
MessageID: strings.TrimSpace(event.ID),
|
||||
Message: message,
|
||||
ComponentRef: firstNonEmptyString(strings.TrimSpace(event.SensorName), strings.TrimSpace(event.SensorType)),
|
||||
}
|
||||
if raw := strings.TrimSpace(event.RawData); raw != "" {
|
||||
item.RawPayload = map[string]any{
|
||||
"raw_data": raw,
|
||||
}
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func convertSensors(sensors []models.SensorReading) *ReanimatorSensors {
|
||||
if len(sensors) == 0 {
|
||||
return nil
|
||||
@@ -836,7 +1001,7 @@ func convertSensors(sensors []models.SensorReading) *ReanimatorSensors {
|
||||
|
||||
out := &ReanimatorSensors{}
|
||||
seenFans := map[string]struct{}{}
|
||||
seenPower := map[string]struct{}{}
|
||||
powerIndex := map[string]int{}
|
||||
seenTemps := map[string]struct{}{}
|
||||
seenOther := map[string]struct{}{}
|
||||
|
||||
@@ -845,10 +1010,12 @@ func convertSensors(sensors []models.SensorReading) *ReanimatorSensors {
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
if !sensorHasNumericReading(s) {
|
||||
continue
|
||||
}
|
||||
status := normalizeSensorStatus(s.Status)
|
||||
sType := strings.ToLower(strings.TrimSpace(s.Type))
|
||||
unit := strings.TrimSpace(s.Unit)
|
||||
location := inferSensorLocation(name)
|
||||
|
||||
switch {
|
||||
case sType == "fan" || strings.EqualFold(unit, "RPM"):
|
||||
@@ -856,49 +1023,41 @@ func convertSensors(sensors []models.SensorReading) *ReanimatorSensors {
|
||||
continue
|
||||
}
|
||||
out.Fans = append(out.Fans, ReanimatorFanSensor{
|
||||
Name: name,
|
||||
Location: location,
|
||||
RPM: int(s.Value),
|
||||
Status: status,
|
||||
Name: name,
|
||||
RPM: int(s.Value),
|
||||
Status: status,
|
||||
})
|
||||
case sType == "power" || sType == "voltage" || sType == "current" || strings.EqualFold(unit, "V") || strings.EqualFold(unit, "A") || strings.EqualFold(unit, "W"):
|
||||
if seenFirst(seenPower, name) {
|
||||
baseName := groupedPowerSensorName(name)
|
||||
if idx, ok := powerIndex[baseName]; ok {
|
||||
mergePowerSensorReading(&out.Power[idx], sType, unit, s.Value, status)
|
||||
continue
|
||||
}
|
||||
item := ReanimatorPowerSensor{
|
||||
Name: name,
|
||||
Location: location,
|
||||
Status: status,
|
||||
}
|
||||
switch {
|
||||
case sType == "current" || strings.EqualFold(unit, "A"):
|
||||
item.CurrentA = s.Value
|
||||
case sType == "power" || strings.EqualFold(unit, "W"):
|
||||
item.PowerW = s.Value
|
||||
default:
|
||||
item.VoltageV = s.Value
|
||||
Name: baseName,
|
||||
Status: status,
|
||||
}
|
||||
mergePowerSensorReading(&item, sType, unit, s.Value, status)
|
||||
powerIndex[baseName] = len(out.Power)
|
||||
out.Power = append(out.Power, item)
|
||||
case sType == "temperature" || strings.EqualFold(unit, "C") || strings.EqualFold(unit, "°C"):
|
||||
if seenFirst(seenTemps, name) {
|
||||
continue
|
||||
}
|
||||
out.Temperatures = append(out.Temperatures, ReanimatorTemperatureSensor{
|
||||
Name: name,
|
||||
Location: location,
|
||||
Celsius: s.Value,
|
||||
Status: status,
|
||||
Name: name,
|
||||
Celsius: s.Value,
|
||||
Status: status,
|
||||
})
|
||||
default:
|
||||
if seenFirst(seenOther, name) {
|
||||
continue
|
||||
}
|
||||
out.Other = append(out.Other, ReanimatorOtherSensor{
|
||||
Name: name,
|
||||
Location: location,
|
||||
Value: s.Value,
|
||||
Unit: unit,
|
||||
Status: status,
|
||||
Name: name,
|
||||
Value: s.Value,
|
||||
Unit: unit,
|
||||
Status: status,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -909,6 +1068,69 @@ func convertSensors(sensors []models.SensorReading) *ReanimatorSensors {
|
||||
return out
|
||||
}
|
||||
|
||||
func groupedPowerSensorName(name string) string {
|
||||
trimmed := strings.TrimSpace(name)
|
||||
lower := strings.ToLower(trimmed)
|
||||
for _, suffix := range []string{"_inputpower", "_inputvoltage", "_inputcurrent"} {
|
||||
if strings.HasSuffix(lower, suffix) {
|
||||
return strings.TrimSpace(trimmed[:len(trimmed)-len(suffix)])
|
||||
}
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
func mergePowerSensorReading(item *ReanimatorPowerSensor, sType, unit string, value float64, status string) {
|
||||
if item == nil {
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case sType == "current" || strings.EqualFold(unit, "A"):
|
||||
item.CurrentA = value
|
||||
case sType == "power" || strings.EqualFold(unit, "W"):
|
||||
item.PowerW = value
|
||||
default:
|
||||
item.VoltageV = value
|
||||
}
|
||||
item.Status = mergeSensorStatus(item.Status, status)
|
||||
}
|
||||
|
||||
func mergeSensorStatus(current, incoming string) string {
|
||||
current = strings.TrimSpace(current)
|
||||
incoming = strings.TrimSpace(incoming)
|
||||
if current == "" {
|
||||
return incoming
|
||||
}
|
||||
if incoming == "" {
|
||||
return current
|
||||
}
|
||||
if sensorStatusRank(incoming) > sensorStatusRank(current) {
|
||||
return incoming
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
func sensorStatusRank(status string) int {
|
||||
switch strings.ToLower(strings.TrimSpace(status)) {
|
||||
case "critical":
|
||||
return 3
|
||||
case "warning":
|
||||
return 2
|
||||
case "ok":
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func sensorHasNumericReading(s models.SensorReading) bool {
|
||||
if strings.TrimSpace(s.RawValue) != "" {
|
||||
if _, err := strconv.ParseFloat(strings.TrimSpace(s.RawValue), 64); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return s.Value != 0
|
||||
}
|
||||
|
||||
func isDeviceBoundFirmwareName(name string) bool {
|
||||
n := strings.TrimSpace(strings.ToLower(name))
|
||||
if n == "" {
|
||||
@@ -937,6 +1159,7 @@ func isDeviceBoundFirmwareName(name string) bool {
|
||||
// HGX baseboard firmware inventory IDs for device-bound components
|
||||
strings.Contains(n, "_fw_gpu_") ||
|
||||
strings.Contains(n, "_fw_nvswitch_") ||
|
||||
strings.Contains(n, "_fw_erot_") ||
|
||||
strings.Contains(n, "_inforom_gpu_") {
|
||||
return true
|
||||
}
|
||||
@@ -944,6 +1167,60 @@ func isDeviceBoundFirmwareName(name string) bool {
|
||||
return cpuMicrocodeFirmwareRegex.MatchString(strings.TrimSpace(name))
|
||||
}
|
||||
|
||||
func normalizeEventLogSource(source string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(source)) {
|
||||
case "redfish":
|
||||
return "redfish"
|
||||
case "sel", "bmc", "ipmi", "idrac", "lifecycle controller", "lifecyclecontroller":
|
||||
return "bmc"
|
||||
case "system", "syslog", "smart", "zfs", "file", "gpu", "dmi", "nvidia driver", "gpu field diagnostics", "fan", "memory", "host":
|
||||
return "host"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeEventLogSeverity(severity models.Severity) string {
|
||||
switch severity {
|
||||
case models.SeverityCritical:
|
||||
return "Critical"
|
||||
case models.SeverityWarning:
|
||||
return "Warning"
|
||||
case models.SeverityInfo:
|
||||
return "Info"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func formatEventLogTime(ts time.Time, collectedAt string) string {
|
||||
if !ts.IsZero() {
|
||||
return ts.UTC().Format(time.RFC3339)
|
||||
}
|
||||
return strings.TrimSpace(collectedAt)
|
||||
}
|
||||
|
||||
func manufacturedYearWeekFromDetails(details map[string]any) string {
|
||||
if details == nil {
|
||||
return ""
|
||||
}
|
||||
value := normalizeManufacturedYearWeek(stringFromDetailMap(details, "manufactured_year_week"))
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
return normalizeManufacturedYearWeek(stringFromDetailMap(details, "mfg_date"))
|
||||
}
|
||||
|
||||
var manufacturedYearWeekRegex = regexp.MustCompile(`^\d{4}-W\d{2}$`)
|
||||
|
||||
func normalizeManufacturedYearWeek(value string) string {
|
||||
value = strings.TrimSpace(strings.ToUpper(value))
|
||||
if manufacturedYearWeekRegex.MatchString(value) {
|
||||
return value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// isDeviceBoundFirmwareFQDD returns true if the description looks like a device-bound FQDD
|
||||
// (e.g. NIC.Integrated.1-1-1, PSU.Slot.1, Disk.Bay.0:..., RAID.SL.3-1, InfiniBand.Slot.1-1).
|
||||
// These firmware entries are already embedded in the device itself and must not appear
|
||||
@@ -989,7 +1266,7 @@ func buildCPUMicrocodeBySocket(firmware []models.FirmwareInfo) map[int]string {
|
||||
}
|
||||
|
||||
// convertCPUs converts CPU information to Reanimator format
|
||||
func convertCPUs(cpus []models.CPU, collectedAt, boardSerial string) []ReanimatorCPU {
|
||||
func convertCPUs(cpus []models.CPU, collectedAt string) []ReanimatorCPU {
|
||||
if len(cpus) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -1018,7 +1295,7 @@ func convertCPUs(cpus []models.CPU, collectedAt, boardSerial string) []Reanimato
|
||||
Threads: cpu.Threads,
|
||||
FrequencyMHz: cpu.FrequencyMHz,
|
||||
MaxFrequencyMHz: cpu.MaxFreqMHz,
|
||||
SerialNumber: generatedCPUSerial(cpu.SerialNumber, boardSerial, cpu.Socket),
|
||||
SerialNumber: strings.TrimSpace(cpu.SerialNumber),
|
||||
Firmware: "",
|
||||
Manufacturer: manufacturer,
|
||||
Status: cpuStatus,
|
||||
@@ -1120,7 +1397,7 @@ func convertStorage(storage []models.Storage, collectedAt string) []ReanimatorSt
|
||||
}
|
||||
|
||||
// convertPCIeDevices converts PCIe devices, GPUs, and network adapters to Reanimator format
|
||||
func convertPCIeDevices(hw *models.HardwareConfig, collectedAt, boardSerial string) []ReanimatorPCIe {
|
||||
func convertPCIeDevices(hw *models.HardwareConfig, collectedAt string) []ReanimatorPCIe {
|
||||
result := make([]ReanimatorPCIe, 0)
|
||||
gpuSlots := make(map[string]struct{}, len(hw.GPUs))
|
||||
nvswitchFirmwareBySlot := buildNVSwitchFirmwareBySlot(hw.Firmware)
|
||||
@@ -1140,12 +1417,23 @@ func convertPCIeDevices(hw *models.HardwareConfig, collectedAt, boardSerial stri
|
||||
continue
|
||||
}
|
||||
|
||||
serialNumber := generatedPCIeSerial(pcie.SerialNumber, boardSerial, pcie.Slot)
|
||||
if isStorageEndpointPCIeDevice(models.HardwareDevice{
|
||||
Kind: models.DeviceKindPCIe,
|
||||
Slot: pcie.Slot,
|
||||
DeviceClass: pcie.DeviceClass,
|
||||
Model: pcie.Description,
|
||||
PartNumber: pcie.PartNumber,
|
||||
Manufacturer: pcie.Manufacturer,
|
||||
}) {
|
||||
continue
|
||||
}
|
||||
|
||||
serialNumber := strings.TrimSpace(pcie.SerialNumber)
|
||||
|
||||
// Determine model: PartNumber > Description (chip name) > DeviceClass (bus width fallback)
|
||||
model := pcie.PartNumber
|
||||
model := normalizePlaceholderDeviceModel(pcie.PartNumber)
|
||||
if model == "" {
|
||||
model = pcie.Description
|
||||
model = normalizePlaceholderDeviceModel(pcie.Description)
|
||||
}
|
||||
if model == "" {
|
||||
model = pcie.DeviceClass
|
||||
@@ -1191,7 +1479,7 @@ func convertPCIeDevices(hw *models.HardwareConfig, collectedAt, boardSerial stri
|
||||
|
||||
// Convert GPUs as PCIe devices
|
||||
for _, gpu := range hw.GPUs {
|
||||
serialNumber := generatedPCIeSerial(gpu.SerialNumber, boardSerial, gpu.Slot)
|
||||
serialNumber := strings.TrimSpace(gpu.SerialNumber)
|
||||
|
||||
status := normalizeStatus(gpu.Status, false)
|
||||
meta := buildStatusMeta(
|
||||
@@ -1233,7 +1521,7 @@ func convertPCIeDevices(hw *models.HardwareConfig, collectedAt, boardSerial stri
|
||||
continue
|
||||
}
|
||||
|
||||
serialNumber := generatedPCIeSerial(nic.SerialNumber, boardSerial, nic.Slot)
|
||||
serialNumber := strings.TrimSpace(nic.SerialNumber)
|
||||
|
||||
status := normalizeStatus(nic.Status, false)
|
||||
meta := buildStatusMeta(
|
||||
@@ -1285,19 +1573,25 @@ func buildNVSwitchFirmwareBySlot(firmware []models.FirmwareInfo) map[string]stri
|
||||
result := make(map[string]string)
|
||||
for _, fw := range firmware {
|
||||
name := strings.TrimSpace(fw.DeviceName)
|
||||
if !strings.HasPrefix(strings.ToUpper(name), "NVSWITCH ") {
|
||||
if strings.HasPrefix(strings.ToUpper(name), "HGX_FW_EROT_") {
|
||||
continue
|
||||
}
|
||||
|
||||
rest := strings.TrimSpace(name[len("NVSwitch "):])
|
||||
if rest == "" {
|
||||
slot := ""
|
||||
switch {
|
||||
case strings.HasPrefix(strings.ToUpper(name), "NVSWITCH "):
|
||||
rest := strings.TrimSpace(name[len("NVSwitch "):])
|
||||
if rest == "" {
|
||||
continue
|
||||
}
|
||||
slot = rest
|
||||
if idx := strings.Index(rest, " ("); idx > 0 {
|
||||
slot = strings.TrimSpace(rest[:idx])
|
||||
}
|
||||
case strings.HasPrefix(strings.ToUpper(name), "HGX_FW_NVSWITCH_"):
|
||||
slot = strings.TrimPrefix(strings.ToUpper(name), "HGX_FW_")
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
slot := rest
|
||||
if idx := strings.Index(rest, " ("); idx > 0 {
|
||||
slot = strings.TrimSpace(rest[:idx])
|
||||
}
|
||||
slot = normalizeNVSwitchSlotForLookup(slot)
|
||||
if slot == "" {
|
||||
continue
|
||||
@@ -1388,28 +1682,6 @@ func seenFirst(seen map[string]struct{}, key string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func inferSensorLocation(name string) string {
|
||||
lower := strings.ToLower(strings.TrimSpace(name))
|
||||
switch {
|
||||
case strings.Contains(lower, "front"):
|
||||
return "Front"
|
||||
case strings.Contains(lower, "rear"):
|
||||
return "Rear"
|
||||
case strings.Contains(lower, "inlet"):
|
||||
return "Front"
|
||||
case strings.Contains(lower, "cpu0"):
|
||||
return "CPU0"
|
||||
case strings.Contains(lower, "cpu1"):
|
||||
return "CPU1"
|
||||
case strings.Contains(lower, "psu0"):
|
||||
return "PSU0"
|
||||
case strings.Contains(lower, "psu1"):
|
||||
return "PSU1"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeSensorStatus(status string) string {
|
||||
return normalizeStatus(status, false)
|
||||
}
|
||||
@@ -1829,29 +2101,6 @@ func boolFromPresentPtr(v *bool, defaultValue bool) bool {
|
||||
return *v
|
||||
}
|
||||
|
||||
func generatedCPUSerial(serial, boardSerial string, socket int) string {
|
||||
if normalized := normalizedSerial(serial); normalized != "" {
|
||||
return normalized
|
||||
}
|
||||
if strings.TrimSpace(boardSerial) != "" {
|
||||
return fmt.Sprintf("%s-CPU-%d", strings.TrimSpace(boardSerial), socket)
|
||||
}
|
||||
return fmt.Sprintf("CPU-%d", socket)
|
||||
}
|
||||
|
||||
func generatedPCIeSerial(serial, boardSerial, slot string) string {
|
||||
if normalized := normalizedSerial(serial); normalized != "" {
|
||||
return normalized
|
||||
}
|
||||
if strings.TrimSpace(slot) == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.TrimSpace(boardSerial) != "" {
|
||||
return fmt.Sprintf("%s-PCIE-%s", strings.TrimSpace(boardSerial), strings.TrimSpace(slot))
|
||||
}
|
||||
return fmt.Sprintf("PCIE-%s", strings.TrimSpace(slot))
|
||||
}
|
||||
|
||||
func floatFromDetailMap(details map[string]any, key string) float64 {
|
||||
if details == nil {
|
||||
return 0
|
||||
@@ -1946,6 +2195,42 @@ func normalizeNetworkDeviceClass(portType, model, description string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func normalizePlaceholderDeviceModel(model string) string {
|
||||
trimmed := strings.TrimSpace(model)
|
||||
switch strings.ToLower(trimmed) {
|
||||
case "", "network device view", "pci device view", "pcie device view", "storage device view":
|
||||
return ""
|
||||
default:
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
|
||||
func hasMeaningfulExporterText(v string) bool {
|
||||
s := strings.ToLower(strings.TrimSpace(v))
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
switch s {
|
||||
case "-", "n/a", "na", "none", "null", "unknown", "network device view", "pci device view", "pcie device view", "storage device view":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func isNumericExporterSlot(slot string) bool {
|
||||
slot = strings.TrimSpace(slot)
|
||||
if slot == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range slot {
|
||||
if r < '0' || r > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// inferStorageStatus determines storage device status
|
||||
func inferStorageStatus(stor models.Storage) string {
|
||||
if !stor.Present {
|
||||
|
||||
@@ -210,7 +210,7 @@ func TestConvertCPUs(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
result := convertCPUs(cpus, "2026-02-10T15:30:00Z", "BOARD-001")
|
||||
result := convertCPUs(cpus, "2026-02-10T15:30:00Z")
|
||||
|
||||
if len(result) != 2 {
|
||||
t.Fatalf("expected 2 CPUs, got %d", len(result))
|
||||
@@ -227,8 +227,8 @@ 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)
|
||||
if result[0].SerialNumber != "" {
|
||||
t.Errorf("expected empty CPU serial when source serial is absent, got %q", result[0].SerialNumber)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,6 +259,158 @@ func TestConvertMemory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_CPUSerialIsNotSynthesizedAndSocketIsDeduped(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Filename: "cpu-dedupe.json",
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
||||
Devices: []models.HardwareDevice{
|
||||
{
|
||||
Kind: models.DeviceKindCPU,
|
||||
Slot: "CPU1",
|
||||
Model: "Xeon Platinum",
|
||||
Cores: 56,
|
||||
Status: "OK",
|
||||
Details: map[string]any{
|
||||
"socket": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
CPUs: []models.CPU{
|
||||
{Socket: 1, Model: "Xeon Platinum", Cores: 56, Status: "OK"},
|
||||
{Socket: 2, Model: "Xeon Platinum", Cores: 56, Status: "OK"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ConvertToReanimator(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
||||
}
|
||||
if len(out.Hardware.CPUs) != 2 {
|
||||
t.Fatalf("expected exactly two CPUs after socket dedupe, got %d", len(out.Hardware.CPUs))
|
||||
}
|
||||
for _, cpu := range out.Hardware.CPUs {
|
||||
if cpu.SerialNumber != "" {
|
||||
t.Fatalf("expected CPU serial to stay empty when source serial is absent, got %q", cpu.SerialNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_ExportsEventLogsAndOmitsPCIeBDFJSON(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Filename: "events.json",
|
||||
CollectedAt: time.Date(2026, 3, 15, 12, 0, 0, 0, time.UTC),
|
||||
Events: []models.Event{
|
||||
{
|
||||
ID: "0x0042",
|
||||
Timestamp: time.Date(2026, 3, 15, 11, 59, 0, 0, time.UTC),
|
||||
Source: "SEL",
|
||||
SensorName: "CPU0_C0D0",
|
||||
Severity: models.SeverityWarning,
|
||||
Description: "Correctable ECC error threshold exceeded",
|
||||
RawData: "sel_record_id=42",
|
||||
},
|
||||
{
|
||||
Source: "LOGPile",
|
||||
Severity: models.SeverityWarning,
|
||||
Description: "internal warning should not leak to event_logs",
|
||||
},
|
||||
},
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
||||
Devices: []models.HardwareDevice{
|
||||
{
|
||||
Kind: models.DeviceKindPCIe,
|
||||
Slot: "",
|
||||
BDF: "0000:18:00.0",
|
||||
DeviceClass: "NetworkController",
|
||||
Manufacturer: "Mellanox",
|
||||
Model: "ConnectX-6",
|
||||
Status: "OK",
|
||||
Details: map[string]any{
|
||||
"manufactured_year_week": "2024-W07",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ConvertToReanimator(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
||||
}
|
||||
if len(out.Hardware.EventLogs) != 1 {
|
||||
t.Fatalf("expected 1 exported event log, got %d", len(out.Hardware.EventLogs))
|
||||
}
|
||||
log := out.Hardware.EventLogs[0]
|
||||
if log.Source != "bmc" {
|
||||
t.Fatalf("expected SEL source to map to bmc, got %#v", log)
|
||||
}
|
||||
if log.ComponentRef != "CPU0_C0D0" {
|
||||
t.Fatalf("expected sensor name to map to component_ref, got %#v", log)
|
||||
}
|
||||
if len(out.Hardware.PCIeDevices) != 1 {
|
||||
t.Fatalf("expected 1 pcie device, got %d", len(out.Hardware.PCIeDevices))
|
||||
}
|
||||
if out.Hardware.PCIeDevices[0].Slot != "0000:18:00.0" {
|
||||
t.Fatalf("expected slot to fall back to BDF, got %#v", out.Hardware.PCIeDevices[0])
|
||||
}
|
||||
if out.Hardware.PCIeDevices[0].ManufacturedYearWeek != "2024-W07" {
|
||||
t.Fatalf("expected manufactured_year_week to be exported, got %#v", out.Hardware.PCIeDevices[0])
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(out)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal() failed: %v", err)
|
||||
}
|
||||
if strings.Contains(string(payload), `"bdf"`) {
|
||||
t.Fatalf("expected pcie bdf field to stay out of JSON payload: %s", payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_EventLogSourceMappingSupportsDellAndHostSyslog(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Filename: "event-source-map.json",
|
||||
CollectedAt: time.Date(2026, 3, 15, 12, 0, 0, 0, time.UTC),
|
||||
Events: []models.Event{
|
||||
{
|
||||
ID: "SYS1001",
|
||||
Timestamp: time.Date(2026, 3, 15, 11, 58, 0, 0, time.UTC),
|
||||
Source: "iDRAC",
|
||||
SensorName: "NIC.Slot.1-1-1",
|
||||
Severity: models.SeverityWarning,
|
||||
Description: "Link is down",
|
||||
},
|
||||
{
|
||||
ID: "syslog_1",
|
||||
Timestamp: time.Date(2026, 3, 15, 11, 59, 0, 0, time.UTC),
|
||||
Source: "syslog",
|
||||
SensorName: "systemd[1]",
|
||||
Severity: models.SeverityInfo,
|
||||
Description: "Started Example Service",
|
||||
},
|
||||
},
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ConvertToReanimator(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
||||
}
|
||||
if len(out.Hardware.EventLogs) != 2 {
|
||||
t.Fatalf("expected 2 event logs, got %d", len(out.Hardware.EventLogs))
|
||||
}
|
||||
if out.Hardware.EventLogs[0].Source != "bmc" {
|
||||
t.Fatalf("expected iDRAC event to map to bmc, got %#v", out.Hardware.EventLogs[0])
|
||||
}
|
||||
if out.Hardware.EventLogs[1].Source != "host" {
|
||||
t.Fatalf("expected syslog event to map to host, got %#v", out.Hardware.EventLogs[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertStorage(t *testing.T) {
|
||||
storage := []models.Storage{
|
||||
{
|
||||
@@ -288,6 +440,46 @@ func TestConvertStorage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_SkipsAMIVirtualStorageDevices(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Filename: "virtual-media.json",
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
||||
Storage: []models.Storage{
|
||||
{
|
||||
Slot: "USB_Device1_Port4",
|
||||
Type: "HDD",
|
||||
Model: "Virtual Cdrom Device",
|
||||
SerialNumber: "AAAABBBBCCCC1",
|
||||
Manufacturer: "American Megatrends Inc.",
|
||||
Interface: "USB",
|
||||
Present: true,
|
||||
},
|
||||
{
|
||||
Slot: "OB01",
|
||||
Type: "NVMe",
|
||||
Model: "Memblaze PBlaze7",
|
||||
SerialNumber: "REAL-NVME-001",
|
||||
Manufacturer: "Memblaze",
|
||||
Interface: "NVMe",
|
||||
Present: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ConvertToReanimator(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
||||
}
|
||||
if len(out.Hardware.Storage) != 1 {
|
||||
t.Fatalf("expected only one real storage device to remain, got %d", len(out.Hardware.Storage))
|
||||
}
|
||||
if out.Hardware.Storage[0].SerialNumber != "REAL-NVME-001" {
|
||||
t.Fatalf("expected virtual AMI storage to be skipped, got %#v", out.Hardware.Storage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertStorage_RemainingEndurance(t *testing.T) {
|
||||
pct100 := 100
|
||||
pct3 := 3
|
||||
@@ -370,16 +562,16 @@ func TestConvertPCIeDevices(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z", "BOARD-001")
|
||||
result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z")
|
||||
|
||||
// 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 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)
|
||||
// Missing serials must remain absent.
|
||||
if result[1].SerialNumber != "" {
|
||||
t.Errorf("expected empty serial for missing device serial, got %q", result[1].SerialNumber)
|
||||
}
|
||||
|
||||
// Check GPU was included
|
||||
@@ -416,20 +608,71 @@ func TestConvertPCIeDevices_NVSwitchWithoutSerialRemainsEmpty(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z", "BOARD-001")
|
||||
result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z")
|
||||
|
||||
if len(result) != 1 {
|
||||
t.Fatalf("expected 1 PCIe device, got %d", len(result))
|
||||
}
|
||||
|
||||
if result[0].SerialNumber != "BOARD-001-PCIE-NVSWITCH1" {
|
||||
t.Fatalf("expected generated NVSwitch serial, got %q", result[0].SerialNumber)
|
||||
if result[0].SerialNumber != "" {
|
||||
t.Fatalf("expected empty 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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_MapsHGXNVSwitchFirmwareToPCIeDevice(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
||||
Firmware: []models.FirmwareInfo{
|
||||
{DeviceName: "HGX_FW_ERoT_NVSwitch_0", Version: "00.02.0192.0000_n00"},
|
||||
{DeviceName: "HGX_FW_NVSwitch_0", Version: "96.10.73.00.01"},
|
||||
},
|
||||
PCIeDevices: []models.PCIeDevice{
|
||||
{
|
||||
Slot: "NVSwitch_0",
|
||||
DeviceClass: "NVSwitch",
|
||||
Manufacturer: "NVIDIA",
|
||||
Details: map[string]any{
|
||||
"temperature_c": 31.59375,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ConvertToReanimator(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
||||
}
|
||||
if len(out.Hardware.PCIeDevices) != 1 {
|
||||
t.Fatalf("expected one NVSwitch PCIe device, got %d", len(out.Hardware.PCIeDevices))
|
||||
}
|
||||
got := out.Hardware.PCIeDevices[0]
|
||||
if got.Firmware != "96.10.73.00.01" {
|
||||
t.Fatalf("expected HGX NVSwitch firmware to map to device, got %#v", got)
|
||||
}
|
||||
if got.TemperatureC != 31.59375 {
|
||||
t.Fatalf("expected NVSwitch temperature to be exported, got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNVSwitchFirmwareBySlot_SkipsERoTFirmware(t *testing.T) {
|
||||
got := buildNVSwitchFirmwareBySlot([]models.FirmwareInfo{
|
||||
{DeviceName: "HGX_FW_ERoT_NVSwitch_0", Version: "00.02.0192.0000_n00"},
|
||||
{DeviceName: "HGX_FW_NVSwitch_0", Version: "96.10.73.00.01"},
|
||||
})
|
||||
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("expected only main NVSwitch firmware to remain, got %#v", got)
|
||||
}
|
||||
if got["NVSWITCH_0"] != "96.10.73.00.01" {
|
||||
t.Fatalf("expected main NVSwitch firmware, got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertPCIeDevices_SkipsDisplayControllerDuplicates(t *testing.T) {
|
||||
hw := &models.HardwareConfig{
|
||||
PCIeDevices: []models.PCIeDevice{
|
||||
@@ -449,7 +692,7 @@ func TestConvertPCIeDevices_SkipsDisplayControllerDuplicates(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z", "BOARD-001")
|
||||
result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z")
|
||||
if len(result) != 1 {
|
||||
t.Fatalf("expected only dedicated GPU record without duplicate display PCIe, got %d", len(result))
|
||||
}
|
||||
@@ -482,7 +725,7 @@ func TestConvertPCIeDevices_MapsGPUStatusHistory(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z", "BOARD-001")
|
||||
result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z")
|
||||
if len(result) != 1 {
|
||||
t.Fatalf("expected 1 converted GPU, got %d", len(result))
|
||||
}
|
||||
@@ -667,11 +910,11 @@ func TestConvertToReanimator_DeduplicatesAllSections(t *testing.T) {
|
||||
if len(out.Hardware.Firmware) != 1 {
|
||||
t.Fatalf("expected deduped firmware len=1, got %d", len(out.Hardware.Firmware))
|
||||
}
|
||||
if len(out.Hardware.CPUs) != 2 {
|
||||
t.Fatalf("expected cpus len=2 (no serial/bdf dedupe), got %d", len(out.Hardware.CPUs))
|
||||
if len(out.Hardware.CPUs) != 1 {
|
||||
t.Fatalf("expected cpus len=1 after socket dedupe, got %d", len(out.Hardware.CPUs))
|
||||
}
|
||||
if len(out.Hardware.Memory) != 2 {
|
||||
t.Fatalf("expected memory len=2 (different serials), got %d", len(out.Hardware.Memory))
|
||||
if len(out.Hardware.Memory) != 1 {
|
||||
t.Fatalf("expected memory len=1 after slot dedupe, got %d", len(out.Hardware.Memory))
|
||||
}
|
||||
if len(out.Hardware.Storage) != 1 {
|
||||
t.Fatalf("expected deduped storage len=1, got %d", len(out.Hardware.Storage))
|
||||
@@ -679,8 +922,8 @@ func TestConvertToReanimator_DeduplicatesAllSections(t *testing.T) {
|
||||
if len(out.Hardware.PowerSupplies) != 1 {
|
||||
t.Fatalf("expected deduped psu len=1, got %d", len(out.Hardware.PowerSupplies))
|
||||
}
|
||||
if len(out.Hardware.PCIeDevices) != 4 {
|
||||
t.Fatalf("expected pcie len=4 with serial->bdf dedupe, got %d", len(out.Hardware.PCIeDevices))
|
||||
if len(out.Hardware.PCIeDevices) != 2 {
|
||||
t.Fatalf("expected pcie len=2 after final pcie-class dedupe, got %d", len(out.Hardware.PCIeDevices))
|
||||
}
|
||||
|
||||
gpuCount := 0
|
||||
@@ -689,8 +932,8 @@ func TestConvertToReanimator_DeduplicatesAllSections(t *testing.T) {
|
||||
gpuCount++
|
||||
}
|
||||
}
|
||||
if gpuCount != 2 {
|
||||
t.Fatalf("expected two #GPU0 records (pcie+gpu kinds), got %d", gpuCount)
|
||||
if gpuCount != 1 {
|
||||
t.Fatalf("expected one merged #GPU0 record, got %d", gpuCount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -914,6 +1157,143 @@ func TestConvertToReanimator_MergesCanonicalAndLegacyDevices(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_DoesNotMergeStorageIntoPCIeBySharedSerial(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Filename: "nvme-redfish.json",
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
||||
Storage: []models.Storage{
|
||||
{
|
||||
Slot: "Disk.Bay.0",
|
||||
Type: "NVMe",
|
||||
Model: "MZQL21T9HCJR-00A07",
|
||||
SerialNumber: "S64GNNFX612200",
|
||||
Manufacturer: "Samsung",
|
||||
Firmware: "GDC5A02Q",
|
||||
Present: true,
|
||||
},
|
||||
},
|
||||
PCIeDevices: []models.PCIeDevice{
|
||||
{
|
||||
Slot: "NVMeSSD1",
|
||||
BDF: "0000:81:00.0",
|
||||
DeviceClass: "MassStorageController",
|
||||
Description: "MZQL21T9HCJR-00A07",
|
||||
SerialNumber: "S64GNNFX612200",
|
||||
Manufacturer: "Samsung",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ConvertToReanimator(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
||||
}
|
||||
if len(out.Hardware.Storage) != 1 {
|
||||
t.Fatalf("expected storage record to survive shared-serial canonical merge, got %d", len(out.Hardware.Storage))
|
||||
}
|
||||
if out.Hardware.Storage[0].Slot != "Disk.Bay.0" {
|
||||
t.Fatalf("expected storage slot Disk.Bay.0, got %q", out.Hardware.Storage[0].Slot)
|
||||
}
|
||||
if len(out.Hardware.PCIeDevices) != 0 {
|
||||
t.Fatalf("expected NVMe storage endpoint to be excluded from pcie export, got %d records", len(out.Hardware.PCIeDevices))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_LeavesStorageControllersInPCIe(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-123"},
|
||||
PCIeDevices: []models.PCIeDevice{
|
||||
{
|
||||
Slot: "PCIe Slot 3",
|
||||
BDF: "0000:5e:00.0",
|
||||
DeviceClass: "MassStorageController",
|
||||
Description: "MegaRAID Controller",
|
||||
PartNumber: "PERC H755",
|
||||
SerialNumber: "RAID-001",
|
||||
Manufacturer: "Dell",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ConvertToReanimator(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
||||
}
|
||||
if len(out.Hardware.PCIeDevices) != 1 {
|
||||
t.Fatalf("expected RAID controller to remain in pcie export, got %d", len(out.Hardware.PCIeDevices))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_PCIePlaceholderModelFallsBackToPCIIDs(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-123"},
|
||||
Devices: []models.HardwareDevice{
|
||||
{
|
||||
Kind: models.DeviceKindNetwork,
|
||||
Slot: "NIC1",
|
||||
Model: "Network Device View",
|
||||
VendorID: 0x15b3,
|
||||
DeviceID: 0x101d,
|
||||
Manufacturer: "Mellanox",
|
||||
Present: boolPtr(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ConvertToReanimator(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
||||
}
|
||||
if len(out.Hardware.PCIeDevices) != 1 {
|
||||
t.Fatalf("expected one pcie export, got %d", len(out.Hardware.PCIeDevices))
|
||||
}
|
||||
if strings.EqualFold(out.Hardware.PCIeDevices[0].Model, "Network Device View") {
|
||||
t.Fatalf("expected placeholder model to be replaced, got %q", out.Hardware.PCIeDevices[0].Model)
|
||||
}
|
||||
if out.Hardware.PCIeDevices[0].SerialNumber != "" {
|
||||
t.Fatalf("expected missing pcie serial to stay empty, got %q", out.Hardware.PCIeDevices[0].SerialNumber)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_SkipsPlaceholderNetworkPCIeRecords(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-123"},
|
||||
Devices: []models.HardwareDevice{
|
||||
{
|
||||
Kind: models.DeviceKindNetwork,
|
||||
Slot: "1",
|
||||
Status: "Unknown",
|
||||
},
|
||||
{
|
||||
Kind: models.DeviceKindNetwork,
|
||||
Slot: "NIC2",
|
||||
Model: "ConnectX-7",
|
||||
Manufacturer: "NVIDIA",
|
||||
Present: boolPtr(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ConvertToReanimator(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
||||
}
|
||||
if len(out.Hardware.PCIeDevices) != 1 {
|
||||
t.Fatalf("expected only one meaningful pcie-class device, got %d", len(out.Hardware.PCIeDevices))
|
||||
}
|
||||
if out.Hardware.PCIeDevices[0].Slot != "NIC2" {
|
||||
t.Fatalf("expected placeholder numeric-slot NIC to be skipped, got %+v", out.Hardware.PCIeDevices)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_ExportsSensorsAndPSUTelemetry(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Filename: "vitals.json",
|
||||
@@ -987,6 +1367,60 @@ func TestConvertToReanimator_ExportsSensorsAndPSUTelemetry(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_SkipsSensorsWithoutNumericReadings(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Filename: "sensor-gaps.json",
|
||||
Sensors: []models.SensorReading{
|
||||
{Name: "CPU0 Temp", Type: "temperature", Status: "OK", RawValue: "N/A"},
|
||||
{Name: "PSU1 Power", Type: "power", Status: "OK", RawValue: ""},
|
||||
{Name: "Fan1", Type: "fan", Status: "OK", RawValue: "not present"},
|
||||
{Name: "Humidity", Type: "humidity", Status: "OK", RawValue: "unknown"},
|
||||
},
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ConvertToReanimator(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
||||
}
|
||||
if out.Hardware.Sensors != nil {
|
||||
t.Fatalf("expected sensors to be omitted when all readings are non-numeric, got %+v", out.Hardware.Sensors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_MergesSiblingPowerSensors(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Filename: "power-sensors.json",
|
||||
Sensors: []models.SensorReading{
|
||||
{Name: "Power Supply Bay 8_InputPower", Type: "power", Value: 231, Unit: "W", Status: "OK"},
|
||||
{Name: "Power Supply Bay 8_InputVoltage", Type: "voltage", Value: 228, Unit: "V", Status: "OK"},
|
||||
},
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ConvertToReanimator(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
||||
}
|
||||
if out.Hardware.Sensors == nil || len(out.Hardware.Sensors.Power) != 1 {
|
||||
t.Fatalf("expected one merged power sensor, got %#v", out.Hardware.Sensors)
|
||||
}
|
||||
got := out.Hardware.Sensors.Power[0]
|
||||
if got.Name != "Power Supply Bay 8" {
|
||||
t.Fatalf("expected merged sensor name, got %q", got.Name)
|
||||
}
|
||||
if got.PowerW != 231 || got.VoltageV != 228 {
|
||||
t.Fatalf("expected merged power/voltage readings, got %#v", got)
|
||||
}
|
||||
if got.Location != "" {
|
||||
t.Fatalf("expected sensor location to be omitted, got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToReanimator_PreservesCanonicalDedupWithoutDeviceVitals(t *testing.T) {
|
||||
input := &models.AnalysisResult{
|
||||
Filename: "dedup-vitals.json",
|
||||
@@ -1336,6 +1770,7 @@ func TestIsDeviceBoundFirmwareName(t *testing.T) {
|
||||
{"NVMe Drive", true},
|
||||
// HGX FW ID patterns (in case Id is used as name)
|
||||
{"HGX_FW_GPU_SXM_1", true},
|
||||
{"HGX_FW_ERoT_NVSwitch_0", true},
|
||||
{"HGX_InfoROM_GPU_SXM_2", true},
|
||||
// System-level firmware — must NOT be excluded
|
||||
{"BIOS", false},
|
||||
|
||||
@@ -20,6 +20,7 @@ type ReanimatorHardware struct {
|
||||
PCIeDevices []ReanimatorPCIe `json:"pcie_devices,omitempty"`
|
||||
PowerSupplies []ReanimatorPSU `json:"power_supplies,omitempty"`
|
||||
Sensors *ReanimatorSensors `json:"sensors,omitempty"`
|
||||
EventLogs []ReanimatorEventLog `json:"event_logs,omitempty"`
|
||||
}
|
||||
|
||||
// ReanimatorBoard represents motherboard/server information
|
||||
@@ -65,6 +66,7 @@ type ReanimatorCPU struct {
|
||||
Status string `json:"status,omitempty"`
|
||||
StatusCheckedAt string `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt string `json:"status_changed_at,omitempty"`
|
||||
ManufacturedYearWeek string `json:"manufactured_year_week,omitempty"`
|
||||
StatusHistory []ReanimatorStatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
}
|
||||
@@ -92,6 +94,7 @@ type ReanimatorMemory struct {
|
||||
Status string `json:"status,omitempty"`
|
||||
StatusCheckedAt string `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt string `json:"status_changed_at,omitempty"`
|
||||
ManufacturedYearWeek string `json:"manufactured_year_week,omitempty"`
|
||||
StatusHistory []ReanimatorStatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
}
|
||||
@@ -125,6 +128,7 @@ type ReanimatorStorage struct {
|
||||
Status string `json:"status,omitempty"`
|
||||
StatusCheckedAt string `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt string `json:"status_changed_at,omitempty"`
|
||||
ManufacturedYearWeek string `json:"manufactured_year_week,omitempty"`
|
||||
StatusHistory []ReanimatorStatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
}
|
||||
@@ -152,7 +156,7 @@ type ReanimatorPCIe struct {
|
||||
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"`
|
||||
BDF string `json:"-"`
|
||||
DeviceClass string `json:"device_class,omitempty"`
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
@@ -167,32 +171,46 @@ type ReanimatorPCIe struct {
|
||||
Status string `json:"status,omitempty"`
|
||||
StatusCheckedAt string `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt string `json:"status_changed_at,omitempty"`
|
||||
ManufacturedYearWeek string `json:"manufactured_year_week,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,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Vendor string `json:"vendor,omitempty"`
|
||||
WattageW int `json:"wattage_w,omitempty"`
|
||||
SerialNumber string `json:"serial_number,omitempty"`
|
||||
PartNumber string `json:"part_number,omitempty"`
|
||||
Firmware string `json:"firmware,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
InputType string `json:"input_type,omitempty"`
|
||||
InputPowerW float64 `json:"input_power_w,omitempty"`
|
||||
OutputPowerW float64 `json:"output_power_w,omitempty"`
|
||||
InputVoltage float64 `json:"input_voltage,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"`
|
||||
StatusHistory []ReanimatorStatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
Slot string `json:"slot"`
|
||||
Present *bool `json:"present,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Vendor string `json:"vendor,omitempty"`
|
||||
WattageW int `json:"wattage_w,omitempty"`
|
||||
SerialNumber string `json:"serial_number,omitempty"`
|
||||
PartNumber string `json:"part_number,omitempty"`
|
||||
Firmware string `json:"firmware,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
InputType string `json:"input_type,omitempty"`
|
||||
InputPowerW float64 `json:"input_power_w,omitempty"`
|
||||
OutputPowerW float64 `json:"output_power_w,omitempty"`
|
||||
InputVoltage float64 `json:"input_voltage,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"`
|
||||
ManufacturedYearWeek string `json:"manufactured_year_week,omitempty"`
|
||||
StatusHistory []ReanimatorStatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
}
|
||||
|
||||
type ReanimatorEventLog struct {
|
||||
Source string `json:"source"`
|
||||
EventTime string `json:"event_time,omitempty"`
|
||||
Severity string `json:"severity,omitempty"`
|
||||
MessageID string `json:"message_id,omitempty"`
|
||||
Message string `json:"message"`
|
||||
ComponentRef string `json:"component_ref,omitempty"`
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
IsActive *bool `json:"is_active,omitempty"`
|
||||
RawPayload map[string]any `json:"raw_payload,omitempty"`
|
||||
}
|
||||
|
||||
type ReanimatorSensors struct {
|
||||
|
||||
Reference in New Issue
Block a user