Files
logpile/internal/exporter/exporter.go
2026-03-15 21:38:28 +03:00

173 lines
3.8 KiB
Go

package exporter
import (
"encoding/csv"
"encoding/json"
"io"
"strings"
"git.mchus.pro/mchus/logpile/internal/models"
)
// Exporter handles data export in various formats
type Exporter struct {
result *models.AnalysisResult
}
// New creates a new exporter
func New(result *models.AnalysisResult) *Exporter {
return &Exporter{result: result}
}
// ExportCSV exports serial numbers to CSV format
func (e *Exporter) ExportCSV(w io.Writer) error {
writer := csv.NewWriter(w)
defer writer.Flush()
// Header
if err := writer.Write([]string{"Component", "Serial Number", "Manufacturer", "Location"}); err != nil {
return err
}
if e.result == nil {
return nil
}
// FRU data
for _, fru := range e.result.FRU {
if !hasUsableSerial(fru.SerialNumber) {
continue
}
name := fru.ProductName
if name == "" {
name = fru.Description
}
if err := writer.Write([]string{
name,
fru.SerialNumber,
fru.Manufacturer,
fru.PartNumber,
}); err != nil {
return err
}
}
// Hardware data
if e.result.Hardware != nil {
// Board
if hasUsableSerial(e.result.Hardware.BoardInfo.SerialNumber) {
if err := writer.Write([]string{
e.result.Hardware.BoardInfo.ProductName,
strings.TrimSpace(e.result.Hardware.BoardInfo.SerialNumber),
e.result.Hardware.BoardInfo.Manufacturer,
"Board",
}); err != nil {
return err
}
}
seenCanonical := make(map[string]struct{})
for _, dev := range canonicalDevicesForExport(e.result.Hardware) {
if !hasUsableSerial(dev.SerialNumber) {
continue
}
serial := strings.TrimSpace(dev.SerialNumber)
seenCanonical[serial] = struct{}{}
component, manufacturer, location := csvFieldsFromCanonicalDevice(dev)
if err := writer.Write([]string{component, serial, manufacturer, location}); err != nil {
return err
}
}
// Legacy network cards
for _, nic := range e.result.Hardware.NetworkCards {
if !hasUsableSerial(nic.SerialNumber) {
continue
}
serial := strings.TrimSpace(nic.SerialNumber)
if _, ok := seenCanonical[serial]; ok {
continue
}
if err := writer.Write([]string{
nic.Model,
serial,
"",
"Network",
}); err != nil {
return err
}
}
}
return nil
}
// ExportJSON exports all data to JSON format
func (e *Exporter) ExportJSON(w io.Writer) error {
encoder := json.NewEncoder(w)
encoder.SetIndent("", " ")
return encoder.Encode(e.result)
}
func hasUsableSerial(serial string) bool {
s := strings.TrimSpace(serial)
if s == "" {
return false
}
switch strings.ToUpper(s) {
case "N/A", "NA", "NONE", "NULL", "UNKNOWN", "-":
return false
default:
return true
}
}
func csvFieldsFromCanonicalDevice(dev models.HardwareDevice) (component, manufacturer, location string) {
component = firstNonEmptyString(
dev.Model,
dev.PartNumber,
dev.DeviceClass,
dev.Kind,
)
manufacturer = firstNonEmptyString(dev.Manufacturer, inferCSVVendor(dev))
location = firstNonEmptyString(dev.Location, dev.Slot, dev.BDF, dev.Kind)
switch dev.Kind {
case models.DeviceKindCPU:
if component == "" {
component = "CPU"
}
if location == "" {
location = "CPU"
}
case models.DeviceKindMemory:
component = firstNonEmptyString(dev.PartNumber, dev.Model, "Memory")
case models.DeviceKindPCIe, models.DeviceKindGPU, models.DeviceKindNetwork:
if location == "" {
location = firstNonEmptyString(dev.Slot, dev.BDF, "PCIe")
}
case models.DeviceKindPSU:
component = firstNonEmptyString(dev.Model, "Power Supply")
}
return component, manufacturer, location
}
func inferCSVVendor(dev models.HardwareDevice) string {
switch dev.Kind {
case models.DeviceKindCPU:
return ""
default:
return ""
}
}
func firstNonEmptyString(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}