Files
logpile/internal/exporter/reanimator_converter.go

816 lines
21 KiB
Go

package exporter
import (
"fmt"
"net/url"
"regexp"
"sort"
"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)
collectedAt := formatRFC3339(result.CollectedAt)
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(convertCPUs(result.Hardware.CPUs, collectedAt)),
Memory: dedupeMemory(convertMemory(result.Hardware.Memory, collectedAt)),
Storage: dedupeStorage(convertStorage(result.Hardware.Storage, collectedAt)),
PCIeDevices: dedupePCIe(convertPCIeDevices(result.Hardware, collectedAt)),
PowerSupplies: dedupePSUs(convertPowerSupplies(result.Hardware.PowerSupply, 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,
}
}
// 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, 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))
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)
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: "", // PCIeDevice doesn't have firmware in models
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,
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 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,
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.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
}
}
// 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 ""
}