Add raw export reanalyze flow for Redfish snapshots

This commit is contained in:
Mikhail Chusavitin
2026-02-24 17:23:26 +03:00
parent 5d9e9d73de
commit 810c4b5ff9
7 changed files with 783 additions and 23 deletions
+115 -9
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"html/template"
@@ -70,15 +71,33 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
)
if looksLikeJSONSnapshot(header.Filename, payload) {
snapshotResult, snapshotErr := parseUploadedSnapshot(payload)
if snapshotErr != nil {
jsonError(w, "Failed to parse snapshot: "+snapshotErr.Error(), http.StatusBadRequest)
if rawPkg, ok, err := parseRawExportPackage(payload); err != nil {
jsonError(w, "Failed to parse raw export package: "+err.Error(), http.StatusBadRequest)
return
}
result = snapshotResult
vendor = strings.TrimSpace(snapshotResult.Protocol)
if vendor == "" {
vendor = "snapshot"
} else if ok {
replayed, replayVendor, replayErr := s.reanalyzeRawExportPackage(rawPkg)
if replayErr != nil {
jsonError(w, "Failed to reanalyze raw export package: "+replayErr.Error(), http.StatusBadRequest)
return
}
result = replayed
vendor = replayVendor
if strings.TrimSpace(vendor) == "" {
vendor = "snapshot"
}
s.SetRawExport(rawPkg)
} else {
snapshotResult, snapshotErr := parseUploadedSnapshot(payload)
if snapshotErr != nil {
jsonError(w, "Failed to parse snapshot: "+snapshotErr.Error(), http.StatusBadRequest)
return
}
result = snapshotResult
vendor = strings.TrimSpace(snapshotResult.Protocol)
if vendor == "" {
vendor = "snapshot"
}
s.SetRawExport(newRawExportFromUploadedFile(header.Filename, header.Header.Get("Content-Type"), payload, result))
}
} else {
// Parse archive
@@ -90,6 +109,7 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
result = p.Result()
applyArchiveSourceMetadata(result)
vendor = p.DetectedVendor()
s.SetRawExport(newRawExportFromUploadedFile(header.Filename, header.Header.Get("Content-Type"), payload, result))
}
s.SetResult(result)
@@ -108,6 +128,77 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
})
}
func (s *Server) reanalyzeRawExportPackage(pkg *RawExportPackage) (*models.AnalysisResult, string, error) {
if pkg == nil {
return nil, "", fmt.Errorf("empty package")
}
switch pkg.Source.Kind {
case "file_bytes":
if strings.TrimSpace(pkg.Source.Encoding) != "base64" {
return nil, "", fmt.Errorf("unsupported file_bytes encoding: %s", pkg.Source.Encoding)
}
data, err := base64.StdEncoding.DecodeString(pkg.Source.Data)
if err != nil {
return nil, "", fmt.Errorf("decode source.data: %w", err)
}
return s.parseUploadedPayload(pkg.Source.Filename, data)
case "live_redfish":
if !strings.EqualFold(strings.TrimSpace(pkg.Source.Protocol), "redfish") {
return nil, "", fmt.Errorf("unsupported live protocol: %s", pkg.Source.Protocol)
}
result, err := collector.ReplayRedfishFromRawPayloads(pkg.Source.RawPayloads, nil)
if err != nil {
return nil, "", err
}
if result != nil {
if strings.TrimSpace(result.Protocol) == "" {
result.Protocol = "redfish"
}
if strings.TrimSpace(result.SourceType) == "" {
result.SourceType = models.SourceTypeAPI
}
if strings.TrimSpace(result.TargetHost) == "" {
result.TargetHost = strings.TrimSpace(pkg.Source.TargetHost)
}
if result.CollectedAt.IsZero() {
result.CollectedAt = time.Now().UTC()
}
if strings.TrimSpace(result.Filename) == "" {
target := result.TargetHost
if target == "" {
target = "snapshot"
}
result.Filename = "redfish://" + target
}
}
return result, "redfish", nil
default:
return nil, "", fmt.Errorf("unsupported raw export source kind: %s", pkg.Source.Kind)
}
}
func (s *Server) parseUploadedPayload(filename string, payload []byte) (*models.AnalysisResult, string, error) {
if looksLikeJSONSnapshot(filename, payload) {
snapshotResult, err := parseUploadedSnapshot(payload)
if err != nil {
return nil, "", err
}
vendor := strings.TrimSpace(snapshotResult.Protocol)
if vendor == "" {
vendor = "snapshot"
}
return snapshotResult, vendor, nil
}
p := parser.NewBMCParser()
if err := p.ParseFromReader(bytes.NewReader(payload), filename); err != nil {
return nil, "", err
}
result := p.Result()
applyArchiveSourceMetadata(result)
return result, p.DetectedVendor(), nil
}
func (s *Server) handleGetParsers(w http.ResponseWriter, r *http.Request) {
jsonResponse(w, map[string]interface{}{
"parsers": parser.ListParsersInfo(),
@@ -667,8 +758,19 @@ func (s *Server) handleExportJSON(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", exportFilename(result, "json")))
if rawPkg := s.GetRawExport(); rawPkg != nil {
rawPkg.ExportedAt = time.Now().UTC()
rawPkg.Analysis = nil
encoder := json.NewEncoder(w)
encoder.SetIndent("", " ")
if err := encoder.Encode(rawPkg); err != nil {
return
}
return
}
exp := exporter.New(result)
exp.ExportJSON(w)
_ = exp.ExportJSON(w)
}
func (s *Server) handleExportReanimator(w http.ResponseWriter, r *http.Request) {
@@ -702,6 +804,7 @@ func (s *Server) handleExportReanimator(w http.ResponseWriter, r *http.Request)
func (s *Server) handleClear(w http.ResponseWriter, r *http.Request) {
s.SetResult(nil)
s.SetDetectedVendor("")
s.SetRawExport(nil)
jsonResponse(w, map[string]string{
"status": "ok",
"message": "Data cleared",
@@ -827,6 +930,9 @@ func (s *Server) startCollectionJob(jobID string, req CollectRequest) {
s.jobManager.AppendJobLog(jobID, "Сбор завершен")
s.SetResult(result)
s.SetDetectedVendor(req.Protocol)
if job, ok := s.jobManager.GetJob(jobID); ok {
s.SetRawExport(newRawExportFromLiveCollect(result, req, job.Logs))
}
}()
}
+104
View File
@@ -0,0 +1,104 @@
package server
import (
"encoding/base64"
"encoding/json"
"time"
"git.mchus.pro/mchus/logpile/internal/models"
)
const rawExportFormatV1 = "logpile.raw-export.v1"
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 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
}
+17
View File
@@ -29,6 +29,7 @@ type Server struct {
mu sync.RWMutex
result *models.AnalysisResult
detectedVendor string
rawExport *RawExportPackage
jobManager *JobManager
collectors *collector.Registry
@@ -107,6 +108,22 @@ func (s *Server) GetResult() *models.AnalysisResult {
return s.result
}
func (s *Server) SetRawExport(pkg *RawExportPackage) {
s.mu.Lock()
defer s.mu.Unlock()
s.rawExport = pkg
}
func (s *Server) GetRawExport() *RawExportPackage {
s.mu.RLock()
defer s.mu.RUnlock()
if s.rawExport == nil {
return nil
}
cloned := *s.rawExport
return &cloned
}
// SetDetectedVendor sets the detected vendor name
func (s *Server) SetDetectedVendor(vendor string) {
s.mu.Lock()