Redfish snapshot/export overhaul and portable release build
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -55,23 +58,48 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Parse archive
|
||||
p := parser.NewBMCParser()
|
||||
if err := p.ParseFromReader(file, header.Filename); err != nil {
|
||||
jsonError(w, "Failed to parse archive: "+err.Error(), http.StatusBadRequest)
|
||||
payload, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
jsonError(w, "Failed to read file", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
result := p.Result()
|
||||
applyArchiveSourceMetadata(result)
|
||||
var (
|
||||
result *models.AnalysisResult
|
||||
vendor string
|
||||
)
|
||||
|
||||
if looksLikeJSONSnapshot(header.Filename, payload) {
|
||||
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"
|
||||
}
|
||||
} else {
|
||||
// Parse archive
|
||||
p := parser.NewBMCParser()
|
||||
if err := p.ParseFromReader(bytes.NewReader(payload), header.Filename); err != nil {
|
||||
jsonError(w, "Failed to parse archive: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
result = p.Result()
|
||||
applyArchiveSourceMetadata(result)
|
||||
vendor = p.DetectedVendor()
|
||||
}
|
||||
|
||||
s.SetResult(result)
|
||||
s.SetDetectedVendor(p.DetectedVendor())
|
||||
s.SetDetectedVendor(vendor)
|
||||
|
||||
jsonResponse(w, map[string]interface{}{
|
||||
"status": "ok",
|
||||
"message": "File uploaded and parsed successfully",
|
||||
"filename": header.Filename,
|
||||
"vendor": p.DetectedVendor(),
|
||||
"vendor": vendor,
|
||||
"stats": map[string]int{
|
||||
"events": len(result.Events),
|
||||
"sensors": len(result.Sensors),
|
||||
@@ -529,7 +557,7 @@ func (s *Server) handleExportCSV(w http.ResponseWriter, r *http.Request) {
|
||||
result := s.GetResult()
|
||||
|
||||
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=serials.csv")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", exportFilename(result, "csv")))
|
||||
|
||||
exp := exporter.New(result)
|
||||
exp.ExportCSV(w)
|
||||
@@ -539,7 +567,7 @@ func (s *Server) handleExportJSON(w http.ResponseWriter, r *http.Request) {
|
||||
result := s.GetResult()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=report.json")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", exportFilename(result, "json")))
|
||||
|
||||
exp := exporter.New(result)
|
||||
exp.ExportJSON(w)
|
||||
@@ -549,7 +577,7 @@ func (s *Server) handleExportTXT(w http.ResponseWriter, r *http.Request) {
|
||||
result := s.GetResult()
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=report.txt")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", exportFilename(result, "txt")))
|
||||
|
||||
exp := exporter.New(result)
|
||||
exp.ExportTXT(w)
|
||||
@@ -682,7 +710,7 @@ func (s *Server) startCollectionJob(jobID string, req CollectRequest) {
|
||||
s.jobManager.UpdateJobStatus(jobID, CollectStatusSuccess, 100, "")
|
||||
s.jobManager.AppendJobLog(jobID, "Сбор завершен")
|
||||
s.SetResult(result)
|
||||
s.SetDetectedVendor("")
|
||||
s.SetDetectedVendor(req.Protocol)
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -754,6 +782,9 @@ func applyCollectSourceMetadata(result *models.AnalysisResult, req CollectReques
|
||||
result.Protocol = req.Protocol
|
||||
result.TargetHost = req.Host
|
||||
result.CollectedAt = time.Now().UTC()
|
||||
if strings.TrimSpace(result.Filename) == "" {
|
||||
result.Filename = fmt.Sprintf("%s://%s", req.Protocol, req.Host)
|
||||
}
|
||||
}
|
||||
|
||||
func toCollectorRequest(req CollectRequest) collector.Request {
|
||||
@@ -769,6 +800,39 @@ func toCollectorRequest(req CollectRequest) collector.Request {
|
||||
}
|
||||
}
|
||||
|
||||
func looksLikeJSONSnapshot(filename string, payload []byte) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
if ext == ".json" {
|
||||
return true
|
||||
}
|
||||
trimmed := bytes.TrimSpace(payload)
|
||||
return len(trimmed) > 0 && (trimmed[0] == '{' || trimmed[0] == '[')
|
||||
}
|
||||
|
||||
func parseUploadedSnapshot(payload []byte) (*models.AnalysisResult, error) {
|
||||
var result models.AnalysisResult
|
||||
if err := json.Unmarshal(payload, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.Hardware == nil && len(result.Events) == 0 && len(result.Sensors) == 0 && len(result.FRU) == 0 {
|
||||
return nil, fmt.Errorf("unsupported snapshot format")
|
||||
}
|
||||
if strings.TrimSpace(result.SourceType) == "" {
|
||||
if result.Protocol != "" {
|
||||
result.SourceType = models.SourceTypeAPI
|
||||
} else {
|
||||
result.SourceType = models.SourceTypeArchive
|
||||
}
|
||||
}
|
||||
if result.CollectedAt.IsZero() {
|
||||
result.CollectedAt = time.Now().UTC()
|
||||
}
|
||||
if strings.TrimSpace(result.Filename) == "" {
|
||||
result.Filename = "uploaded_snapshot.json"
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (s *Server) getCollector(protocol string) (collector.Connector, bool) {
|
||||
if s.collectors == nil {
|
||||
s.collectors = collector.NewDefaultRegistry()
|
||||
@@ -808,3 +872,59 @@ func isGPUDevice(deviceClass string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func exportFilename(result *models.AnalysisResult, ext string) string {
|
||||
date := time.Now().UTC().Format("2006-01-02")
|
||||
model := "SERVER MODEL"
|
||||
sn := "SERVER SN"
|
||||
|
||||
if result != nil {
|
||||
if !result.CollectedAt.IsZero() {
|
||||
date = result.CollectedAt.UTC().Format("2006-01-02")
|
||||
}
|
||||
if result.Hardware != nil {
|
||||
if m := strings.TrimSpace(result.Hardware.BoardInfo.ProductName); m != "" {
|
||||
model = m
|
||||
}
|
||||
if serial := strings.TrimSpace(result.Hardware.BoardInfo.SerialNumber); serial != "" {
|
||||
sn = serial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
model = sanitizeFilenamePart(model)
|
||||
sn = sanitizeFilenamePart(sn)
|
||||
ext = strings.TrimPrefix(strings.TrimSpace(ext), ".")
|
||||
if ext == "" {
|
||||
ext = "txt"
|
||||
}
|
||||
return fmt.Sprintf("%s (%s) - %s.%s", date, model, sn, ext)
|
||||
}
|
||||
|
||||
func sanitizeFilenamePart(v string) string {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
return "-"
|
||||
}
|
||||
|
||||
replacer := strings.NewReplacer(
|
||||
"/", "_",
|
||||
"\\", "_",
|
||||
":", "_",
|
||||
"*", "_",
|
||||
"?", "_",
|
||||
"\"", "_",
|
||||
"<", "_",
|
||||
">", "_",
|
||||
"|", "_",
|
||||
"\n", " ",
|
||||
"\r", " ",
|
||||
"\t", " ",
|
||||
)
|
||||
v = replacer.Replace(v)
|
||||
v = strings.Join(strings.Fields(v), " ")
|
||||
if v == "" {
|
||||
return "-"
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user