chore: align codebase with bible engineering contracts
- identifier-normalization: use strings.EqualFold in h3c/parser.go
- import-export: CSV now uses UTF-8 BOM and semicolon delimiter
- go-code-style: translate all Russian source strings to English (ADL-007)
- go-background-tasks: add Type, Message, Result fields to Job struct
- go-api: wrap list endpoints in {items, total_count, page, per_page, total_pages}
- module-structure: rename helpers.go → context_sleep.go
- build-version-display: htmlError renders version footer on error pages
- go-logging: migrate all log.Printf calls to log/slog with structured attrs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+5
-5
@@ -4,7 +4,7 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -49,8 +49,8 @@ func main() {
|
|||||||
srv := server.New(cfg)
|
srv := server.New(cfg)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://localhost:%d", *port)
|
url := fmt.Sprintf("http://localhost:%d", *port)
|
||||||
log.Printf("LOGPile starting on %s", url)
|
slog.Info("LOGPile starting", "url", url)
|
||||||
log.Printf("Registered parsers: %v", parser.ListParsers())
|
slog.Info("registered parsers", "parsers", parser.ListParsers())
|
||||||
|
|
||||||
// Open browser automatically
|
// Open browser automatically
|
||||||
if !*noBrowser {
|
if !*noBrowser {
|
||||||
@@ -61,7 +61,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := runServer(srv); err != nil {
|
if err := runServer(srv); err != nil {
|
||||||
log.Printf("FATAL: %v", err)
|
slog.Error("fatal error", "err", err)
|
||||||
maybeWaitForCrashInput(*holdOnCrash)
|
maybeWaitForCrashInput(*holdOnCrash)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ func openBrowser(url string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
log.Printf("Failed to open browser: %v", err)
|
slog.Warn("failed to open browser", "err", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ func (c *IPMIMockConnector) Protocol() string {
|
|||||||
|
|
||||||
func (c *IPMIMockConnector) Collect(ctx context.Context, req Request, emit ProgressFn) (*models.AnalysisResult, error) {
|
func (c *IPMIMockConnector) Collect(ctx context.Context, req Request, emit ProgressFn) (*models.AnalysisResult, error) {
|
||||||
steps := []Progress{
|
steps := []Progress{
|
||||||
{Status: "running", Progress: 20, Message: "IPMI: подключение к BMC..."},
|
{Status: "running", Progress: 20, Message: "IPMI: connecting to BMC..."},
|
||||||
{Status: "running", Progress: 55, Message: "IPMI: чтение инвентаря..."},
|
{Status: "running", Progress: 55, Message: "IPMI: reading inventory..."},
|
||||||
{Status: "running", Progress: 85, Message: "IPMI: нормализация данных..."},
|
{Status: "running", Progress: 85, Message: "IPMI: normalizing data..."},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, step := range steps {
|
for _, step := range steps {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -124,14 +124,14 @@ func (c *RedfishConnector) debugf(format string, args ...interface{}) {
|
|||||||
if !c.debug {
|
if !c.debug {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("redfish-debug: "+format, args...)
|
slog.Debug("redfish-debug: " + fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RedfishConnector) debugSnapshotf(format string, args ...interface{}) {
|
func (c *RedfishConnector) debugSnapshotf(format string, args ...interface{}) {
|
||||||
if !c.debugSnapshot {
|
if !c.debugSnapshot {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("redfish-snapshot-debug: "+format, args...)
|
slog.Debug("redfish-snapshot-debug: " + fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit ProgressFn) (*models.AnalysisResult, error) {
|
func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit ProgressFn) (*models.AnalysisResult, error) {
|
||||||
@@ -149,7 +149,7 @@ func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit Progre
|
|||||||
hintClient := c.httpClientWithTimeout(req, 4*time.Second)
|
hintClient := c.httpClientWithTimeout(req, 4*time.Second)
|
||||||
|
|
||||||
if emit != nil {
|
if emit != nil {
|
||||||
emit(Progress{Status: "running", Progress: 10, Message: "Redfish: подключение к BMC..."})
|
emit(Progress{Status: "running", Progress: 10, Message: "Redfish: connecting to BMC..."})
|
||||||
}
|
}
|
||||||
discoveryCtx := withRedfishTelemetryPhase(ctx, "discovery")
|
discoveryCtx := withRedfishTelemetryPhase(ctx, "discovery")
|
||||||
serviceRootDoc, err := c.getJSON(discoveryCtx, snapshotClient, req, baseURL, "/redfish/v1")
|
serviceRootDoc, err := c.getJSON(discoveryCtx, snapshotClient, req, baseURL, "/redfish/v1")
|
||||||
@@ -192,7 +192,7 @@ func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit Progre
|
|||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 25,
|
Progress: 25,
|
||||||
Message: fmt.Sprintf("Redfish: профили mode=%s active=%s", acquisitionPlan.Mode, formatActiveModuleLog(activeModules)),
|
Message: fmt.Sprintf("Redfish: profiles mode=%s active=%s", acquisitionPlan.Mode, formatActiveModuleLog(activeModules)),
|
||||||
ActiveModules: activeModules,
|
ActiveModules: activeModules,
|
||||||
ModuleScores: moduleScores,
|
ModuleScores: moduleScores,
|
||||||
DebugInfo: &CollectDebugInfo{
|
DebugInfo: &CollectDebugInfo{
|
||||||
@@ -229,33 +229,32 @@ func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit Progre
|
|||||||
seedPaths := resolvedPlan.SeedPaths
|
seedPaths := resolvedPlan.SeedPaths
|
||||||
criticalPaths := resolvedPlan.CriticalPaths
|
criticalPaths := resolvedPlan.CriticalPaths
|
||||||
if len(acquisitionPlan.Profiles) > 0 {
|
if len(acquisitionPlan.Profiles) > 0 {
|
||||||
log.Printf(
|
slog.Info("redfish-profile-plan",
|
||||||
"redfish-profile-plan: mode=%s profiles=%s notes=%s scores=%s req=%d err=%d p95=%dms avg=%dms throttled=%t",
|
"mode", acquisitionPlan.Mode,
|
||||||
acquisitionPlan.Mode,
|
"profiles", strings.Join(acquisitionPlan.Profiles, ","),
|
||||||
strings.Join(acquisitionPlan.Profiles, ","),
|
"notes", strings.Join(acquisitionPlan.Notes, "; "),
|
||||||
strings.Join(acquisitionPlan.Notes, "; "),
|
"scores", formatModuleScoreLog(moduleScores),
|
||||||
formatModuleScoreLog(moduleScores),
|
"req", telemetrySummary.Requests,
|
||||||
telemetrySummary.Requests,
|
"err", telemetrySummary.Errors,
|
||||||
telemetrySummary.Errors,
|
"p95_ms", telemetrySummary.P95.Milliseconds(),
|
||||||
telemetrySummary.P95.Milliseconds(),
|
"avg_ms", telemetrySummary.Avg.Milliseconds(),
|
||||||
telemetrySummary.Avg.Milliseconds(),
|
"throttled", throttled,
|
||||||
throttled,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if emit != nil {
|
if emit != nil {
|
||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 30,
|
Progress: 30,
|
||||||
Message: "Redfish: чтение структуры Redfish...",
|
Message: "Redfish: reading Redfish structure...",
|
||||||
CurrentPhase: "snapshot",
|
CurrentPhase: "snapshot",
|
||||||
ETASeconds: acquisitionPlan.Tuning.ETABaseline.SnapshotSeconds,
|
ETASeconds: acquisitionPlan.Tuning.ETABaseline.SnapshotSeconds,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if emit != nil {
|
if emit != nil {
|
||||||
emit(Progress{Status: "running", Progress: 55, Message: "Redfish: подготовка snapshot...", CurrentPhase: "snapshot", ETASeconds: acquisitionPlan.Tuning.ETABaseline.SnapshotSeconds})
|
emit(Progress{Status: "running", Progress: 55, Message: "Redfish: preparing snapshot...", CurrentPhase: "snapshot", ETASeconds: acquisitionPlan.Tuning.ETABaseline.SnapshotSeconds})
|
||||||
emit(Progress{Status: "running", Progress: 80, Message: "Redfish: подготовка расширенного snapshot...", CurrentPhase: "snapshot", ETASeconds: acquisitionPlan.Tuning.ETABaseline.SnapshotSeconds})
|
emit(Progress{Status: "running", Progress: 80, Message: "Redfish: preparing extended snapshot...", CurrentPhase: "snapshot", ETASeconds: acquisitionPlan.Tuning.ETABaseline.SnapshotSeconds})
|
||||||
emit(Progress{Status: "running", Progress: 90, Message: "Redfish: сбор расширенного snapshot...", CurrentPhase: "snapshot", ETASeconds: acquisitionPlan.Tuning.ETABaseline.SnapshotSeconds})
|
emit(Progress{Status: "running", Progress: 90, Message: "Redfish: collecting extended snapshot...", CurrentPhase: "snapshot", ETASeconds: acquisitionPlan.Tuning.ETABaseline.SnapshotSeconds})
|
||||||
}
|
}
|
||||||
// collectCtx covers all data-fetching phases (snapshot, prefetch, plan-B).
|
// collectCtx covers all data-fetching phases (snapshot, prefetch, plan-B).
|
||||||
// Cancelling it via the skip signal aborts only the collection phases while
|
// Cancelling it via the skip signal aborts only the collection phases while
|
||||||
@@ -270,10 +269,10 @@ func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit Progre
|
|||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 97,
|
Progress: 97,
|
||||||
Message: "Redfish: пропуск зависших запросов, анализ уже собранных данных...",
|
Message: "Redfish: skipping stalled requests, analyzing collected data...",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
log.Printf("redfish: skip-hung triggered, cancelling collection phases")
|
slog.Info("redfish: skip-hung triggered, cancelling collection phases")
|
||||||
cancelCollect()
|
cancelCollect()
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
}
|
}
|
||||||
@@ -296,15 +295,14 @@ func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit Progre
|
|||||||
for p := range prefetchedCritical {
|
for p := range prefetchedCritical {
|
||||||
delete(fetchErrMap, p)
|
delete(fetchErrMap, p)
|
||||||
}
|
}
|
||||||
log.Printf(
|
slog.Info("redfish-prefetch-metrics",
|
||||||
"redfish-prefetch-metrics: enabled=%t candidates=%d targets=%d docs=%d added=%d dur=%s skip=%s",
|
"enabled", prefetchMetrics.Enabled,
|
||||||
prefetchMetrics.Enabled,
|
"candidates", prefetchMetrics.Candidates,
|
||||||
prefetchMetrics.Candidates,
|
"targets", prefetchMetrics.Targets,
|
||||||
prefetchMetrics.Targets,
|
"docs", prefetchMetrics.Docs,
|
||||||
prefetchMetrics.Docs,
|
"added", prefetchMetrics.Added,
|
||||||
prefetchMetrics.Added,
|
"dur", prefetchMetrics.Duration.Round(time.Millisecond),
|
||||||
prefetchMetrics.Duration.Round(time.Millisecond),
|
"skip", firstNonEmpty(prefetchMetrics.SkipReason, "-"),
|
||||||
firstNonEmpty(prefetchMetrics.SkipReason, "-"),
|
|
||||||
)
|
)
|
||||||
if recoveredN := c.recoverCriticalRedfishDocsPlanB(withRedfishTelemetryPhase(collectCtx, "critical_plan_b"), criticalClient, req, baseURL, criticalPaths, rawTree, fetchErrMap, acquisitionPlan.Tuning, emit); recoveredN > 0 {
|
if recoveredN := c.recoverCriticalRedfishDocsPlanB(withRedfishTelemetryPhase(collectCtx, "critical_plan_b"), criticalClient, req, baseURL, criticalPaths, rawTree, fetchErrMap, acquisitionPlan.Tuning, emit); recoveredN > 0 {
|
||||||
c.debugSnapshotf("critical plan-b recovered docs=%d", recoveredN)
|
c.debugSnapshotf("critical plan-b recovered docs=%d", recoveredN)
|
||||||
@@ -319,7 +317,7 @@ func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit Progre
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if emit != nil {
|
if emit != nil {
|
||||||
emit(Progress{Status: "running", Progress: 99, Message: "Redfish: анализ raw snapshot..."})
|
emit(Progress{Status: "running", Progress: 99, Message: "Redfish: analyzing raw snapshot..."})
|
||||||
}
|
}
|
||||||
// Collect hardware event logs separately (not part of tree-walk to avoid bloat).
|
// Collect hardware event logs separately (not part of tree-walk to avoid bloat).
|
||||||
rawLogEntries := c.collectRedfishLogEntries(withRedfishTelemetryPhase(ctx, "log_entries"), snapshotClient, req, baseURL, systemPaths, managerPaths)
|
rawLogEntries := c.collectRedfishLogEntries(withRedfishTelemetryPhase(ctx, "log_entries"), snapshotClient, req, baseURL, systemPaths, managerPaths)
|
||||||
@@ -443,38 +441,36 @@ func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit Progre
|
|||||||
}
|
}
|
||||||
totalElapsed := time.Since(collectStart).Round(time.Second)
|
totalElapsed := time.Since(collectStart).Round(time.Second)
|
||||||
if !result.InventoryLastModifiedAt.IsZero() {
|
if !result.InventoryLastModifiedAt.IsZero() {
|
||||||
log.Printf("redfish-collect: inventory last modified at %s (age: %s)",
|
slog.Info("redfish-collect: inventory last modified",
|
||||||
result.InventoryLastModifiedAt.Format(time.RFC3339),
|
"at", result.InventoryLastModifiedAt.Format(time.RFC3339),
|
||||||
time.Since(result.InventoryLastModifiedAt).Round(time.Minute),
|
"age", time.Since(result.InventoryLastModifiedAt).Round(time.Minute),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
log.Printf(
|
slog.Info("redfish-postprobe-metrics",
|
||||||
"redfish-postprobe-metrics: nvme_candidates=%d nvme_selected=%d nvme_added=%d candidates=%d selected=%d skipped_explicit=%d added=%d dur=%s",
|
"nvme_candidates", postProbeMetrics.NVMECandidates,
|
||||||
postProbeMetrics.NVMECandidates,
|
"nvme_selected", postProbeMetrics.NVMESelected,
|
||||||
postProbeMetrics.NVMESelected,
|
"nvme_added", postProbeMetrics.NVMEAdded,
|
||||||
postProbeMetrics.NVMEAdded,
|
"candidates", postProbeMetrics.CollectionCandidates,
|
||||||
postProbeMetrics.CollectionCandidates,
|
"selected", postProbeMetrics.CollectionSelected,
|
||||||
postProbeMetrics.CollectionSelected,
|
"skipped_explicit", postProbeMetrics.SkippedExplicit,
|
||||||
postProbeMetrics.SkippedExplicit,
|
"added", postProbeMetrics.Added,
|
||||||
postProbeMetrics.Added,
|
"dur", postProbeMetrics.Duration.Round(time.Millisecond),
|
||||||
postProbeMetrics.Duration.Round(time.Millisecond),
|
|
||||||
)
|
)
|
||||||
log.Printf(
|
slog.Info("redfish-telemetry",
|
||||||
"redfish-telemetry: req=%d err=%d err_rate=%.2f avg=%dms p95=%dms throttled=%t snapshot_workers=%d prefetch_workers=%d timing_top=%s",
|
"req", telemetrySummary.Requests,
|
||||||
telemetrySummary.Requests,
|
"err", telemetrySummary.Errors,
|
||||||
telemetrySummary.Errors,
|
"err_rate", telemetrySummary.ErrorRate,
|
||||||
telemetrySummary.ErrorRate,
|
"avg_ms", telemetrySummary.Avg.Milliseconds(),
|
||||||
telemetrySummary.Avg.Milliseconds(),
|
"p95_ms", telemetrySummary.P95.Milliseconds(),
|
||||||
telemetrySummary.P95.Milliseconds(),
|
"throttled", throttled,
|
||||||
throttled,
|
"snapshot_workers", acquisitionPlan.Tuning.SnapshotWorkers,
|
||||||
acquisitionPlan.Tuning.SnapshotWorkers,
|
"prefetch_workers", acquisitionPlan.Tuning.PrefetchWorkers,
|
||||||
acquisitionPlan.Tuning.PrefetchWorkers,
|
"timing_top", firstNonEmpty(snapshotTimingSummary, "-"),
|
||||||
firstNonEmpty(snapshotTimingSummary, "-"),
|
|
||||||
)
|
)
|
||||||
for _, line := range redfishPhaseTelemetryLogLines(phaseTelemetry) {
|
for _, line := range redfishPhaseTelemetryLogLines(phaseTelemetry) {
|
||||||
log.Printf("redfish-telemetry-phase: %s", line)
|
slog.Info("redfish-telemetry-phase", "line", line)
|
||||||
}
|
}
|
||||||
log.Printf("redfish-collect: completed in %s (docs=%d, fetch_errors=%d)", totalElapsed, len(rawTree), len(fetchErrMap))
|
slog.Info("redfish-collect: completed", "elapsed", totalElapsed, "docs", len(rawTree), "fetch_errors", len(fetchErrMap))
|
||||||
if emit != nil {
|
if emit != nil {
|
||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
@@ -491,7 +487,7 @@ func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit Progre
|
|||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 100,
|
Progress: 100,
|
||||||
Message: fmt.Sprintf("Redfish: сбор завершен за %s", totalElapsed),
|
Message: fmt.Sprintf("Redfish: collection completed in %s", totalElapsed),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
@@ -611,7 +607,7 @@ func (c *RedfishConnector) prefetchCriticalRedfishDocs(
|
|||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 96,
|
Progress: 96,
|
||||||
Message: fmt.Sprintf("Redfish: prefetch пропущен (адаптивно, кандидатов=%d)", metrics.Candidates),
|
Message: fmt.Sprintf("Redfish: prefetch skipped (adaptive, candidates=%d)", metrics.Candidates),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return nil, metrics
|
return nil, metrics
|
||||||
@@ -620,7 +616,7 @@ func (c *RedfishConnector) prefetchCriticalRedfishDocs(
|
|||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 96,
|
Progress: 96,
|
||||||
Message: fmt.Sprintf("Redfish: prefetch критичных endpoint (адаптивно %d/%d)...", len(targets), len(candidates)),
|
Message: fmt.Sprintf("Redfish: prefetch critical endpoints (adaptive %d/%d)...", len(targets), len(candidates)),
|
||||||
CurrentPhase: "prefetch",
|
CurrentPhase: "prefetch",
|
||||||
ETASeconds: int(estimateProgressETA(time.Now(), 0, len(targets), 2*time.Second).Seconds()),
|
ETASeconds: int(estimateProgressETA(time.Now(), 0, len(targets), 2*time.Second).Seconds()),
|
||||||
})
|
})
|
||||||
@@ -706,7 +702,7 @@ func (c *RedfishConnector) prefetchCriticalRedfishDocs(
|
|||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 96,
|
Progress: 96,
|
||||||
Message: fmt.Sprintf("Redfish: prefetch завершен (адаптивно targets=%d, docs=%d)", len(targets), len(out)),
|
Message: fmt.Sprintf("Redfish: prefetch completed (adaptive targets=%d, docs=%d)", len(targets), len(out)),
|
||||||
CurrentPhase: "prefetch",
|
CurrentPhase: "prefetch",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1397,7 +1393,7 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht
|
|||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 92 + int(minInt32(n/200, 6)),
|
Progress: 92 + int(minInt32(n/200, 6)),
|
||||||
Message: fmt.Sprintf("Redfish snapshot: heartbeat документов=%d (ok=%d, seen=%d), ETA≈%s, корни=%s, последний=%s", n, outN, seenN, eta, strings.Join(roots, ", "), compactProgressPath(last)),
|
Message: fmt.Sprintf("Redfish snapshot: heartbeat docs=%d (ok=%d, seen=%d), ETA≈%s, roots=%s, last=%s", n, outN, seenN, eta, strings.Join(roots, ", "), compactProgressPath(last)),
|
||||||
CurrentPhase: "snapshot",
|
CurrentPhase: "snapshot",
|
||||||
ETASeconds: int(estimateSnapshotETA(crawlStart, int(n), seenN, len(jobs), workers, client.Timeout).Seconds()),
|
ETASeconds: int(estimateSnapshotETA(crawlStart, int(n), seenN, len(jobs), workers, client.Timeout).Seconds()),
|
||||||
})
|
})
|
||||||
@@ -1434,7 +1430,7 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht
|
|||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 92 + int(minInt32(n/200, 6)),
|
Progress: 92 + int(minInt32(n/200, 6)),
|
||||||
Message: fmt.Sprintf("Redfish snapshot: ошибка на %s", compactProgressPath(current)),
|
Message: fmt.Sprintf("Redfish snapshot: error on %s", compactProgressPath(current)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
wg.Done()
|
wg.Done()
|
||||||
@@ -1489,7 +1485,7 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht
|
|||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 92 + int(minInt32(n/200, 6)),
|
Progress: 92 + int(minInt32(n/200, 6)),
|
||||||
Message: fmt.Sprintf("Redfish snapshot: ошибка на %s", compactProgressPath(current)),
|
Message: fmt.Sprintf("Redfish snapshot: error on %s", compactProgressPath(current)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1512,7 +1508,7 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht
|
|||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 92 + int(minInt32(n/200, 6)),
|
Progress: 92 + int(minInt32(n/200, 6)),
|
||||||
Message: fmt.Sprintf("Redfish snapshot: документов=%d, ETA≈%s, корни=%s, последний=%s", n, eta, strings.Join(roots, ", "), compactProgressPath(last)),
|
Message: fmt.Sprintf("Redfish snapshot: docs=%d, ETA≈%s, roots=%s, last=%s", n, eta, strings.Join(roots, ", "), compactProgressPath(last)),
|
||||||
CurrentPhase: "snapshot",
|
CurrentPhase: "snapshot",
|
||||||
ETASeconds: int(estimateSnapshotETA(crawlStart, int(n), seenN, len(jobs), workers, client.Timeout).Seconds()),
|
ETASeconds: int(estimateSnapshotETA(crawlStart, int(n), seenN, len(jobs), workers, client.Timeout).Seconds()),
|
||||||
})
|
})
|
||||||
@@ -1574,7 +1570,7 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht
|
|||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 97,
|
Progress: 97,
|
||||||
Message: fmt.Sprintf("Redfish snapshot: post-probe NVMe (%d/%d, ETA≈%s), коллекция=%s", i+1, len(driveCollections), formatETA(estimateProgressETA(nvmeProbeStart, i, len(driveCollections), 2*time.Second)), compactProgressPath(path)),
|
Message: fmt.Sprintf("Redfish snapshot: post-probe NVMe (%d/%d, ETA≈%s), collection=%s", i+1, len(driveCollections), formatETA(estimateProgressETA(nvmeProbeStart, i, len(driveCollections), 2*time.Second)), compactProgressPath(path)),
|
||||||
CurrentPhase: "snapshot_postprobe_nvme",
|
CurrentPhase: "snapshot_postprobe_nvme",
|
||||||
ETASeconds: int(estimateProgressETA(nvmeProbeStart, i, len(driveCollections), 2*time.Second).Seconds()),
|
ETASeconds: int(estimateProgressETA(nvmeProbeStart, i, len(driveCollections), 2*time.Second).Seconds()),
|
||||||
})
|
})
|
||||||
@@ -1622,7 +1618,7 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht
|
|||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 98,
|
Progress: 98,
|
||||||
Message: fmt.Sprintf("Redfish snapshot: post-probe коллекций (%d/%d, ETA≈%s), текущая=%s", i+1, len(postProbeCollections), formatETA(estimateProgressETA(postProbeStart, i, len(postProbeCollections), 3*time.Second)), compactProgressPath(path)),
|
Message: fmt.Sprintf("Redfish snapshot: post-probe collections (%d/%d, ETA≈%s), current=%s", i+1, len(postProbeCollections), formatETA(estimateProgressETA(postProbeStart, i, len(postProbeCollections), 3*time.Second)), compactProgressPath(path)),
|
||||||
CurrentPhase: "snapshot_postprobe_collections",
|
CurrentPhase: "snapshot_postprobe_collections",
|
||||||
ETASeconds: int(estimateProgressETA(postProbeStart, i, len(postProbeCollections), 3*time.Second).Seconds()),
|
ETASeconds: int(estimateProgressETA(postProbeStart, i, len(postProbeCollections), 3*time.Second).Seconds()),
|
||||||
})
|
})
|
||||||
@@ -1641,14 +1637,14 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht
|
|||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 98,
|
Progress: 98,
|
||||||
Message: fmt.Sprintf("Redfish snapshot: post-probe добавлено %d документов", addedPostProbe),
|
Message: fmt.Sprintf("Redfish snapshot: post-probe added %d docs", addedPostProbe),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if emit != nil {
|
if emit != nil {
|
||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 98,
|
Progress: 98,
|
||||||
Message: fmt.Sprintf("Redfish snapshot: post-probe метрики candidates=%d selected=%d skipped_explicit=%d added=%d", postProbeMetrics.CollectionCandidates, postProbeMetrics.CollectionSelected, postProbeMetrics.SkippedExplicit, postProbeMetrics.Added),
|
Message: fmt.Sprintf("Redfish snapshot: post-probe metrics candidates=%d selected=%d skipped_explicit=%d added=%d", postProbeMetrics.CollectionCandidates, postProbeMetrics.CollectionSelected, postProbeMetrics.SkippedExplicit, postProbeMetrics.Added),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1656,7 +1652,7 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht
|
|||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 98,
|
Progress: 98,
|
||||||
Message: fmt.Sprintf("Redfish snapshot: собрано %d документов", len(out)),
|
Message: fmt.Sprintf("Redfish snapshot: collected %d docs", len(out)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1671,14 +1667,14 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht
|
|||||||
return asString(errorList[i]["path"]) < asString(errorList[j]["path"])
|
return asString(errorList[i]["path"]) < asString(errorList[j]["path"])
|
||||||
})
|
})
|
||||||
if summary := timings.Summary(12); summary != "" {
|
if summary := timings.Summary(12); summary != "" {
|
||||||
log.Printf("redfish-snapshot-timing: %s", summary)
|
slog.Info("redfish-snapshot-timing", "summary", summary)
|
||||||
}
|
}
|
||||||
if emit != nil {
|
if emit != nil {
|
||||||
if summary := timings.Summary(3); summary != "" {
|
if summary := timings.Summary(3); summary != "" {
|
||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 98,
|
Progress: 98,
|
||||||
Message: fmt.Sprintf("Redfish snapshot: топ веток по времени: %s", summary),
|
Message: fmt.Sprintf("Redfish snapshot: top branches by time: %s", summary),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2979,14 +2975,14 @@ func (c *RedfishConnector) recoverCriticalRedfishDocsPlanB(ctx context.Context,
|
|||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 97,
|
Progress: 97,
|
||||||
Message: fmt.Sprintf("Redfish: расширенная диагностика выключена, пропущено %d тяжелых diagnostic endpoint", skippedDiagnosticTargets),
|
Message: fmt.Sprintf("Redfish: extended diagnostics disabled, skipped %d heavy diagnostic endpoints", skippedDiagnosticTargets),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
totalETA := redfishCriticalCooldown() + estimatePlanBETA(len(targets))
|
totalETA := redfishCriticalCooldown() + estimatePlanBETA(len(targets))
|
||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 97,
|
Progress: 97,
|
||||||
Message: fmt.Sprintf("Redfish: cooldown перед повторным добором критичных endpoint... ETA≈%s", formatETA(totalETA)),
|
Message: fmt.Sprintf("Redfish: cooldown before retrying critical endpoints... ETA≈%s", formatETA(totalETA)),
|
||||||
CurrentPhase: "critical_plan_b",
|
CurrentPhase: "critical_plan_b",
|
||||||
ETASeconds: int(totalETA.Seconds()),
|
ETASeconds: int(totalETA.Seconds()),
|
||||||
})
|
})
|
||||||
@@ -3072,17 +3068,17 @@ func (c *RedfishConnector) recoverCriticalRedfishDocsPlanB(ctx context.Context,
|
|||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 97,
|
Progress: 97,
|
||||||
Message: fmt.Sprintf("Redfish: plan-B топ веток по времени: %s", summary),
|
Message: fmt.Sprintf("Redfish: plan-B top branches by time: %s", summary),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 97,
|
Progress: 97,
|
||||||
Message: fmt.Sprintf("Redfish: plan-B завершен за %s (targets=%d, recovered=%d)", time.Since(planBStart).Round(time.Second), len(targets), recovered),
|
Message: fmt.Sprintf("Redfish: plan-B completed in %s (targets=%d, recovered=%d)", time.Since(planBStart).Round(time.Second), len(targets), recovered),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if summary := timings.Summary(12); summary != "" {
|
if summary := timings.Summary(12); summary != "" {
|
||||||
log.Printf("redfish-planb-timing: %s", summary)
|
slog.Info("redfish-planb-timing", "summary", summary)
|
||||||
}
|
}
|
||||||
return recovered
|
return recovered
|
||||||
}
|
}
|
||||||
@@ -3143,7 +3139,7 @@ func (c *RedfishConnector) recoverProfilePlanBDocs(ctx context.Context, client *
|
|||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 98,
|
Progress: 98,
|
||||||
Message: fmt.Sprintf("Redfish: profile plan-B добирает %d endpoint...", len(targets)),
|
Message: fmt.Sprintf("Redfish: profile plan-B fetching %d endpoints...", len(targets)),
|
||||||
CurrentPhase: "profile_plan_b",
|
CurrentPhase: "profile_plan_b",
|
||||||
ETASeconds: int(estimateProgressETA(planBStart, 0, len(targets), 2*time.Second).Seconds()),
|
ETASeconds: int(estimateProgressETA(planBStart, 0, len(targets), 2*time.Second).Seconds()),
|
||||||
})
|
})
|
||||||
@@ -3167,19 +3163,18 @@ func (c *RedfishConnector) recoverProfilePlanBDocs(ctx context.Context, client *
|
|||||||
recovered++
|
recovered++
|
||||||
}
|
}
|
||||||
if recovered > 0 {
|
if recovered > 0 {
|
||||||
log.Printf(
|
slog.Info("redfish-profile-planb",
|
||||||
"redfish-profile-planb: mode=%s profiles=%s targets=%d recovered=%d",
|
"mode", plan.Mode,
|
||||||
plan.Mode,
|
"profiles", strings.Join(plan.Profiles, ","),
|
||||||
strings.Join(plan.Profiles, ","),
|
"targets", len(targets),
|
||||||
len(targets),
|
"recovered", recovered,
|
||||||
recovered,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if emit != nil {
|
if emit != nil {
|
||||||
emit(Progress{
|
emit(Progress{
|
||||||
Status: "running",
|
Status: "running",
|
||||||
Progress: 98,
|
Progress: 98,
|
||||||
Message: fmt.Sprintf("Redfish: profile plan-B завершен за %s (targets=%d, recovered=%d)", time.Since(planBStart).Round(time.Second), len(targets), recovered),
|
Message: fmt.Sprintf("Redfish: profile plan-B completed in %s (targets=%d, recovered=%d)", time.Since(planBStart).Round(time.Second), len(targets), recovered),
|
||||||
CurrentPhase: "profile_plan_b",
|
CurrentPhase: "profile_plan_b",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package collector
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -62,7 +62,7 @@ func (c *RedfishConnector) collectRedfishLogEntries(ctx context.Context, client
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(out) > 0 {
|
if len(out) > 0 {
|
||||||
log.Printf("redfish: collected %d hardware log entries (Systems+Managers SEL, window=7d)", len(out))
|
slog.Info("redfish: collected hardware log entries", "count", len(out), "window", "7d")
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package collector
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log/slog"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -32,7 +32,7 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
|||||||
emit(Progress{Status: "running", Progress: 10, Message: "Redfish snapshot: replay service root..."})
|
emit(Progress{Status: "running", Progress: 10, Message: "Redfish snapshot: replay service root..."})
|
||||||
}
|
}
|
||||||
if _, err := r.getJSON("/redfish/v1"); err != nil {
|
if _, err := r.getJSON("/redfish/v1"); err != nil {
|
||||||
log.Printf("redfish replay: service root /redfish/v1 missing from snapshot, continuing with defaults: %v", err)
|
slog.Warn("redfish replay: service root /redfish/v1 missing from snapshot, continuing with defaults", "err", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
systemPaths := r.discoverMemberPaths("/redfish/v1/Systems", "/redfish/v1/Systems/1")
|
systemPaths := r.discoverMemberPaths("/redfish/v1/Systems", "/redfish/v1/Systems/1")
|
||||||
@@ -219,7 +219,7 @@ func inferInventoryLastModifiedTime(snapshot map[string]interface{}) time.Time {
|
|||||||
for _, layout := range []string{time.RFC3339, time.RFC3339Nano} {
|
for _, layout := range []string{time.RFC3339, time.RFC3339Nano} {
|
||||||
if ts, err := time.Parse(layout, raw); err == nil {
|
if ts, err := time.Parse(layout, raw); err == nil {
|
||||||
t := ts.UTC()
|
t := ts.UTC()
|
||||||
log.Printf("redfish replay: inventory last modified at %s (InventoryData/Status.LastModifiedTime)", t.Format(time.RFC3339))
|
slog.Info("redfish replay: inventory last modified", "at", t.Format(time.RFC3339), "source", "InventoryData/Status.LastModifiedTime")
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ func New(result *models.AnalysisResult) *Exporter {
|
|||||||
|
|
||||||
// ExportCSV exports serial numbers to CSV format
|
// ExportCSV exports serial numbers to CSV format
|
||||||
func (e *Exporter) ExportCSV(w io.Writer) error {
|
func (e *Exporter) ExportCSV(w io.Writer) error {
|
||||||
|
if _, err := w.Write([]byte{0xEF, 0xBB, 0xBF}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
writer := csv.NewWriter(w)
|
writer := csv.NewWriter(w)
|
||||||
|
writer.Comma = ';'
|
||||||
defer writer.Flush()
|
defer writer.Flush()
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
|
|||||||
@@ -52,7 +52,13 @@ func TestExportCSV_IncludesAllComponentTypesWithUsableSerials(t *testing.T) {
|
|||||||
t.Fatalf("ExportCSV failed: %v", err)
|
t.Fatalf("ExportCSV failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := csv.NewReader(bytes.NewReader(buf.Bytes())).ReadAll()
|
b := buf.Bytes()
|
||||||
|
if len(b) >= 3 && b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF {
|
||||||
|
b = b[3:] // strip UTF-8 BOM
|
||||||
|
}
|
||||||
|
r := csv.NewReader(bytes.NewReader(b))
|
||||||
|
r.Comma = ';'
|
||||||
|
rows, err := r.ReadAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("read csv: %v", err)
|
t.Fatalf("read csv: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-27
@@ -2867,9 +2867,9 @@ func parseKeyValueBlocks(content string) []map[string]string {
|
|||||||
|
|
||||||
func findCPUIndex(items []models.CPU, target models.CPU) int {
|
func findCPUIndex(items []models.CPU, target models.CPU) int {
|
||||||
targetSocket := target.Socket
|
targetSocket := target.Socket
|
||||||
targetPPIN := strings.ToLower(strings.TrimSpace(target.PPIN))
|
targetPPIN := strings.TrimSpace(target.PPIN)
|
||||||
targetSerial := strings.ToLower(strings.TrimSpace(target.SerialNumber))
|
targetSerial := strings.TrimSpace(target.SerialNumber)
|
||||||
targetModel := strings.ToLower(strings.TrimSpace(target.Model))
|
targetModel := strings.TrimSpace(target.Model)
|
||||||
|
|
||||||
for i := range items {
|
for i := range items {
|
||||||
cpu := items[i]
|
cpu := items[i]
|
||||||
@@ -2880,18 +2880,18 @@ func findCPUIndex(items []models.CPU, target models.CPU) int {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ppin := strings.ToLower(strings.TrimSpace(cpu.PPIN))
|
ppin := strings.TrimSpace(cpu.PPIN)
|
||||||
if targetPPIN != "" && ppin != "" && targetPPIN == ppin {
|
if targetPPIN != "" && ppin != "" && strings.EqualFold(targetPPIN, ppin) {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
serial := strings.ToLower(strings.TrimSpace(cpu.SerialNumber))
|
serial := strings.TrimSpace(cpu.SerialNumber)
|
||||||
if targetSerial != "" && serial != "" && targetSerial == serial {
|
if targetSerial != "" && serial != "" && strings.EqualFold(targetSerial, serial) {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
model := strings.ToLower(strings.TrimSpace(cpu.Model))
|
model := strings.TrimSpace(cpu.Model)
|
||||||
if targetSocket == 0 && cpu.Socket == 0 && targetModel != "" && model == targetModel {
|
if targetSocket == 0 && cpu.Socket == 0 && targetModel != "" && strings.EqualFold(model, targetModel) {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2931,15 +2931,15 @@ func mergeCPU(dst *models.CPU, src models.CPU) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func findMemoryIndex(items []models.MemoryDIMM, target models.MemoryDIMM) int {
|
func findMemoryIndex(items []models.MemoryDIMM, target models.MemoryDIMM) int {
|
||||||
targetSerial := strings.ToLower(strings.TrimSpace(target.SerialNumber))
|
targetSerial := strings.TrimSpace(target.SerialNumber)
|
||||||
targetSlot := strings.ToLower(strings.TrimSpace(target.Slot))
|
targetSlot := strings.TrimSpace(target.Slot)
|
||||||
for i := range items {
|
for i := range items {
|
||||||
serial := strings.ToLower(strings.TrimSpace(items[i].SerialNumber))
|
serial := strings.TrimSpace(items[i].SerialNumber)
|
||||||
slot := strings.ToLower(strings.TrimSpace(items[i].Slot))
|
slot := strings.TrimSpace(items[i].Slot)
|
||||||
if targetSerial != "" && serial != "" && targetSerial == serial {
|
if targetSerial != "" && serial != "" && strings.EqualFold(targetSerial, serial) {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
if targetSerial == "" && targetSlot != "" && slot != "" && targetSlot == slot {
|
if targetSerial == "" && targetSlot != "" && slot != "" && strings.EqualFold(targetSlot, slot) {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2993,15 +2993,15 @@ func dedupeStorage(items []models.Storage) []models.Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func findStorageIndex(items []models.Storage, target models.Storage) int {
|
func findStorageIndex(items []models.Storage, target models.Storage) int {
|
||||||
targetSerial := strings.ToLower(strings.TrimSpace(target.SerialNumber))
|
targetSerial := strings.TrimSpace(target.SerialNumber)
|
||||||
targetSlot := strings.ToLower(strings.TrimSpace(target.Slot))
|
targetSlot := strings.TrimSpace(target.Slot)
|
||||||
for i := range items {
|
for i := range items {
|
||||||
serial := strings.ToLower(strings.TrimSpace(items[i].SerialNumber))
|
serial := strings.TrimSpace(items[i].SerialNumber)
|
||||||
slot := strings.ToLower(strings.TrimSpace(items[i].Slot))
|
slot := strings.TrimSpace(items[i].Slot)
|
||||||
if targetSerial != "" && serial != "" && targetSerial == serial {
|
if targetSerial != "" && serial != "" && strings.EqualFold(targetSerial, serial) {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
if targetSerial == "" && targetSlot != "" && slot != "" && targetSlot == slot {
|
if targetSerial == "" && targetSlot != "" && slot != "" && strings.EqualFold(targetSlot, slot) {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3248,15 +3248,15 @@ func isPSUEmpty(p models.PSU) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func findPSUIndex(items []models.PSU, target models.PSU) int {
|
func findPSUIndex(items []models.PSU, target models.PSU) int {
|
||||||
targetSerial := strings.ToLower(strings.TrimSpace(target.SerialNumber))
|
targetSerial := strings.TrimSpace(target.SerialNumber)
|
||||||
targetSlot := strings.ToLower(strings.TrimSpace(target.Slot))
|
targetSlot := strings.TrimSpace(target.Slot)
|
||||||
for i := range items {
|
for i := range items {
|
||||||
serial := strings.ToLower(strings.TrimSpace(items[i].SerialNumber))
|
serial := strings.TrimSpace(items[i].SerialNumber)
|
||||||
slot := strings.ToLower(strings.TrimSpace(items[i].Slot))
|
slot := strings.TrimSpace(items[i].Slot)
|
||||||
if targetSerial != "" && serial != "" && targetSerial == serial {
|
if targetSerial != "" && serial != "" && strings.EqualFold(targetSerial, serial) {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
if targetSerial == "" && targetSlot != "" && slot != "" && targetSlot == slot {
|
if targetSerial == "" && targetSlot != "" && slot != "" && strings.EqualFold(targetSlot, slot) {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,12 +39,15 @@ type CollectJobResponse struct {
|
|||||||
|
|
||||||
type CollectJobStatusResponse struct {
|
type CollectJobStatusResponse struct {
|
||||||
JobID string `json:"job_id"`
|
JobID string `json:"job_id"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Progress *int `json:"progress,omitempty"`
|
Progress *int `json:"progress,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
CurrentPhase string `json:"current_phase,omitempty"`
|
CurrentPhase string `json:"current_phase,omitempty"`
|
||||||
ETASeconds *int `json:"eta_seconds,omitempty"`
|
ETASeconds *int `json:"eta_seconds,omitempty"`
|
||||||
Logs []string `json:"logs,omitempty"`
|
Logs []string `json:"logs,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
|
Result map[string]interface{} `json:"result,omitempty"`
|
||||||
ActiveModules []CollectModuleStatus `json:"active_modules,omitempty"`
|
ActiveModules []CollectModuleStatus `json:"active_modules,omitempty"`
|
||||||
ModuleScores []CollectModuleStatus `json:"module_scores,omitempty"`
|
ModuleScores []CollectModuleStatus `json:"module_scores,omitempty"`
|
||||||
DebugInfo *CollectDebugInfo `json:"debug_info,omitempty"`
|
DebugInfo *CollectDebugInfo `json:"debug_info,omitempty"`
|
||||||
@@ -63,12 +66,15 @@ type CollectRequestMeta struct {
|
|||||||
|
|
||||||
type Job struct {
|
type Job struct {
|
||||||
ID string
|
ID string
|
||||||
|
Type string
|
||||||
Status string
|
Status string
|
||||||
Progress int
|
Progress int
|
||||||
|
Message string
|
||||||
CurrentPhase string
|
CurrentPhase string
|
||||||
ETASeconds int
|
ETASeconds int
|
||||||
Logs []string
|
Logs []string
|
||||||
Error string
|
Error string
|
||||||
|
Result map[string]interface{}
|
||||||
ActiveModules []CollectModuleStatus
|
ActiveModules []CollectModuleStatus
|
||||||
ModuleScores []CollectModuleStatus
|
ModuleScores []CollectModuleStatus
|
||||||
DebugInfo *CollectDebugInfo
|
DebugInfo *CollectDebugInfo
|
||||||
@@ -107,11 +113,14 @@ func (j *Job) toStatusResponse() CollectJobStatusResponse {
|
|||||||
progress := j.Progress
|
progress := j.Progress
|
||||||
resp := CollectJobStatusResponse{
|
resp := CollectJobStatusResponse{
|
||||||
JobID: j.ID,
|
JobID: j.ID,
|
||||||
|
Type: j.Type,
|
||||||
Status: j.Status,
|
Status: j.Status,
|
||||||
Progress: &progress,
|
Progress: &progress,
|
||||||
|
Message: j.Message,
|
||||||
CurrentPhase: j.CurrentPhase,
|
CurrentPhase: j.CurrentPhase,
|
||||||
Logs: append([]string(nil), j.Logs...),
|
Logs: append([]string(nil), j.Logs...),
|
||||||
Error: j.Error,
|
Error: j.Error,
|
||||||
|
Result: j.Result,
|
||||||
ActiveModules: append([]CollectModuleStatus(nil), j.ActiveModules...),
|
ActiveModules: append([]CollectModuleStatus(nil), j.ActiveModules...),
|
||||||
ModuleScores: append([]CollectModuleStatus(nil), j.ModuleScores...),
|
ModuleScores: append([]CollectModuleStatus(nil), j.ModuleScores...),
|
||||||
DebugInfo: cloneCollectDebugInfo(j.DebugInfo),
|
DebugInfo: cloneCollectDebugInfo(j.DebugInfo),
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ func TestBuildSpecification_ZeroSizeMemoryWithInventoryIsShown(t *testing.T) {
|
|||||||
|
|
||||||
spec := buildSpecification(hw)
|
spec := buildSpecification(hw)
|
||||||
for _, line := range spec {
|
for _, line := range spec {
|
||||||
if line.Category == "Память" && line.Name == "Hynix HMCG88AEBRA115N (size unknown)" && line.Quantity == 1 {
|
if line.Category == "Memory" && line.Name == "Hynix HMCG88AEBRA115N (size unknown)" && line.Quantity == 1 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+67
-46
@@ -38,13 +38,13 @@ func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
tmplContent, err := WebFS.ReadFile("templates/index.html")
|
tmplContent, err := WebFS.ReadFile("templates/index.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Template not found", http.StatusInternalServerError)
|
s.htmlError(w, "Template not found", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl, err := template.New("index").Parse(string(tmplContent))
|
tmpl, err := template.New("index").Parse(string(tmplContent))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Template parse error", http.StatusInternalServerError)
|
s.htmlError(w, "Template parse error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ func (s *Server) handleChartCurrent(w http.ResponseWriter, r *http.Request) {
|
|||||||
if result == nil || result.Hardware == nil {
|
if result == nil || result.Hardware == nil {
|
||||||
html, err := chartviewer.RenderHTML(nil, title)
|
html, err := chartviewer.RenderHTML(nil, title)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "failed to render viewer", http.StatusInternalServerError)
|
s.htmlError(w, "failed to render viewer", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
@@ -80,7 +80,7 @@ func (s *Server) handleChartCurrent(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
snapshotBytes, err := currentReanimatorSnapshotBytes(result)
|
snapshotBytes, err := currentReanimatorSnapshotBytes(result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "failed to build reanimator snapshot: "+err.Error(), http.StatusInternalServerError)
|
s.htmlError(w, "failed to build reanimator snapshot: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ func (s *Server) handleChartCurrent(w http.ResponseWriter, r *http.Request) {
|
|||||||
PrintMode: printMode,
|
PrintMode: printMode,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "failed to render chart: "+err.Error(), http.StatusInternalServerError)
|
s.htmlError(w, "failed to render chart: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +395,7 @@ func uniqueSortedExtensions(exts []string) []string {
|
|||||||
func (s *Server) handleGetEvents(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGetEvents(w http.ResponseWriter, r *http.Request) {
|
||||||
result := s.GetResult()
|
result := s.GetResult()
|
||||||
if result == nil {
|
if result == nil {
|
||||||
jsonResponse(w, []interface{}{})
|
jsonList(w, []interface{}{}, 0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,18 +408,18 @@ func (s *Server) handleGetEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
return events[i].Timestamp.After(events[j].Timestamp)
|
return events[i].Timestamp.After(events[j].Timestamp)
|
||||||
})
|
})
|
||||||
|
|
||||||
jsonResponse(w, events)
|
jsonList(w, events, len(events))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleGetSensors(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGetSensors(w http.ResponseWriter, r *http.Request) {
|
||||||
result := s.GetResult()
|
result := s.GetResult()
|
||||||
if result == nil {
|
if result == nil {
|
||||||
jsonResponse(w, []interface{}{})
|
jsonList(w, []interface{}{}, 0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sensors := append([]models.SensorReading{}, result.Sensors...)
|
sensors := append([]models.SensorReading{}, result.Sensors...)
|
||||||
sensors = append(sensors, synthesizePSUVoltageSensors(result.Hardware)...)
|
sensors = append(sensors, synthesizePSUVoltageSensors(result.Hardware)...)
|
||||||
jsonResponse(w, sensors)
|
jsonList(w, sensors, len(sensors))
|
||||||
}
|
}
|
||||||
|
|
||||||
func synthesizePSUVoltageSensors(hw *models.HardwareConfig) []models.SensorReading {
|
func synthesizePSUVoltageSensors(hw *models.HardwareConfig) []models.SensorReading {
|
||||||
@@ -533,7 +533,7 @@ func buildSpecification(hw *models.HardwareConfig) []SpecLine {
|
|||||||
float64(cpu.FrequencyMHz)/1000,
|
float64(cpu.FrequencyMHz)/1000,
|
||||||
cpu.Cores,
|
cpu.Cores,
|
||||||
intFromDetails(cpu.Details, "tdp_w"))
|
intFromDetails(cpu.Details, "tdp_w"))
|
||||||
spec = append(spec, SpecLine{Category: "Процессор", Name: name, Quantity: count})
|
spec = append(spec, SpecLine{Category: "CPU", Name: name, Quantity: count})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memory - group by size, type and frequency (only installed modules)
|
// Memory - group by size, type and frequency (only installed modules)
|
||||||
@@ -568,7 +568,7 @@ func buildSpecification(hw *models.HardwareConfig) []SpecLine {
|
|||||||
memGroups[key]++
|
memGroups[key]++
|
||||||
}
|
}
|
||||||
for key, count := range memGroups {
|
for key, count := range memGroups {
|
||||||
spec = append(spec, SpecLine{Category: "Память", Name: key, Quantity: count})
|
spec = append(spec, SpecLine{Category: "Memory", Name: key, Quantity: count})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Storage - group by type and capacity
|
// Storage - group by type and capacity
|
||||||
@@ -586,7 +586,7 @@ func buildSpecification(hw *models.HardwareConfig) []SpecLine {
|
|||||||
storGroups[key]++
|
storGroups[key]++
|
||||||
}
|
}
|
||||||
for key, count := range storGroups {
|
for key, count := range storGroups {
|
||||||
spec = append(spec, SpecLine{Category: "Накопитель", Name: key, Quantity: count})
|
spec = append(spec, SpecLine{Category: "Storage", Name: key, Quantity: count})
|
||||||
}
|
}
|
||||||
|
|
||||||
// PCIe devices - group by device class/name and manufacturer
|
// PCIe devices - group by device class/name and manufacturer
|
||||||
@@ -609,7 +609,7 @@ func buildSpecification(hw *models.HardwareConfig) []SpecLine {
|
|||||||
}
|
}
|
||||||
for key, count := range pcieGroups {
|
for key, count := range pcieGroups {
|
||||||
pcie := pcieDetails[key]
|
pcie := pcieDetails[key]
|
||||||
category := "PCIe устройство"
|
category := "PCIe Device"
|
||||||
name := key
|
name := key
|
||||||
|
|
||||||
// Determine category based on device class or known GPU names
|
// Determine category based on device class or known GPU names
|
||||||
@@ -618,11 +618,11 @@ func buildSpecification(hw *models.HardwareConfig) []SpecLine {
|
|||||||
isNetwork := deviceClass == "Network" || strings.Contains(deviceClass, "ConnectX")
|
isNetwork := deviceClass == "Network" || strings.Contains(deviceClass, "ConnectX")
|
||||||
|
|
||||||
if isGPU {
|
if isGPU {
|
||||||
category = "Графический процессор"
|
category = "GPU"
|
||||||
} else if isNetwork {
|
} else if isNetwork {
|
||||||
category = "Сетевой адаптер"
|
category = "Network Adapter"
|
||||||
} else if deviceClass == "NVMe" || deviceClass == "RAID" || deviceClass == "SAS" || deviceClass == "SATA" || deviceClass == "Storage" {
|
} else if deviceClass == "NVMe" || deviceClass == "RAID" || deviceClass == "SAS" || deviceClass == "SATA" || deviceClass == "Storage" {
|
||||||
category = "Контроллер"
|
category = "Controller"
|
||||||
}
|
}
|
||||||
|
|
||||||
spec = append(spec, SpecLine{Category: category, Name: name, Quantity: count})
|
spec = append(spec, SpecLine{Category: category, Name: name, Quantity: count})
|
||||||
@@ -643,7 +643,7 @@ func buildSpecification(hw *models.HardwareConfig) []SpecLine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for key, count := range psuGroups {
|
for key, count := range psuGroups {
|
||||||
spec = append(spec, SpecLine{Category: "Блок питания", Name: key, Quantity: count})
|
spec = append(spec, SpecLine{Category: "Power Supply", Name: key, Quantity: count})
|
||||||
}
|
}
|
||||||
|
|
||||||
return spec
|
return spec
|
||||||
@@ -664,7 +664,7 @@ func nonEmptyStrings(values ...string) []string {
|
|||||||
func (s *Server) handleGetSerials(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGetSerials(w http.ResponseWriter, r *http.Request) {
|
||||||
result := s.GetResult()
|
result := s.GetResult()
|
||||||
if result == nil {
|
if result == nil {
|
||||||
jsonResponse(w, []interface{}{})
|
jsonList(w, []interface{}{}, 0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -714,7 +714,7 @@ func (s *Server) handleGetSerials(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonResponse(w, serials)
|
jsonList(w, serials, len(serials))
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizePCIeSerialComponentName(p models.PCIeDevice) string {
|
func normalizePCIeSerialComponentName(p models.PCIeDevice) string {
|
||||||
@@ -768,11 +768,12 @@ func hasUsableFirmwareVersion(version string) bool {
|
|||||||
func (s *Server) handleGetFirmware(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGetFirmware(w http.ResponseWriter, r *http.Request) {
|
||||||
result := s.GetResult()
|
result := s.GetResult()
|
||||||
if result == nil || result.Hardware == nil {
|
if result == nil || result.Hardware == nil {
|
||||||
jsonResponse(w, []interface{}{})
|
jsonList(w, []interface{}{}, 0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonResponse(w, buildFirmwareEntries(result.Hardware))
|
entries := buildFirmwareEntries(result.Hardware)
|
||||||
|
jsonList(w, entries, len(entries))
|
||||||
}
|
}
|
||||||
|
|
||||||
type parseErrorEntry struct {
|
type parseErrorEntry struct {
|
||||||
@@ -941,8 +942,7 @@ func looksLikeErrorLogLine(line string) bool {
|
|||||||
if s == "" {
|
if s == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return strings.Contains(s, "ошибка") ||
|
return strings.Contains(s, "error") ||
|
||||||
strings.Contains(s, "error") ||
|
|
||||||
strings.Contains(s, "failed") ||
|
strings.Contains(s, "failed") ||
|
||||||
strings.Contains(s, "timeout") ||
|
strings.Contains(s, "timeout") ||
|
||||||
strings.Contains(s, "deadline exceeded")
|
strings.Contains(s, "deadline exceeded")
|
||||||
@@ -977,7 +977,7 @@ func parseErrorSeverityFromMessage(msg string) string {
|
|||||||
if strings.HasPrefix(s, "status 404 ") || strings.HasPrefix(s, "status 405 ") || strings.HasPrefix(s, "status 410 ") || strings.HasPrefix(s, "status 501 ") {
|
if strings.HasPrefix(s, "status 404 ") || strings.HasPrefix(s, "status 405 ") || strings.HasPrefix(s, "status 410 ") || strings.HasPrefix(s, "status 501 ") {
|
||||||
return "info"
|
return "info"
|
||||||
}
|
}
|
||||||
if strings.Contains(s, "ошибка") || strings.Contains(s, "error") || strings.Contains(s, "failed") {
|
if strings.Contains(s, "error") || strings.Contains(s, "failed") {
|
||||||
return "warning"
|
return "warning"
|
||||||
}
|
}
|
||||||
return "info"
|
return "info"
|
||||||
@@ -1316,7 +1316,7 @@ func (s *Server) handleConvertReanimatorBatch(w http.ResponseWriter, r *http.Req
|
|||||||
|
|
||||||
tempDir, err := os.MkdirTemp("", "logpile-convert-input-*")
|
tempDir, err := os.MkdirTemp("", "logpile-convert-input-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonError(w, "Не удалось создать временную директорию", http.StatusInternalServerError)
|
jsonError(w, "Failed to create temp directory", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1363,7 +1363,7 @@ func (s *Server) handleConvertReanimatorBatch(w http.ResponseWriter, r *http.Req
|
|||||||
|
|
||||||
if len(inputFiles) == 0 {
|
if len(inputFiles) == 0 {
|
||||||
_ = os.RemoveAll(tempDir)
|
_ = os.RemoveAll(tempDir)
|
||||||
jsonError(w, "Нет файлов поддерживаемого типа для конвертации", http.StatusBadRequest)
|
jsonError(w, "No supported files to convert", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1376,9 +1376,9 @@ func (s *Server) handleConvertReanimatorBatch(w http.ResponseWriter, r *http.Req
|
|||||||
TLSMode: "insecure",
|
TLSMode: "insecure",
|
||||||
})
|
})
|
||||||
s.markConvertJob(job.ID)
|
s.markConvertJob(job.ID)
|
||||||
s.jobManager.AppendJobLog(job.ID, fmt.Sprintf("Запущена пакетная конвертация: %d файлов", len(inputFiles)))
|
s.jobManager.AppendJobLog(job.ID, fmt.Sprintf("Batch conversion started: %d files", len(inputFiles)))
|
||||||
if skipped > 0 {
|
if skipped > 0 {
|
||||||
s.jobManager.AppendJobLog(job.ID, fmt.Sprintf("Пропущено неподдерживаемых файлов: %d", skipped))
|
s.jobManager.AppendJobLog(job.ID, fmt.Sprintf("Skipped unsupported files: %d", skipped))
|
||||||
}
|
}
|
||||||
s.jobManager.UpdateJobStatus(job.ID, CollectStatusRunning, 0, "")
|
s.jobManager.UpdateJobStatus(job.ID, CollectStatusRunning, 0, "")
|
||||||
|
|
||||||
@@ -1406,7 +1406,7 @@ func (s *Server) runConvertJob(jobID, tempDir string, inputFiles []convertInputF
|
|||||||
|
|
||||||
resultFile, err := os.CreateTemp("", "logpile-convert-result-*.zip")
|
resultFile, err := os.CreateTemp("", "logpile-convert-result-*.zip")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.jobManager.UpdateJobStatus(jobID, CollectStatusFailed, 100, "не удалось создать zip")
|
s.jobManager.UpdateJobStatus(jobID, CollectStatusFailed, 100, "failed to create zip")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resultPath := resultFile.Name()
|
resultPath := resultFile.Name()
|
||||||
@@ -1418,7 +1418,7 @@ func (s *Server) runConvertJob(jobID, tempDir string, inputFiles []convertInputF
|
|||||||
totalProcess := len(inputFiles)
|
totalProcess := len(inputFiles)
|
||||||
|
|
||||||
for i, in := range inputFiles {
|
for i, in := range inputFiles {
|
||||||
s.jobManager.AppendJobLog(jobID, fmt.Sprintf("Обработка %s", in.Name))
|
s.jobManager.AppendJobLog(jobID, fmt.Sprintf("Processing %s", in.Name))
|
||||||
payload, err := os.ReadFile(in.Path)
|
payload, err := os.ReadFile(in.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
failures = append(failures, fmt.Sprintf("%s: %v", in.Name, err))
|
failures = append(failures, fmt.Sprintf("%s: %v", in.Name, err))
|
||||||
@@ -1471,13 +1471,13 @@ func (s *Server) runConvertJob(jobID, tempDir string, inputFiles []convertInputF
|
|||||||
if success == 0 {
|
if success == 0 {
|
||||||
_ = zw.Close()
|
_ = zw.Close()
|
||||||
_ = os.Remove(resultPath)
|
_ = os.Remove(resultPath)
|
||||||
s.jobManager.UpdateJobStatus(jobID, CollectStatusFailed, 100, "Не удалось конвертировать ни один файл")
|
s.jobManager.UpdateJobStatus(jobID, CollectStatusFailed, 100, "Failed to convert any file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
summaryLines := []string{fmt.Sprintf("Конвертировано %d из %d файлов", success, total)}
|
summaryLines := []string{fmt.Sprintf("Converted %d of %d files", success, total)}
|
||||||
if skipped > 0 {
|
if skipped > 0 {
|
||||||
summaryLines = append(summaryLines, fmt.Sprintf("Пропущено неподдерживаемых: %d", skipped))
|
summaryLines = append(summaryLines, fmt.Sprintf("Skipped unsupported: %d", skipped))
|
||||||
}
|
}
|
||||||
summaryLines = append(summaryLines, failures...)
|
summaryLines = append(summaryLines, failures...)
|
||||||
if entry, err := zw.Create("convert-summary.txt"); err == nil {
|
if entry, err := zw.Create("convert-summary.txt"); err == nil {
|
||||||
@@ -1485,7 +1485,7 @@ func (s *Server) runConvertJob(jobID, tempDir string, inputFiles []convertInputF
|
|||||||
}
|
}
|
||||||
if err := zw.Close(); err != nil {
|
if err := zw.Close(); err != nil {
|
||||||
_ = os.Remove(resultPath)
|
_ = os.Remove(resultPath)
|
||||||
s.jobManager.UpdateJobStatus(jobID, CollectStatusFailed, 100, "Не удалось упаковать результаты")
|
s.jobManager.UpdateJobStatus(jobID, CollectStatusFailed, 100, "Failed to pack results")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1638,7 +1638,7 @@ func (s *Server) handleCollectStart(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
job := s.jobManager.CreateJob(req)
|
job := s.jobManager.CreateJob(req)
|
||||||
s.jobManager.AppendJobLog(job.ID, "Клиент: "+s.ClientVersionString())
|
s.jobManager.AppendJobLog(job.ID, "Client: "+s.ClientVersionString())
|
||||||
s.startCollectionJob(job.ID, req)
|
s.startCollectionJob(job.ID, req)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -1667,7 +1667,7 @@ func pingHost(host string, port int, total, need int) (bool, string) {
|
|||||||
}
|
}
|
||||||
n := int(successes.Load())
|
n := int(successes.Load())
|
||||||
if n < need {
|
if n < need {
|
||||||
return false, fmt.Sprintf("Хост недоступен: только %d из %d попыток подключения к %s прошли успешно (требуется минимум %d)", n, total, addr, need)
|
return false, fmt.Sprintf("Host unreachable: only %d of %d connection attempts to %s succeeded (minimum required: %d)", n, total, addr, need)
|
||||||
}
|
}
|
||||||
return true, ""
|
return true, ""
|
||||||
}
|
}
|
||||||
@@ -1684,12 +1684,12 @@ func (s *Server) handleCollectProbe(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
connector, ok := s.getCollector(req.Protocol)
|
connector, ok := s.getCollector(req.Protocol)
|
||||||
if !ok {
|
if !ok {
|
||||||
jsonError(w, "Коннектор для протокола не зарегистрирован", http.StatusBadRequest)
|
jsonError(w, "Protocol connector not registered", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
prober, ok := connector.(collector.Prober)
|
prober, ok := connector.(collector.Prober)
|
||||||
if !ok {
|
if !ok {
|
||||||
jsonError(w, "Проверка подключения для протокола не поддерживается", http.StatusBadRequest)
|
jsonError(w, "Connection probe not supported for this protocol", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1703,16 +1703,16 @@ func (s *Server) handleCollectProbe(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
result, err := prober.Probe(ctx, toCollectorRequest(req))
|
result, err := prober.Probe(ctx, toCollectorRequest(req))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonError(w, "Проверка подключения не удалась: "+err.Error(), http.StatusBadRequest)
|
jsonError(w, "Connection probe failed: "+err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
message := "Связь с BMC установлена"
|
message := "BMC connection established"
|
||||||
if result != nil {
|
if result != nil {
|
||||||
if result.HostPoweredOn {
|
if result.HostPoweredOn {
|
||||||
message = "Связь с BMC установлена, host включён."
|
message = "BMC connection established, host is powered on."
|
||||||
} else {
|
} else {
|
||||||
message = "Связь с BMC установлена, host выключен. Данные инвентаря могут быть неполными."
|
message = "BMC connection established, host is powered off. Inventory data may be incomplete."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1797,8 +1797,8 @@ func (s *Server) startCollectionJob(jobID string, req CollectRequest) {
|
|||||||
go func() {
|
go func() {
|
||||||
connector, ok := s.getCollector(req.Protocol)
|
connector, ok := s.getCollector(req.Protocol)
|
||||||
if !ok {
|
if !ok {
|
||||||
s.jobManager.UpdateJobStatus(jobID, CollectStatusFailed, 100, "Коннектор для протокола не зарегистрирован")
|
s.jobManager.UpdateJobStatus(jobID, CollectStatusFailed, 100, "Protocol connector not registered")
|
||||||
s.jobManager.AppendJobLog(jobID, "Сбор завершен с ошибкой")
|
s.jobManager.AppendJobLog(jobID, "Collection completed with error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1872,7 +1872,7 @@ func (s *Server) startCollectionJob(jobID string, req CollectRequest) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.jobManager.UpdateJobStatus(jobID, CollectStatusFailed, 100, err.Error())
|
s.jobManager.UpdateJobStatus(jobID, CollectStatusFailed, 100, err.Error())
|
||||||
s.jobManager.AppendJobLog(jobID, "Сбор завершен с ошибкой")
|
s.jobManager.AppendJobLog(jobID, "Collection completed with error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1882,7 +1882,7 @@ func (s *Server) startCollectionJob(jobID string, req CollectRequest) {
|
|||||||
|
|
||||||
applyCollectSourceMetadata(result, req)
|
applyCollectSourceMetadata(result, req)
|
||||||
s.jobManager.UpdateJobStatus(jobID, CollectStatusSuccess, 100, "")
|
s.jobManager.UpdateJobStatus(jobID, CollectStatusSuccess, 100, "")
|
||||||
s.jobManager.AppendJobLog(jobID, "Сбор завершен")
|
s.jobManager.AppendJobLog(jobID, "Collection completed")
|
||||||
s.SetResult(result)
|
s.SetResult(result)
|
||||||
s.SetDetectedVendor(req.Protocol)
|
s.SetDetectedVendor(req.Protocol)
|
||||||
if job, ok := s.jobManager.GetJob(jobID); ok {
|
if job, ok := s.jobManager.GetJob(jobID); ok {
|
||||||
@@ -2142,6 +2142,27 @@ func jsonError(w http.ResponseWriter, message string, code int) {
|
|||||||
json.NewEncoder(w).Encode(map[string]string{"error": message})
|
json.NewEncoder(w).Encode(map[string]string{"error": message})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) htmlError(w http.ResponseWriter, message string, code int) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(code)
|
||||||
|
version := normalizeDisplayVersion(s.config.AppVersion)
|
||||||
|
fmt.Fprintf(w, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Error %d</title></head>`+
|
||||||
|
`<body><h1>Error %d</h1><p>%s</p>`+
|
||||||
|
`<footer style="margin-top:2em;color:#999;font-size:12px">LOGPile %s</footer></body></html>`,
|
||||||
|
code, code, template.HTMLEscapeString(message), template.HTMLEscapeString(version))
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonList(w http.ResponseWriter, items interface{}, totalCount int) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"items": items,
|
||||||
|
"total_count": totalCount,
|
||||||
|
"page": 1,
|
||||||
|
"per_page": totalCount,
|
||||||
|
"total_pages": 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// isGPUDevice checks if device class indicates a GPU
|
// isGPUDevice checks if device class indicates a GPU
|
||||||
func isGPUDevice(deviceClass string) bool {
|
func isGPUDevice(deviceClass string) bool {
|
||||||
// Standard PCI class names
|
// Standard PCI class names
|
||||||
|
|||||||
@@ -51,17 +51,20 @@ func TestHandleGetSerials_WithGPUs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse response
|
// Parse response
|
||||||
var serials []struct {
|
var resp struct {
|
||||||
|
Items []struct {
|
||||||
Component string `json:"component"`
|
Component string `json:"component"`
|
||||||
Location string `json:"location,omitempty"`
|
Location string `json:"location,omitempty"`
|
||||||
SerialNumber string `json:"serial_number"`
|
SerialNumber string `json:"serial_number"`
|
||||||
Manufacturer string `json:"manufacturer,omitempty"`
|
Manufacturer string `json:"manufacturer,omitempty"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
|
} `json:"items"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewDecoder(w.Body).Decode(&serials); err != nil {
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||||||
t.Fatalf("Failed to decode response: %v", err)
|
t.Fatalf("Failed to decode response: %v", err)
|
||||||
}
|
}
|
||||||
|
serials := resp.Items
|
||||||
|
|
||||||
// Check that we have GPU entries
|
// Check that we have GPU entries
|
||||||
gpuCount := 0
|
gpuCount := 0
|
||||||
@@ -115,13 +118,16 @@ func TestHandleGetSerials_WithoutGPUSerials(t *testing.T) {
|
|||||||
srv.handleGetSerials(w, req)
|
srv.handleGetSerials(w, req)
|
||||||
|
|
||||||
// Parse response
|
// Parse response
|
||||||
var serials []struct {
|
var resp struct {
|
||||||
|
Items []struct {
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
|
} `json:"items"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewDecoder(w.Body).Decode(&serials); err != nil {
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||||||
t.Fatalf("Failed to decode response: %v", err)
|
t.Fatalf("Failed to decode response: %v", err)
|
||||||
}
|
}
|
||||||
|
serials := resp.Items
|
||||||
|
|
||||||
// Check that GPUs without serial numbers are not included
|
// Check that GPUs without serial numbers are not included
|
||||||
for _, s := range serials {
|
for _, s := range serials {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package server
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -22,9 +23,11 @@ func (m *JobManager) CreateJob(req CollectRequest) *Job {
|
|||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
job := &Job{
|
job := &Job{
|
||||||
ID: generateJobID(),
|
ID: generateJobID(),
|
||||||
|
Type: req.Protocol,
|
||||||
Status: CollectStatusQueued,
|
Status: CollectStatusQueued,
|
||||||
Progress: 0,
|
Progress: 0,
|
||||||
Logs: []string{formatCollectLogLine(now, "Задача поставлена в очередь")},
|
Message: "Job queued",
|
||||||
|
Logs: []string{formatCollectLogLine(now, "Job queued")},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
RequestMeta: CollectRequestMeta{
|
RequestMeta: CollectRequestMeta{
|
||||||
@@ -66,7 +69,7 @@ func (m *JobManager) CancelJob(id string) (*Job, bool) {
|
|||||||
job.Status = CollectStatusCanceled
|
job.Status = CollectStatusCanceled
|
||||||
job.Error = ""
|
job.Error = ""
|
||||||
job.UpdatedAt = time.Now().UTC()
|
job.UpdatedAt = time.Now().UTC()
|
||||||
job.Logs = append(job.Logs, formatCollectLogLine(job.UpdatedAt, "Сбор отменен пользователем"))
|
job.Logs = append(job.Logs, formatCollectLogLine(job.UpdatedAt, "Collection canceled by user"))
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelFn := job.cancel
|
cancelFn := job.cancel
|
||||||
@@ -122,6 +125,7 @@ func (m *JobManager) AppendJobLog(id, message string) (*Job, bool) {
|
|||||||
job.Logs = append(job.Logs, message)
|
job.Logs = append(job.Logs, message)
|
||||||
job.UpdatedAt = time.Now().UTC()
|
job.UpdatedAt = time.Now().UTC()
|
||||||
job.Logs[len(job.Logs)-1] = formatCollectLogLine(job.UpdatedAt, message)
|
job.Logs[len(job.Logs)-1] = formatCollectLogLine(job.UpdatedAt, message)
|
||||||
|
job.Message = message
|
||||||
|
|
||||||
cloned := cloneJob(job)
|
cloned := cloneJob(job)
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
@@ -202,7 +206,7 @@ func (m *JobManager) SkipJob(id string) (*Job, bool) {
|
|||||||
skipFn := job.skipFn
|
skipFn := job.skipFn
|
||||||
job.skipFn = nil
|
job.skipFn = nil
|
||||||
job.UpdatedAt = time.Now().UTC()
|
job.UpdatedAt = time.Now().UTC()
|
||||||
job.Logs = append(job.Logs, formatCollectLogLine(job.UpdatedAt, "Пропуск зависших запросов по команде пользователя"))
|
job.Logs = append(job.Logs, formatCollectLogLine(job.UpdatedAt, "Skipping stalled requests on user request"))
|
||||||
cloned := cloneJob(job)
|
cloned := cloneJob(job)
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
|
||||||
@@ -212,6 +216,18 @@ func (m *JobManager) SkipJob(id string) (*Job, bool) {
|
|||||||
return cloned, true
|
return cloned, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *JobManager) SetJobResult(id string, result map[string]interface{}) bool {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
job, ok := m.jobs[id]
|
||||||
|
if !ok || job == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
job.Result = result
|
||||||
|
job.UpdatedAt = time.Now().UTC()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (m *JobManager) AttachJobCancel(id string, cancelFn context.CancelFunc) bool {
|
func (m *JobManager) AttachJobCancel(id string, cancelFn context.CancelFunc) bool {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
@@ -265,6 +281,9 @@ func cloneJob(job *Job) *Job {
|
|||||||
cloned.DebugInfo = cloneCollectDebugInfo(job.DebugInfo)
|
cloned.DebugInfo = cloneCollectDebugInfo(job.DebugInfo)
|
||||||
cloned.CurrentPhase = job.CurrentPhase
|
cloned.CurrentPhase = job.CurrentPhase
|
||||||
cloned.ETASeconds = job.ETASeconds
|
cloned.ETASeconds = job.ETASeconds
|
||||||
|
if job.Result != nil {
|
||||||
|
cloned.Result = maps.Clone(job.Result)
|
||||||
|
}
|
||||||
cloned.cancel = nil
|
cloned.cancel = nil
|
||||||
cloned.skipFn = nil
|
cloned.skipFn = nil
|
||||||
return &cloned
|
return &cloned
|
||||||
|
|||||||
Reference in New Issue
Block a user