package exporter import ( "fmt" "net/url" "regexp" "strings" "time" "git.mchus.pro/mchus/logpile/internal/models" ) // 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) export := &ReanimatorExport{ Filename: result.Filename, SourceType: normalizeSourceType(result.SourceType), Protocol: normalizeProtocol(result.Protocol), TargetHost: targetHost, CollectedAt: formatRFC3339(result.CollectedAt), Hardware: ReanimatorHardware{ Board: convertBoard(result.Hardware.BoardInfo), Firmware: convertFirmware(result.Hardware.Firmware), CPUs: convertCPUs(result.Hardware.CPUs), Memory: convertMemory(result.Hardware.Memory), Storage: convertStorage(result.Hardware.Storage), PCIeDevices: convertPCIeDevices(result.Hardware), PowerSupplies: convertPowerSupplies(result.Hardware.PowerSupply), }, } 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, } } // 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 { result = append(result, ReanimatorFirmware{ DeviceName: fw.DeviceName, Version: fw.Version, }) } return result } // convertCPUs converts CPU information to Reanimator format func convertCPUs(cpus []models.CPU) []ReanimatorCPU { if len(cpus) == 0 { return nil } result := make([]ReanimatorCPU, 0, len(cpus)) for _, cpu := range cpus { manufacturer := inferCPUManufacturer(cpu.Model) 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: "Unknown", }) } return result } // convertMemory converts memory modules to Reanimator format func convertMemory(memory []models.MemoryDIMM) []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" } } 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, }) } return result } // convertStorage converts storage devices to Reanimator format func convertStorage(storage []models.Storage) []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) 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, }) } return result } // convertPCIeDevices converts PCIe devices, GPUs, and network adapters to Reanimator format func convertPCIeDevices(hw *models.HardwareConfig) []ReanimatorPCIe { result := make([]ReanimatorPCIe, 0) // Convert regular PCIe devices for _, pcie := range hw.PCIeDevices { serialNumber := normalizedSerial(pcie.SerialNumber) // Determine model (prefer PartNumber, fallback to DeviceClass) model := pcie.PartNumber if model == "" { model = pcie.DeviceClass } 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: "", // PCIeDevice doesn't have firmware in models Status: "Unknown", }) } // Convert GPUs as PCIe devices for _, gpu := range hw.GPUs { serialNumber := normalizedSerial(gpu.SerialNumber) // Determine device class deviceClass := "DisplayController" 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, Status: normalizeStatus(gpu.Status, false), }) } // Convert network adapters as PCIe devices for _, nic := range hw.NetworkAdapters { if !nic.Present { continue } serialNumber := normalizedSerial(nic.SerialNumber) 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: normalizeStatus(nic.Status, false), }) } return result } // convertPowerSupplies converts power supplies to Reanimator format func convertPowerSupplies(psus []models.PSU) []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) 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, }) } return result } // 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 } } // 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 "" }