Add Reanimator format export support
Implement export to Reanimator format for asset tracking integration. Features: - New API endpoint: GET /api/export/reanimator - Web UI button "Экспорт Reanimator" in Configuration tab - Auto-detect CPU manufacturer (Intel/AMD/ARM/Ampere) - Generate PCIe serial numbers if missing - Merge GPUs and NetworkAdapters into pcie_devices - Filter components without serial numbers - RFC3339 timestamp format - Full compliance with Reanimator specification Changes: - Add reanimator_models.go: data models for Reanimator format - Add reanimator_converter.go: conversion functions - Add reanimator_converter_test.go: unit tests - Add reanimator_integration_test.go: integration tests - Update handlers.go: add handleExportReanimator - Update server.go: register /api/export/reanimator route - Update index.html: add export button - Update CLAUDE.md: document export behavior - Add REANIMATOR_EXPORT.md: implementation summary Tests: All tests passing (15+ new tests) Format spec: example/docs/INTEGRATION_GUIDE.md Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,395 @@
|
||||
package exporter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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 (required field)
|
||||
targetHost := result.TargetHost
|
||||
if targetHost == "" {
|
||||
// Try to extract from filename (e.g., "redfish://10.10.10.103")
|
||||
if strings.HasPrefix(result.Filename, "redfish://") {
|
||||
targetHost = strings.TrimPrefix(result.Filename, "redfish://")
|
||||
} else if strings.HasPrefix(result.Filename, "ipmi://") {
|
||||
targetHost = strings.TrimPrefix(result.Filename, "ipmi://")
|
||||
} else {
|
||||
targetHost = "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
boardSerial := result.Hardware.BoardInfo.SerialNumber
|
||||
|
||||
export := &ReanimatorExport{
|
||||
Filename: result.Filename,
|
||||
SourceType: result.SourceType,
|
||||
Protocol: 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: board.Manufacturer,
|
||||
ProductName: 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: "OK", // CPUs are typically OK if detected
|
||||
})
|
||||
}
|
||||
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 := mem.Status
|
||||
if 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: "OK",
|
||||
})
|
||||
}
|
||||
|
||||
// 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"
|
||||
if gpu.Model != "" {
|
||||
deviceClass = gpu.Model
|
||||
}
|
||||
|
||||
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: inferGPUStatus(gpu.Status),
|
||||
})
|
||||
}
|
||||
|
||||
// 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: inferNetworkStatus(nic.Status),
|
||||
})
|
||||
}
|
||||
|
||||
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 := psu.Status
|
||||
if status == "" {
|
||||
if psu.Present {
|
||||
status = "OK"
|
||||
} else {
|
||||
status = "Empty"
|
||||
}
|
||||
}
|
||||
|
||||
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 "Empty"
|
||||
}
|
||||
return "OK"
|
||||
}
|
||||
|
||||
// inferGPUStatus converts GPU status to Reanimator status
|
||||
func inferGPUStatus(status string) string {
|
||||
if status == "" {
|
||||
return "OK"
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
// inferNetworkStatus converts network adapter status to Reanimator status
|
||||
func inferNetworkStatus(status string) string {
|
||||
if status == "" {
|
||||
return "OK"
|
||||
}
|
||||
return status
|
||||
}
|
||||
Reference in New Issue
Block a user