344 lines
9.9 KiB
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
|
|
}
|