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)) if slot != "" { return "slot:" + slot } if serial != "" { return "sn:" + serial } if bdf != "" { return "bdf:" + bdf } return strings.ToLower(strings.TrimSpace(item.DeviceClass)) + "|" + strings.ToLower(strings.TrimSpace(item.Model)) } 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 "" }