Files
logpile/internal/server/raw_export.go

344 lines
9.9 KiB
Go

package server
import (
"archive/zip"
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"strings"
"time"
"git.mchus.pro/mchus/logpile/internal/models"
)
const rawExportFormatV1 = "logpile.raw-export.v1"
const (
rawExportBundlePackageFile = "raw_export.json"
rawExportBundleLogFile = "collect.log"
rawExportBundleFieldsFile = "parser_fields.json"
)
type RawExportPackage struct {
Format string `json:"format"`
ExportedAt time.Time `json:"exported_at"`
Source RawExportSource `json:"source"`
Analysis *models.AnalysisResult `json:"analysis_result,omitempty"`
}
type RawExportSource struct {
Kind string `json:"kind"` // file_bytes | live_redfish | snapshot_json
Filename string `json:"filename,omitempty"`
MIMEType string `json:"mime_type,omitempty"`
Encoding string `json:"encoding,omitempty"` // base64
Data string `json:"data,omitempty"`
Protocol string `json:"protocol,omitempty"`
TargetHost string `json:"target_host,omitempty"`
RawPayloads map[string]any `json:"raw_payloads,omitempty"`
CollectLogs []string `json:"collect_logs,omitempty"`
CollectMeta *CollectRequestMeta `json:"collect_meta,omitempty"`
}
func newRawExportFromUploadedFile(filename, mimeType string, payload []byte, result *models.AnalysisResult) *RawExportPackage {
return &RawExportPackage{
Format: rawExportFormatV1,
ExportedAt: time.Now().UTC(),
Source: RawExportSource{
Kind: "file_bytes",
Filename: filename,
MIMEType: mimeType,
Encoding: "base64",
Data: base64.StdEncoding.EncodeToString(payload),
Protocol: resultProtocol(result),
TargetHost: resultTargetHost(result),
},
}
}
func newRawExportFromLiveCollect(result *models.AnalysisResult, req CollectRequest, logs []string) *RawExportPackage {
rawPayloads := map[string]any{}
if result != nil && result.RawPayloads != nil {
for k, v := range result.RawPayloads {
rawPayloads[k] = v
}
}
meta := CollectRequestMeta{
Host: req.Host,
Protocol: req.Protocol,
Port: req.Port,
Username: req.Username,
AuthType: req.AuthType,
TLSMode: req.TLSMode,
}
return &RawExportPackage{
Format: rawExportFormatV1,
ExportedAt: time.Now().UTC(),
Source: RawExportSource{
Kind: "live_redfish",
Protocol: req.Protocol,
TargetHost: req.Host,
RawPayloads: rawPayloads,
CollectLogs: append([]string(nil), logs...),
CollectMeta: &meta,
},
}
}
func parseRawExportPackage(payload []byte) (*RawExportPackage, bool, error) {
var pkg RawExportPackage
if err := json.Unmarshal(payload, &pkg); err != nil {
return nil, false, err
}
if pkg.Format != rawExportFormatV1 {
return nil, false, nil
}
if pkg.ExportedAt.IsZero() {
pkg.ExportedAt = time.Now().UTC()
}
return &pkg, true, nil
}
func buildRawExportBundle(pkg *RawExportPackage, result *models.AnalysisResult, clientVersion string) ([]byte, error) {
if pkg == nil {
return nil, fmt.Errorf("nil raw export package")
}
pkgCopy := *pkg
pkgCopy.Analysis = nil
pkgCopy.ExportedAt = time.Now().UTC()
jsonBytes, err := json.MarshalIndent(&pkgCopy, "", " ")
if err != nil {
return nil, err
}
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
jf, err := zw.Create(rawExportBundlePackageFile)
if err != nil {
return nil, err
}
if _, err := jf.Write(jsonBytes); err != nil {
return nil, err
}
lf, err := zw.Create(rawExportBundleLogFile)
if err != nil {
return nil, err
}
if _, err := io.WriteString(lf, buildHumanReadableCollectionLog(&pkgCopy, result, clientVersion)); err != nil {
return nil, err
}
ff, err := zw.Create(rawExportBundleFieldsFile)
if err != nil {
return nil, err
}
fieldsJSON, err := json.MarshalIndent(buildParserFieldSummary(result), "", " ")
if err != nil {
return nil, err
}
if _, err := ff.Write(fieldsJSON); err != nil {
return nil, err
}
if err := zw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func parseRawExportBundle(payload []byte) (*RawExportPackage, bool, error) {
if len(payload) < 4 || payload[0] != 'P' || payload[1] != 'K' {
return nil, false, nil
}
zr, err := zip.NewReader(bytes.NewReader(payload), int64(len(payload)))
if err != nil {
return nil, false, nil
}
for _, f := range zr.File {
if f.Name != rawExportBundlePackageFile {
continue
}
rc, err := f.Open()
if err != nil {
return nil, true, err
}
defer rc.Close()
body, err := io.ReadAll(rc)
if err != nil {
return nil, true, err
}
pkg, ok, err := parseRawExportPackage(body)
return pkg, ok, err
}
return nil, false, nil
}
func buildHumanReadableCollectionLog(pkg *RawExportPackage, result *models.AnalysisResult, clientVersion string) string {
var b strings.Builder
now := time.Now().UTC().Format(time.RFC3339)
fmt.Fprintf(&b, "LOGPile Raw Export Log\n")
fmt.Fprintf(&b, "Generated: %s\n", now)
if clientVersion != "" {
fmt.Fprintf(&b, "Client: %s\n", clientVersion)
}
if pkg != nil {
fmt.Fprintf(&b, "Format: %s\n", pkg.Format)
fmt.Fprintf(&b, "Source Kind: %s\n", pkg.Source.Kind)
if pkg.Source.Protocol != "" {
fmt.Fprintf(&b, "Protocol: %s\n", pkg.Source.Protocol)
}
if pkg.Source.TargetHost != "" {
fmt.Fprintf(&b, "Target Host: %s\n", pkg.Source.TargetHost)
}
if pkg.Source.Filename != "" {
fmt.Fprintf(&b, "Source Filename: %s\n", pkg.Source.Filename)
}
}
if pkg != nil && len(pkg.Source.CollectLogs) > 0 {
b.WriteString("\n=== Redfish Collection Log ===\n")
for _, line := range pkg.Source.CollectLogs {
b.WriteString(line)
b.WriteByte('\n')
}
}
b.WriteString("\n=== Parsed Field Summary ===\n")
if result == nil || result.Hardware == nil {
b.WriteString("No parsed hardware data available\n")
return b.String()
}
hw := result.Hardware
fmt.Fprintf(&b, "Board: manufacturer=%s model=%s serial=%s part=%s\n",
hw.BoardInfo.Manufacturer, hw.BoardInfo.ProductName, hw.BoardInfo.SerialNumber, hw.BoardInfo.PartNumber)
fmt.Fprintf(&b, "Counts: cpus=%d memory=%d storage=%d pcie=%d gpus=%d nics=%d psus=%d firmware=%d\n",
len(hw.CPUs), len(hw.Memory), len(hw.Storage), len(hw.PCIeDevices), len(hw.GPUs), len(hw.NetworkAdapters), len(hw.PowerSupply), len(hw.Firmware))
if len(hw.CPUs) > 0 {
b.WriteString("\n[CPUs]\n")
for _, cpu := range hw.CPUs {
fmt.Fprintf(&b, "- socket=%d model=%s cores=%d threads=%d serial=%s\n", cpu.Socket, cpu.Model, cpu.Cores, cpu.Threads, cpu.SerialNumber)
}
}
if len(hw.Memory) > 0 {
b.WriteString("\n[Memory]\n")
for _, m := range hw.Memory {
fmt.Fprintf(&b, "- slot=%s location=%s size_mb=%d part=%s serial=%s status=%s\n", m.Slot, m.Location, m.SizeMB, m.PartNumber, m.SerialNumber, m.Status)
}
}
if len(hw.Storage) > 0 {
b.WriteString("\n[Storage]\n")
for _, s := range hw.Storage {
fmt.Fprintf(&b, "- slot=%s type=%s model=%s size_gb=%d serial=%s\n", s.Slot, s.Type, s.Model, s.SizeGB, s.SerialNumber)
}
}
if len(hw.Volumes) > 0 {
b.WriteString("\n[Volumes]\n")
for _, v := range hw.Volumes {
name := v.Name
if name == "" {
name = v.ID
}
fmt.Fprintf(&b, "- controller=%s name=%s raid=%s size_gb=%d status=%s\n", v.Controller, name, v.RAIDLevel, v.SizeGB, v.Status)
}
}
if len(hw.PCIeDevices) > 0 {
b.WriteString("\n[PCIe Devices]\n")
for _, d := range hw.PCIeDevices {
fmt.Fprintf(&b, "- slot=%s class=%s model=%s bdf=%s vendor=%s serial=%s\n", d.Slot, d.DeviceClass, d.PartNumber, d.BDF, d.Manufacturer, d.SerialNumber)
}
}
if len(hw.GPUs) > 0 {
b.WriteString("\n[GPUs]\n")
for _, g := range hw.GPUs {
fmt.Fprintf(&b, "- slot=%s model=%s bdf=%s serial=%s status=%s\n", g.Slot, g.Model, g.BDF, g.SerialNumber, g.Status)
}
}
if len(hw.NetworkAdapters) > 0 {
b.WriteString("\n[Network Adapters]\n")
for _, n := range hw.NetworkAdapters {
fmt.Fprintf(&b, "- slot=%s location=%s model=%s serial=%s status=%s\n", n.Slot, n.Location, n.Model, n.SerialNumber, n.Status)
}
}
if len(hw.PowerSupply) > 0 {
b.WriteString("\n[Power Supplies]\n")
for _, p := range hw.PowerSupply {
fmt.Fprintf(&b, "- slot=%s model=%s serial=%s status=%s watt=%d\n", p.Slot, p.Model, p.SerialNumber, p.Status, p.WattageW)
}
}
if len(hw.Firmware) > 0 {
b.WriteString("\n[Firmware]\n")
for _, fw := range hw.Firmware {
fmt.Fprintf(&b, "- device=%s version=%s\n", fw.DeviceName, fw.Version)
}
}
return b.String()
}
func buildParserFieldSummary(result *models.AnalysisResult) map[string]any {
out := map[string]any{
"generated_at": time.Now().UTC(),
}
if result == nil {
out["available"] = false
return out
}
out["available"] = true
out["filename"] = result.Filename
out["source_type"] = result.SourceType
out["protocol"] = result.Protocol
out["target_host"] = result.TargetHost
out["collected_at"] = result.CollectedAt
if result.Hardware == nil {
out["hardware"] = map[string]any{}
return out
}
hw := result.Hardware
out["hardware"] = map[string]any{
"board": hw.BoardInfo,
"counts": map[string]int{
"cpus": len(hw.CPUs),
"memory": len(hw.Memory),
"storage": len(hw.Storage),
"volumes": len(hw.Volumes),
"pcie": len(hw.PCIeDevices),
"gpus": len(hw.GPUs),
"nics": len(hw.NetworkAdapters),
"psus": len(hw.PowerSupply),
"firmware": len(hw.Firmware),
"events": len(result.Events),
"sensors": len(result.Sensors),
"fru": len(result.FRU),
},
"cpus": hw.CPUs,
"memory": hw.Memory,
"storage": hw.Storage,
"volumes": hw.Volumes,
"pcie_devices": hw.PCIeDevices,
"gpus": hw.GPUs,
"network_adapters": hw.NetworkAdapters,
"power_supplies": hw.PowerSupply,
"firmware": hw.Firmware,
}
return out
}
func resultProtocol(result *models.AnalysisResult) string {
if result == nil {
return ""
}
return result.Protocol
}
func resultTargetHost(result *models.AnalysisResult) string {
if result == nil {
return ""
}
return result.TargetHost
}