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 "" }