Export raw bundles with collection logs and parser field snapshot
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
@@ -10,6 +15,12 @@ import (
|
||||
|
||||
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"`
|
||||
@@ -89,6 +100,222 @@ func parseRawExportPackage(payload []byte) (*RawExportPackage, bool, error) {
|
||||
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.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),
|
||||
"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,
|
||||
"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 ""
|
||||
|
||||
Reference in New Issue
Block a user