438 lines
11 KiB
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 ""
|
|
}
|