Files
logpile/internal/exporter/reanimator_converter.go

438 lines
11 KiB
Go

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)
boardSerial := result.Hardware.BoardInfo.SerialNumber
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, boardSerial),
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, boardSerial string) []ReanimatorPCIe {
result := make([]ReanimatorPCIe, 0)
// Convert regular PCIe devices
for _, pcie := range hw.PCIeDevices {
serialNumber := pcie.SerialNumber
if serialNumber == "" || serialNumber == "N/A" {
// Generate serial number
serialNumber = generatePCIeSerialNumber(boardSerial, pcie.Slot, pcie.BDF)
}
// 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 := gpu.SerialNumber
if serialNumber == "" {
// Generate serial number
serialNumber = generatePCIeSerialNumber(boardSerial, gpu.Slot, gpu.BDF)
}
// 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 := nic.SerialNumber
if serialNumber == "" {
// Generate serial number
serialNumber = generatePCIeSerialNumber(boardSerial, nic.Slot, "")
}
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 ""
}
// generatePCIeSerialNumber generates a serial number for PCIe device
func generatePCIeSerialNumber(boardSerial, slot, bdf string) string {
if slot != "" {
return fmt.Sprintf("%s-PCIE-%s", boardSerial, slot)
}
if bdf != "" {
// Use BDF as identifier (e.g., "0000:18:00.0" -> "0000-18-00-0")
safeBDF := strings.ReplaceAll(strings.ReplaceAll(bdf, ":", "-"), ".", "-")
return fmt.Sprintf("%s-PCIE-%s", boardSerial, safeBDF)
}
return fmt.Sprintf("%s-PCIE-UNKNOWN", boardSerial)
}
// 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 "warning":
return "Warning"
case "critical":
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 ""
}