Files
logpile/internal/exporter/reanimator_converter.go
Michael Chus 8ca173c99b fix(exporter): preserve all HGX GPUs with generic PCIe slot name
Supermicro HGX BMC reports all 8 B200 GPU PCIe devices with Name
"PCIe Device" — a generic label shared by every GPU, not a unique
hardware position. pcieDedupKey used slot as the primary key, so all
8 GPUs collapsed to one entry in the UI (the first, serial 1654925165720).

Add isGenericPCIeSlotName to detect non-positional slot labels and fall
through to serial/BDF for dedup instead, preserving each GPU separately.
Positional slots (#GPU0, SLOT-NIC1, etc.) continue to use slot-first dedup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 16:05:49 +03:00

2419 lines
72 KiB
Go

package exporter
import (
"fmt"
"net/url"
"regexp"
"sort"
"strconv"
"strings"
"time"
"git.mchus.pro/mchus/logpile/internal/models"
"git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids"
)
var cpuMicrocodeFirmwareRegex = regexp.MustCompile(`(?i)^cpu\d+\s+microcode$`)
var cpuMicrocodeFirmwareCaptureRegex = regexp.MustCompile(`(?i)^cpu(\d+)\s+microcode$`)
// ConvertToReanimator converts AnalysisResult to Reanimator export format
func ConvertToReanimator(result *models.AnalysisResult) (*ReanimatorExport, error) {
if result == nil {
return nil, fmt.Errorf("no data available for export")
}
if result.Hardware == nil {
return nil, fmt.Errorf("no hardware data available for export")
}
if result.Hardware.BoardInfo.SerialNumber == "" {
return nil, fmt.Errorf("board serial_number is required for Reanimator export")
}
// Determine target host (optional field)
targetHost := inferTargetHost(result.TargetHost, result.Filename)
collectedAt := formatRFC3339(reanimatorCollectedAt(result))
devices := canonicalDevicesForExport(result.Hardware)
export := &ReanimatorExport{
Filename: result.Filename,
SourceType: normalizeSourceType(result.SourceType),
Protocol: normalizeProtocol(result.Protocol),
TargetHost: targetHost,
CollectedAt: collectedAt,
Hardware: ReanimatorHardware{
Board: convertBoard(result.Hardware.BoardInfo),
Firmware: dedupeFirmware(convertFirmware(result.Hardware.Firmware)),
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),
},
}
return export, nil
}
// reanimatorCollectedAt returns the best timestamp for Reanimator export collected_at.
// Prefers InventoryLastModifiedAt when it is set and no older than 30 days; falls back
// to CollectedAt (and ultimately to now via formatRFC3339).
func reanimatorCollectedAt(result *models.AnalysisResult) time.Time {
inv := result.InventoryLastModifiedAt
if !inv.IsZero() && time.Since(inv) <= 30*24*time.Hour {
return inv
}
return result.CollectedAt
}
// formatRFC3339 formats time in RFC3339 format, returns current time if zero
func formatRFC3339(t time.Time) string {
if t.IsZero() {
return time.Now().UTC().Format(time.RFC3339)
}
return t.UTC().Format(time.RFC3339)
}
// convertBoard converts BoardInfo to Reanimator format
func convertBoard(board models.BoardInfo) ReanimatorBoard {
return ReanimatorBoard{
Manufacturer: normalizeNullableString(board.Manufacturer),
ProductName: normalizeNullableString(board.ProductName),
SerialNumber: board.SerialNumber,
PartNumber: board.PartNumber,
UUID: board.UUID,
}
}
func canonicalDevicesForExport(hw *models.HardwareConfig) []models.HardwareDevice {
if hw == nil {
return nil
}
merged := append([]models.HardwareDevice{}, hw.Devices...)
merged = append(merged, buildDevicesFromLegacy(hw)...)
hw.Devices = dedupeCanonicalDevices(merged)
return hw.Devices
}
func buildDevicesFromLegacy(hw *models.HardwareConfig) []models.HardwareDevice {
if hw == nil {
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)
}
for _, cpu := range hw.CPUs {
details := mergeDetailMaps(nil, cpu.Details)
details = mergeDetailMaps(details, map[string]any{
"socket": cpu.Socket,
})
appendDevice(models.HardwareDevice{
Kind: models.DeviceKindCPU,
Slot: fmt.Sprintf("CPU%d", cpu.Socket),
Model: cpu.Model,
SerialNumber: cpu.SerialNumber,
Cores: cpu.Cores,
Threads: cpu.Threads,
FrequencyMHz: cpu.FrequencyMHz,
MaxFreqMHz: cpu.MaxFreqMHz,
Status: cpu.Status,
StatusCheckedAt: cpu.StatusCheckedAt,
StatusChangedAt: cpu.StatusChangedAt,
StatusAtCollect: cpu.StatusAtCollect,
StatusHistory: cpu.StatusHistory,
ErrorDescription: cpu.ErrorDescription,
Details: details,
})
}
for _, mem := range hw.Memory {
present := mem.Present
details := mergeDetailMaps(nil, mem.Details)
details = mergeDetailMaps(details, map[string]any{
"max_speed_mhz": mem.MaxSpeedMHz,
"current_speed_mhz": mem.CurrentSpeedMHz,
})
appendDevice(models.HardwareDevice{
Kind: models.DeviceKindMemory,
Slot: mem.Slot,
Location: mem.Location,
Manufacturer: mem.Manufacturer,
SerialNumber: mem.SerialNumber,
PartNumber: mem.PartNumber,
Type: mem.Type,
Present: &present,
SizeMB: mem.SizeMB,
Status: mem.Status,
StatusCheckedAt: mem.StatusCheckedAt,
StatusChangedAt: mem.StatusChangedAt,
StatusAtCollect: mem.StatusAtCollect,
StatusHistory: mem.StatusHistory,
ErrorDescription: mem.ErrorDescription,
Details: details,
})
}
for _, stor := range hw.Storage {
present := stor.Present
appendDevice(models.HardwareDevice{
Kind: models.DeviceKindStorage,
Slot: stor.Slot,
Model: stor.Model,
Manufacturer: stor.Manufacturer,
RemainingEndurancePct: stor.RemainingEndurancePct,
SerialNumber: stor.SerialNumber,
Firmware: stor.Firmware,
Type: stor.Type,
Interface: stor.Interface,
Present: &present,
SizeGB: stor.SizeGB,
Status: stor.Status,
StatusCheckedAt: stor.StatusCheckedAt,
StatusChangedAt: stor.StatusChangedAt,
StatusAtCollect: stor.StatusAtCollect,
StatusHistory: stor.StatusHistory,
ErrorDescription: stor.ErrorDescription,
Details: mergeDetailMaps(nil, stor.Details),
})
}
for _, pcie := range hw.PCIeDevices {
// Use PartNumber as model when available; fall back to chip description.
// Description contains the chip/product name (e.g. "BCM57414 NetXtreme-E …")
// while PartNumber is a part/product code. Prefer PartNumber when set.
pcieModel := pcie.PartNumber
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,
BDF: pcie.BDF,
DeviceClass: pcie.DeviceClass,
VendorID: pcie.VendorID,
DeviceID: pcie.DeviceID,
Model: pcieModel,
PartNumber: pcie.PartNumber,
Manufacturer: pcie.Manufacturer,
SerialNumber: pcie.SerialNumber,
LinkWidth: pcie.LinkWidth,
LinkSpeed: pcie.LinkSpeed,
MaxLinkWidth: pcie.MaxLinkWidth,
MaxLinkSpeed: pcie.MaxLinkSpeed,
NUMANode: pcie.NUMANode,
Status: pcie.Status,
StatusCheckedAt: pcie.StatusCheckedAt,
StatusChangedAt: pcie.StatusChangedAt,
StatusAtCollect: pcie.StatusAtCollect,
StatusHistory: pcie.StatusHistory,
ErrorDescription: pcie.ErrorDescription,
Details: details,
})
}
for _, gpu := range hw.GPUs {
details := mergeDetailMaps(nil, gpu.Details)
details = mergeDetailMaps(details, map[string]any{
"uuid": gpu.UUID,
"video_bios": gpu.VideoBIOS,
"irq": gpu.IRQ,
"bus_type": gpu.BusType,
"dma_size": gpu.DMASize,
"dma_mask": gpu.DMAMask,
"device_minor": gpu.DeviceMinor,
"temperature": gpu.Temperature,
"mem_temperature": gpu.MemTemperature,
"power": gpu.Power,
"max_power": gpu.MaxPower,
"clock_speed": gpu.ClockSpeed,
})
appendDevice(models.HardwareDevice{
Kind: models.DeviceKindGPU,
Slot: gpu.Slot,
Location: gpu.Location,
BDF: gpu.BDF,
DeviceClass: "DisplayController",
VendorID: gpu.VendorID,
DeviceID: gpu.DeviceID,
Model: gpu.Model,
PartNumber: gpu.PartNumber,
Manufacturer: gpu.Manufacturer,
SerialNumber: gpu.SerialNumber,
Firmware: gpu.Firmware,
LinkWidth: gpu.CurrentLinkWidth,
LinkSpeed: gpu.CurrentLinkSpeed,
MaxLinkWidth: gpu.MaxLinkWidth,
MaxLinkSpeed: gpu.MaxLinkSpeed,
TemperatureC: gpu.Temperature,
Status: gpu.Status,
StatusCheckedAt: gpu.StatusCheckedAt,
StatusChangedAt: gpu.StatusChangedAt,
StatusAtCollect: gpu.StatusAtCollect,
StatusHistory: gpu.StatusHistory,
ErrorDescription: gpu.ErrorDescription,
Details: details,
})
}
for _, nic := range hw.NetworkAdapters {
present := nic.Present
appendDevice(models.HardwareDevice{
Kind: models.DeviceKindNetwork,
Slot: nic.Slot,
Location: nic.Location,
BDF: nic.BDF,
VendorID: nic.VendorID,
DeviceID: nic.DeviceID,
Model: nic.Model,
PartNumber: nic.PartNumber,
Manufacturer: nic.Vendor,
SerialNumber: nic.SerialNumber,
Firmware: nic.Firmware,
PortCount: nic.PortCount,
PortType: nic.PortType,
MACAddresses: nic.MACAddresses,
LinkWidth: nic.LinkWidth,
LinkSpeed: nic.LinkSpeed,
MaxLinkWidth: nic.MaxLinkWidth,
MaxLinkSpeed: nic.MaxLinkSpeed,
NUMANode: nic.NUMANode,
Present: &present,
Status: nic.Status,
StatusCheckedAt: nic.StatusCheckedAt,
StatusChangedAt: nic.StatusChangedAt,
StatusAtCollect: nic.StatusAtCollect,
StatusHistory: nic.StatusHistory,
ErrorDescription: nic.ErrorDescription,
Details: mergeDetailMaps(nil, nic.Details),
})
}
for _, psu := range hw.PowerSupply {
present := psu.Present
appendDevice(models.HardwareDevice{
Kind: models.DeviceKindPSU,
Slot: psu.Slot,
Model: psu.Model,
PartNumber: psu.PartNumber,
Manufacturer: psu.Vendor,
SerialNumber: psu.SerialNumber,
Firmware: psu.Firmware,
Present: &present,
WattageW: psu.WattageW,
InputType: psu.InputType,
InputPowerW: psu.InputPowerW,
OutputPowerW: psu.OutputPowerW,
InputVoltage: psu.InputVoltage,
TemperatureC: psu.TemperatureC,
Status: psu.Status,
StatusCheckedAt: psu.StatusCheckedAt,
StatusChangedAt: psu.StatusChangedAt,
StatusAtCollect: psu.StatusAtCollect,
StatusHistory: psu.StatusHistory,
ErrorDescription: psu.ErrorDescription,
Details: mergeDetailMaps(nil, psu.Details),
})
}
return dedupeCanonicalDevices(all)
}
func dedupeCanonicalDevices(items []models.HardwareDevice) []models.HardwareDevice {
type scored struct {
item models.HardwareDevice
score int
}
byKey := make(map[string]scored, len(items))
order := make([]string, 0, len(items))
noKey := make([]models.HardwareDevice, 0)
for _, item := range items {
key := canonicalKey(item)
if key == "" {
noKey = append(noKey, item)
continue
}
curr := scored{item: item, score: canonicalScore(item)}
prev, ok := byKey[key]
if !ok {
byKey[key] = curr
order = append(order, key)
continue
}
if curr.score > prev.score {
curr.item = mergeCanonicalDevice(curr.item, prev.item)
curr.score = canonicalScore(curr.item)
byKey[key] = curr
continue
}
prev.item = mergeCanonicalDevice(prev.item, curr.item)
prev.score = canonicalScore(prev.item)
byKey[key] = prev
}
// Secondary pass: for PCIe-class items without serial/BDF (noKey), try to merge
// into an existing keyed entry with the same model+manufacturer. This handles
// the case where a device appears both in PCIeDevices (with BDF) and
// NetworkAdapters (without BDF) — e.g. Inspur outboardPCIeCard vs PCIeCard
// with the same model. Do not apply this to storage: repeated NVMe slots often
// share the same model string and would collapse incorrectly.
// deviceIdentity returns the best available model name for secondary matching,
// preferring Model over DeviceClass (which may hold a resolved device name).
deviceIdentity := func(d models.HardwareDevice) string {
if m := strings.ToLower(strings.TrimSpace(d.Model)); m != "" {
return m
}
if dc := strings.ToLower(strings.TrimSpace(d.DeviceClass)); dc != "" && !isGenericDeviceClass(dc) {
return dc
}
return ""
}
var unmatched []models.HardwareDevice
for _, item := range noKey {
mergeKind := canonicalMergeKind(item.Kind)
if mergeKind != "pcie-class" {
unmatched = append(unmatched, item)
continue
}
identity := deviceIdentity(item)
mfr := strings.ToLower(strings.TrimSpace(item.Manufacturer))
if identity == "" {
unmatched = append(unmatched, item)
continue
}
matchKey := ""
matchCount := 0
for _, k := range order {
existing := byKey[k].item
if canonicalMergeKind(existing.Kind) == mergeKind &&
deviceIdentity(existing) == identity &&
strings.ToLower(strings.TrimSpace(existing.Manufacturer)) == mfr {
matchKey = k
matchCount++
}
}
if matchCount == 1 {
prev := byKey[matchKey]
prev.item = mergeCanonicalDevice(prev.item, item)
prev.score = canonicalScore(prev.item)
byKey[matchKey] = prev
} else {
unmatched = append(unmatched, item)
}
}
unmatched = dedupeLooseCanonicalDevices(unmatched)
out := make([]models.HardwareDevice, 0, len(order)+len(unmatched))
for _, key := range order {
out = append(out, byKey[key].item)
}
out = append(out, unmatched...)
for i := range out {
out[i].ID = out[i].Kind + ":" + strconv.Itoa(i)
}
return out
}
func dedupeLooseCanonicalDevices(items []models.HardwareDevice) []models.HardwareDevice {
if len(items) <= 1 {
return items
}
out := make([]models.HardwareDevice, 0, len(items))
seen := make(map[string]int, len(items))
for _, item := range items {
key := canonicalLooseKey(item)
if key == "" {
out = append(out, item)
continue
}
if idx, ok := seen[key]; ok {
out[idx] = mergeCanonicalDevice(out[idx], item)
continue
}
seen[key] = len(out)
out = append(out, item)
}
return out
}
func mergeCanonicalDevice(primary, secondary models.HardwareDevice) models.HardwareDevice {
fillString := func(dst *string, src string) {
if strings.TrimSpace(*dst) == "" && strings.TrimSpace(src) != "" {
*dst = src
}
}
fillInt := func(dst *int, src int) {
if *dst == 0 && src != 0 {
*dst = src
}
}
fillFloat := func(dst *float64, src float64) {
if *dst == 0 && src != 0 {
*dst = src
}
}
fillString(&primary.Kind, secondary.Kind)
fillString(&primary.Source, secondary.Source)
fillString(&primary.Slot, secondary.Slot)
fillString(&primary.Location, secondary.Location)
fillString(&primary.BDF, secondary.BDF)
fillString(&primary.DeviceClass, secondary.DeviceClass)
fillInt(&primary.VendorID, secondary.VendorID)
fillInt(&primary.DeviceID, secondary.DeviceID)
fillString(&primary.Model, secondary.Model)
fillString(&primary.PartNumber, secondary.PartNumber)
fillString(&primary.Manufacturer, secondary.Manufacturer)
fillString(&primary.SerialNumber, secondary.SerialNumber)
fillString(&primary.Firmware, secondary.Firmware)
fillString(&primary.Type, secondary.Type)
fillString(&primary.Interface, secondary.Interface)
if primary.Present == nil && secondary.Present != nil {
primary.Present = secondary.Present
}
fillInt(&primary.SizeMB, secondary.SizeMB)
fillInt(&primary.SizeGB, secondary.SizeGB)
fillInt(&primary.Cores, secondary.Cores)
fillInt(&primary.Threads, secondary.Threads)
fillInt(&primary.FrequencyMHz, secondary.FrequencyMHz)
fillInt(&primary.MaxFreqMHz, secondary.MaxFreqMHz)
fillInt(&primary.PortCount, secondary.PortCount)
fillString(&primary.PortType, secondary.PortType)
if len(primary.MACAddresses) == 0 && len(secondary.MACAddresses) > 0 {
primary.MACAddresses = secondary.MACAddresses
}
if primary.RemainingEndurancePct == nil && secondary.RemainingEndurancePct != nil {
primary.RemainingEndurancePct = secondary.RemainingEndurancePct
}
fillInt(&primary.LinkWidth, secondary.LinkWidth)
fillString(&primary.LinkSpeed, secondary.LinkSpeed)
fillInt(&primary.MaxLinkWidth, secondary.MaxLinkWidth)
fillString(&primary.MaxLinkSpeed, secondary.MaxLinkSpeed)
fillInt(&primary.WattageW, secondary.WattageW)
fillString(&primary.InputType, secondary.InputType)
fillInt(&primary.InputPowerW, secondary.InputPowerW)
fillInt(&primary.OutputPowerW, secondary.OutputPowerW)
fillFloat(&primary.InputVoltage, secondary.InputVoltage)
fillInt(&primary.TemperatureC, secondary.TemperatureC)
fillString(&primary.Status, secondary.Status)
if primary.StatusCheckedAt == nil && secondary.StatusCheckedAt != nil {
primary.StatusCheckedAt = secondary.StatusCheckedAt
}
if primary.StatusChangedAt == nil && secondary.StatusChangedAt != nil {
primary.StatusChangedAt = secondary.StatusChangedAt
}
if primary.StatusAtCollect == nil && secondary.StatusAtCollect != nil {
primary.StatusAtCollect = secondary.StatusAtCollect
}
if len(primary.StatusHistory) == 0 && len(secondary.StatusHistory) > 0 {
primary.StatusHistory = secondary.StatusHistory
}
fillString(&primary.ErrorDescription, secondary.ErrorDescription)
primary.Details = mergeDetailMaps(primary.Details, secondary.Details)
return primary
}
func mergeDetailMaps(primary, secondary map[string]any) map[string]any {
if len(secondary) == 0 {
return primary
}
if primary == nil {
primary = make(map[string]any, len(secondary))
}
for k, v := range secondary {
if _, exists := primary[k]; !exists {
primary[k] = v
}
}
return primary
}
func canonicalKey(item models.HardwareDevice) string {
kind := canonicalMergeKind(item.Kind)
if sn := normalizedSerial(item.SerialNumber); sn != "" {
return kind + "|sn:" + strings.ToLower(sn)
}
if bdf := strings.ToLower(strings.TrimSpace(item.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("%s|slotid:%s|%d|%d", kind, slot, item.VendorID, item.DeviceID)
}
if slot != "" && model != "" && mfr != "" {
return kind + "|slotmodel:" + slot + "|" + model + "|" + mfr
}
if 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) != "" {
score += 6
}
if strings.TrimSpace(item.BDF) != "" {
score += 4
}
if strings.TrimSpace(item.Model) != "" {
score += 3
}
if strings.TrimSpace(item.Firmware) != "" {
score += 2
}
if strings.TrimSpace(item.Status) != "" {
score++
}
return score
}
// convertFirmware converts firmware information to Reanimator format
func convertFirmware(firmware []models.FirmwareInfo) []ReanimatorFirmware {
if len(firmware) == 0 {
return nil
}
result := make([]ReanimatorFirmware, 0, len(firmware))
for _, fw := range firmware {
if isDeviceBoundFirmwareName(fw.DeviceName) || isDeviceBoundFirmwareFQDD(fw.Description) {
continue
}
result = append(result, ReanimatorFirmware{
DeviceName: fw.DeviceName,
Version: fw.Version,
})
}
if len(result) == 0 {
return nil
}
return result
}
func convertCPUsFromDevices(devices []models.HardwareDevice, collectedAt, boardSerial string, microcodeBySocket map[int]string) []ReanimatorCPU {
result := make([]ReanimatorCPU, 0)
for _, d := range devices {
if d.Kind != models.DeviceKindCPU {
continue
}
if d.Present != nil && !*d.Present {
continue
}
socket := parseSocketFromSlot(d.Slot)
if v, ok := d.Details["socket"].(int); ok {
socket = v
}
cpuStatus := normalizeStatus(d.Status, false)
if strings.TrimSpace(d.Status) == "" {
cpuStatus = "Unknown"
}
meta := buildStatusMeta(cpuStatus, d.StatusCheckedAt, d.StatusChangedAt, d.StatusHistory, d.ErrorDescription, collectedAt)
result = append(result, ReanimatorCPU{
Socket: socket,
Model: d.Model,
Cores: d.Cores,
Threads: d.Threads,
FrequencyMHz: d.FrequencyMHz,
MaxFrequencyMHz: d.MaxFreqMHz,
TemperatureC: floatFromDetailMap(d.Details, "temperature_c"),
PowerW: floatFromDetailMap(d.Details, "power_w"),
Throttled: boolPtrFromDetailMap(d.Details, "throttled"),
CorrectableErrorCount: int64FromDetailMap(d.Details, "correctable_error_count"),
UncorrectableErrorCount: int64FromDetailMap(d.Details, "uncorrectable_error_count"),
LifeRemainingPct: floatFromDetailMap(d.Details, "life_remaining_pct"),
LifeUsedPct: floatFromDetailMap(d.Details, "life_used_pct"),
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,
ManufacturedYearWeek: manufacturedYearWeekFromDetails(d.Details),
StatusHistory: meta.StatusHistory,
ErrorDescription: meta.ErrorDescription,
})
}
return result
}
func convertMemoryFromDevices(devices []models.HardwareDevice, collectedAt string) []ReanimatorMemory {
result := make([]ReanimatorMemory, 0)
for _, d := range devices {
if d.Kind != models.DeviceKindMemory {
continue
}
present := boolFromPresentPtr(d.Present, true)
status := normalizeStatus(d.Status, true)
mem := models.MemoryDIMM{
Present: present,
SizeMB: d.SizeMB,
Type: d.Type,
Description: stringFromDetailMap(d.Details, "description"),
Manufacturer: d.Manufacturer,
SerialNumber: d.SerialNumber,
PartNumber: d.PartNumber,
Status: d.Status,
}
if !mem.IsInstalledInventory() || status == "Empty" || strings.TrimSpace(d.SerialNumber) == "" {
continue
}
meta := buildStatusMeta(status, d.StatusCheckedAt, d.StatusChangedAt, d.StatusHistory, d.ErrorDescription, collectedAt)
result = append(result, ReanimatorMemory{
Slot: d.Slot,
Location: d.Location,
SizeMB: d.SizeMB,
Type: d.Type,
MaxSpeedMHz: intFromDetailMap(d.Details, "max_speed_mhz"),
CurrentSpeedMHz: intFromDetailMap(d.Details, "current_speed_mhz"),
TemperatureC: floatFromDetailMap(d.Details, "temperature_c"),
CorrectableECCErrorCount: int64FromDetailMap(d.Details, "correctable_ecc_error_count"),
UncorrectableECCErrorCount: int64FromDetailMap(d.Details, "uncorrectable_ecc_error_count"),
LifeRemainingPct: floatFromDetailMap(d.Details, "life_remaining_pct"),
LifeUsedPct: floatFromDetailMap(d.Details, "life_used_pct"),
SpareBlocksRemainingPct: floatFromDetailMap(d.Details, "spare_blocks_remaining_pct"),
PerformanceDegraded: boolPtrFromDetailMap(d.Details, "performance_degraded"),
DataLossDetected: boolPtrFromDetailMap(d.Details, "data_loss_detected"),
Manufacturer: d.Manufacturer,
SerialNumber: d.SerialNumber,
PartNumber: d.PartNumber,
Status: status,
StatusCheckedAt: meta.StatusCheckedAt,
StatusChangedAt: meta.StatusChangedAt,
ManufacturedYearWeek: manufacturedYearWeekFromDetails(d.Details),
StatusHistory: meta.StatusHistory,
ErrorDescription: meta.ErrorDescription,
})
}
return result
}
func convertStorageFromDevices(devices []models.HardwareDevice, collectedAt string) []ReanimatorStorage {
result := make([]ReanimatorStorage, 0)
for _, d := range devices {
if d.Kind != models.DeviceKindStorage {
continue
}
if isVirtualExportStorageDevice(d) {
continue
}
if !shouldExportStorageDevice(d) {
continue
}
present := boolFromPresentPtr(d.Present, true)
status := inferStorageStatus(models.Storage{Present: present})
if strings.TrimSpace(d.Status) != "" {
status = normalizeStatus(d.Status, !present)
}
meta := buildStatusMeta(status, d.StatusCheckedAt, d.StatusChangedAt, d.StatusHistory, d.ErrorDescription, collectedAt)
presentValue := present
result = append(result, ReanimatorStorage{
Slot: d.Slot,
Type: d.Type,
Model: d.Model,
SizeGB: d.SizeGB,
SerialNumber: d.SerialNumber,
Manufacturer: d.Manufacturer,
Firmware: d.Firmware,
Interface: d.Interface,
Present: &presentValue,
TemperatureC: floatFromDetailMap(d.Details, "temperature_c"),
PowerOnHours: int64FromDetailMap(d.Details, "power_on_hours"),
PowerCycles: int64FromDetailMap(d.Details, "power_cycles"),
UnsafeShutdowns: int64FromDetailMap(d.Details, "unsafe_shutdowns"),
MediaErrors: int64FromDetailMap(d.Details, "media_errors"),
ErrorLogEntries: int64FromDetailMap(d.Details, "error_log_entries"),
WrittenBytes: int64FromDetailMap(d.Details, "written_bytes"),
ReadBytes: int64FromDetailMap(d.Details, "read_bytes"),
LifeUsedPct: floatFromDetailMap(d.Details, "life_used_pct"),
RemainingEndurancePct: d.RemainingEndurancePct,
LifeRemainingPct: floatFromDetailMap(d.Details, "life_remaining_pct"),
AvailableSparePct: floatFromDetailMap(d.Details, "available_spare_pct"),
ReallocatedSectors: int64FromDetailMap(d.Details, "reallocated_sectors"),
CurrentPendingSectors: int64FromDetailMap(d.Details, "current_pending_sectors"),
OfflineUncorrectable: int64FromDetailMap(d.Details, "offline_uncorrectable"),
Status: status,
StatusCheckedAt: meta.StatusCheckedAt,
StatusChangedAt: meta.StatusChangedAt,
ManufacturedYearWeek: manufacturedYearWeekFromDetails(d.Details),
StatusHistory: meta.StatusHistory,
ErrorDescription: meta.ErrorDescription,
})
}
return result
}
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 := normalizePlaceholderDeviceModel(d.Model)
if model == "" {
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 {
model = pciids.DeviceName(d.VendorID, d.DeviceID)
}
manufacturer := d.Manufacturer
if manufacturer == "" && d.VendorID != 0 {
manufacturer = pciids.VendorName(d.VendorID)
}
temperatureC := firstNonZeroFloat(
float64(d.TemperatureC),
floatFromDetailMap(d.Details, "temperature_c"),
floatFromDetailMap(d.Details, "temperature"),
)
powerW := firstNonZeroFloat(
floatFromDetailMap(d.Details, "power_w"),
float64(intFromDetailMap(d.Details, "power")),
)
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: slot,
VendorID: d.VendorID,
DeviceID: d.DeviceID,
NUMANode: d.NUMANode,
TemperatureC: temperatureC,
PowerW: powerW,
LifeRemainingPct: floatFromDetailMap(d.Details, "life_remaining_pct"),
LifeUsedPct: floatFromDetailMap(d.Details, "life_used_pct"),
ECCCorrectedTotal: int64FromDetailMap(d.Details, "ecc_corrected_total"),
ECCUncorrectedTotal: int64FromDetailMap(d.Details, "ecc_uncorrected_total"),
HWSlowdown: boolPtrFromDetailMap(d.Details, "hw_slowdown"),
BatteryChargePct: floatFromDetailMap(d.Details, "battery_charge_pct"),
BatteryHealthPct: floatFromDetailMap(d.Details, "battery_health_pct"),
BatteryTemperatureC: floatFromDetailMap(d.Details, "battery_temperature_c"),
BatteryVoltageV: floatFromDetailMap(d.Details, "battery_voltage_v"),
BatteryReplaceRequired: boolPtrFromDetailMap(d.Details, "battery_replace_required"),
SFPTemperatureC: floatFromDetailMap(d.Details, "sfp_temperature_c"),
SFPTXPowerDBm: floatFromDetailMap(d.Details, "sfp_tx_power_dbm"),
SFPRXPowerDBm: floatFromDetailMap(d.Details, "sfp_rx_power_dbm"),
SFPVoltageV: floatFromDetailMap(d.Details, "sfp_voltage_v"),
SFPBiasMA: floatFromDetailMap(d.Details, "sfp_bias_ma"),
BDF: d.BDF,
DeviceClass: deviceClass,
Manufacturer: manufacturer,
Model: model,
LinkWidth: d.LinkWidth,
LinkSpeed: d.LinkSpeed,
MaxLinkWidth: d.MaxLinkWidth,
MaxLinkSpeed: d.MaxLinkSpeed,
MACAddresses: append([]string(nil), d.MACAddresses...),
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,
})
}
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 {
if d.Kind != models.DeviceKindPSU {
continue
}
present := d.Present != nil && *d.Present
if !present || strings.TrimSpace(d.SerialNumber) == "" {
continue
}
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,
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
}
out := &ReanimatorSensors{}
seenFans := map[string]struct{}{}
powerIndex := map[string]int{}
seenTemps := map[string]struct{}{}
seenOther := map[string]struct{}{}
for _, s := range sensors {
name := strings.TrimSpace(s.Name)
if name == "" {
continue
}
if !sensorHasNumericReading(s) {
continue
}
status := normalizeSensorStatus(s.Status)
sType := strings.ToLower(strings.TrimSpace(s.Type))
unit := strings.TrimSpace(s.Unit)
switch {
case sType == "fan" || strings.EqualFold(unit, "RPM"):
if seenFirst(seenFans, name) {
continue
}
out.Fans = append(out.Fans, ReanimatorFanSensor{
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"):
baseName := groupedPowerSensorName(name)
if idx, ok := powerIndex[baseName]; ok {
mergePowerSensorReading(&out.Power[idx], sType, unit, s.Value, status)
continue
}
item := ReanimatorPowerSensor{
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,
Celsius: s.Value,
Status: status,
})
default:
if seenFirst(seenOther, name) {
continue
}
out.Other = append(out.Other, ReanimatorOtherSensor{
Name: name,
Value: s.Value,
Unit: unit,
Status: status,
})
}
}
if len(out.Fans) == 0 && len(out.Power) == 0 && len(out.Temperatures) == 0 && len(out.Other) == 0 {
return nil
}
return out
}
func groupedPowerSensorName(name string) string {
trimmed := strings.TrimSpace(name)
lower := strings.ToLower(trimmed)
inputSuffixes := []string{"_inputpower", "_inputvoltage", "_inputcurrent", "_pin", "_vin", "_iin"}
for _, suffix := range inputSuffixes {
if strings.HasSuffix(lower, suffix) {
return strings.TrimSpace(trimmed[:len(trimmed)-len(suffix)])
}
}
outputSuffixes := []string{"_outputpower", "_outputvoltage", "_outputcurrent", "_pout", "_vout", "_iout"}
for _, suffix := range outputSuffixes {
if strings.HasSuffix(lower, suffix) {
return strings.TrimSpace(trimmed[:len(trimmed)-len(suffix)]) + "_Output"
}
}
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 == "" {
return false
}
if strings.HasPrefix(n, "gpu ") ||
strings.HasPrefix(n, "nvswitch ") ||
strings.HasPrefix(n, "nic ") ||
strings.HasPrefix(n, "hdd ") ||
strings.HasPrefix(n, "ssd ") ||
strings.HasPrefix(n, "nvme ") ||
strings.HasPrefix(n, "psu") ||
// Supermicro Redfish FirmwareInventory names "GPU1 System Slot0", "NIC1 System Slot0 ..."
// where the number follows immediately after the type prefix (no space separator).
(strings.HasPrefix(n, "gpu") && len(n) > 3 && n[3] >= '0' && n[3] <= '9') ||
(strings.HasPrefix(n, "nic") && len(n) > 3 && n[3] >= '0' && n[3] <= '9') ||
// "NVMeController1" — storage controller bound to an NVMe device slot
strings.HasPrefix(n, "nvmecontroller") ||
// "Power supply N" — Supermicro PSU firmware (distinct from generic "PSU" prefix)
strings.HasPrefix(n, "power supply") ||
// "Software Inventory" — generic label used by HGX baseboard for all per-component
// firmware slots (GPU, NVSwitch, PCIeRetimer, ERoT, InfoROM, etc.). The useful name
// is only in the inventory item Id, not the Name field, so the entry is not actionable.
n == "software inventory" ||
// 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
}
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
// in hardware.firmware.
func isDeviceBoundFirmwareFQDD(desc string) bool {
d := strings.ToLower(strings.TrimSpace(desc))
if d == "" {
return false
}
// "raid." covers all RAID controller/backplane FQDDs: RAID.SL.*, RAID.Integrated.*, RAID.Backplane.*
// "infiniband." covers Mellanox InfiniBand/Ethernet adapters exposed as InfiniBand.Slot.*
for _, prefix := range []string{"nic.", "psu.", "disk.", "raid.", "gpu.", "infiniband.", "fc."} {
if strings.HasPrefix(d, prefix) {
return true
}
}
return false
}
func buildCPUMicrocodeBySocket(firmware []models.FirmwareInfo) map[int]string {
if len(firmware) == 0 {
return nil
}
out := make(map[int]string)
for _, fw := range firmware {
m := cpuMicrocodeFirmwareCaptureRegex.FindStringSubmatch(strings.TrimSpace(fw.DeviceName))
if len(m) != 2 || strings.TrimSpace(fw.Version) == "" {
continue
}
socket, err := strconv.Atoi(m[1])
if err != nil {
continue
}
if _, exists := out[socket]; exists {
continue
}
out[socket] = strings.TrimSpace(fw.Version)
}
if len(out) == 0 {
return nil
}
return out
}
// convertCPUs converts CPU information to Reanimator format
func convertCPUs(cpus []models.CPU, collectedAt string) []ReanimatorCPU {
if len(cpus) == 0 {
return nil
}
result := make([]ReanimatorCPU, 0, len(cpus))
for _, cpu := range cpus {
manufacturer := inferCPUManufacturer(cpu.Model)
cpuStatus := normalizeStatus(cpu.Status, false)
if strings.TrimSpace(cpu.Status) == "" {
cpuStatus = "Unknown"
}
meta := buildStatusMeta(
cpuStatus,
cpu.StatusCheckedAt,
cpu.StatusChangedAt,
cpu.StatusHistory,
cpu.ErrorDescription,
collectedAt,
)
result = append(result, ReanimatorCPU{
Socket: cpu.Socket,
Model: cpu.Model,
Cores: cpu.Cores,
Threads: cpu.Threads,
FrequencyMHz: cpu.FrequencyMHz,
MaxFrequencyMHz: cpu.MaxFreqMHz,
SerialNumber: strings.TrimSpace(cpu.SerialNumber),
Firmware: "",
Manufacturer: manufacturer,
Status: cpuStatus,
StatusCheckedAt: meta.StatusCheckedAt,
StatusChangedAt: meta.StatusChangedAt,
StatusHistory: meta.StatusHistory,
ErrorDescription: meta.ErrorDescription,
})
}
return result
}
// convertMemory converts memory modules to Reanimator format
func convertMemory(memory []models.MemoryDIMM, collectedAt string) []ReanimatorMemory {
if len(memory) == 0 {
return nil
}
result := make([]ReanimatorMemory, 0, len(memory))
for _, mem := range memory {
if !mem.IsInstalledInventory() || normalizeStatus(mem.Status, true) == "Empty" || strings.TrimSpace(mem.SerialNumber) == "" {
continue
}
status := normalizeStatus(mem.Status, true)
meta := buildStatusMeta(
status,
mem.StatusCheckedAt,
mem.StatusChangedAt,
mem.StatusHistory,
mem.ErrorDescription,
collectedAt,
)
result = append(result, ReanimatorMemory{
Slot: mem.Slot,
Location: mem.Location,
SizeMB: mem.SizeMB,
Type: mem.Type,
MaxSpeedMHz: mem.MaxSpeedMHz,
CurrentSpeedMHz: mem.CurrentSpeedMHz,
Manufacturer: mem.Manufacturer,
SerialNumber: mem.SerialNumber,
PartNumber: mem.PartNumber,
Status: status,
StatusCheckedAt: meta.StatusCheckedAt,
StatusChangedAt: meta.StatusChangedAt,
StatusHistory: meta.StatusHistory,
ErrorDescription: meta.ErrorDescription,
})
}
return result
}
// convertStorage converts storage devices to Reanimator format
func convertStorage(storage []models.Storage, collectedAt string) []ReanimatorStorage {
if len(storage) == 0 {
return nil
}
result := make([]ReanimatorStorage, 0, len(storage))
for _, stor := range storage {
if isVirtualLegacyStorageDevice(stor) {
continue
}
if !shouldExportLegacyStorage(stor) {
continue
}
status := inferStorageStatus(stor)
if strings.TrimSpace(stor.Status) != "" {
status = normalizeStatus(stor.Status, !stor.Present)
}
meta := buildStatusMeta(
status,
stor.StatusCheckedAt,
stor.StatusChangedAt,
stor.StatusHistory,
stor.ErrorDescription,
collectedAt,
)
present := stor.Present
result = append(result, ReanimatorStorage{
Slot: stor.Slot,
Type: stor.Type,
Model: stor.Model,
SizeGB: stor.SizeGB,
SerialNumber: stor.SerialNumber,
Manufacturer: stor.Manufacturer,
Firmware: stor.Firmware,
Interface: stor.Interface,
Present: &present,
RemainingEndurancePct: stor.RemainingEndurancePct,
Status: status,
StatusCheckedAt: meta.StatusCheckedAt,
StatusChangedAt: meta.StatusChangedAt,
StatusHistory: meta.StatusHistory,
ErrorDescription: meta.ErrorDescription,
})
}
return result
}
func shouldExportStorageDevice(d models.HardwareDevice) bool {
if normalizedSerial(d.SerialNumber) != "" {
return true
}
if strings.TrimSpace(d.Slot) != "" {
return true
}
if hasMeaningfulExporterText(d.Model) {
return true
}
if hasMeaningfulExporterText(d.Type) || hasMeaningfulExporterText(d.Interface) {
return true
}
if d.SizeGB > 0 {
return true
}
return d.Present != nil
}
func shouldExportLegacyStorage(stor models.Storage) bool {
if normalizedSerial(stor.SerialNumber) != "" {
return true
}
if strings.TrimSpace(stor.Slot) != "" {
return true
}
if hasMeaningfulExporterText(stor.Model) {
return true
}
if hasMeaningfulExporterText(stor.Type) || hasMeaningfulExporterText(stor.Interface) {
return true
}
if stor.SizeGB > 0 {
return true
}
return stor.Present
}
func isVirtualLegacyStorageDevice(stor models.Storage) bool {
return isVirtualExportStorageDevice(models.HardwareDevice{
Kind: models.DeviceKindStorage,
Slot: stor.Slot,
Model: stor.Model,
Manufacturer: stor.Manufacturer,
})
}
// convertPCIeDevices converts PCIe devices, GPUs, and network adapters to Reanimator format
func convertPCIeDevices(hw *models.HardwareConfig, collectedAt string) []ReanimatorPCIe {
result := make([]ReanimatorPCIe, 0)
gpuSlots := make(map[string]struct{}, len(hw.GPUs))
nvswitchFirmwareBySlot := buildNVSwitchFirmwareBySlot(hw.Firmware)
for _, gpu := range hw.GPUs {
slot := strings.ToLower(strings.TrimSpace(gpu.Slot))
if slot != "" {
gpuSlots[slot] = struct{}{}
}
}
// Convert regular PCIe devices
for _, pcie := range hw.PCIeDevices {
slot := strings.ToLower(strings.TrimSpace(pcie.Slot))
if _, isDedicatedGPU := gpuSlots[slot]; isDedicatedGPU || isDisplayClass(pcie.DeviceClass) {
// Skip GPU-like PCIe entries to avoid duplicates:
// dedicated GPUs are exported from hw.GPUs with richer metadata.
continue
}
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 := normalizePlaceholderDeviceModel(pcie.PartNumber)
if model == "" {
model = normalizePlaceholderDeviceModel(pcie.Description)
}
if model == "" {
model = pcie.DeviceClass
}
status := normalizeStatus(pcie.Status, false)
firmware := ""
if isNVSwitchPCIeDevice(pcie) {
firmware = nvswitchFirmwareBySlot[normalizeNVSwitchSlotForLookup(pcie.Slot)]
}
meta := buildStatusMeta(
status,
pcie.StatusCheckedAt,
pcie.StatusChangedAt,
pcie.StatusHistory,
pcie.ErrorDescription,
collectedAt,
)
result = append(result, ReanimatorPCIe{
Slot: pcie.Slot,
VendorID: pcie.VendorID,
DeviceID: pcie.DeviceID,
BDF: pcie.BDF,
DeviceClass: normalizeLegacyPCIeDeviceClass(pcie.DeviceClass),
Manufacturer: pcie.Manufacturer,
Model: model,
LinkWidth: pcie.LinkWidth,
LinkSpeed: pcie.LinkSpeed,
MaxLinkWidth: pcie.MaxLinkWidth,
MaxLinkSpeed: pcie.MaxLinkSpeed,
MACAddresses: append([]string(nil), pcie.MACAddresses...),
NUMANode: pcie.NUMANode,
SerialNumber: serialNumber,
Firmware: firmware,
Status: status,
StatusCheckedAt: meta.StatusCheckedAt,
StatusChangedAt: meta.StatusChangedAt,
StatusHistory: meta.StatusHistory,
ErrorDescription: meta.ErrorDescription,
})
}
// Convert GPUs as PCIe devices
for _, gpu := range hw.GPUs {
serialNumber := strings.TrimSpace(gpu.SerialNumber)
status := normalizeStatus(gpu.Status, false)
meta := buildStatusMeta(
status,
gpu.StatusCheckedAt,
gpu.StatusChangedAt,
gpu.StatusHistory,
gpu.ErrorDescription,
collectedAt,
)
result = append(result, ReanimatorPCIe{
Slot: gpu.Slot,
VendorID: gpu.VendorID,
DeviceID: gpu.DeviceID,
BDF: gpu.BDF,
DeviceClass: "VideoController",
Manufacturer: gpu.Manufacturer,
Model: gpu.Model,
LinkWidth: gpu.CurrentLinkWidth,
LinkSpeed: gpu.CurrentLinkSpeed,
MaxLinkWidth: gpu.MaxLinkWidth,
MaxLinkSpeed: gpu.MaxLinkSpeed,
SerialNumber: serialNumber,
Firmware: gpu.Firmware,
TemperatureC: float64(gpu.Temperature),
PowerW: float64(gpu.Power),
Status: status,
StatusCheckedAt: meta.StatusCheckedAt,
StatusChangedAt: meta.StatusChangedAt,
StatusHistory: meta.StatusHistory,
ErrorDescription: meta.ErrorDescription,
})
}
// Convert network adapters as PCIe devices
for _, nic := range hw.NetworkAdapters {
if !nic.Present {
continue
}
serialNumber := strings.TrimSpace(nic.SerialNumber)
status := normalizeStatus(nic.Status, false)
meta := buildStatusMeta(
status,
nic.StatusCheckedAt,
nic.StatusChangedAt,
nic.StatusHistory,
nic.ErrorDescription,
collectedAt,
)
result = append(result, ReanimatorPCIe{
Slot: nic.Slot,
VendorID: nic.VendorID,
DeviceID: nic.DeviceID,
BDF: "",
DeviceClass: normalizeNetworkDeviceClass(nic.PortType, nic.Model, nic.Description),
Manufacturer: nic.Vendor,
Model: nic.Model,
LinkWidth: 0,
LinkSpeed: "",
MaxLinkWidth: 0,
MaxLinkSpeed: "",
MACAddresses: append([]string(nil), nic.MACAddresses...),
NUMANode: nic.NUMANode,
SerialNumber: serialNumber,
Firmware: nic.Firmware,
Status: status,
StatusCheckedAt: meta.StatusCheckedAt,
StatusChangedAt: meta.StatusChangedAt,
StatusHistory: meta.StatusHistory,
ErrorDescription: meta.ErrorDescription,
})
}
return result
}
func isNVSwitchPCIeDevice(pcie models.PCIeDevice) bool {
deviceClass := strings.TrimSpace(pcie.DeviceClass)
if strings.EqualFold(deviceClass, "NVSwitch") {
return true
}
slot := normalizeNVSwitchSlotForLookup(pcie.Slot)
return strings.HasPrefix(slot, "NVSWITCH")
}
func buildNVSwitchFirmwareBySlot(firmware []models.FirmwareInfo) map[string]string {
result := make(map[string]string)
for _, fw := range firmware {
name := strings.TrimSpace(fw.DeviceName)
if strings.HasPrefix(strings.ToUpper(name), "HGX_FW_EROT_") {
continue
}
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 = normalizeNVSwitchSlotForLookup(slot)
if slot == "" {
continue
}
if _, exists := result[slot]; exists {
continue
}
version := strings.TrimSpace(fw.Version)
if version == "" {
continue
}
result[slot] = version
}
return result
}
func normalizeNVSwitchSlotForLookup(slot string) string {
normalized := strings.ToUpper(strings.TrimSpace(slot))
if strings.HasPrefix(normalized, "NVSWITCHNVSWITCH") {
return "NVSWITCH" + strings.TrimPrefix(normalized, "NVSWITCHNVSWITCH")
}
return normalized
}
func isDisplayClass(deviceClass string) bool {
class := strings.ToLower(strings.TrimSpace(deviceClass))
return strings.Contains(class, "display") ||
strings.Contains(class, "vga") ||
strings.Contains(class, "3d controller")
}
// convertPowerSupplies converts power supplies to Reanimator format
func convertPowerSupplies(psus []models.PSU, collectedAt string) []ReanimatorPSU {
if len(psus) == 0 {
return nil
}
result := make([]ReanimatorPSU, 0, len(psus))
for _, psu := range psus {
// Skip PSUs without serial number (if not present)
if !psu.Present || psu.SerialNumber == "" {
continue
}
status := normalizeStatus(psu.Status, false)
meta := buildStatusMeta(
status,
psu.StatusCheckedAt,
psu.StatusChangedAt,
psu.StatusHistory,
psu.ErrorDescription,
collectedAt,
)
result = append(result, ReanimatorPSU{
Slot: psu.Slot,
Model: psu.Model,
Vendor: psu.Vendor,
WattageW: psu.WattageW,
SerialNumber: psu.SerialNumber,
PartNumber: psu.PartNumber,
Firmware: psu.Firmware,
Status: status,
InputType: psu.InputType,
InputPowerW: float64(psu.InputPowerW),
OutputPowerW: float64(psu.OutputPowerW),
InputVoltage: psu.InputVoltage,
TemperatureC: float64(psu.TemperatureC),
StatusCheckedAt: meta.StatusCheckedAt,
StatusChangedAt: meta.StatusChangedAt,
StatusHistory: meta.StatusHistory,
ErrorDescription: meta.ErrorDescription,
})
}
return result
}
func seenFirst(seen map[string]struct{}, key string) bool {
normalized := strings.ToLower(strings.TrimSpace(key))
if normalized == "" {
return false
}
if _, ok := seen[normalized]; ok {
return true
}
seen[normalized] = struct{}{}
return false
}
func normalizeSensorStatus(status string) string {
return normalizeStatus(status, false)
}
type convertedStatusMeta struct {
StatusCheckedAt string
StatusChangedAt string
StatusHistory []ReanimatorStatusHistoryEntry
ErrorDescription string
}
func buildStatusMeta(
currentStatus string,
checkedAt *time.Time,
changedAt *time.Time,
history []models.StatusHistoryEntry,
errorDescription string,
collectedAt string,
) convertedStatusMeta {
meta := convertedStatusMeta{
StatusCheckedAt: formatOptionalRFC3339(checkedAt),
StatusChangedAt: formatOptionalRFC3339(changedAt),
ErrorDescription: strings.TrimSpace(errorDescription),
}
convertedHistory := make([]ReanimatorStatusHistoryEntry, 0, len(history))
for _, h := range history {
changed := formatOptionalRFC3339(&h.ChangedAt)
if changed == "" {
continue
}
convertedHistory = append(convertedHistory, ReanimatorStatusHistoryEntry{
Status: normalizeStatus(h.Status, true),
ChangedAt: changed,
Details: strings.TrimSpace(h.Details),
})
}
sort.Slice(convertedHistory, func(i, j int) bool {
return convertedHistory[i].ChangedAt < convertedHistory[j].ChangedAt
})
if len(convertedHistory) > 0 {
meta.StatusHistory = convertedHistory
if meta.StatusChangedAt == "" {
meta.StatusChangedAt = convertedHistory[len(convertedHistory)-1].ChangedAt
}
}
if meta.StatusCheckedAt == "" && len(meta.StatusHistory) > 0 {
meta.StatusCheckedAt = meta.StatusHistory[len(meta.StatusHistory)-1].ChangedAt
}
if meta.StatusCheckedAt == "" && strings.TrimSpace(currentStatus) != "" && collectedAt != "" {
meta.StatusCheckedAt = collectedAt
}
return meta
}
func formatOptionalRFC3339(t *time.Time) string {
if t == nil || t.IsZero() {
return ""
}
return t.UTC().Format(time.RFC3339)
}
func dedupeFirmware(items []ReanimatorFirmware) []ReanimatorFirmware {
if len(items) < 2 {
return items
}
seen := make(map[string]struct{}, len(items))
result := make([]ReanimatorFirmware, 0, len(items))
for _, item := range items {
key := strings.ToLower(strings.TrimSpace(item.DeviceName))
if key == "" {
key = strings.ToLower(strings.TrimSpace(item.Version))
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
result = append(result, item)
}
return result
}
func dedupeCPUs(items []ReanimatorCPU) []ReanimatorCPU {
if len(items) < 2 {
return items
}
seen := make(map[int]struct{}, len(items))
result := make([]ReanimatorCPU, 0, len(items))
for _, item := range items {
if _, ok := seen[item.Socket]; ok {
continue
}
seen[item.Socket] = struct{}{}
result = append(result, item)
}
return result
}
func dedupeMemory(items []ReanimatorMemory) []ReanimatorMemory {
if len(items) < 2 {
return items
}
seen := make(map[string]struct{}, len(items))
result := make([]ReanimatorMemory, 0, len(items))
for _, item := range items {
key := strings.ToLower(strings.TrimSpace(item.Slot))
if key == "" {
key = strings.ToLower(strings.TrimSpace(item.Location))
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
result = append(result, item)
}
return result
}
func dedupeStorage(items []ReanimatorStorage) []ReanimatorStorage {
if len(items) < 2 {
return items
}
seen := make(map[string]struct{}, len(items))
result := make([]ReanimatorStorage, 0, len(items))
for _, item := range items {
key := strings.ToLower(strings.TrimSpace(item.SerialNumber))
if key == "" {
key = "slot:" + strings.ToLower(strings.TrimSpace(item.Slot))
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
result = append(result, item)
}
return result
}
func dedupePSUs(items []ReanimatorPSU) []ReanimatorPSU {
if len(items) < 2 {
return items
}
seen := make(map[string]struct{}, len(items))
result := make([]ReanimatorPSU, 0, len(items))
for _, item := range items {
key := strings.ToLower(strings.TrimSpace(item.SerialNumber))
if key == "" {
key = "slot:" + strings.ToLower(strings.TrimSpace(item.Slot))
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
result = append(result, item)
}
return result
}
func dedupePCIe(items []ReanimatorPCIe) []ReanimatorPCIe {
if len(items) < 2 {
return items
}
type scored struct {
item ReanimatorPCIe
score int
idx int
}
byKey := make(map[string]scored, len(items))
order := make([]string, 0, len(items))
for i, item := range items {
key := pcieDedupKey(item)
curr := scored{item: item, score: pcieQualityScore(item), idx: i}
existing, ok := byKey[key]
if !ok {
byKey[key] = curr
order = append(order, key)
continue
}
if curr.score > existing.score {
byKey[key] = curr
}
}
result := make([]ReanimatorPCIe, 0, len(byKey))
for _, key := range order {
result = append(result, byKey[key].item)
}
return result
}
func pcieDedupKey(item ReanimatorPCIe) string {
slot := strings.ToLower(strings.TrimSpace(item.Slot))
serial := strings.ToLower(strings.TrimSpace(item.SerialNumber))
bdf := strings.ToLower(strings.TrimSpace(item.BDF))
// Generic slot names (e.g. "PCIe Device" from HGX BMC) are not unique
// hardware positions — multiple distinct devices share the same name.
// Fall through to serial/BDF so they are not incorrectly collapsed.
if slot != "" && !isGenericPCIeSlotName(slot) {
return "slot:" + slot
}
if serial != "" {
return "sn:" + serial
}
if bdf != "" {
return "bdf:" + bdf
}
if slot != "" {
return "slot:" + slot
}
return strings.ToLower(strings.TrimSpace(item.DeviceClass)) + "|" + strings.ToLower(strings.TrimSpace(item.Model))
}
// isGenericPCIeSlotName reports whether slot is a generic device-type label
// rather than a unique hardware position identifier.
func isGenericPCIeSlotName(slot string) bool {
switch slot {
case "pcie device", "pcie slot", "pcie":
return true
}
return false
}
func pcieQualityScore(item ReanimatorPCIe) int {
score := 0
if strings.TrimSpace(item.SerialNumber) != "" {
score += 4
}
if strings.TrimSpace(item.Model) != "" && !isGenericPCIeModel(item.Model) {
score += 3
}
status := strings.ToLower(strings.TrimSpace(item.Status))
if status == "ok" || status == "warning" || status == "critical" {
score += 2
}
if strings.TrimSpace(item.BDF) != "" {
score++
}
if strings.EqualFold(strings.TrimSpace(item.DeviceClass), "DisplayController") {
score++
}
return score
}
func isGenericPCIeModel(model string) bool {
switch strings.ToLower(strings.TrimSpace(model)) {
case "", "unknown", "vga", "3d controller", "display controller":
return true
default:
return false
}
}
// isGenericDeviceClass returns true for Redfish topology class labels that are
// not meaningful device identifiers (e.g. "SingleFunction", "DisplayController").
func isGenericDeviceClass(dc string) bool {
switch strings.ToLower(strings.TrimSpace(dc)) {
case "", "pcie device", "display", "display controller", "displaycontroller",
"vga", "3d controller", "network", "network controller", "network adapter",
"storage", "storage controller", "other", "unknown",
"singlefunction", "multifunction", "simulated":
return true
default:
return false
}
}
// inferCPUManufacturer determines CPU manufacturer from model string
func inferCPUManufacturer(model string) string {
upper := strings.ToUpper(model)
// Intel patterns
if strings.Contains(upper, "INTEL") ||
strings.Contains(upper, "XEON") ||
strings.Contains(upper, "CORE I") {
return "Intel"
}
// AMD patterns
if strings.Contains(upper, "AMD") ||
strings.Contains(upper, "EPYC") ||
strings.Contains(upper, "RYZEN") ||
strings.Contains(upper, "THREADRIPPER") {
return "AMD"
}
// ARM patterns
if strings.Contains(upper, "ARM") ||
strings.Contains(upper, "CORTEX") {
return "ARM"
}
// Ampere patterns
if strings.Contains(upper, "AMPERE") ||
strings.Contains(upper, "ALTRA") {
return "Ampere"
}
return ""
}
func normalizedSerial(serial string) string {
s := strings.TrimSpace(serial)
if s == "" {
return ""
}
switch strings.ToUpper(s) {
case "N/A", "NA", "NONE", "NULL", "UNKNOWN", "-":
return ""
default:
return s
}
}
func parseSocketFromSlot(slot string) int {
s := strings.TrimSpace(strings.ToUpper(slot))
s = strings.TrimPrefix(s, "CPU")
if s == "" {
return 0
}
v, err := strconv.Atoi(s)
if err != nil {
return 0
}
return v
}
func intFromDetailMap(details map[string]any, key string) int {
if details == nil {
return 0
}
v, ok := details[key]
if !ok {
return 0
}
switch n := v.(type) {
case int:
return n
case int64:
return int(n)
case int32:
return int(n)
case float64:
return int(n)
case float32:
return int(n)
case string:
i, err := strconv.Atoi(strings.TrimSpace(n))
if err == nil {
return i
}
return 0
default:
return 0
}
}
func stringFromDetailMap(details map[string]any, key string) string {
if details == nil {
return ""
}
v, ok := details[key]
if !ok {
return ""
}
switch s := v.(type) {
case string:
return strings.TrimSpace(s)
default:
return strings.TrimSpace(fmt.Sprint(s))
}
}
func int64FromDetailMap(details map[string]any, key string) int64 {
if details == nil {
return 0
}
v, ok := details[key]
if !ok {
return 0
}
switch n := v.(type) {
case int:
return int64(n)
case int64:
return n
case int32:
return int64(n)
case float64:
return int64(n)
case float32:
return int64(n)
case string:
i, err := strconv.ParseInt(strings.TrimSpace(n), 10, 64)
if err == nil {
return i
}
return 0
default:
return 0
}
}
func boolPtrFromDetailMap(details map[string]any, key string) *bool {
if details == nil {
return nil
}
v, ok := details[key]
if !ok {
return nil
}
switch b := v.(type) {
case bool:
return &b
case string:
s := strings.ToLower(strings.TrimSpace(b))
if s == "true" || s == "1" || s == "yes" {
value := true
return &value
}
if s == "false" || s == "0" || s == "no" {
value := false
return &value
}
}
return nil
}
func boolFromPresentPtr(v *bool, defaultValue bool) bool {
if v == nil {
return defaultValue
}
return *v
}
func floatFromDetailMap(details map[string]any, key string) float64 {
if details == nil {
return 0
}
v, ok := details[key]
if !ok {
return 0
}
switch n := v.(type) {
case float64:
return n
case float32:
return float64(n)
case int:
return float64(n)
case int64:
return float64(n)
case int32:
return float64(n)
case string:
f, err := strconv.ParseFloat(strings.TrimSpace(n), 64)
if err == nil {
return f
}
return 0
default:
return 0
}
}
func firstNonZeroInt(values ...int) int {
for _, v := range values {
if v != 0 {
return v
}
}
return 0
}
func firstNonZeroFloat(values ...float64) float64 {
for _, v := range values {
if v != 0 {
return v
}
}
return 0
}
func normalizePCIeDeviceClass(d models.HardwareDevice) string {
switch d.Kind {
case models.DeviceKindGPU:
return "VideoController"
case models.DeviceKindNetwork:
return normalizeNetworkDeviceClass(d.PortType, d.Model, stringFromDetailMap(d.Details, "description"))
default:
return normalizeLegacyPCIeDeviceClass(d.DeviceClass)
}
}
func normalizeLegacyPCIeDeviceClass(deviceClass string) string {
switch strings.ToLower(strings.TrimSpace(deviceClass)) {
case "", "network", "network controller", "networkcontroller", "ethernet", "ethernet controller", "ethernetcontroller":
return "NetworkController"
case "fibre channel", "fibre channel controller", "fibrechannelcontroller", "fc":
return "FibreChannelController"
case "display", "displaycontroller", "display controller", "vga":
return "DisplayController"
case "video", "video controller", "videocontroller", "3d controller":
return "VideoController"
case "processing accelerator", "processingaccelerator":
return "ProcessingAccelerator"
case "mass storage controller", "massstoragecontroller":
return "MassStorageController"
case "storage controller", "storagecontroller":
return "StorageController"
default:
return strings.TrimSpace(deviceClass)
}
}
func normalizeNetworkDeviceClass(portType, model, description string) string {
joined := strings.ToLower(strings.TrimSpace(strings.Join([]string{portType, model, description}, " ")))
switch {
case strings.Contains(joined, "fibre channel") || strings.Contains(joined, " fibrechannel") || strings.Contains(joined, "fc "):
return "FibreChannelController"
default:
return "NetworkController"
}
}
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 {
return "Unknown"
}
return "Unknown"
}
func normalizeSourceType(sourceType string) string {
normalized := strings.ToLower(strings.TrimSpace(sourceType))
switch normalized {
case "api", "logfile", "manual":
return normalized
case "archive":
return "logfile"
default:
return ""
}
}
func normalizeProtocol(protocol string) string {
normalized := strings.ToLower(strings.TrimSpace(protocol))
switch normalized {
case "redfish", "ipmi", "snmp", "ssh":
return normalized
default:
return ""
}
}
func normalizeNullableString(v string) string {
trimmed := strings.TrimSpace(v)
if strings.EqualFold(trimmed, "NULL") {
return ""
}
return trimmed
}
func normalizeStatus(status string, allowEmpty bool) string {
switch strings.ToLower(strings.TrimSpace(status)) {
case "ok":
return "OK"
case "pass":
return "OK"
case "warning":
return "Warning"
case "critical":
return "Critical"
case "fail":
return "Critical"
case "unknown":
return "Unknown"
case "empty":
if allowEmpty {
return "Empty"
}
return "Unknown"
default:
if allowEmpty {
return "Unknown"
}
return "Unknown"
}
}
var (
ipv4Regex = regexp.MustCompile(`(?:^|[^0-9])((?:\d{1,3}\.){3}\d{1,3})(?:[^0-9]|$)`)
)
func inferTargetHost(targetHost, filename string) string {
if trimmed := strings.TrimSpace(targetHost); trimmed != "" {
return trimmed
}
candidate := strings.TrimSpace(filename)
if candidate == "" {
return ""
}
if parsed, err := url.Parse(candidate); err == nil && parsed.Hostname() != "" {
return parsed.Hostname()
}
if submatches := ipv4Regex.FindStringSubmatch(candidate); len(submatches) > 1 {
return submatches[1]
}
return ""
}