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:
395
internal/exporter/reanimator_converter.go
Normal file
395
internal/exporter/reanimator_converter.go
Normal file
@@ -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
|
||||
}
|
||||
377
internal/exporter/reanimator_converter_test.go
Normal file
377
internal/exporter/reanimator_converter_test.go
Normal file
@@ -0,0 +1,377 @@
|
||||
package exporter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
func TestConvertToReanimator(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input *models.AnalysisResult
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "nil result",
|
||||
input: nil,
|
||||
wantErr: true,
|
||||
errMsg: "no data available",
|
||||
},
|
||||
{
|
||||
name: "no hardware",
|
||||
input: &models.AnalysisResult{
|
||||
Filename: "test.json",
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "no hardware data available",
|
||||
},
|
||||
{
|
||||
name: "no board serial",
|
||||
input: &models.AnalysisResult{
|
||||
Filename: "test.json",
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "board serial_number is required",
|
||||
},
|
||||
{
|
||||
name: "valid minimal data",
|
||||
input: &models.AnalysisResult{
|
||||
Filename: "test.json",
|
||||
SourceType: "api",
|
||||
Protocol: "redfish",
|
||||
TargetHost: "10.10.10.10",
|
||||
CollectedAt: time.Date(2026, 2, 10, 15, 30, 0, 0, time.UTC),
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{
|
||||
Manufacturer: "Supermicro",
|
||||
ProductName: "X12DPG-QT6",
|
||||
SerialNumber: "TEST123",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := ConvertToReanimator(tt.input)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("expected error containing %q, got nil", tt.errMsg)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
if result == nil {
|
||||
t.Error("expected non-nil result")
|
||||
return
|
||||
}
|
||||
if result.Hardware.Board.SerialNumber != tt.input.Hardware.BoardInfo.SerialNumber {
|
||||
t.Errorf("board serial mismatch: got %q, want %q",
|
||||
result.Hardware.Board.SerialNumber,
|
||||
tt.input.Hardware.BoardInfo.SerialNumber)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInferCPUManufacturer(t *testing.T) {
|
||||
tests := []struct {
|
||||
model string
|
||||
want string
|
||||
}{
|
||||
{"INTEL(R) XEON(R) GOLD 6530", "Intel"},
|
||||
{"Intel Core i9-12900K", "Intel"},
|
||||
{"AMD EPYC 7763", "AMD"},
|
||||
{"AMD Ryzen 9 5950X", "AMD"},
|
||||
{"ARM Cortex-A78", "ARM"},
|
||||
{"Ampere Altra Max", "Ampere"},
|
||||
{"Unknown CPU Model", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.model, func(t *testing.T) {
|
||||
got := inferCPUManufacturer(tt.model)
|
||||
if got != tt.want {
|
||||
t.Errorf("inferCPUManufacturer(%q) = %q, want %q", tt.model, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePCIeSerialNumber(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
boardSerial string
|
||||
slot string
|
||||
bdf string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "with slot",
|
||||
boardSerial: "TEST123",
|
||||
slot: "PCIeCard1",
|
||||
bdf: "0000:18:00.0",
|
||||
want: "TEST123-PCIE-PCIeCard1",
|
||||
},
|
||||
{
|
||||
name: "without slot, with bdf",
|
||||
boardSerial: "TEST123",
|
||||
slot: "",
|
||||
bdf: "0000:18:00.0",
|
||||
want: "TEST123-PCIE-0000-18-00-0",
|
||||
},
|
||||
{
|
||||
name: "without slot and bdf",
|
||||
boardSerial: "TEST123",
|
||||
slot: "",
|
||||
bdf: "",
|
||||
want: "TEST123-PCIE-UNKNOWN",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := generatePCIeSerialNumber(tt.boardSerial, tt.slot, tt.bdf)
|
||||
if got != tt.want {
|
||||
t.Errorf("generatePCIeSerialNumber() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInferStorageStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
stor models.Storage
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "present",
|
||||
stor: models.Storage{
|
||||
Present: true,
|
||||
},
|
||||
want: "OK",
|
||||
},
|
||||
{
|
||||
name: "not present",
|
||||
stor: models.Storage{
|
||||
Present: false,
|
||||
},
|
||||
want: "Empty",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := inferStorageStatus(tt.stor)
|
||||
if got != tt.want {
|
||||
t.Errorf("inferStorageStatus() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertCPUs(t *testing.T) {
|
||||
cpus := []models.CPU{
|
||||
{
|
||||
Socket: 0,
|
||||
Model: "INTEL(R) XEON(R) GOLD 6530",
|
||||
Cores: 32,
|
||||
Threads: 64,
|
||||
FrequencyMHz: 2100,
|
||||
MaxFreqMHz: 4000,
|
||||
},
|
||||
{
|
||||
Socket: 1,
|
||||
Model: "AMD EPYC 7763",
|
||||
Cores: 64,
|
||||
Threads: 128,
|
||||
FrequencyMHz: 2450,
|
||||
MaxFreqMHz: 3500,
|
||||
},
|
||||
}
|
||||
|
||||
result := convertCPUs(cpus)
|
||||
|
||||
if len(result) != 2 {
|
||||
t.Fatalf("expected 2 CPUs, got %d", len(result))
|
||||
}
|
||||
|
||||
if result[0].Manufacturer != "Intel" {
|
||||
t.Errorf("expected Intel manufacturer for first CPU, got %q", result[0].Manufacturer)
|
||||
}
|
||||
|
||||
if result[1].Manufacturer != "AMD" {
|
||||
t.Errorf("expected AMD manufacturer for second CPU, got %q", result[1].Manufacturer)
|
||||
}
|
||||
|
||||
if result[0].Status != "OK" {
|
||||
t.Errorf("expected OK status, got %q", result[0].Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertMemory(t *testing.T) {
|
||||
memory := []models.MemoryDIMM{
|
||||
{
|
||||
Slot: "CPU0_C0D0",
|
||||
Present: true,
|
||||
SizeMB: 32768,
|
||||
Type: "DDR5",
|
||||
SerialNumber: "TEST-MEM-001",
|
||||
Status: "OK",
|
||||
},
|
||||
{
|
||||
Slot: "CPU0_C1D0",
|
||||
Present: false,
|
||||
},
|
||||
}
|
||||
|
||||
result := convertMemory(memory)
|
||||
|
||||
if len(result) != 2 {
|
||||
t.Fatalf("expected 2 memory modules, got %d", len(result))
|
||||
}
|
||||
|
||||
if result[0].Status != "OK" {
|
||||
t.Errorf("expected OK status for first module, got %q", result[0].Status)
|
||||
}
|
||||
|
||||
if result[1].Status != "Empty" {
|
||||
t.Errorf("expected Empty status for second module, got %q", result[1].Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertStorage(t *testing.T) {
|
||||
storage := []models.Storage{
|
||||
{
|
||||
Slot: "OB01",
|
||||
Type: "NVMe",
|
||||
Model: "INTEL SSDPF2KX076T1",
|
||||
SerialNumber: "BTAX41900GF87P6DGN",
|
||||
Present: true,
|
||||
},
|
||||
{
|
||||
Slot: "OB02",
|
||||
Type: "NVMe",
|
||||
Model: "INTEL SSDPF2KX076T1",
|
||||
SerialNumber: "", // No serial - should be skipped
|
||||
Present: true,
|
||||
},
|
||||
}
|
||||
|
||||
result := convertStorage(storage)
|
||||
|
||||
if len(result) != 1 {
|
||||
t.Fatalf("expected 1 storage device (skipped one without serial), got %d", len(result))
|
||||
}
|
||||
|
||||
if result[0].Status != "OK" {
|
||||
t.Errorf("expected OK status, got %q", result[0].Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertPCIeDevices(t *testing.T) {
|
||||
hw := &models.HardwareConfig{
|
||||
PCIeDevices: []models.PCIeDevice{
|
||||
{
|
||||
Slot: "PCIeCard1",
|
||||
VendorID: 32902,
|
||||
DeviceID: 2912,
|
||||
BDF: "0000:18:00.0",
|
||||
DeviceClass: "MassStorageController",
|
||||
Manufacturer: "Intel",
|
||||
PartNumber: "RSP3DD080F",
|
||||
SerialNumber: "RAID-001",
|
||||
},
|
||||
{
|
||||
Slot: "PCIeCard2",
|
||||
DeviceClass: "NetworkController",
|
||||
Manufacturer: "Mellanox",
|
||||
SerialNumber: "", // Should be generated
|
||||
},
|
||||
},
|
||||
GPUs: []models.GPU{
|
||||
{
|
||||
Slot: "GPU1",
|
||||
Model: "NVIDIA A100",
|
||||
Manufacturer: "NVIDIA",
|
||||
SerialNumber: "GPU-001",
|
||||
Status: "OK",
|
||||
},
|
||||
},
|
||||
NetworkAdapters: []models.NetworkAdapter{
|
||||
{
|
||||
Slot: "NIC1",
|
||||
Model: "ConnectX-6",
|
||||
Vendor: "Mellanox",
|
||||
Present: true,
|
||||
SerialNumber: "NIC-001",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
boardSerial := "TEST123"
|
||||
result := convertPCIeDevices(hw, boardSerial)
|
||||
|
||||
// Should have: 2 PCIe devices + 1 GPU + 1 NIC = 4 total
|
||||
if len(result) != 4 {
|
||||
t.Fatalf("expected 4 PCIe devices total, got %d", len(result))
|
||||
}
|
||||
|
||||
// Check that serial was generated for second PCIe device
|
||||
if result[1].SerialNumber != "TEST123-PCIE-PCIeCard2" {
|
||||
t.Errorf("expected generated serial TEST123-PCIE-PCIeCard2, got %q", result[1].SerialNumber)
|
||||
}
|
||||
|
||||
// Check GPU was included
|
||||
foundGPU := false
|
||||
for _, dev := range result {
|
||||
if dev.SerialNumber == "GPU-001" {
|
||||
foundGPU = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundGPU {
|
||||
t.Error("expected GPU to be included in PCIe devices")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertPowerSupplies(t *testing.T) {
|
||||
psus := []models.PSU{
|
||||
{
|
||||
Slot: "0",
|
||||
Present: true,
|
||||
Model: "GW-CRPS3000LW",
|
||||
Vendor: "Great Wall",
|
||||
WattageW: 3000,
|
||||
SerialNumber: "PSU-001",
|
||||
Status: "OK",
|
||||
},
|
||||
{
|
||||
Slot: "1",
|
||||
Present: false,
|
||||
SerialNumber: "", // Not present, should be skipped
|
||||
},
|
||||
}
|
||||
|
||||
result := convertPowerSupplies(psus)
|
||||
|
||||
if len(result) != 1 {
|
||||
t.Fatalf("expected 1 PSU (skipped empty), got %d", len(result))
|
||||
}
|
||||
|
||||
if result[0].Status != "OK" {
|
||||
t.Errorf("expected OK status, got %q", result[0].Status)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
113
internal/exporter/reanimator_models.go
Normal file
113
internal/exporter/reanimator_models.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package exporter
|
||||
|
||||
// ReanimatorExport represents the top-level structure for Reanimator format export
|
||||
type ReanimatorExport struct {
|
||||
Filename string `json:"filename"`
|
||||
SourceType string `json:"source_type"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
TargetHost string `json:"target_host"`
|
||||
CollectedAt string `json:"collected_at"` // RFC3339 format
|
||||
Hardware ReanimatorHardware `json:"hardware"`
|
||||
}
|
||||
|
||||
// ReanimatorHardware contains all hardware components
|
||||
type ReanimatorHardware struct {
|
||||
Board ReanimatorBoard `json:"board"`
|
||||
Firmware []ReanimatorFirmware `json:"firmware,omitempty"`
|
||||
CPUs []ReanimatorCPU `json:"cpus,omitempty"`
|
||||
Memory []ReanimatorMemory `json:"memory,omitempty"`
|
||||
Storage []ReanimatorStorage `json:"storage,omitempty"`
|
||||
PCIeDevices []ReanimatorPCIe `json:"pcie_devices,omitempty"`
|
||||
PowerSupplies []ReanimatorPSU `json:"power_supplies,omitempty"`
|
||||
}
|
||||
|
||||
// ReanimatorBoard represents motherboard/server information
|
||||
type ReanimatorBoard struct {
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
ProductName string `json:"product_name,omitempty"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
PartNumber string `json:"part_number,omitempty"`
|
||||
UUID string `json:"uuid,omitempty"`
|
||||
}
|
||||
|
||||
// ReanimatorFirmware represents firmware version information
|
||||
type ReanimatorFirmware struct {
|
||||
DeviceName string `json:"device_name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// ReanimatorCPU represents processor information
|
||||
type ReanimatorCPU struct {
|
||||
Socket int `json:"socket"`
|
||||
Model string `json:"model"`
|
||||
Cores int `json:"cores,omitempty"`
|
||||
Threads int `json:"threads,omitempty"`
|
||||
FrequencyMHz int `json:"frequency_mhz,omitempty"`
|
||||
MaxFrequencyMHz int `json:"max_frequency_mhz,omitempty"`
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// ReanimatorMemory represents a memory module (DIMM)
|
||||
type ReanimatorMemory struct {
|
||||
Slot string `json:"slot"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Present bool `json:"present"`
|
||||
SizeMB int `json:"size_mb,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
MaxSpeedMHz int `json:"max_speed_mhz,omitempty"`
|
||||
CurrentSpeedMHz int `json:"current_speed_mhz,omitempty"`
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
SerialNumber string `json:"serial_number,omitempty"`
|
||||
PartNumber string `json:"part_number,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// ReanimatorStorage represents a storage device
|
||||
type ReanimatorStorage struct {
|
||||
Slot string `json:"slot"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Model string `json:"model"`
|
||||
SizeGB int `json:"size_gb,omitempty"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
Firmware string `json:"firmware,omitempty"`
|
||||
Interface string `json:"interface,omitempty"`
|
||||
Present bool `json:"present"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// ReanimatorPCIe represents a PCIe device
|
||||
type ReanimatorPCIe struct {
|
||||
Slot string `json:"slot"`
|
||||
VendorID int `json:"vendor_id,omitempty"`
|
||||
DeviceID int `json:"device_id,omitempty"`
|
||||
BDF string `json:"bdf,omitempty"`
|
||||
DeviceClass string `json:"device_class,omitempty"`
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
LinkWidth int `json:"link_width,omitempty"`
|
||||
LinkSpeed string `json:"link_speed,omitempty"`
|
||||
MaxLinkWidth int `json:"max_link_width,omitempty"`
|
||||
MaxLinkSpeed string `json:"max_link_speed,omitempty"`
|
||||
SerialNumber string `json:"serial_number,omitempty"`
|
||||
Firmware string `json:"firmware,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// ReanimatorPSU represents a power supply unit
|
||||
type ReanimatorPSU struct {
|
||||
Slot string `json:"slot"`
|
||||
Present bool `json:"present"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Vendor string `json:"vendor,omitempty"`
|
||||
WattageW int `json:"wattage_w,omitempty"`
|
||||
SerialNumber string `json:"serial_number,omitempty"`
|
||||
PartNumber string `json:"part_number,omitempty"`
|
||||
Firmware string `json:"firmware,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
InputType string `json:"input_type,omitempty"`
|
||||
InputPowerW int `json:"input_power_w,omitempty"`
|
||||
OutputPowerW int `json:"output_power_w,omitempty"`
|
||||
InputVoltage float64 `json:"input_voltage,omitempty"`
|
||||
}
|
||||
@@ -601,6 +601,30 @@ func (s *Server) handleExportTXT(w http.ResponseWriter, r *http.Request) {
|
||||
exp.ExportTXT(w)
|
||||
}
|
||||
|
||||
func (s *Server) handleExportReanimator(w http.ResponseWriter, r *http.Request) {
|
||||
result := s.GetResult()
|
||||
if result == nil || result.Hardware == nil {
|
||||
jsonError(w, "No hardware data available for export", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
reanimatorData, err := exporter.ConvertToReanimator(result)
|
||||
if err != nil {
|
||||
jsonError(w, fmt.Sprintf("Export failed: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", exportFilename(result, "reanimator.json")))
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.SetIndent("", " ")
|
||||
if err := encoder.Encode(reanimatorData); err != nil {
|
||||
// Log error, but likely too late to send error response
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleClear(w http.ResponseWriter, r *http.Request) {
|
||||
s.SetResult(nil)
|
||||
s.SetDetectedVendor("")
|
||||
|
||||
@@ -68,6 +68,7 @@ func (s *Server) setupRoutes() {
|
||||
s.mux.HandleFunc("GET /api/export/csv", s.handleExportCSV)
|
||||
s.mux.HandleFunc("GET /api/export/json", s.handleExportJSON)
|
||||
s.mux.HandleFunc("GET /api/export/txt", s.handleExportTXT)
|
||||
s.mux.HandleFunc("GET /api/export/reanimator", s.handleExportReanimator)
|
||||
s.mux.HandleFunc("DELETE /api/clear", s.handleClear)
|
||||
s.mux.HandleFunc("POST /api/shutdown", s.handleShutdown)
|
||||
s.mux.HandleFunc("POST /api/collect", s.handleCollectStart)
|
||||
|
||||
Reference in New Issue
Block a user