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:
293
internal/exporter/reanimator_integration_test.go
Normal file
293
internal/exporter/reanimator_integration_test.go
Normal file
@@ -0,0 +1,293 @@
|
||||
package exporter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
// TestFullReanimatorExport tests complete export with realistic data
|
||||
func TestFullReanimatorExport(t *testing.T) {
|
||||
// Create a realistic AnalysisResult similar to import-example-full.json
|
||||
result := &models.AnalysisResult{
|
||||
Filename: "redfish://10.10.10.103",
|
||||
SourceType: "api",
|
||||
Protocol: "redfish",
|
||||
TargetHost: "10.10.10.103",
|
||||
CollectedAt: time.Date(2026, 2, 10, 15, 30, 0, 0, time.UTC),
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{
|
||||
Manufacturer: "Supermicro",
|
||||
ProductName: "X12DPG-QT6",
|
||||
SerialNumber: "21D634101",
|
||||
PartNumber: "X12DPG-QT6-REV1.01",
|
||||
UUID: "d7ef2fe5-2fd0-11f0-910a-346f11040868",
|
||||
},
|
||||
Firmware: []models.FirmwareInfo{
|
||||
{DeviceName: "BIOS", Version: "06.08.05"},
|
||||
{DeviceName: "BMC", Version: "5.17.00"},
|
||||
{DeviceName: "CPLD", Version: "01.02.03"},
|
||||
},
|
||||
CPUs: []models.CPU{
|
||||
{
|
||||
Socket: 0,
|
||||
Model: "INTEL(R) XEON(R) GOLD 6530",
|
||||
Cores: 32,
|
||||
Threads: 64,
|
||||
FrequencyMHz: 2100,
|
||||
MaxFreqMHz: 4000,
|
||||
},
|
||||
{
|
||||
Socket: 1,
|
||||
Model: "INTEL(R) XEON(R) GOLD 6530",
|
||||
Cores: 32,
|
||||
Threads: 64,
|
||||
FrequencyMHz: 2100,
|
||||
MaxFreqMHz: 4000,
|
||||
},
|
||||
},
|
||||
Memory: []models.MemoryDIMM{
|
||||
{
|
||||
Slot: "CPU0_C0D0",
|
||||
Location: "CPU0_C0D0",
|
||||
Present: true,
|
||||
SizeMB: 32768,
|
||||
Type: "DDR5",
|
||||
MaxSpeedMHz: 4800,
|
||||
CurrentSpeedMHz: 4800,
|
||||
Manufacturer: "Hynix",
|
||||
SerialNumber: "80AD032419E17CEEC1",
|
||||
PartNumber: "HMCG88AGBRA191N",
|
||||
Status: "OK",
|
||||
},
|
||||
{
|
||||
Slot: "CPU0_C1D0",
|
||||
Location: "CPU0_C1D0",
|
||||
Present: false,
|
||||
SizeMB: 0,
|
||||
Type: "",
|
||||
MaxSpeedMHz: 0,
|
||||
CurrentSpeedMHz: 0,
|
||||
Status: "Empty",
|
||||
},
|
||||
},
|
||||
Storage: []models.Storage{
|
||||
{
|
||||
Slot: "OB01",
|
||||
Type: "NVMe",
|
||||
Model: "INTEL SSDPF2KX076T1",
|
||||
SizeGB: 7680,
|
||||
SerialNumber: "BTAX41900GF87P6DGN",
|
||||
Manufacturer: "Intel",
|
||||
Firmware: "9CV10510",
|
||||
Interface: "NVMe",
|
||||
Present: true,
|
||||
},
|
||||
{
|
||||
Slot: "FP00HDD00",
|
||||
Type: "HDD",
|
||||
Model: "ST12000NM0008",
|
||||
SizeGB: 12000,
|
||||
SerialNumber: "ZJV01234ABC",
|
||||
Manufacturer: "Seagate",
|
||||
Firmware: "SN03",
|
||||
Interface: "SATA",
|
||||
Present: true,
|
||||
},
|
||||
},
|
||||
PCIeDevices: []models.PCIeDevice{
|
||||
{
|
||||
Slot: "PCIeCard1",
|
||||
VendorID: 32902,
|
||||
DeviceID: 2912,
|
||||
BDF: "0000:18:00.0",
|
||||
DeviceClass: "MassStorageController",
|
||||
Manufacturer: "Intel",
|
||||
PartNumber: "RAID Controller RSP3DD080F",
|
||||
LinkWidth: 8,
|
||||
LinkSpeed: "Gen3",
|
||||
MaxLinkWidth: 8,
|
||||
MaxLinkSpeed: "Gen3",
|
||||
SerialNumber: "RAID-001-12345",
|
||||
},
|
||||
{
|
||||
Slot: "PCIeCard2",
|
||||
VendorID: 5555,
|
||||
DeviceID: 4401,
|
||||
BDF: "0000:3b:00.0",
|
||||
DeviceClass: "NetworkController",
|
||||
Manufacturer: "Mellanox",
|
||||
PartNumber: "ConnectX-5",
|
||||
LinkWidth: 16,
|
||||
LinkSpeed: "Gen3",
|
||||
MaxLinkWidth: 16,
|
||||
MaxLinkSpeed: "Gen3",
|
||||
SerialNumber: "MT2892012345",
|
||||
},
|
||||
},
|
||||
PowerSupply: []models.PSU{
|
||||
{
|
||||
Slot: "0",
|
||||
Present: true,
|
||||
Model: "GW-CRPS3000LW",
|
||||
Vendor: "Great Wall",
|
||||
WattageW: 3000,
|
||||
SerialNumber: "2P06C102610",
|
||||
PartNumber: "V0310C9000000000",
|
||||
Firmware: "00.03.05",
|
||||
Status: "OK",
|
||||
InputType: "ACWideRange",
|
||||
InputPowerW: 137,
|
||||
OutputPowerW: 104,
|
||||
InputVoltage: 215.25,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Convert to Reanimator format
|
||||
reanimator, err := ConvertToReanimator(result)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify top-level fields
|
||||
if reanimator.Filename != "redfish://10.10.10.103" {
|
||||
t.Errorf("Filename mismatch: got %q", reanimator.Filename)
|
||||
}
|
||||
|
||||
if reanimator.SourceType != "api" {
|
||||
t.Errorf("SourceType mismatch: got %q", reanimator.SourceType)
|
||||
}
|
||||
|
||||
if reanimator.Protocol != "redfish" {
|
||||
t.Errorf("Protocol mismatch: got %q", reanimator.Protocol)
|
||||
}
|
||||
|
||||
if reanimator.TargetHost != "10.10.10.103" {
|
||||
t.Errorf("TargetHost mismatch: got %q", reanimator.TargetHost)
|
||||
}
|
||||
|
||||
if reanimator.CollectedAt != "2026-02-10T15:30:00Z" {
|
||||
t.Errorf("CollectedAt mismatch: got %q", reanimator.CollectedAt)
|
||||
}
|
||||
|
||||
// Verify hardware sections
|
||||
hw := reanimator.Hardware
|
||||
|
||||
// Board
|
||||
if hw.Board.SerialNumber != "21D634101" {
|
||||
t.Errorf("Board serial mismatch: got %q", hw.Board.SerialNumber)
|
||||
}
|
||||
|
||||
// Firmware
|
||||
if len(hw.Firmware) != 3 {
|
||||
t.Errorf("Expected 3 firmware entries, got %d", len(hw.Firmware))
|
||||
}
|
||||
|
||||
// CPUs
|
||||
if len(hw.CPUs) != 2 {
|
||||
t.Fatalf("Expected 2 CPUs, got %d", len(hw.CPUs))
|
||||
}
|
||||
|
||||
if hw.CPUs[0].Manufacturer != "Intel" {
|
||||
t.Errorf("CPU manufacturer not inferred: got %q", hw.CPUs[0].Manufacturer)
|
||||
}
|
||||
|
||||
if hw.CPUs[0].Status != "OK" {
|
||||
t.Errorf("CPU status mismatch: got %q", hw.CPUs[0].Status)
|
||||
}
|
||||
|
||||
// Memory (should include empty slots)
|
||||
if len(hw.Memory) != 2 {
|
||||
t.Errorf("Expected 2 memory entries (including empty), got %d", len(hw.Memory))
|
||||
}
|
||||
|
||||
if hw.Memory[1].Status != "Empty" {
|
||||
t.Errorf("Empty memory slot status mismatch: got %q", hw.Memory[1].Status)
|
||||
}
|
||||
|
||||
// Storage
|
||||
if len(hw.Storage) != 2 {
|
||||
t.Errorf("Expected 2 storage devices, got %d", len(hw.Storage))
|
||||
}
|
||||
|
||||
if hw.Storage[0].Status != "OK" {
|
||||
t.Errorf("Storage status mismatch: got %q", hw.Storage[0].Status)
|
||||
}
|
||||
|
||||
// PCIe devices
|
||||
if len(hw.PCIeDevices) != 2 {
|
||||
t.Errorf("Expected 2 PCIe devices, got %d", len(hw.PCIeDevices))
|
||||
}
|
||||
|
||||
if hw.PCIeDevices[0].Model == "" {
|
||||
t.Error("PCIe model should be populated from PartNumber")
|
||||
}
|
||||
|
||||
// Power supplies
|
||||
if len(hw.PowerSupplies) != 1 {
|
||||
t.Errorf("Expected 1 PSU, got %d", len(hw.PowerSupplies))
|
||||
}
|
||||
|
||||
// Verify JSON marshaling works
|
||||
jsonData, err := json.MarshalIndent(reanimator, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal to JSON: %v", err)
|
||||
}
|
||||
|
||||
// Check that JSON contains expected fields
|
||||
jsonStr := string(jsonData)
|
||||
expectedFields := []string{
|
||||
`"filename"`,
|
||||
`"source_type"`,
|
||||
`"protocol"`,
|
||||
`"target_host"`,
|
||||
`"collected_at"`,
|
||||
`"hardware"`,
|
||||
`"board"`,
|
||||
`"cpus"`,
|
||||
`"memory"`,
|
||||
`"storage"`,
|
||||
`"pcie_devices"`,
|
||||
`"power_supplies"`,
|
||||
`"firmware"`,
|
||||
}
|
||||
|
||||
for _, field := range expectedFields {
|
||||
if !strings.Contains(jsonStr, field) {
|
||||
t.Errorf("JSON missing expected field: %s", field)
|
||||
}
|
||||
}
|
||||
|
||||
// Optional: print JSON for manual inspection (commented out for normal test runs)
|
||||
// t.Logf("Generated Reanimator JSON:\n%s", string(jsonData))
|
||||
}
|
||||
|
||||
// TestReanimatorExportWithoutTargetHost tests that target_host is inferred from filename
|
||||
func TestReanimatorExportWithoutTargetHost(t *testing.T) {
|
||||
result := &models.AnalysisResult{
|
||||
Filename: "redfish://192.168.1.100",
|
||||
SourceType: "api",
|
||||
Protocol: "redfish",
|
||||
TargetHost: "", // Empty - should be inferred
|
||||
CollectedAt: time.Now(),
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{
|
||||
SerialNumber: "TEST123",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reanimator, err := ConvertToReanimator(result)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToReanimator failed: %v", err)
|
||||
}
|
||||
|
||||
if reanimator.TargetHost != "192.168.1.100" {
|
||||
t.Errorf("Expected target_host to be inferred from filename, got %q", reanimator.TargetHost)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user