Add raw export reanalyze flow for Redfish snapshots
This commit is contained in:
+115
-9
@@ -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))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user