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 }