- identifier-normalization: use strings.EqualFold in h3c/parser.go
- import-export: CSV now uses UTF-8 BOM and semicolon delimiter
- go-code-style: translate all Russian source strings to English (ADL-007)
- go-background-tasks: add Type, Message, Result fields to Job struct
- go-api: wrap list endpoints in {items, total_count, page, per_page, total_pages}
- module-structure: rename helpers.go → context_sleep.go
- build-version-display: htmlError renders version footer on error pages
- go-logging: migrate all log.Printf calls to log/slog with structured attrs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
177 lines
3.9 KiB
Go
177 lines
3.9 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 {
|
|
if _, err := w.Write([]byte{0xEF, 0xBB, 0xBF}); err != nil {
|
|
return err
|
|
}
|
|
writer := csv.NewWriter(w)
|
|
writer.Comma = ';'
|
|
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 ""
|
|
}
|