package exporter import ( "fmt" "net/url" "regexp" "sort" "strconv" "strings" "time" "git.mchus.pro/mchus/logpile/internal/models" ) var cpuMicrocodeFirmwareRegex = 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(result.CollectedAt) 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: convertCPUsFromDevices(devices, collectedAt), Memory: convertMemoryFromDevices(devices, collectedAt), Storage: convertStorageFromDevices(devices, collectedAt), PCIeDevices: convertPCIeFromDevices(devices, collectedAt), PowerSupplies: convertPSUsFromDevices(devices, collectedAt), }, } return export, nil } // 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 } if len(hw.Devices) > 0 { return hw.Devices } hw.Devices = buildDevicesFromLegacy(hw) 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)) appendDevice := func(d models.HardwareDevice) { all = append(all, d) } for _, cpu := range hw.CPUs { 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: map[string]any{ "socket": cpu.Socket, }, }) } for _, mem := range hw.Memory { if !mem.Present || mem.SizeMB == 0 { continue } present := mem.Present 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: map[string]any{ "max_speed_mhz": mem.MaxSpeedMHz, "current_speed_mhz": mem.CurrentSpeedMHz, }, }) } for _, stor := range hw.Storage { if !stor.Present { continue } present := stor.Present appendDevice(models.HardwareDevice{ Kind: models.DeviceKindStorage, Slot: stor.Slot, Model: stor.Model, Manufacturer: stor.Manufacturer, 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, }) } for _, pcie := range hw.PCIeDevices { appendDevice(models.HardwareDevice{ Kind: models.DeviceKindPCIe, Slot: pcie.Slot, BDF: pcie.BDF, DeviceClass: pcie.DeviceClass, VendorID: pcie.VendorID, DeviceID: pcie.DeviceID, Model: pcie.PartNumber, PartNumber: pcie.PartNumber, Manufacturer: pcie.Manufacturer, SerialNumber: pcie.SerialNumber, LinkWidth: pcie.LinkWidth, LinkSpeed: pcie.LinkSpeed, MaxLinkWidth: pcie.MaxLinkWidth, MaxLinkSpeed: pcie.MaxLinkSpeed, Status: pcie.Status, StatusCheckedAt: pcie.StatusCheckedAt, StatusChangedAt: pcie.StatusChangedAt, StatusAtCollect: pcie.StatusAtCollect, StatusHistory: pcie.StatusHistory, ErrorDescription: pcie.ErrorDescription, }) } for _, gpu := range hw.GPUs { 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: 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, }, }) } for _, nic := range hw.NetworkAdapters { if !nic.Present { continue } present := nic.Present appendDevice(models.HardwareDevice{ Kind: models.DeviceKindNetwork, Slot: nic.Slot, Location: nic.Location, 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, Present: &present, Status: nic.Status, StatusCheckedAt: nic.StatusCheckedAt, StatusChangedAt: nic.StatusChangedAt, StatusAtCollect: nic.StatusAtCollect, StatusHistory: nic.StatusHistory, ErrorDescription: nic.ErrorDescription, }) } 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, }) } 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 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. // 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 { 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 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) } } 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 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 } 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 { if sn := normalizedSerial(item.SerialNumber); sn != "" { return "sn:" + strings.ToLower(sn) } if bdf := strings.ToLower(strings.TrimSpace(item.BDF)); bdf != "" { return "bdf:" + bdf } return "" } 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 string) []ReanimatorCPU { result := make([]ReanimatorCPU, 0) for _, d := range devices { if d.Kind != models.DeviceKindCPU { 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.StatusAtCollect, 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, Manufacturer: inferCPUManufacturer(d.Model), Status: cpuStatus, StatusCheckedAt: meta.StatusCheckedAt, StatusChangedAt: meta.StatusChangedAt, StatusAtCollect: meta.StatusAtCollection, 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 := d.Present != nil && *d.Present if !present || d.SizeMB == 0 { continue } status := normalizeStatus(d.Status, true) if strings.TrimSpace(d.Status) == "" { if present { status = "OK" } else { status = "Empty" } } meta := buildStatusMeta(status, d.StatusCheckedAt, d.StatusChangedAt, d.StatusAtCollect, d.StatusHistory, d.ErrorDescription, collectedAt) result = append(result, ReanimatorMemory{ Slot: d.Slot, Location: d.Location, Present: present, SizeMB: d.SizeMB, Type: d.Type, MaxSpeedMHz: intFromDetailMap(d.Details, "max_speed_mhz"), CurrentSpeedMHz: intFromDetailMap(d.Details, "current_speed_mhz"), Manufacturer: d.Manufacturer, SerialNumber: d.SerialNumber, PartNumber: d.PartNumber, Status: status, StatusCheckedAt: meta.StatusCheckedAt, StatusChangedAt: meta.StatusChangedAt, StatusAtCollect: meta.StatusAtCollection, 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 strings.TrimSpace(d.SerialNumber) == "" { continue } present := d.Present == nil || *d.Present status := inferStorageStatus(models.Storage{Present: present}) if strings.TrimSpace(d.Status) != "" { status = normalizeStatus(d.Status, false) } meta := buildStatusMeta(status, d.StatusCheckedAt, d.StatusChangedAt, d.StatusAtCollect, d.StatusHistory, d.ErrorDescription, collectedAt) 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: present, Status: status, StatusCheckedAt: meta.StatusCheckedAt, StatusChangedAt: meta.StatusChangedAt, StatusAtCollect: meta.StatusAtCollection, 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 } deviceClass := d.DeviceClass if d.Kind == models.DeviceKindGPU && strings.TrimSpace(deviceClass) == "" { deviceClass = "DisplayController" } model := d.Model if model == "" { model = d.PartNumber } temperatureC := d.TemperatureC if temperatureC == 0 { temperatureC = firstNonZeroInt( intFromDetailMap(d.Details, "temperature_c"), intFromDetailMap(d.Details, "temperature"), ) } powerW := firstNonZeroInt( intFromDetailMap(d.Details, "power_w"), intFromDetailMap(d.Details, "power"), ) voltageV := firstNonZeroFloat( floatFromDetailMap(d.Details, "voltage_v"), floatFromDetailMap(d.Details, "voltage"), floatFromDetailMap(d.Details, "input_voltage"), d.InputVoltage, ) status := normalizeStatus(d.Status, false) meta := buildStatusMeta(status, d.StatusCheckedAt, d.StatusChangedAt, d.StatusAtCollect, d.StatusHistory, d.ErrorDescription, collectedAt) result = append(result, ReanimatorPCIe{ Slot: d.Slot, VendorID: d.VendorID, DeviceID: d.DeviceID, BDF: d.BDF, DeviceClass: deviceClass, Manufacturer: d.Manufacturer, Model: model, LinkWidth: d.LinkWidth, LinkSpeed: d.LinkSpeed, MaxLinkWidth: d.MaxLinkWidth, MaxLinkSpeed: d.MaxLinkSpeed, SerialNumber: normalizedSerial(d.SerialNumber), Firmware: d.Firmware, TemperatureC: temperatureC, PowerW: powerW, VoltageV: voltageV, Status: status, StatusCheckedAt: meta.StatusCheckedAt, StatusChangedAt: meta.StatusChangedAt, StatusAtCollect: meta.StatusAtCollection, StatusHistory: meta.StatusHistory, ErrorDescription: meta.ErrorDescription, }) } return result } 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.StatusAtCollect, d.StatusHistory, d.ErrorDescription, collectedAt) result = append(result, ReanimatorPSU{ Slot: d.Slot, Present: present, Model: d.Model, Vendor: d.Manufacturer, WattageW: d.WattageW, SerialNumber: d.SerialNumber, PartNumber: d.PartNumber, Firmware: d.Firmware, Status: status, InputType: d.InputType, InputPowerW: d.InputPowerW, OutputPowerW: d.OutputPowerW, InputVoltage: d.InputVoltage, TemperatureC: d.TemperatureC, StatusCheckedAt: meta.StatusCheckedAt, StatusChangedAt: meta.StatusChangedAt, StatusAtCollect: meta.StatusAtCollection, StatusHistory: meta.StatusHistory, ErrorDescription: meta.ErrorDescription, }) } return result } 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") || // HGX baseboard firmware inventory IDs for device-bound components strings.Contains(n, "_fw_gpu_") || strings.Contains(n, "_fw_nvswitch_") || strings.Contains(n, "_inforom_gpu_") { return true } return cpuMicrocodeFirmwareRegex.MatchString(strings.TrimSpace(name)) } // 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.Backplane.Firmware.0). // 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 } for _, prefix := range []string{"nic.", "psu.", "disk.", "raid.backplane.", "gpu."} { if strings.HasPrefix(d, prefix) { return true } } return false } // 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.StatusAtCollect, 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, Manufacturer: manufacturer, Status: cpuStatus, StatusCheckedAt: meta.StatusCheckedAt, StatusChangedAt: meta.StatusChangedAt, StatusAtCollect: meta.StatusAtCollection, 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 { status := normalizeStatus(mem.Status, true) if strings.TrimSpace(mem.Status) == "" { if mem.Present { status = "OK" } else { status = "Empty" } } meta := buildStatusMeta( status, mem.StatusCheckedAt, mem.StatusChangedAt, mem.StatusAtCollect, mem.StatusHistory, mem.ErrorDescription, collectedAt, ) result = append(result, ReanimatorMemory{ Slot: mem.Slot, Location: mem.Location, Present: mem.Present, 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, StatusAtCollect: meta.StatusAtCollection, 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 { // Skip storage without serial number if stor.SerialNumber == "" { continue } status := inferStorageStatus(stor) if strings.TrimSpace(stor.Status) != "" { status = normalizeStatus(stor.Status, false) } meta := buildStatusMeta( status, stor.StatusCheckedAt, stor.StatusChangedAt, stor.StatusAtCollect, stor.StatusHistory, stor.ErrorDescription, collectedAt, ) 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: stor.Present, Status: status, StatusCheckedAt: meta.StatusCheckedAt, StatusChangedAt: meta.StatusChangedAt, StatusAtCollect: meta.StatusAtCollection, StatusHistory: meta.StatusHistory, ErrorDescription: meta.ErrorDescription, }) } return result } // 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 } serialNumber := normalizedSerial(pcie.SerialNumber) // Determine model (prefer PartNumber, fallback to DeviceClass) model := pcie.PartNumber 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.StatusAtCollect, pcie.StatusHistory, pcie.ErrorDescription, collectedAt, ) result = append(result, ReanimatorPCIe{ Slot: pcie.Slot, VendorID: pcie.VendorID, DeviceID: pcie.DeviceID, BDF: pcie.BDF, DeviceClass: pcie.DeviceClass, Manufacturer: pcie.Manufacturer, Model: model, LinkWidth: pcie.LinkWidth, LinkSpeed: pcie.LinkSpeed, MaxLinkWidth: pcie.MaxLinkWidth, MaxLinkSpeed: pcie.MaxLinkSpeed, SerialNumber: serialNumber, Firmware: firmware, Status: status, StatusCheckedAt: meta.StatusCheckedAt, StatusChangedAt: meta.StatusChangedAt, StatusAtCollect: meta.StatusAtCollection, StatusHistory: meta.StatusHistory, ErrorDescription: meta.ErrorDescription, }) } // Convert GPUs as PCIe devices for _, gpu := range hw.GPUs { serialNumber := normalizedSerial(gpu.SerialNumber) // Determine device class deviceClass := "DisplayController" status := normalizeStatus(gpu.Status, false) meta := buildStatusMeta( status, gpu.StatusCheckedAt, gpu.StatusChangedAt, gpu.StatusAtCollect, gpu.StatusHistory, gpu.ErrorDescription, collectedAt, ) result = append(result, ReanimatorPCIe{ Slot: gpu.Slot, VendorID: gpu.VendorID, DeviceID: gpu.DeviceID, BDF: gpu.BDF, DeviceClass: deviceClass, Manufacturer: gpu.Manufacturer, Model: gpu.Model, LinkWidth: gpu.CurrentLinkWidth, LinkSpeed: gpu.CurrentLinkSpeed, MaxLinkWidth: gpu.MaxLinkWidth, MaxLinkSpeed: gpu.MaxLinkSpeed, SerialNumber: serialNumber, Firmware: gpu.Firmware, TemperatureC: gpu.Temperature, PowerW: gpu.Power, Status: status, StatusCheckedAt: meta.StatusCheckedAt, StatusChangedAt: meta.StatusChangedAt, StatusAtCollect: meta.StatusAtCollection, StatusHistory: meta.StatusHistory, ErrorDescription: meta.ErrorDescription, }) } // Convert network adapters as PCIe devices for _, nic := range hw.NetworkAdapters { if !nic.Present { continue } serialNumber := normalizedSerial(nic.SerialNumber) status := normalizeStatus(nic.Status, false) meta := buildStatusMeta( status, nic.StatusCheckedAt, nic.StatusChangedAt, nic.StatusAtCollect, nic.StatusHistory, nic.ErrorDescription, collectedAt, ) result = append(result, ReanimatorPCIe{ Slot: nic.Slot, VendorID: nic.VendorID, DeviceID: nic.DeviceID, BDF: "", DeviceClass: "NetworkController", Manufacturer: nic.Vendor, Model: nic.Model, LinkWidth: 0, LinkSpeed: "", MaxLinkWidth: 0, MaxLinkSpeed: "", SerialNumber: serialNumber, Firmware: nic.Firmware, Status: status, StatusCheckedAt: meta.StatusCheckedAt, StatusChangedAt: meta.StatusChangedAt, StatusAtCollect: meta.StatusAtCollection, 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), "NVSWITCH ") { continue } rest := strings.TrimSpace(name[len("NVSwitch "):]) if rest == "" { continue } slot := rest if idx := strings.Index(rest, " ("); idx > 0 { slot = strings.TrimSpace(rest[:idx]) } 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.StatusAtCollect, psu.StatusHistory, psu.ErrorDescription, collectedAt, ) result = append(result, ReanimatorPSU{ Slot: psu.Slot, Present: psu.Present, Model: psu.Model, Vendor: psu.Vendor, WattageW: psu.WattageW, SerialNumber: psu.SerialNumber, PartNumber: psu.PartNumber, Firmware: psu.Firmware, Status: status, InputType: psu.InputType, InputPowerW: psu.InputPowerW, OutputPowerW: psu.OutputPowerW, InputVoltage: psu.InputVoltage, TemperatureC: psu.TemperatureC, StatusCheckedAt: meta.StatusCheckedAt, StatusChangedAt: meta.StatusChangedAt, StatusAtCollect: meta.StatusAtCollection, StatusHistory: meta.StatusHistory, ErrorDescription: meta.ErrorDescription, }) } return result } type convertedStatusMeta struct { StatusCheckedAt string StatusChangedAt string StatusAtCollection *ReanimatorStatusAtCollection StatusHistory []ReanimatorStatusHistoryEntry ErrorDescription string } func buildStatusMeta( currentStatus string, checkedAt *time.Time, changedAt *time.Time, statusAtCollection *models.StatusAtCollection, 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 statusAtCollection != nil { at := formatOptionalRFC3339(&statusAtCollection.At) if at != "" && strings.TrimSpace(statusAtCollection.Status) != "" { meta.StatusAtCollection = &ReanimatorStatusAtCollection{ Status: normalizeStatus(statusAtCollection.Status, true), At: at, } } } if meta.StatusAtCollection == nil && strings.TrimSpace(currentStatus) != "" && collectedAt != "" { meta.StatusAtCollection = &ReanimatorStatusAtCollection{ Status: currentStatus, At: collectedAt, } } 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 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 } // 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 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 "" }