export: align reanimator contract v2.7

This commit is contained in:
Mikhail Chusavitin
2026-03-15 23:27:32 +03:00
parent 9007f1b360
commit 476630190d
31 changed files with 3502 additions and 689 deletions
+419 -134
View File
@@ -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 {