Files
logpile/internal/collector/redfish.go
Michael Chus 57de3ba6eb 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>
2026-06-13 14:35:39 +03:00

6760 lines
204 KiB
Go

package collector
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
"os"
"path"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"git.mchus.pro/mchus/logpile/internal/collector/redfishprofile"
"git.mchus.pro/mchus/logpile/internal/models"
"git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids"
)
type RedfishConnector struct {
timeout time.Duration
debug bool
debugSnapshot bool
}
type redfishPrefetchMetrics struct {
Enabled bool
Candidates int
Targets int
Docs int
Added int
Duration time.Duration
SkipReason string
}
type redfishPostProbeMetrics struct {
NVMECandidates int
NVMESelected int
NVMEAdded int
CollectionCandidates int
CollectionSelected int
SkippedExplicit int
Added int
Duration time.Duration
}
type redfishRequestTelemetry struct {
mu sync.Mutex
overall redfishPhaseTelemetry
byPhase map[string]*redfishPhaseTelemetry
}
type redfishTelemetrySummary struct {
Requests int
Errors int
ErrorRate float64
Avg time.Duration
P95 time.Duration
}
type redfishTelemetryContextKey struct{}
type redfishTelemetryPhaseContextKey struct{}
type redfishPhaseTelemetry struct {
requests int
errors int
durations []time.Duration
lastAvg time.Duration
lastP95 time.Duration
}
func NewRedfishConnector() *RedfishConnector {
debug := false
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_DEBUG")); v != "" && v != "0" && !strings.EqualFold(v, "false") {
debug = true
}
debugSnapshot := false
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_SNAPSHOT_DEBUG")); v != "" && v != "0" && !strings.EqualFold(v, "false") {
debugSnapshot = true
}
return &RedfishConnector{
timeout: 10 * time.Second,
debug: debug,
debugSnapshot: debugSnapshot || debug,
}
}
func (c *RedfishConnector) Protocol() string {
return "redfish"
}
func (c *RedfishConnector) Probe(ctx context.Context, req Request) (*ProbeResult, error) {
baseURL, err := c.baseURL(req)
if err != nil {
return nil, err
}
client := c.httpClientWithTimeout(req, c.timeout)
if _, err := c.getJSON(ctx, client, req, baseURL, "/redfish/v1"); err != nil {
return nil, fmt.Errorf("redfish service root: %w", err)
}
systemPaths := c.discoverMemberPaths(ctx, client, req, baseURL, "/redfish/v1/Systems", "/redfish/v1/Systems/1")
primarySystem := firstNonEmptyPath(systemPaths, "/redfish/v1/Systems/1")
systemDoc, err := c.getJSON(ctx, client, req, baseURL, primarySystem)
if err != nil {
return nil, fmt.Errorf("redfish system: %w", err)
}
powerState := redfishSystemPowerState(systemDoc)
return &ProbeResult{
Reachable: true,
Protocol: "redfish",
HostPowerState: powerState,
HostPoweredOn: isRedfishHostPoweredOn(powerState),
SystemPath: primarySystem,
}, nil
}
func (c *RedfishConnector) debugf(format string, args ...interface{}) {
if !c.debug {
return
}
slog.Debug("redfish-debug: " + fmt.Sprintf(format, args...))
}
func (c *RedfishConnector) debugSnapshotf(format string, args ...interface{}) {
if !c.debugSnapshot {
return
}
slog.Debug("redfish-snapshot-debug: " + fmt.Sprintf(format, args...))
}
func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit ProgressFn) (*models.AnalysisResult, error) {
collectStart := time.Now()
telemetry := newRedfishRequestTelemetry()
ctx = context.WithValue(ctx, redfishTelemetryContextKey{}, telemetry)
baseURL, err := c.baseURL(req)
if err != nil {
return nil, err
}
snapshotClient := c.httpClientWithTimeout(req, redfishSnapshotRequestTimeout())
prefetchClient := c.httpClientWithTimeout(req, redfishPrefetchRequestTimeout())
criticalClient := c.httpClientWithTimeout(req, redfishCriticalRequestTimeout())
hintClient := c.httpClientWithTimeout(req, 4*time.Second)
if emit != nil {
emit(Progress{Status: "running", Progress: 10, Message: "Redfish: connecting to BMC..."})
}
discoveryCtx := withRedfishTelemetryPhase(ctx, "discovery")
serviceRootDoc, err := c.getJSON(discoveryCtx, snapshotClient, req, baseURL, "/redfish/v1")
if err != nil {
return nil, fmt.Errorf("redfish service root: %w", err)
}
systemPaths := c.discoverMemberPaths(discoveryCtx, snapshotClient, req, baseURL, "/redfish/v1/Systems", "/redfish/v1/Systems/1")
primarySystem := firstNonEmptyPath(systemPaths, "/redfish/v1/Systems/1")
chassisPaths := c.discoverMemberPaths(discoveryCtx, snapshotClient, req, baseURL, "/redfish/v1/Chassis", "/redfish/v1/Chassis/1")
managerPaths := c.discoverMemberPaths(discoveryCtx, snapshotClient, req, baseURL, "/redfish/v1/Managers", "/redfish/v1/Managers/1")
primaryChassis := firstNonEmptyPath(chassisPaths, "/redfish/v1/Chassis/1")
primaryManager := firstNonEmptyPath(managerPaths, "/redfish/v1/Managers/1")
systemDoc, _ := c.getJSON(discoveryCtx, snapshotClient, req, baseURL, primarySystem)
chassisDoc, _ := c.getJSON(discoveryCtx, snapshotClient, req, baseURL, primaryChassis)
managerDoc, _ := c.getJSON(discoveryCtx, snapshotClient, req, baseURL, primaryManager)
resourceHints := append(append([]string{}, systemPaths...), append(chassisPaths, managerPaths...)...)
hintDocs := c.collectProfileHintDocs(discoveryCtx, hintClient, req, baseURL, primarySystem, primaryChassis)
signals := redfishprofile.CollectSignals(serviceRootDoc, systemDoc, chassisDoc, managerDoc, resourceHints, hintDocs...)
matchResult := redfishprofile.MatchProfiles(signals)
acquisitionPlan := redfishprofile.BuildAcquisitionPlan(signals)
telemetrySummary := telemetry.Snapshot()
phaseTelemetry := telemetry.PhaseSnapshots()
adaptedTuning, throttled := adaptRedfishAcquisitionTuning(acquisitionPlan.Tuning, telemetrySummary)
acquisitionPlan.Tuning = adaptedTuning
activeModules := make([]ModuleActivation, 0, len(matchResult.Scores))
moduleScores := make([]ModuleScore, 0, len(matchResult.Scores))
for _, score := range matchResult.Scores {
moduleScores = append(moduleScores, ModuleScore{
Name: score.Name,
Score: score.Score,
Active: score.Active,
Priority: score.Priority,
})
if score.Active {
activeModules = append(activeModules, ModuleActivation{Name: score.Name, Score: score.Score})
}
}
if emit != nil {
emit(Progress{
Status: "running",
Progress: 25,
Message: fmt.Sprintf("Redfish: profiles mode=%s active=%s", acquisitionPlan.Mode, formatActiveModuleLog(activeModules)),
ActiveModules: activeModules,
ModuleScores: moduleScores,
DebugInfo: &CollectDebugInfo{
AdaptiveThrottled: throttled,
SnapshotWorkers: acquisitionPlan.Tuning.SnapshotWorkers,
PrefetchWorkers: acquisitionPlan.Tuning.PrefetchWorkers,
PrefetchEnabled: acquisitionPlan.Tuning.PrefetchEnabled,
PhaseTelemetry: buildCollectPhaseTelemetry(phaseTelemetry),
},
})
if throttled {
emit(Progress{
Status: "running",
Progress: 26,
Message: fmt.Sprintf("Redfish: adaptive throttling p95=%dms err=%.0f%% workers=%d prefetch=%d", telemetrySummary.P95.Milliseconds(), telemetrySummary.ErrorRate*100, acquisitionPlan.Tuning.SnapshotWorkers, acquisitionPlan.Tuning.PrefetchWorkers),
ActiveModules: activeModules,
ModuleScores: moduleScores,
DebugInfo: &CollectDebugInfo{
AdaptiveThrottled: throttled,
SnapshotWorkers: acquisitionPlan.Tuning.SnapshotWorkers,
PrefetchWorkers: acquisitionPlan.Tuning.PrefetchWorkers,
PrefetchEnabled: acquisitionPlan.Tuning.PrefetchEnabled,
PhaseTelemetry: buildCollectPhaseTelemetry(phaseTelemetry),
},
})
}
}
resolvedPlan := redfishprofile.ResolveAcquisitionPlan(matchResult, acquisitionPlan, redfishprofile.DiscoveredResources{
SystemPaths: systemPaths,
ChassisPaths: chassisPaths,
ManagerPaths: managerPaths,
}, signals)
acquisitionPlan = resolvedPlan.Plan
seedPaths := resolvedPlan.SeedPaths
criticalPaths := resolvedPlan.CriticalPaths
if len(acquisitionPlan.Profiles) > 0 {
slog.Info("redfish-profile-plan",
"mode", acquisitionPlan.Mode,
"profiles", strings.Join(acquisitionPlan.Profiles, ","),
"notes", strings.Join(acquisitionPlan.Notes, "; "),
"scores", formatModuleScoreLog(moduleScores),
"req", telemetrySummary.Requests,
"err", telemetrySummary.Errors,
"p95_ms", telemetrySummary.P95.Milliseconds(),
"avg_ms", telemetrySummary.Avg.Milliseconds(),
"throttled", throttled,
)
}
if emit != nil {
emit(Progress{
Status: "running",
Progress: 30,
Message: "Redfish: reading Redfish structure...",
CurrentPhase: "snapshot",
ETASeconds: acquisitionPlan.Tuning.ETABaseline.SnapshotSeconds,
})
}
if emit != nil {
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: preparing extended 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).
// Cancelling it via the skip signal aborts only the collection phases while
// leaving the replay phase intact so results from already-fetched data are preserved.
collectCtx, cancelCollect := context.WithCancel(ctx)
defer cancelCollect()
if req.SkipHungCh != nil {
go func() {
select {
case <-req.SkipHungCh:
if emit != nil {
emit(Progress{
Status: "running",
Progress: 97,
Message: "Redfish: skipping stalled requests, analyzing collected data...",
})
}
slog.Info("redfish: skip-hung triggered, cancelling collection phases")
cancelCollect()
case <-ctx.Done():
}
}()
}
c.debugSnapshotf("snapshot crawl start host=%s port=%d", req.Host, req.Port)
rawTree, fetchErrors, postProbeMetrics, snapshotTimingSummary := c.collectRawRedfishTree(withRedfishTelemetryPhase(collectCtx, "snapshot"), snapshotClient, req, baseURL, seedPaths, acquisitionPlan.Tuning, emit)
c.debugSnapshotf("snapshot crawl done docs=%d", len(rawTree))
fetchErrMap := redfishFetchErrorListToMap(fetchErrors)
prefetchedCritical, prefetchMetrics := c.prefetchCriticalRedfishDocs(withRedfishTelemetryPhase(collectCtx, "prefetch"), prefetchClient, req, baseURL, criticalPaths, rawTree, fetchErrMap, acquisitionPlan.Tuning, emit)
for p, doc := range prefetchedCritical {
if _, exists := rawTree[p]; exists {
continue
}
rawTree[p] = doc
prefetchMetrics.Added++
}
for p := range prefetchedCritical {
delete(fetchErrMap, p)
}
slog.Info("redfish-prefetch-metrics",
"enabled", prefetchMetrics.Enabled,
"candidates", prefetchMetrics.Candidates,
"targets", prefetchMetrics.Targets,
"docs", prefetchMetrics.Docs,
"added", prefetchMetrics.Added,
"dur", prefetchMetrics.Duration.Round(time.Millisecond),
"skip", firstNonEmpty(prefetchMetrics.SkipReason, "-"),
)
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)
}
if recoveredN := c.recoverProfilePlanBDocs(withRedfishTelemetryPhase(collectCtx, "profile_plan_b"), criticalClient, req, baseURL, acquisitionPlan, rawTree, emit); recoveredN > 0 {
c.debugSnapshotf("profile plan-b recovered docs=%d", recoveredN)
}
// Hide transient fetch errors for endpoints that were eventually recovered into rawTree.
for p := range fetchErrMap {
if _, ok := rawTree[p]; ok {
delete(fetchErrMap, p)
}
}
if emit != nil {
emit(Progress{Status: "running", Progress: 99, Message: "Redfish: analyzing raw snapshot..."})
}
// 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)
var debugPayloads map[string]any
if req.DebugPayloads {
debugPayloads = c.collectDebugPayloads(ctx, snapshotClient, req, baseURL, systemPaths)
}
rawPayloads := map[string]any{
"redfish_tree": rawTree,
"redfish_profiles": map[string]any{
"mode": acquisitionPlan.Mode,
"profiles": acquisitionPlan.Profiles,
"active_modules": activeModules,
"module_scores": moduleScores,
"notes": acquisitionPlan.Notes,
"plan_b_paths": acquisitionPlan.PlanBPaths,
"scoped_paths": map[string]any{
"system_seed_suffixes": acquisitionPlan.ScopedPaths.SystemSeedSuffixes,
"system_critical_suffixes": acquisitionPlan.ScopedPaths.SystemCriticalSuffixes,
"chassis_seed_suffixes": acquisitionPlan.ScopedPaths.ChassisSeedSuffixes,
"chassis_critical_suffixes": acquisitionPlan.ScopedPaths.ChassisCriticalSuffixes,
"manager_seed_suffixes": acquisitionPlan.ScopedPaths.ManagerSeedSuffixes,
"manager_critical_suffixes": acquisitionPlan.ScopedPaths.ManagerCriticalSuffixes,
},
"tuning": map[string]any{
"snapshot_max_documents": acquisitionPlan.Tuning.SnapshotMaxDocuments,
"snapshot_workers": acquisitionPlan.Tuning.SnapshotWorkers,
"snapshot_exclude_contains": acquisitionPlan.Tuning.SnapshotExcludeContains,
"prefetch_workers": acquisitionPlan.Tuning.PrefetchWorkers,
"prefetch_enabled": boolPointerValue(acquisitionPlan.Tuning.PrefetchEnabled),
"nvme_post_probe": boolPointerValue(acquisitionPlan.Tuning.NVMePostProbeEnabled),
"post_probe_policy": map[string]any{
"direct_nvme_disk_bay": acquisitionPlan.Tuning.PostProbePolicy.EnableDirectNVMEDiskBayProbe,
"numeric_collection": acquisitionPlan.Tuning.PostProbePolicy.EnableNumericCollectionProbe,
"sensor_collection": acquisitionPlan.Tuning.PostProbePolicy.EnableSensorCollectionProbe,
},
"prefetch_policy": map[string]any{
"include_suffixes": acquisitionPlan.Tuning.PrefetchPolicy.IncludeSuffixes,
"exclude_contains": acquisitionPlan.Tuning.PrefetchPolicy.ExcludeContains,
},
"recovery_policy": map[string]any{
"critical_collection_member_retry": acquisitionPlan.Tuning.RecoveryPolicy.EnableCriticalCollectionMemberRetry,
"critical_slow_probe": acquisitionPlan.Tuning.RecoveryPolicy.EnableCriticalSlowProbe,
"profile_plan_b": acquisitionPlan.Tuning.RecoveryPolicy.EnableProfilePlanB,
},
"rate_policy": map[string]any{
"target_p95_latency_ms": acquisitionPlan.Tuning.RatePolicy.TargetP95LatencyMS,
"throttle_p95_latency_ms": acquisitionPlan.Tuning.RatePolicy.ThrottleP95LatencyMS,
"min_snapshot_workers": acquisitionPlan.Tuning.RatePolicy.MinSnapshotWorkers,
"min_prefetch_workers": acquisitionPlan.Tuning.RatePolicy.MinPrefetchWorkers,
"disable_prefetch_error": acquisitionPlan.Tuning.RatePolicy.DisablePrefetchOnErrors,
},
"eta_baseline": map[string]any{
"discovery_seconds": acquisitionPlan.Tuning.ETABaseline.DiscoverySeconds,
"snapshot_seconds": acquisitionPlan.Tuning.ETABaseline.SnapshotSeconds,
"prefetch_seconds": acquisitionPlan.Tuning.ETABaseline.PrefetchSeconds,
"critical_plan_b_seconds": acquisitionPlan.Tuning.ETABaseline.CriticalPlanBSeconds,
"profile_plan_b_seconds": acquisitionPlan.Tuning.ETABaseline.ProfilePlanBSeconds,
},
},
"request_telemetry": map[string]any{
"requests": telemetry.Snapshot().Requests,
"errors": telemetry.Snapshot().Errors,
"error_rate": telemetry.Snapshot().ErrorRate,
"avg_ms": telemetry.Snapshot().Avg.Milliseconds(),
"p95_ms": telemetry.Snapshot().P95.Milliseconds(),
"phases": redfishPhaseTelemetryPayload(phaseTelemetry),
},
},
"redfish_telemetry": map[string]any{
"requests": telemetrySummary.Requests,
"errors": telemetrySummary.Errors,
"error_rate": telemetrySummary.ErrorRate,
"avg_ms": telemetrySummary.Avg.Milliseconds(),
"p95_ms": telemetrySummary.P95.Milliseconds(),
"adaptive_throttled": throttled,
"snapshot_timing_top": snapshotTimingSummary,
"snapshot_workers": acquisitionPlan.Tuning.SnapshotWorkers,
"prefetch_workers": acquisitionPlan.Tuning.PrefetchWorkers,
"prefetch_enabled": boolPointerValue(acquisitionPlan.Tuning.PrefetchEnabled),
"nvme_post_probe": boolPointerValue(acquisitionPlan.Tuning.NVMePostProbeEnabled),
"post_probe_policy": map[string]any{
"direct_nvme_disk_bay": acquisitionPlan.Tuning.PostProbePolicy.EnableDirectNVMEDiskBayProbe,
"numeric_collection": acquisitionPlan.Tuning.PostProbePolicy.EnableNumericCollectionProbe,
"sensor_collection": acquisitionPlan.Tuning.PostProbePolicy.EnableSensorCollectionProbe,
},
"prefetch_policy": map[string]any{
"include_suffixes": acquisitionPlan.Tuning.PrefetchPolicy.IncludeSuffixes,
"exclude_contains": acquisitionPlan.Tuning.PrefetchPolicy.ExcludeContains,
},
"recovery_policy": map[string]any{
"critical_collection_member_retry": acquisitionPlan.Tuning.RecoveryPolicy.EnableCriticalCollectionMemberRetry,
"critical_slow_probe": acquisitionPlan.Tuning.RecoveryPolicy.EnableCriticalSlowProbe,
"profile_plan_b": acquisitionPlan.Tuning.RecoveryPolicy.EnableProfilePlanB,
},
"target_p95_latency_ms": acquisitionPlan.Tuning.RatePolicy.TargetP95LatencyMS,
"throttle_p95_latency_ms": acquisitionPlan.Tuning.RatePolicy.ThrottleP95LatencyMS,
"eta_baseline": map[string]any{
"discovery_seconds": acquisitionPlan.Tuning.ETABaseline.DiscoverySeconds,
"snapshot_seconds": acquisitionPlan.Tuning.ETABaseline.SnapshotSeconds,
"prefetch_seconds": acquisitionPlan.Tuning.ETABaseline.PrefetchSeconds,
"critical_plan_b_seconds": acquisitionPlan.Tuning.ETABaseline.CriticalPlanBSeconds,
"profile_plan_b_seconds": acquisitionPlan.Tuning.ETABaseline.ProfilePlanBSeconds,
},
"phases": redfishPhaseTelemetryPayload(phaseTelemetry),
},
}
if len(fetchErrMap) > 0 {
rawPayloads["redfish_fetch_errors"] = redfishFetchErrorMapToList(fetchErrMap)
}
if len(rawLogEntries) > 0 {
rawPayloads["redfish_log_entries"] = rawLogEntries
}
if len(debugPayloads) > 0 {
rawPayloads["redfish_debug_payloads"] = debugPayloads
}
// Unified tunnel: live collection and raw import go through the same analyzer over redfish_tree.
result, err := ReplayRedfishFromRawPayloads(rawPayloads, nil)
if err != nil {
return nil, err
}
totalElapsed := time.Since(collectStart).Round(time.Second)
if !result.InventoryLastModifiedAt.IsZero() {
slog.Info("redfish-collect: inventory last modified",
"at", result.InventoryLastModifiedAt.Format(time.RFC3339),
"age", time.Since(result.InventoryLastModifiedAt).Round(time.Minute),
)
}
slog.Info("redfish-postprobe-metrics",
"nvme_candidates", postProbeMetrics.NVMECandidates,
"nvme_selected", postProbeMetrics.NVMESelected,
"nvme_added", postProbeMetrics.NVMEAdded,
"candidates", postProbeMetrics.CollectionCandidates,
"selected", postProbeMetrics.CollectionSelected,
"skipped_explicit", postProbeMetrics.SkippedExplicit,
"added", postProbeMetrics.Added,
"dur", postProbeMetrics.Duration.Round(time.Millisecond),
)
slog.Info("redfish-telemetry",
"req", telemetrySummary.Requests,
"err", telemetrySummary.Errors,
"err_rate", telemetrySummary.ErrorRate,
"avg_ms", telemetrySummary.Avg.Milliseconds(),
"p95_ms", telemetrySummary.P95.Milliseconds(),
"throttled", throttled,
"snapshot_workers", acquisitionPlan.Tuning.SnapshotWorkers,
"prefetch_workers", acquisitionPlan.Tuning.PrefetchWorkers,
"timing_top", firstNonEmpty(snapshotTimingSummary, "-"),
)
for _, line := range redfishPhaseTelemetryLogLines(phaseTelemetry) {
slog.Info("redfish-telemetry-phase", "line", line)
}
slog.Info("redfish-collect: completed", "elapsed", totalElapsed, "docs", len(rawTree), "fetch_errors", len(fetchErrMap))
if emit != nil {
emit(Progress{
Status: "running",
Progress: 99,
Message: fmt.Sprintf("Redfish telemetry: req=%d err=%d p95=%dms throttled=%t", telemetrySummary.Requests, telemetrySummary.Errors, telemetrySummary.P95.Milliseconds(), throttled),
DebugInfo: &CollectDebugInfo{
AdaptiveThrottled: throttled,
SnapshotWorkers: acquisitionPlan.Tuning.SnapshotWorkers,
PrefetchWorkers: acquisitionPlan.Tuning.PrefetchWorkers,
PrefetchEnabled: acquisitionPlan.Tuning.PrefetchEnabled,
PhaseTelemetry: buildCollectPhaseTelemetry(phaseTelemetry),
},
})
emit(Progress{
Status: "running",
Progress: 100,
Message: fmt.Sprintf("Redfish: collection completed in %s", totalElapsed),
})
}
return result, nil
}
// collectDebugPayloads fetches vendor-specific diagnostic endpoints on a best-effort basis.
// Results are stored in rawPayloads["redfish_debug_payloads"] and exported with the bundle.
// Enabled only when Request.DebugPayloads is true.
func (c *RedfishConnector) collectDebugPayloads(ctx context.Context, client *http.Client, req Request, baseURL string, systemPaths []string) map[string]any {
out := map[string]any{}
for _, systemPath := range systemPaths {
// AMI/MSI: inventory CRC groups — reveals which groups are supported by this BMC.
if doc, err := c.getJSON(ctx, client, req, baseURL, joinPath(systemPath, "/Oem/Ami/Inventory/Crc")); err == nil {
out[joinPath(systemPath, "/Oem/Ami/Inventory/Crc")] = doc
}
}
return out
}
func firstNonEmptyPath(paths []string, fallback string) string {
for _, p := range paths {
if strings.TrimSpace(p) != "" {
return p
}
}
return fallback
}
func isRedfishHostPoweredOn(state string) bool {
switch strings.ToLower(strings.TrimSpace(state)) {
case "on", "poweringon":
return true
default:
return false
}
}
func redfishSystemPowerState(systemDoc map[string]interface{}) string {
if len(systemDoc) == 0 {
return ""
}
if state := strings.TrimSpace(asString(systemDoc["PowerState"])); state != "" {
return state
}
if summary, ok := systemDoc["PowerSummary"].(map[string]interface{}); ok {
return strings.TrimSpace(asString(summary["PowerState"]))
}
return ""
}
func (c *RedfishConnector) postJSON(ctx context.Context, client *http.Client, req Request, baseURL, resourcePath string, payload map[string]any) error {
body, err := json.Marshal(payload)
if err != nil {
return err
}
target := strings.TrimSpace(resourcePath)
if !strings.HasPrefix(strings.ToLower(target), "http://") && !strings.HasPrefix(strings.ToLower(target), "https://") {
target = baseURL + normalizeRedfishPath(target)
}
u, err := url.Parse(target)
if err != nil {
return err
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), strings.NewReader(string(body)))
if err != nil {
return err
}
httpReq.Header.Set("Content-Type", "application/json")
switch req.AuthType {
case "password":
httpReq.SetBasicAuth(req.Username, req.Password)
case "token":
httpReq.Header.Set("Authorization", "Bearer "+req.Token)
}
resp, err := client.Do(httpReq)
if err != nil {
return err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("status %d from %s: %s", resp.StatusCode, resourcePath, strings.TrimSpace(string(respBody)))
}
return nil
}
func (c *RedfishConnector) prefetchCriticalRedfishDocs(
ctx context.Context,
client *http.Client,
req Request,
baseURL string,
criticalPaths []string,
rawTree map[string]interface{},
fetchErrMap map[string]string,
tuning redfishprofile.AcquisitionTuning,
emit ProgressFn,
) (map[string]interface{}, redfishPrefetchMetrics) {
metrics := redfishPrefetchMetrics{
Enabled: redfishPrefetchEnabled(tuning),
}
if !metrics.Enabled || len(criticalPaths) == 0 {
metrics.SkipReason = "disabled-or-empty"
return nil, metrics
}
candidates := redfishPrefetchTargets(criticalPaths, tuning)
metrics.Candidates = len(candidates)
if len(candidates) == 0 {
metrics.SkipReason = "no-candidates"
return nil, metrics
}
targets := redfishAdaptivePrefetchTargets(candidates, rawTree, fetchErrMap)
metrics.Targets = len(targets)
if len(targets) == 0 {
metrics.SkipReason = "not-needed"
if emit != nil {
emit(Progress{
Status: "running",
Progress: 96,
Message: fmt.Sprintf("Redfish: prefetch skipped (adaptive, candidates=%d)", metrics.Candidates),
})
}
return nil, metrics
}
if emit != nil {
emit(Progress{
Status: "running",
Progress: 96,
Message: fmt.Sprintf("Redfish: prefetch critical endpoints (adaptive %d/%d)...", len(targets), len(candidates)),
CurrentPhase: "prefetch",
ETASeconds: int(estimateProgressETA(time.Now(), 0, len(targets), 2*time.Second).Seconds()),
})
}
start := time.Now()
out := make(map[string]interface{}, len(targets))
seen := make(map[string]struct{}, len(targets))
var mu sync.Mutex
addDoc := func(path string, doc map[string]interface{}) {
path = normalizeRedfishPath(path)
if path == "" || len(doc) == 0 {
return
}
mu.Lock()
if _, exists := seen[path]; !exists {
seen[path] = struct{}{}
out[path] = doc
}
mu.Unlock()
}
workerN := redfishPrefetchWorkers(tuning)
jobs := make(chan string, len(targets))
var wg sync.WaitGroup
for i := 0; i < workerN; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for p := range jobs {
doc, err := c.getJSONWithRetry(ctx, client, req, baseURL, p, redfishPrefetchRetryAttempts(), redfishPrefetchRetryBackoff())
if err != nil {
continue
}
addDoc(p, doc)
memberPaths := redfishCollectionMemberRefs(doc)
if len(memberPaths) == 0 {
continue
}
if maxMembers := redfishPrefetchMemberRecoveryMax(); maxMembers > 0 && len(memberPaths) > maxMembers {
memberPaths = memberPaths[:maxMembers]
}
for _, memberPath := range memberPaths {
memberPath = normalizeRedfishPath(memberPath)
if memberPath == "" {
continue
}
mu.Lock()
_, exists := seen[memberPath]
mu.Unlock()
if exists {
continue
}
memberDoc, err := c.getJSONWithRetry(ctx, client, req, baseURL, memberPath, redfishPrefetchMemberRetryAttempts(), redfishPrefetchRetryBackoff())
if err != nil {
continue
}
addDoc(memberPath, memberDoc)
}
}
}()
}
for _, p := range targets {
select {
case jobs <- p:
case <-ctx.Done():
close(jobs)
wg.Wait()
metrics.Docs = len(out)
metrics.Duration = time.Since(start)
metrics.SkipReason = "ctx-cancelled"
return out, metrics
}
}
close(jobs)
wg.Wait()
metrics.Docs = len(out)
metrics.Duration = time.Since(start)
if emit != nil {
emit(Progress{
Status: "running",
Progress: 96,
Message: fmt.Sprintf("Redfish: prefetch completed (adaptive targets=%d, docs=%d)", len(targets), len(out)),
CurrentPhase: "prefetch",
})
}
return out, metrics
}
func redfishAdaptivePrefetchTargets(candidates []string, rawTree map[string]interface{}, fetchErrs map[string]string) []string {
out := make([]string, 0, len(candidates))
seen := make(map[string]struct{}, len(candidates))
for _, p := range candidates {
p = normalizeRedfishPath(p)
if p == "" {
continue
}
if _, exists := seen[p]; exists {
continue
}
needsFetch := false
docAny, inTree := rawTree[p]
if !inTree {
needsFetch = true
if msg, hasErr := fetchErrs[p]; hasErr && !isRetryableRedfishFetchError(fmt.Errorf("%s", msg)) {
needsFetch = false
}
} else if doc, ok := docAny.(map[string]interface{}); ok {
needsFetch = redfishCollectionNeedsMemberRecovery(doc, rawTree, fetchErrs)
}
if !needsFetch {
continue
}
seen[p] = struct{}{}
out = append(out, p)
}
return out
}
func redfishCollectionNeedsMemberRecovery(collectionDoc map[string]interface{}, rawTree map[string]interface{}, fetchErrs map[string]string) bool {
memberPaths := redfishCollectionMemberRefs(collectionDoc)
if len(memberPaths) == 0 {
return false
}
for _, memberPath := range memberPaths {
memberPath = normalizeRedfishPath(memberPath)
if memberPath == "" {
continue
}
if _, exists := rawTree[memberPath]; exists {
continue
}
if msg, hasErr := fetchErrs[memberPath]; hasErr && !isRetryableRedfishFetchError(fmt.Errorf("%s", msg)) {
continue
}
return true
}
return false
}
func (c *RedfishConnector) httpClient(req Request) *http.Client {
return c.httpClientWithTimeout(req, c.timeout)
}
func (c *RedfishConnector) httpClientWithTimeout(req Request, timeout time.Duration) *http.Client {
transport := &http.Transport{}
if req.TLSMode == "insecure" {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec
}
return &http.Client{
Transport: transport,
Timeout: timeout,
}
}
func (c *RedfishConnector) baseURL(req Request) (string, error) {
host := strings.TrimSpace(req.Host)
if host == "" {
return "", fmt.Errorf("empty host")
}
if strings.HasPrefix(host, "http://") || strings.HasPrefix(host, "https://") {
u, err := url.Parse(host)
if err != nil {
return "", fmt.Errorf("invalid host URL: %w", err)
}
u.Path = ""
u.RawQuery = ""
u.Fragment = ""
return strings.TrimRight(u.String(), "/"), nil
}
scheme := "https"
if req.TLSMode == "insecure" && req.Port == 80 {
scheme = "http"
}
return fmt.Sprintf("%s://%s:%d", scheme, host, req.Port), nil
}
func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Client, req Request, baseURL, systemPath string) []models.Storage {
var out []models.Storage
storageMembers, _ := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(systemPath, "/Storage"))
for _, member := range storageMembers {
// "Drives" can be embedded refs or a link to a collection.
if driveCollection, ok := member["Drives"].(map[string]interface{}); ok {
if driveCollectionPath := asString(driveCollection["@odata.id"]); driveCollectionPath != "" {
driveDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, driveCollectionPath)
if err == nil {
for _, driveDoc := range driveDocs {
if isVirtualStorageDrive(driveDoc) {
continue
}
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
}
if len(driveDocs) == 0 {
for _, driveDoc := range c.probeDirectDiskBayChildren(ctx, client, req, baseURL, driveCollectionPath) {
if isVirtualStorageDrive(driveDoc) {
continue
}
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
}
}
}
continue
}
}
if drives, ok := member["Drives"].([]interface{}); ok {
for _, driveAny := range drives {
driveRef, ok := driveAny.(map[string]interface{})
if !ok {
continue
}
odata := asString(driveRef["@odata.id"])
if odata == "" {
continue
}
driveDoc, err := c.getJSON(ctx, client, req, baseURL, odata)
if err != nil {
continue
}
if isVirtualStorageDrive(driveDoc) {
continue
}
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
}
continue
}
// Some implementations return drive fields right in storage member object.
if looksLikeDrive(member) {
if isVirtualStorageDrive(member) {
continue
}
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, member, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(member, supplementalDocs...))
}
// Supermicro/RAID implementations can expose physical disks under chassis enclosures
// linked from Storage.Links.Enclosures, while Storage.Drives stays empty.
for _, enclosurePath := range redfishLinkRefs(member, "Links", "Enclosures") {
driveDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(enclosurePath, "/Drives"))
if err == nil {
for _, driveDoc := range driveDocs {
if looksLikeDrive(driveDoc) && !isVirtualStorageDrive(driveDoc) {
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
}
}
if len(driveDocs) == 0 {
for _, driveDoc := range c.probeDirectDiskBayChildren(ctx, client, req, baseURL, joinPath(enclosurePath, "/Drives")) {
if isVirtualStorageDrive(driveDoc) {
continue
}
out = append(out, parseDrive(driveDoc))
}
}
}
}
}
// IntelVROC often exposes rich drive inventory via dedicated child collections.
for _, driveDoc := range c.collectKnownStorageMembers(ctx, client, req, baseURL, systemPath, []string{
"/Storage/IntelVROC/Drives",
"/Storage/IntelVROC/Controllers/1/Drives",
}) {
if looksLikeDrive(driveDoc) && !isVirtualStorageDrive(driveDoc) {
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
}
}
// Fallback for platforms that expose disks in SimpleStorage.
simpleStorageMembers, _ := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(systemPath, "/SimpleStorage"))
for _, member := range simpleStorageMembers {
devices, ok := member["Devices"].([]interface{})
if !ok {
continue
}
for _, devAny := range devices {
devDoc, ok := devAny.(map[string]interface{})
if !ok || !looksLikeDrive(devDoc) || isVirtualStorageDrive(devDoc) {
continue
}
out = append(out, parseDriveWithSupplementalDocs(devDoc))
}
}
// Fallback for platforms exposing physical drives under Chassis.
chassisPaths := c.discoverMemberPaths(ctx, client, req, baseURL, "/redfish/v1/Chassis", "/redfish/v1/Chassis/1")
for _, chassisPath := range chassisPaths {
driveDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(chassisPath, "/Drives"))
if err != nil {
continue
}
for _, driveDoc := range driveDocs {
if !looksLikeDrive(driveDoc) || isVirtualStorageDrive(driveDoc) {
continue
}
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
}
}
for _, chassisPath := range chassisPaths {
if !isSupermicroNVMeBackplanePath(chassisPath) {
continue
}
for _, driveDoc := range c.probeSupermicroNVMeDiskBays(ctx, client, req, baseURL, chassisPath) {
if !looksLikeDrive(driveDoc) || isVirtualStorageDrive(driveDoc) {
continue
}
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
}
}
out = dedupeStorage(out)
return out
}
func (c *RedfishConnector) collectStorageVolumes(ctx context.Context, client *http.Client, req Request, baseURL, systemPath string) []models.StorageVolume {
var out []models.StorageVolume
storageMembers, _ := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(systemPath, "/Storage"))
for _, member := range storageMembers {
controller := firstNonEmpty(asString(member["Id"]), asString(member["Name"]))
volumeCollectionPath := redfishLinkedPath(member, "Volumes")
if volumeCollectionPath == "" {
continue
}
volumeDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, volumeCollectionPath)
if err != nil {
continue
}
for _, volDoc := range volumeDocs {
if !looksLikeVolume(volDoc) {
continue
}
out = append(out, parseStorageVolume(volDoc, controller))
}
}
for _, volDoc := range c.collectKnownStorageMembers(ctx, client, req, baseURL, systemPath, []string{
"/Storage/IntelVROC/Volumes",
"/Storage/HA-RAID/Volumes",
"/Storage/MRVL.HA-RAID/Volumes",
}) {
if !looksLikeVolume(volDoc) {
continue
}
out = append(out, parseStorageVolume(volDoc, storageControllerFromPath(asString(volDoc["@odata.id"]))))
}
return dedupeStorageVolumes(out)
}
func (c *RedfishConnector) collectNICs(ctx context.Context, client *http.Client, req Request, baseURL string, chassisPaths []string) []models.NetworkAdapter {
var nics []models.NetworkAdapter
for _, chassisPath := range chassisPaths {
adapterDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(chassisPath, "/NetworkAdapters"))
if err != nil {
continue
}
for _, doc := range adapterDocs {
nic := parseNIC(doc)
adapterFunctionDocs := c.getNetworkAdapterFunctionDocs(ctx, client, req, baseURL, doc)
for _, pciePath := range networkAdapterPCIeDevicePaths(doc) {
pcieDoc, err := c.getJSON(ctx, client, req, baseURL, pciePath)
if err != nil {
continue
}
functionDocs := c.getLinkedPCIeFunctions(ctx, client, req, baseURL, pcieDoc)
for _, adapterFnDoc := range adapterFunctionDocs {
functionDocs = append(functionDocs, c.getLinkedPCIeFunctions(ctx, client, req, baseURL, adapterFnDoc)...)
}
functionDocs = dedupeJSONDocsByPath(functionDocs)
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, pcieDoc, "EnvironmentMetrics", "Metrics")
for _, fn := range functionDocs {
supplementalDocs = append(supplementalDocs, c.getLinkedSupplementalDocs(ctx, client, req, baseURL, fn, "EnvironmentMetrics", "Metrics")...)
}
enrichNICFromPCIe(&nic, pcieDoc, functionDocs, supplementalDocs)
}
nics = append(nics, nic)
}
}
return dedupeNetworkAdapters(nics)
}
func (c *RedfishConnector) collectPSUs(ctx context.Context, client *http.Client, req Request, baseURL string, chassisPaths []string) []models.PSU {
var out []models.PSU
seen := make(map[string]int)
idx := 1
for _, chassisPath := range chassisPaths {
// Redfish 2022+/X14+ commonly uses PowerSubsystem as the primary source.
if memberDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(chassisPath, "/PowerSubsystem/PowerSupplies")); err == nil && len(memberDocs) > 0 {
for _, doc := range memberDocs {
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, doc, "EnvironmentMetrics", "Metrics")
idx = appendPSU(&out, seen, parsePSUWithSupplementalDocs(doc, idx, supplementalDocs...), idx)
}
continue
}
// Legacy source: embedded array in Chassis/<id>/Power.
if powerDoc, err := c.getJSON(ctx, client, req, baseURL, joinPath(chassisPath, "/Power")); err == nil {
if members, ok := powerDoc["PowerSupplies"].([]interface{}); ok && len(members) > 0 {
for _, item := range members {
doc, ok := item.(map[string]interface{})
if !ok {
continue
}
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, doc, "EnvironmentMetrics", "Metrics")
idx = appendPSU(&out, seen, parsePSUWithSupplementalDocs(doc, idx, supplementalDocs...), idx)
}
}
}
}
return out
}
func appendPSU(out *[]models.PSU, seen map[string]int, psu models.PSU, currentIdx int) int {
nextIdx := currentIdx + 1
keys := psuIdentityKeys(psu)
if len(keys) == 0 {
return nextIdx
}
for _, key := range keys {
if idx, ok := seen[key]; ok {
(*out)[idx] = mergePSUEntries((*out)[idx], psu)
for _, mergedKey := range psuIdentityKeys((*out)[idx]) {
seen[mergedKey] = idx
}
return nextIdx
}
}
idx := len(*out)
for _, key := range keys {
seen[key] = idx
}
*out = append(*out, psu)
return nextIdx
}
func (c *RedfishConnector) collectKnownStorageMembers(ctx context.Context, client *http.Client, req Request, baseURL, systemPath string, relativeCollections []string) []map[string]interface{} {
var out []map[string]interface{}
for _, rel := range relativeCollections {
docs, err := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(systemPath, rel))
if err != nil || len(docs) == 0 {
continue
}
out = append(out, docs...)
}
return out
}
func redfishLinkedPath(doc map[string]interface{}, key string) string {
if v, ok := doc[key].(map[string]interface{}); ok {
return asString(v["@odata.id"])
}
return ""
}
func (c *RedfishConnector) getLinkedSupplementalDocs(
ctx context.Context,
client *http.Client,
req Request,
baseURL string,
doc map[string]interface{},
keys ...string,
) []map[string]interface{} {
if len(doc) == 0 || len(keys) == 0 {
return nil
}
var out []map[string]interface{}
seen := make(map[string]struct{})
for _, key := range keys {
path := normalizeRedfishPath(redfishLinkedPath(doc, key))
if path == "" {
continue
}
if _, ok := seen[path]; ok {
continue
}
supplementalDoc, err := c.getJSON(ctx, client, req, baseURL, path)
if err != nil || len(supplementalDoc) == 0 {
continue
}
seen[path] = struct{}{}
out = append(out, supplementalDoc)
}
return out
}
func (c *RedfishConnector) collectGPUs(ctx context.Context, client *http.Client, req Request, baseURL string, systemPaths, chassisPaths []string) []models.GPU {
collections := make([]string, 0, len(systemPaths)*3+len(chassisPaths)*2)
for _, systemPath := range systemPaths {
collections = append(collections, joinPath(systemPath, "/PCIeDevices"))
collections = append(collections, joinPath(systemPath, "/Accelerators"))
collections = append(collections, joinPath(systemPath, "/GraphicsControllers"))
}
for _, chassisPath := range chassisPaths {
collections = append(collections, joinPath(chassisPath, "/PCIeDevices"))
collections = append(collections, joinPath(chassisPath, "/Accelerators"))
}
var out []models.GPU
seen := make(map[string]struct{})
idx := 1
for _, collectionPath := range collections {
memberDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, collectionPath)
if err != nil || len(memberDocs) == 0 {
continue
}
for _, doc := range memberDocs {
functionDocs := c.getLinkedPCIeFunctions(ctx, client, req, baseURL, doc)
if !looksLikeGPU(doc, functionDocs) {
continue
}
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, doc, "EnvironmentMetrics", "Metrics")
for _, fn := range functionDocs {
supplementalDocs = append(supplementalDocs, c.getLinkedSupplementalDocs(ctx, client, req, baseURL, fn, "EnvironmentMetrics", "Metrics")...)
}
gpu := parseGPUWithSupplementalDocs(doc, functionDocs, supplementalDocs, idx)
idx++
if shouldSkipGenericGPUDuplicate(out, gpu) {
continue
}
key := gpuDocDedupKey(doc, gpu)
if key == "" {
continue
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, gpu)
}
}
return dropModelOnlyGPUPlaceholders(out)
}
func (c *RedfishConnector) collectPCIeDevices(ctx context.Context, client *http.Client, req Request, baseURL string, systemPaths, chassisPaths []string) []models.PCIeDevice {
collections := make([]string, 0, len(systemPaths)+len(chassisPaths))
for _, systemPath := range systemPaths {
collections = append(collections, joinPath(systemPath, "/PCIeDevices"))
}
for _, chassisPath := range chassisPaths {
collections = append(collections, joinPath(chassisPath, "/PCIeDevices"))
}
var out []models.PCIeDevice
for _, collectionPath := range collections {
memberDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, collectionPath)
if err != nil || len(memberDocs) == 0 {
continue
}
for _, doc := range memberDocs {
functionDocs := c.getLinkedPCIeFunctions(ctx, client, req, baseURL, doc)
if looksLikeGPU(doc, functionDocs) {
continue
}
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, doc, "EnvironmentMetrics", "Metrics")
supplementalDocs = append(supplementalDocs, c.getChassisScopedPCIeSupplementalDocs(ctx, client, req, baseURL, doc)...)
for _, fn := range functionDocs {
supplementalDocs = append(supplementalDocs, c.getLinkedSupplementalDocs(ctx, client, req, baseURL, fn, "EnvironmentMetrics", "Metrics")...)
}
dev := parsePCIeDeviceWithSupplementalDocs(doc, functionDocs, supplementalDocs)
if isUnidentifiablePCIeDevice(dev) {
continue
}
out = append(out, dev)
}
}
// Fallback: some BMCs expose only PCIeFunctions collection without PCIeDevices.
for _, systemPath := range systemPaths {
functionDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(systemPath, "/PCIeFunctions"))
if err != nil || len(functionDocs) == 0 {
continue
}
for idx, fn := range functionDocs {
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, fn, "EnvironmentMetrics", "Metrics")
dev := parsePCIeFunctionWithSupplementalDocs(fn, supplementalDocs, idx+1)
out = append(out, dev)
}
}
return dedupePCIeDevices(out)
}
func (c *RedfishConnector) getChassisScopedPCIeSupplementalDocs(ctx context.Context, client *http.Client, req Request, baseURL string, doc map[string]interface{}) []map[string]interface{} {
docPath := normalizeRedfishPath(asString(doc["@odata.id"]))
chassisPath := chassisPathForPCIeDoc(docPath)
if chassisPath == "" {
return nil
}
out := make([]map[string]interface{}, 0, 6)
seen := make(map[string]struct{})
add := func(path string) {
path = normalizeRedfishPath(path)
if path == "" {
return
}
if _, ok := seen[path]; ok {
return
}
supplementalDoc, err := c.getJSON(ctx, client, req, baseURL, path)
if err != nil || len(supplementalDoc) == 0 {
return
}
seen[path] = struct{}{}
out = append(out, supplementalDoc)
}
if looksLikeNVSwitchPCIeDoc(doc) {
add(joinPath(chassisPath, "/EnvironmentMetrics"))
add(joinPath(chassisPath, "/ThermalSubsystem/ThermalMetrics"))
}
deviceDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(chassisPath, "/Devices"))
if err == nil {
for _, deviceDoc := range deviceDocs {
if !redfishPCIeMatchesChassisDeviceDoc(doc, deviceDoc) {
continue
}
add(asString(deviceDoc["@odata.id"]))
}
}
return out
}
func (c *RedfishConnector) discoverMemberPaths(ctx context.Context, client *http.Client, req Request, baseURL, collectionPath, fallbackPath string) []string {
collection, err := c.getJSON(ctx, client, req, baseURL, collectionPath)
if err == nil {
if refs, ok := collection["Members"].([]interface{}); ok && len(refs) > 0 {
paths := make([]string, 0, len(refs))
for _, refAny := range refs {
ref, ok := refAny.(map[string]interface{})
if !ok {
continue
}
memberPath := asString(ref["@odata.id"])
if memberPath != "" {
paths = append(paths, memberPath)
}
}
if len(paths) > 0 {
return paths
}
}
}
if fallbackPath != "" {
return []string{fallbackPath}
}
return nil
}
func (c *RedfishConnector) collectProfileHintDocs(ctx context.Context, client *http.Client, req Request, baseURL, systemPath, chassisPath string) []map[string]interface{} {
paths := []string{
"/redfish/v1/UpdateService/FirmwareInventory",
joinPath(systemPath, "/NetworkInterfaces"),
joinPath(chassisPath, "/Drives"),
joinPath(chassisPath, "/NetworkAdapters"),
}
seen := make(map[string]struct{}, len(paths))
docs := make([]map[string]interface{}, 0, len(paths))
for _, path := range paths {
path = normalizeRedfishPath(path)
if path == "" {
continue
}
if _, ok := seen[path]; ok {
continue
}
seen[path] = struct{}{}
doc, err := c.getJSON(ctx, client, req, baseURL, path)
if err != nil {
continue
}
docs = append(docs, doc)
}
return docs
}
func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *http.Client, req Request, baseURL string, seedPaths []string, tuning redfishprofile.AcquisitionTuning, emit ProgressFn) (map[string]interface{}, []map[string]interface{}, redfishPostProbeMetrics, string) {
maxDocuments := redfishSnapshotMaxDocuments(tuning)
workers := redfishSnapshotWorkers(tuning)
const heartbeatInterval = 5 * time.Second
crawlStart := time.Now()
memoryClient := c.httpClientWithTimeout(req, redfishSnapshotMemoryRequestTimeout())
memoryGate := make(chan struct{}, redfishSnapshotMemoryConcurrency())
postProbeClient := c.httpClientWithTimeout(req, redfishSnapshotPostProbeRequestTimeout())
branchLimiter := newRedfishSnapshotBranchLimiter(redfishSnapshotBranchConcurrency())
branchRetryPause := redfishSnapshotBranchRequeueBackoff()
timings := newRedfishPathTimingCollector(4)
postProbeMetrics := redfishPostProbeMetrics{}
out := make(map[string]interface{}, maxDocuments)
fetchErrors := make(map[string]string)
seen := make(map[string]struct{}, maxDocuments)
rootCounts := make(map[string]int)
var mu sync.Mutex
var processed int32
var lastPath atomic.Value
// Workers enqueue newly discovered links into the same queue they consume.
// The queue capacity must be at least the crawl cap to avoid producer/consumer
// deadlock when several workers discover many links at once.
jobs := make(chan string, maxDocuments)
var wg sync.WaitGroup
enqueue := func(path string) {
path = normalizeRedfishPath(path)
if !shouldCrawlPath(path) {
return
}
for _, pattern := range tuning.SnapshotExcludeContains {
if pattern != "" && strings.Contains(path, pattern) {
return
}
}
mu.Lock()
if len(seen) >= maxDocuments {
mu.Unlock()
return
}
if _, ok := seen[path]; ok {
mu.Unlock()
return
}
seen[path] = struct{}{}
wg.Add(1)
mu.Unlock()
jobs <- path
}
enqueue("/redfish/v1")
for _, seed := range seedPaths {
enqueue(seed)
}
c.debugSnapshotf("snapshot queue initialized workers=%d max_documents=%d", workers, maxDocuments)
stopHeartbeat := make(chan struct{})
if emit != nil {
go func() {
ticker := time.NewTicker(heartbeatInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
n := atomic.LoadInt32(&processed)
mu.Lock()
countsCopy := make(map[string]int, len(rootCounts))
for k, v := range rootCounts {
countsCopy[k] = v
}
seenN := len(seen)
outN := len(out)
mu.Unlock()
roots := topRoots(countsCopy, 2)
last := "/redfish/v1"
if v := lastPath.Load(); v != nil {
if s, ok := v.(string); ok && s != "" {
last = s
}
}
eta := formatETA(estimateSnapshotETA(crawlStart, int(n), seenN, len(jobs), workers, client.Timeout))
emit(Progress{
Status: "running",
Progress: 92 + int(minInt32(n/200, 6)),
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",
ETASeconds: int(estimateSnapshotETA(crawlStart, int(n), seenN, len(jobs), workers, client.Timeout).Seconds()),
})
case <-stopHeartbeat:
return
case <-ctx.Done():
return
}
}
}()
}
for i := 0; i < workers; i++ {
go func(workerID int) {
for current := range jobs {
if !branchLimiter.tryAcquire(current) {
select {
case jobs <- current:
c.debugSnapshotf("worker=%d requeue branch-busy path=%s branch=%s queue_len=%d", workerID, current, redfishSnapshotBranchKey(current), len(jobs))
select {
case <-time.After(branchRetryPause):
case <-ctx.Done():
}
continue
default:
}
if !branchLimiter.waitAcquire(ctx, current, branchRetryPause) {
n := atomic.AddInt32(&processed, 1)
mu.Lock()
if _, ok := fetchErrors[current]; !ok && ctx.Err() != nil {
fetchErrors[current] = ctx.Err().Error()
}
mu.Unlock()
if emit != nil && ctx.Err() != nil {
emit(Progress{
Status: "running",
Progress: 92 + int(minInt32(n/200, 6)),
Message: fmt.Sprintf("Redfish snapshot: error on %s", compactProgressPath(current)),
})
}
wg.Done()
continue
}
}
lastPath.Store(current)
c.debugSnapshotf("worker=%d fetch start path=%s queue_len=%d", workerID, current, len(jobs))
fetchStart := time.Now()
doc, err := func() (map[string]interface{}, error) {
defer branchLimiter.release(current)
if !isRedfishMemoryMemberPath(current) {
return c.getJSON(ctx, client, req, baseURL, current)
}
select {
case memoryGate <- struct{}{}:
case <-ctx.Done():
return nil, ctx.Err()
}
defer func() { <-memoryGate }()
return c.getJSONWithRetry(
ctx,
memoryClient,
req,
baseURL,
current,
redfishSnapshotMemoryRetryAttempts(),
redfishSnapshotMemoryRetryBackoff(),
)
}()
timings.Observe(current, time.Since(fetchStart), err != nil)
if err == nil {
mu.Lock()
out[current] = doc
rootCounts[redfishTopRoot(current)]++
mu.Unlock()
for _, ref := range extractODataIDs(doc) {
enqueue(ref)
}
}
n := atomic.AddInt32(&processed, 1)
if err != nil {
mu.Lock()
if _, ok := fetchErrors[current]; !ok {
fetchErrors[current] = err.Error()
}
mu.Unlock()
c.debugSnapshotf("worker=%d fetch error path=%s err=%v", workerID, current, err)
if emit != nil && shouldReportSnapshotFetchError(err) {
emit(Progress{
Status: "running",
Progress: 92 + int(minInt32(n/200, 6)),
Message: fmt.Sprintf("Redfish snapshot: error on %s", compactProgressPath(current)),
})
}
}
if emit != nil && n%40 == 0 {
mu.Lock()
countsCopy := make(map[string]int, len(rootCounts))
for k, v := range rootCounts {
countsCopy[k] = v
}
seenN := len(seen)
mu.Unlock()
roots := topRoots(countsCopy, 2)
last := current
if v := lastPath.Load(); v != nil {
if s, ok := v.(string); ok && s != "" {
last = s
}
}
eta := formatETA(estimateSnapshotETA(crawlStart, int(n), seenN, len(jobs), workers, client.Timeout))
emit(Progress{
Status: "running",
Progress: 92 + int(minInt32(n/200, 6)),
Message: fmt.Sprintf("Redfish snapshot: docs=%d, ETA≈%s, roots=%s, last=%s", n, eta, strings.Join(roots, ", "), compactProgressPath(last)),
CurrentPhase: "snapshot",
ETASeconds: int(estimateSnapshotETA(crawlStart, int(n), seenN, len(jobs), workers, client.Timeout).Seconds()),
})
}
if n%20 == 0 || err != nil {
mu.Lock()
seenN := len(seen)
outN := len(out)
mu.Unlock()
c.debugSnapshotf("snapshot progress processed=%d stored=%d seen=%d queue_len=%d", n, outN, seenN, len(jobs))
}
wg.Done()
}
}(i + 1)
}
wg.Wait()
close(stopHeartbeat)
close(jobs)
// Profile-owned policy for direct NVMe Disk.Bay probing.
postProbeTotalStart := time.Now()
driveCollections := make([]string, 0)
if tuning.PostProbePolicy.EnableDirectNVMEDiskBayProbe {
for path, docAny := range out {
normalized := normalizeRedfishPath(path)
if !strings.HasSuffix(normalized, "/Drives") {
continue
}
postProbeMetrics.NVMECandidates++
doc, _ := docAny.(map[string]interface{})
if !shouldAdaptiveNVMeProbe(doc) {
continue
}
// Skip chassis types that cannot contain NVMe storage (e.g. GPU modules,
// RoT components, NVSwitch zones on HGX systems) to avoid probing hundreds
// of Disk.Bay.N URLs against chassis that will never have drives.
chassisPath := strings.TrimSuffix(normalized, "/Drives")
if chassisDocAny, ok := out[chassisPath]; ok {
if chassisDoc, ok := chassisDocAny.(map[string]interface{}); ok {
if !chassisTypeCanHaveNVMe(asString(chassisDoc["ChassisType"])) {
continue
}
}
}
driveCollections = append(driveCollections, normalized)
}
}
sort.Strings(driveCollections)
postProbeMetrics.NVMESelected = len(driveCollections)
nvmeProbeStart := time.Now()
nvmePostProbeEnabled := redfishNVMePostProbeEnabled(tuning)
for i, path := range driveCollections {
if !nvmePostProbeEnabled {
break
}
if emit != nil && len(driveCollections) > 0 && (i == 0 || i%4 == 0 || i == len(driveCollections)-1) {
emit(Progress{
Status: "running",
Progress: 97,
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",
ETASeconds: int(estimateProgressETA(nvmeProbeStart, i, len(driveCollections), 2*time.Second).Seconds()),
})
}
for _, bayPath := range directDiskBayCandidates(path) {
doc, err := c.getJSON(ctx, client, req, baseURL, bayPath)
if err != nil {
continue
}
if !looksLikeDrive(doc) {
continue
}
normalizedBayPath := normalizeRedfishPath(bayPath)
if _, exists := out[normalizedBayPath]; exists {
continue
}
out[normalizedBayPath] = doc
postProbeMetrics.NVMEAdded++
c.debugSnapshotf("snapshot nvme bay probe hit path=%s", bayPath)
}
}
// Profile-owned policy for numeric collection post-probe.
postProbeCollections := make([]string, 0)
for path, docAny := range out {
normalized := normalizeRedfishPath(path)
if !shouldPostProbeCollectionPath(normalized, tuning) {
continue
}
postProbeMetrics.CollectionCandidates++
doc, _ := docAny.(map[string]interface{})
if shouldAdaptivePostProbeCollectionPath(normalized, doc, tuning) {
postProbeCollections = append(postProbeCollections, normalized)
continue
}
if redfishCollectionHasExplicitMembers(doc) {
postProbeMetrics.SkippedExplicit++
}
}
sort.Strings(postProbeCollections)
postProbeMetrics.CollectionSelected = len(postProbeCollections)
postProbeStart := time.Now()
addedPostProbe := 0
for i, path := range postProbeCollections {
if emit != nil && len(postProbeCollections) > 0 && (i == 0 || i%8 == 0 || i == len(postProbeCollections)-1) {
emit(Progress{
Status: "running",
Progress: 98,
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",
ETASeconds: int(estimateProgressETA(postProbeStart, i, len(postProbeCollections), 3*time.Second).Seconds()),
})
}
for childPath, doc := range c.probeDirectRedfishCollectionChildren(ctx, postProbeClient, req, baseURL, path) {
if _, exists := out[childPath]; exists {
continue
}
out[childPath] = doc
addedPostProbe++
}
}
postProbeMetrics.Added = addedPostProbe
postProbeMetrics.Duration = time.Since(postProbeTotalStart)
if emit != nil && addedPostProbe > 0 {
emit(Progress{
Status: "running",
Progress: 98,
Message: fmt.Sprintf("Redfish snapshot: post-probe added %d docs", addedPostProbe),
})
}
if emit != nil {
emit(Progress{
Status: "running",
Progress: 98,
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),
})
}
if emit != nil {
emit(Progress{
Status: "running",
Progress: 98,
Message: fmt.Sprintf("Redfish snapshot: collected %d docs", len(out)),
})
}
errorList := make([]map[string]interface{}, 0, len(fetchErrors))
for p, msg := range fetchErrors {
errorList = append(errorList, map[string]interface{}{
"path": p,
"error": msg,
})
}
sort.Slice(errorList, func(i, j int) bool {
return asString(errorList[i]["path"]) < asString(errorList[j]["path"])
})
if summary := timings.Summary(12); summary != "" {
slog.Info("redfish-snapshot-timing", "summary", summary)
}
if emit != nil {
if summary := timings.Summary(3); summary != "" {
emit(Progress{
Status: "running",
Progress: 98,
Message: fmt.Sprintf("Redfish snapshot: top branches by time: %s", summary),
})
}
}
return out, errorList, postProbeMetrics, timings.Summary(12)
}
func (c *RedfishConnector) probeSupermicroNVMeDiskBays(ctx context.Context, client *http.Client, req Request, baseURL, backplanePath string) []map[string]interface{} {
return c.probeDirectDiskBayChildren(ctx, client, req, baseURL, joinPath(backplanePath, "/Drives"))
}
func isSupermicroNVMeBackplanePath(path string) bool {
path = normalizeRedfishPath(path)
return strings.Contains(path, "/Chassis/NVMeSSD.") && strings.Contains(path, ".StorageBackplane")
}
func supermicroNVMeDiskBayCandidates(backplanePath string) []string {
return directDiskBayCandidates(joinPath(backplanePath, "/Drives"))
}
func (c *RedfishConnector) probeDirectDiskBayChildren(ctx context.Context, client *http.Client, req Request, baseURL, drivesCollectionPath string) []map[string]interface{} {
var out []map[string]interface{}
for _, path := range directDiskBayCandidates(drivesCollectionPath) {
doc, err := c.getJSON(ctx, client, req, baseURL, path)
if err != nil || !looksLikeDrive(doc) {
continue
}
out = append(out, doc)
}
return out
}
func directDiskBayCandidates(drivesCollectionPath string) []string {
const maxBays = 128
prefix := normalizeRedfishPath(drivesCollectionPath)
out := make([]string, 0, maxBays*3)
for i := 0; i < maxBays; i++ {
out = append(out, fmt.Sprintf("%s/Disk.Bay.%d", prefix, i))
out = append(out, fmt.Sprintf("%s/Disk.Bay%d", prefix, i))
out = append(out, fmt.Sprintf("%s/%d", prefix, i))
}
return out
}
func (c *RedfishConnector) probeDirectRedfishCollectionChildren(ctx context.Context, client *http.Client, req Request, baseURL, collectionPath string) map[string]map[string]interface{} {
normalized := normalizeRedfishPath(collectionPath)
maxItems, startIndex, missBudget := directNumericProbePlan(normalized)
if maxItems <= 0 {
return nil
}
out := make(map[string]map[string]interface{})
consecutiveMisses := 0
for i := startIndex; i <= maxItems; i++ {
path := fmt.Sprintf("%s/%d", normalized, i)
doc, err := c.getJSON(ctx, client, req, baseURL, path)
if err != nil {
consecutiveMisses++
if consecutiveMisses >= missBudget {
break
}
continue
}
consecutiveMisses = 0
if !looksLikeRedfishResource(doc) {
continue
}
out[normalizeRedfishPath(path)] = doc
}
return out
}
func (c *RedfishConnector) probeDirectRedfishCollectionChildrenSlow(ctx context.Context, client *http.Client, req Request, baseURL, collectionPath string) map[string]map[string]interface{} {
normalized := normalizeRedfishPath(collectionPath)
maxItems, startIndex, missBudget := directNumericProbePlan(normalized)
if maxItems <= 0 {
return nil
}
out := make(map[string]map[string]interface{})
consecutiveMisses := 0
for i := startIndex; i <= maxItems; i++ {
if len(out) > 0 || i > startIndex {
select {
case <-time.After(redfishCriticalSlowGap()):
case <-ctx.Done():
return out
}
}
path := fmt.Sprintf("%s/%d", normalized, i)
doc, err := c.getJSONWithRetry(ctx, client, req, baseURL, path, redfishCriticalPlanBAttempts(), redfishCriticalRetryBackoff())
if err != nil {
consecutiveMisses++
if consecutiveMisses >= missBudget {
break
}
continue
}
consecutiveMisses = 0
if !looksLikeRedfishResource(doc) {
continue
}
out[normalizeRedfishPath(path)] = doc
}
return out
}
func directNumericProbePlan(collectionPath string) (maxItems, startIndex, missBudget int) {
switch {
case strings.HasSuffix(collectionPath, "/Systems"):
return 32, 1, 8
case strings.HasSuffix(collectionPath, "/Chassis"):
return 64, 1, 12
case strings.HasSuffix(collectionPath, "/Managers"):
return 16, 1, 6
case strings.HasSuffix(collectionPath, "/Processors"):
return 32, 1, 12
case strings.HasSuffix(collectionPath, "/Memory"):
return 512, 1, 48
case strings.HasSuffix(collectionPath, "/Storage"):
return 128, 1, 24
case strings.HasSuffix(collectionPath, "/Drives"):
return 256, 0, 24
case strings.HasSuffix(collectionPath, "/Volumes"):
return 128, 1, 16
case strings.HasSuffix(collectionPath, "/PCIeDevices"):
return 256, 1, 24
case strings.HasSuffix(collectionPath, "/PCIeFunctions"):
return 512, 1, 32
case strings.HasSuffix(collectionPath, "/NetworkAdapters"):
return 128, 1, 20
case strings.HasSuffix(collectionPath, "/NetworkPorts"):
return 256, 1, 24
case strings.HasSuffix(collectionPath, "/Ports"):
return 256, 1, 24
case strings.HasSuffix(collectionPath, "/EthernetInterfaces"):
return 256, 1, 24
case strings.HasSuffix(collectionPath, "/Certificates"):
return 256, 1, 24
case strings.HasSuffix(collectionPath, "/Accounts"):
return 128, 1, 16
case strings.HasSuffix(collectionPath, "/LogServices"):
return 32, 1, 8
case strings.HasSuffix(collectionPath, "/Sensors"):
return 512, 1, 48
case strings.HasSuffix(collectionPath, "/Temperatures"):
return 256, 1, 32
case strings.HasSuffix(collectionPath, "/Fans"):
return 256, 1, 32
case strings.HasSuffix(collectionPath, "/Voltages"):
return 256, 1, 32
case strings.HasSuffix(collectionPath, "/PowerSupplies"):
return 64, 1, 16
default:
return 0, 0, 0
}
}
func shouldPostProbeCollectionPath(path string, tuning redfishprofile.AcquisitionTuning) bool {
path = normalizeRedfishPath(path)
if !tuning.PostProbePolicy.EnableNumericCollectionProbe {
return false
}
sensorProbeEnabled := tuning.PostProbePolicy.EnableSensorCollectionProbe || redfishSnapshotSensorPostProbeEnabled()
// Restrict expensive post-probe to collections that historically recover
// missing inventory/telemetry on partially implemented BMCs.
switch {
case strings.HasSuffix(path, "/Sensors"),
strings.HasSuffix(path, "/ThresholdSensors"),
strings.HasSuffix(path, "/DiscreteSensors"),
strings.HasSuffix(path, "/Temperatures"),
strings.HasSuffix(path, "/Fans"),
strings.HasSuffix(path, "/Voltages"):
return sensorProbeEnabled
case strings.HasSuffix(path, "/PowerSupplies"),
strings.HasSuffix(path, "/EthernetInterfaces"),
strings.HasSuffix(path, "/NetworkPorts"),
strings.HasSuffix(path, "/Ports"),
strings.HasSuffix(path, "/PCIeDevices"),
strings.HasSuffix(path, "/PCIeFunctions"),
strings.HasSuffix(path, "/Drives"),
strings.HasSuffix(path, "/Volumes"):
return true
default:
return false
}
}
func shouldAdaptivePostProbeCollectionPath(path string, collectionDoc map[string]interface{}, tuning redfishprofile.AcquisitionTuning) bool {
path = normalizeRedfishPath(path)
if !shouldPostProbeCollectionPath(path, tuning) {
return false
}
if len(collectionDoc) == 0 {
return true
}
memberRefs := redfishCollectionMemberRefs(collectionDoc)
if len(memberRefs) == 0 {
return true
}
// If the collection reports an explicit non-zero member count that already
// matches the number of discovered member refs, every member is accounted
// for and numeric probing cannot find anything new.
if odataCount := asInt(collectionDoc["Members@odata.count"]); odataCount > 0 && odataCount == len(memberRefs) {
return false
}
return redfishCollectionHasNumericMemberRefs(memberRefs)
}
func shouldAdaptiveNVMeProbe(collectionDoc map[string]interface{}) bool {
if len(collectionDoc) == 0 {
return true
}
return !redfishCollectionHasExplicitMembers(collectionDoc)
}
// chassisTypeCanHaveNVMe returns false for Redfish ChassisType values that
// represent compute/network/management sub-modules with no storage capability.
// Used to skip expensive Disk.Bay.N probing on HGX GPU, NVSwitch, PCIeRetimer,
// RoT and similar component chassis that expose an empty /Drives collection.
func chassisTypeCanHaveNVMe(chassisType string) bool {
switch strings.ToLower(strings.TrimSpace(chassisType)) {
case "module", // GPU SXM, NVLinkManagementNIC, PCIeRetimer
"component", // ERoT, IRoT, BMC, FPGA sub-chassis
"zone": // HGX_Chassis_0 fabric zone
return false
default:
return true
}
}
func redfishCollectionHasNumericMemberRefs(memberRefs []string) bool {
for _, memberPath := range memberRefs {
if redfishPathTailIsNumeric(memberPath) {
return true
}
}
return false
}
func redfishPathTailIsNumeric(path string) bool {
normalized := normalizeRedfishPath(path)
if normalized == "" {
return false
}
parts := strings.Split(strings.Trim(normalized, "/"), "/")
if len(parts) == 0 {
return false
}
tail := strings.TrimSpace(parts[len(parts)-1])
if tail == "" {
return false
}
for _, r := range tail {
if r < '0' || r > '9' {
return false
}
}
return true
}
func looksLikeRedfishResource(doc map[string]interface{}) bool {
if len(doc) == 0 {
return false
}
if asString(doc["@odata.id"]) != "" {
return true
}
if asString(doc["Id"]) != "" || asString(doc["Name"]) != "" {
return true
}
if _, ok := doc["Status"]; ok {
return true
}
if _, ok := doc["Reading"]; ok {
return true
}
if _, ok := doc["ReadingCelsius"]; ok {
return true
}
return false
}
// isHardwareInventoryCollectionPath reports whether the path is a hardware
// inventory collection that is expected to have members when the machine is
// powered on and the BMC has finished initializing.
func isHardwareInventoryCollectionPath(p string) bool {
for _, suffix := range []string{
"/PCIeDevices",
"/NetworkAdapters",
"/Processors",
"/Drives",
"/Storage",
"/EthernetInterfaces",
} {
if strings.HasSuffix(p, suffix) {
return true
}
}
return false
}
func shouldSlowProbeCriticalCollection(p string, tuning redfishprofile.AcquisitionTuning) bool {
p = normalizeRedfishPath(p)
if !tuning.RecoveryPolicy.EnableCriticalSlowProbe {
return false
}
for _, suffix := range []string{
"/Processors",
"/Memory",
"/Storage",
"/Drives",
"/Volumes",
"/PCIeDevices",
"/PCIeFunctions",
"/NetworkAdapters",
"/EthernetInterfaces",
"/NetworkInterfaces",
"/Sensors",
"/Fans",
"/Temperatures",
"/Voltages",
} {
if strings.HasSuffix(p, suffix) {
return true
}
}
return false
}
func mergeRedfishPaths(groups ...[]string) []string {
seen := make(map[string]struct{})
out := make([]string, 0)
for _, group := range groups {
for _, path := range group {
path = normalizeRedfishPath(path)
if path == "" {
continue
}
if _, ok := seen[path]; ok {
continue
}
seen[path] = struct{}{}
out = append(out, path)
}
}
return out
}
func redfishFetchErrorListToMap(list []map[string]interface{}) map[string]string {
out := make(map[string]string, len(list))
for _, item := range list {
p := normalizeRedfishPath(asString(item["path"]))
if p == "" {
continue
}
out[p] = asString(item["error"])
}
return out
}
func redfishFetchErrorMapToList(m map[string]string) []map[string]interface{} {
if len(m) == 0 {
return nil
}
out := make([]map[string]interface{}, 0, len(m))
for p, msg := range m {
out = append(out, map[string]interface{}{"path": p, "error": msg})
}
sort.Slice(out, func(i, j int) bool {
return asString(out[i]["path"]) < asString(out[j]["path"])
})
return out
}
func isRetryableRedfishFetchError(err error) bool {
if err == nil {
return false
}
msg := strings.ToLower(err.Error())
if strings.Contains(msg, "timeout") || strings.Contains(msg, "deadline exceeded") || strings.Contains(msg, "connection reset") || strings.Contains(msg, "unexpected eof") {
return true
}
if strings.HasPrefix(msg, "status 500 ") || strings.HasPrefix(msg, "status 502 ") || strings.HasPrefix(msg, "status 503 ") || strings.HasPrefix(msg, "status 504 ") {
return true
}
return false
}
func redfishSnapshotRequestTimeout() time.Duration {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_SNAPSHOT_TIMEOUT")); v != "" {
if d, err := time.ParseDuration(v); err == nil && d > 0 {
return d
}
}
return 12 * time.Second
}
func redfishSnapshotPostProbeRequestTimeout() time.Duration {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_POSTPROBE_TIMEOUT")); v != "" {
if d, err := time.ParseDuration(v); err == nil && d > 0 {
return d
}
}
// Post-probe probes non-existent numeric paths expecting fast 404s.
// A short timeout prevents BMCs that hang on unknown paths from stalling
// the entire collection for minutes (e.g. HPE iLO on NetworkAdapters Ports).
return 4 * time.Second
}
func redfishSnapshotWorkers(tuning redfishprofile.AcquisitionTuning) int {
if tuning.SnapshotWorkers >= 1 && tuning.SnapshotWorkers <= 16 {
return tuning.SnapshotWorkers
}
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_SNAPSHOT_WORKERS")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 16 {
return n
}
}
return 4
}
func redfishPrefetchEnabled(tuning redfishprofile.AcquisitionTuning) bool {
if tuning.PrefetchEnabled != nil {
return *tuning.PrefetchEnabled
}
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_PREFETCH_ENABLED")); v != "" {
switch strings.ToLower(v) {
case "0", "false", "off", "no":
return false
default:
return true
}
}
return true
}
func redfishPrefetchRequestTimeout() time.Duration {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_PREFETCH_TIMEOUT")); v != "" {
if d, err := time.ParseDuration(v); err == nil && d > 0 {
return d
}
}
return 20 * time.Second
}
func redfishPrefetchRetryAttempts() int {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_PREFETCH_RETRIES")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 8 {
return n
}
}
return 2
}
func redfishPrefetchMemberRetryAttempts() int {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_PREFETCH_MEMBER_RETRIES")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 6 {
return n
}
}
return 1
}
func redfishPrefetchMemberRecoveryMax() int {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_PREFETCH_MEMBER_RECOVERY_MAX")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 512 {
return n
}
}
return 48
}
func redfishPrefetchRetryBackoff() time.Duration {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_PREFETCH_BACKOFF")); v != "" {
if d, err := time.ParseDuration(v); err == nil && d >= 0 {
return d
}
}
return 900 * time.Millisecond
}
func redfishPrefetchWorkers(tuning redfishprofile.AcquisitionTuning) int {
if tuning.PrefetchWorkers >= 1 && tuning.PrefetchWorkers <= 8 {
return tuning.PrefetchWorkers
}
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_PREFETCH_WORKERS")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 8 {
return n
}
}
return 2
}
func redfishSnapshotSensorPostProbeEnabled() bool {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_SENSOR_POSTPROBE")); v != "" {
switch strings.ToLower(v) {
case "1", "true", "on", "yes":
return true
default:
return false
}
}
return false
}
func redfishNVMePostProbeEnabled(tuning redfishprofile.AcquisitionTuning) bool {
if tuning.NVMePostProbeEnabled != nil {
return *tuning.NVMePostProbeEnabled
}
return true
}
func redfishPrefetchTargets(criticalPaths []string, tuning redfishprofile.AcquisitionTuning) []string {
if len(criticalPaths) == 0 {
return nil
}
out := make([]string, 0, len(criticalPaths))
seen := make(map[string]struct{}, len(criticalPaths))
for _, p := range criticalPaths {
p = normalizeRedfishPath(p)
if p == "" || !shouldPrefetchCriticalPath(p, tuning) {
continue
}
if _, ok := seen[p]; ok {
continue
}
seen[p] = struct{}{}
out = append(out, p)
}
return out
}
func shouldPrefetchCriticalPath(p string, tuning redfishprofile.AcquisitionTuning) bool {
p = normalizeRedfishPath(p)
if p == "" {
return false
}
for _, noisy := range tuning.PrefetchPolicy.ExcludeContains {
if strings.Contains(p, noisy) {
return false
}
}
for _, suffix := range tuning.PrefetchPolicy.IncludeSuffixes {
if strings.HasSuffix(p, suffix) {
return true
}
}
parts := strings.Split(strings.Trim(p, "/"), "/")
return len(parts) == 4 && parts[0] == "redfish" && parts[1] == "v1" &&
(parts[2] == "Systems" || parts[2] == "Chassis" || parts[2] == "Managers")
}
func redfishCriticalRequestTimeout() time.Duration {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_CRITICAL_TIMEOUT")); v != "" {
if d, err := time.ParseDuration(v); err == nil && d > 0 {
return d
}
}
return 45 * time.Second
}
func redfishCriticalRetryAttempts() int {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_CRITICAL_RETRIES")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 10 {
return n
}
}
return 3
}
func redfishCriticalPlanBAttempts() int {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_CRITICAL_PLANB_RETRIES")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 10 {
return n
}
}
return 3
}
func redfishCriticalMemberRetryAttempts() int {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_CRITICAL_MEMBER_RETRIES")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 6 {
return n
}
}
return 1
}
func redfishCriticalMemberRecoveryMax() int {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_CRITICAL_MEMBER_RECOVERY_MAX")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 1024 {
return n
}
}
return 48
}
func redfishCriticalRetryBackoff() time.Duration {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_CRITICAL_BACKOFF")); v != "" {
if d, err := time.ParseDuration(v); err == nil && d >= 0 {
return d
}
}
return 1500 * time.Millisecond
}
func redfishCriticalCooldown() time.Duration {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_CRITICAL_COOLDOWN")); v != "" {
if d, err := time.ParseDuration(v); err == nil && d >= 0 {
return d
}
}
return 4 * time.Second
}
func redfishCriticalSlowGap() time.Duration {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_CRITICAL_SLOW_GAP")); v != "" {
if d, err := time.ParseDuration(v); err == nil && d >= 0 {
return d
}
}
return 1200 * time.Millisecond
}
func redfishSnapshotMemoryRequestTimeout() time.Duration {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_MEMORY_TIMEOUT")); v != "" {
if d, err := time.ParseDuration(v); err == nil && d > 0 {
return d
}
}
return 25 * time.Second
}
func redfishSnapshotMemoryRetryAttempts() int {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_MEMORY_RETRIES")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 8 {
return n
}
}
return 2
}
func redfishSnapshotMemoryRetryBackoff() time.Duration {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_MEMORY_BACKOFF")); v != "" {
if d, err := time.ParseDuration(v); err == nil && d >= 0 {
return d
}
}
return 800 * time.Millisecond
}
func redfishSnapshotMemoryConcurrency() int {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_MEMORY_WORKERS")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 8 {
return n
}
}
return 1
}
func redfishSnapshotBranchConcurrency() int {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_BRANCH_WORKERS")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 8 {
return n
}
}
return 1
}
func redfishSnapshotBranchRequeueBackoff() time.Duration {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_BRANCH_BACKOFF")); v != "" {
if d, err := time.ParseDuration(v); err == nil && d >= 0 {
return d
}
}
return 35 * time.Millisecond
}
type redfishSnapshotBranchLimiter struct {
limit int
mu sync.Mutex
inFlight map[string]int
}
func newRedfishSnapshotBranchLimiter(limit int) *redfishSnapshotBranchLimiter {
if limit < 1 {
limit = 1
}
return &redfishSnapshotBranchLimiter{
limit: limit,
inFlight: make(map[string]int),
}
}
func (l *redfishSnapshotBranchLimiter) tryAcquire(path string) bool {
branch := redfishSnapshotBranchKey(path)
if branch == "" {
return true
}
l.mu.Lock()
defer l.mu.Unlock()
if l.inFlight[branch] >= l.limit {
return false
}
l.inFlight[branch]++
return true
}
func (l *redfishSnapshotBranchLimiter) waitAcquire(ctx context.Context, path string, backoff time.Duration) bool {
branch := redfishSnapshotBranchKey(path)
if branch == "" {
return true
}
if backoff < 0 {
backoff = 0
}
for {
if l.tryAcquire(path) {
return true
}
if ctx.Err() != nil {
return false
}
select {
case <-time.After(backoff):
case <-ctx.Done():
return false
}
}
}
func (l *redfishSnapshotBranchLimiter) release(path string) {
branch := redfishSnapshotBranchKey(path)
if branch == "" {
return
}
l.mu.Lock()
defer l.mu.Unlock()
switch n := l.inFlight[branch]; {
case n <= 1:
delete(l.inFlight, branch)
default:
l.inFlight[branch] = n - 1
}
}
func redfishLinkRefs(doc map[string]interface{}, topKey, nestedKey string) []string {
top, ok := doc[topKey].(map[string]interface{})
if !ok {
return nil
}
items, ok := top[nestedKey].([]interface{})
if !ok {
return nil
}
out := make([]string, 0, len(items))
for _, itemAny := range items {
item, ok := itemAny.(map[string]interface{})
if !ok {
continue
}
if p := asString(item["@odata.id"]); p != "" {
out = append(out, p)
}
}
return out
}
func pcieDeviceDedupKey(dev models.PCIeDevice) string {
if bdf := strings.TrimSpace(dev.BDF); looksLikeCanonicalBDF(bdf) {
return strings.ToLower(bdf)
}
if s := strings.TrimSpace(dev.SerialNumber); s != "" {
return s
}
return firstNonEmpty(
strings.TrimSpace(dev.Slot)+"|"+strings.TrimSpace(dev.PartNumber)+"|"+strings.TrimSpace(dev.DeviceClass),
strings.TrimSpace(dev.Slot)+"|"+strings.TrimSpace(dev.DeviceClass),
strings.TrimSpace(dev.PartNumber)+"|"+strings.TrimSpace(dev.DeviceClass),
strings.TrimSpace(dev.Description)+"|"+strings.TrimSpace(dev.DeviceClass),
)
}
func looksLikeCanonicalBDF(bdf string) bool {
bdf = strings.TrimSpace(strings.ToLower(bdf))
if bdf == "" {
return false
}
// Accept common forms: 0000:65:00.0 or 65:00.0
if strings.Count(bdf, ":") == 2 && strings.Contains(bdf, ".") {
return true
}
if strings.Count(bdf, ":") == 1 && strings.Contains(bdf, ".") {
return true
}
return false
}
func sanitizeRedfishBDF(bdf string) string {
bdf = strings.TrimSpace(bdf)
if looksLikeCanonicalBDF(bdf) {
return bdf
}
return ""
}
func shouldCrawlPath(path string) bool {
if path == "" {
return false
}
normalized := normalizeRedfishPath(path)
if isAllowedNVSwitchFabricPath(normalized) {
return true
}
if strings.Contains(normalized, "/Chassis/") &&
strings.Contains(normalized, "/PCIeDevices/") &&
strings.HasSuffix(normalized, "/PCIeFunctions") {
// Avoid crawling entire chassis PCIeFunctions collections. Concrete member
// docs can still be reached through direct links such as
// NetworkDeviceFunction Links.PCIeFunction.
return false
}
if strings.Contains(normalized, "/Memory/") {
after := strings.SplitN(normalized, "/Memory/", 2)
if len(after) == 2 && strings.Count(after[1], "/") >= 1 {
// Keep direct DIMM resources and selected metrics subresources, but skip
// unrelated nested branches like Assembly.
return strings.HasSuffix(normalized, "/MemoryMetrics")
}
}
if strings.Contains(normalized, "/Processors/") {
after := strings.SplitN(normalized, "/Processors/", 2)
if len(after) == 2 && strings.Count(after[1], "/") >= 1 {
return strings.HasSuffix(normalized, "/ProcessorMetrics")
}
}
// Non-inventory top-level service branches.
for _, prefix := range []string{
"/redfish/v1/AccountService",
"/redfish/v1/CertificateService",
"/redfish/v1/EventService",
"/redfish/v1/Registries",
"/redfish/v1/SessionService",
"/redfish/v1/TaskService",
} {
if strings.HasPrefix(normalized, prefix) {
return false
}
}
// Manager-specific configuration paths (not hardware inventory).
if strings.Contains(normalized, "/Managers/") {
for _, part := range []string{
"/FirewallRules",
"/KvmService",
"/LldpService",
"/SecurityService",
"/SmtpService",
"/SnmpService",
"/SyslogService",
"/VirtualMedia",
"/VncService",
"/Certificates",
} {
if strings.Contains(normalized, part) {
return false
}
}
}
// Per-CPU operating frequency configurations — not hardware inventory.
if strings.HasSuffix(normalized, "/OperatingConfigs") {
return false
}
// Per-core/thread sub-processors — inventory is captured at the top processor level.
if strings.Contains(normalized, "/SubProcessors") {
return false
}
// Non-inventory system endpoints.
for _, part := range []string{
"/BootOptions",
"/HostPostCode",
"/Bios/Settings",
"/GetServerAllUSBStatus",
"/Oem/Public/KVM",
"/SecureBoot/SecureBootDatabases",
// HPE iLO WorkloadPerformanceAdvisor — operational/advisory data, not inventory.
"/WorkloadPerformanceAdvisor",
} {
if strings.Contains(normalized, part) {
return false
}
}
heavyParts := []string{
"/JsonSchemas",
"/LogServices/",
"/Entries/",
"/TelemetryService/",
"/MetricReports/",
"/SessionService/Sessions",
"/TaskService/Tasks",
}
for _, part := range heavyParts {
if strings.Contains(path, part) {
return false
}
}
return true
}
func isAllowedNVSwitchFabricPath(path string) bool {
if !strings.HasPrefix(path, "/redfish/v1/Fabrics/") {
return false
}
if !strings.Contains(path, "/Switches/NVSwitch_") {
return false
}
if strings.HasSuffix(path, "/Switches") || strings.Contains(path, "/Ports/NVLink_") || strings.HasSuffix(path, "/Ports") {
return true
}
if strings.Contains(path, "/Switches/NVSwitch_") && !strings.Contains(path, "/Ports/") {
return true
}
return false
}
func isRedfishMemoryMemberPath(path string) bool {
normalized := normalizeRedfishPath(path)
if !strings.Contains(normalized, "/Systems/") {
return false
}
if !strings.Contains(normalized, "/Memory/") {
return false
}
if strings.Contains(normalized, "/MemoryMetrics") || strings.Contains(normalized, "/Assembly") {
return false
}
after := strings.SplitN(normalized, "/Memory/", 2)
if len(after) != 2 {
return false
}
suffix := strings.TrimSpace(after[1])
if suffix == "" || strings.Contains(suffix, "/") {
return false
}
return true
}
func redfishCollectionHasExplicitMembers(doc map[string]interface{}) bool {
return len(redfishCollectionMemberRefs(doc)) > 0
}
func redfishSnapshotBranchKey(path string) string {
normalized := normalizeRedfishPath(path)
if normalized == "" || normalized == "/redfish/v1" {
return ""
}
parts := strings.Split(strings.Trim(normalized, "/"), "/")
if len(parts) < 3 {
return normalized
}
if parts[0] != "redfish" || parts[1] != "v1" {
return normalized
}
// Keep subsystem branches independent, e.g. Systems/1/Memory vs Systems/1/PCIeDevices.
if len(parts) >= 5 && (parts[2] == "Systems" || parts[2] == "Chassis" || parts[2] == "Managers") {
return "/" + strings.Join(parts[:5], "/")
}
if len(parts) >= 4 {
return "/" + strings.Join(parts[:4], "/")
}
return "/" + strings.Join(parts[:3], "/")
}
func (c *RedfishConnector) getLinkedPCIeFunctions(ctx context.Context, client *http.Client, req Request, baseURL string, doc map[string]interface{}) []map[string]interface{} {
// Newer Redfish payloads often keep function references in Links.PCIeFunctions.
if links, ok := doc["Links"].(map[string]interface{}); ok {
if refs, ok := links["PCIeFunctions"].([]interface{}); ok && len(refs) > 0 {
out := make([]map[string]interface{}, 0, len(refs))
for _, refAny := range refs {
ref, ok := refAny.(map[string]interface{})
if !ok {
continue
}
memberPath := asString(ref["@odata.id"])
if memberPath == "" {
continue
}
memberDoc, err := c.getJSON(ctx, client, req, baseURL, memberPath)
if err != nil {
continue
}
out = append(out, memberDoc)
}
return out
}
if ref, ok := links["PCIeFunction"].(map[string]interface{}); ok {
memberPath := asString(ref["@odata.id"])
if memberPath != "" {
memberDoc, err := c.getJSON(ctx, client, req, baseURL, memberPath)
if err == nil {
return []map[string]interface{}{memberDoc}
}
}
}
}
// Some implementations expose a collection object in PCIeFunctions.@odata.id.
if pcieFunctions, ok := doc["PCIeFunctions"].(map[string]interface{}); ok {
if collectionPath := asString(pcieFunctions["@odata.id"]); collectionPath != "" {
memberDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, collectionPath)
if err == nil {
return memberDocs
}
}
}
return nil
}
func (c *RedfishConnector) getNetworkAdapterFunctionDocs(ctx context.Context, client *http.Client, req Request, baseURL string, adapterDoc map[string]interface{}) []map[string]interface{} {
ndfCol, ok := adapterDoc["NetworkDeviceFunctions"].(map[string]interface{})
if !ok {
return nil
}
colPath := asString(ndfCol["@odata.id"])
if colPath == "" {
return nil
}
funcDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, colPath)
if err != nil {
return nil
}
return funcDocs
}
func (c *RedfishConnector) getCollectionMembers(ctx context.Context, client *http.Client, req Request, baseURL, collectionPath string) ([]map[string]interface{}, error) {
collection, err := c.getJSON(ctx, client, req, baseURL, collectionPath)
if err != nil {
return nil, err
}
memberPaths := redfishCollectionMemberRefs(collection)
if len(memberPaths) == 0 {
return []map[string]interface{}{}, nil
}
out := make([]map[string]interface{}, 0, len(memberPaths))
for _, memberPath := range memberPaths {
memberDoc, err := c.getJSON(ctx, client, req, baseURL, memberPath)
if err != nil {
continue
}
if strings.TrimSpace(asString(memberDoc["@odata.id"])) == "" {
memberDoc["@odata.id"] = normalizeRedfishPath(memberPath)
}
out = append(out, memberDoc)
}
return out, nil
}
func (c *RedfishConnector) getJSON(ctx context.Context, client *http.Client, req Request, baseURL, requestPath string) (map[string]interface{}, error) {
start := time.Now()
rel := requestPath
if rel == "" {
rel = "/"
}
if !strings.HasPrefix(rel, "/") {
rel = "/" + rel
}
u, err := url.Parse(baseURL)
if err != nil {
return nil, err
}
u.Path = path.Join(strings.TrimSuffix(u.Path, "/"), rel)
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
httpReq.Header.Set("Accept", "application/json")
switch req.AuthType {
case "password":
httpReq.SetBasicAuth(req.Username, req.Password)
case "token":
httpReq.Header.Set("X-Auth-Token", req.Token)
httpReq.Header.Set("Authorization", "Bearer "+req.Token)
}
resp, err := client.Do(httpReq)
if err != nil {
recordRedfishTelemetry(ctx, time.Since(start), true)
c.debugf("http get path=%s error=%v dur=%s", requestPath, err, time.Since(start).Round(time.Millisecond))
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
err := fmt.Errorf("status %d from %s: %s", resp.StatusCode, requestPath, strings.TrimSpace(string(body)))
recordRedfishTelemetry(ctx, time.Since(start), true)
c.debugf("http get path=%s status=%d dur=%s", requestPath, resp.StatusCode, time.Since(start).Round(time.Millisecond))
return nil, err
}
var doc map[string]interface{}
dec := json.NewDecoder(resp.Body)
dec.UseNumber()
if err := dec.Decode(&doc); err != nil {
recordRedfishTelemetry(ctx, time.Since(start), true)
c.debugf("http get path=%s decode_error=%v dur=%s", requestPath, err, time.Since(start).Round(time.Millisecond))
return nil, err
}
recordRedfishTelemetry(ctx, time.Since(start), false)
c.debugf("http get path=%s status=%d dur=%s", requestPath, resp.StatusCode, time.Since(start).Round(time.Millisecond))
return doc, nil
}
func (c *RedfishConnector) getJSONWithRetry(ctx context.Context, client *http.Client, req Request, baseURL, requestPath string, attempts int, backoff time.Duration) (map[string]interface{}, error) {
if attempts < 1 {
attempts = 1
}
var lastErr error
for i := 0; i < attempts; i++ {
doc, err := c.getJSON(ctx, client, req, baseURL, requestPath)
if err == nil {
return doc, nil
}
lastErr = err
if i == attempts-1 || !isRetryableRedfishFetchError(err) {
break
}
if backoff > 0 {
select {
case <-time.After(backoff * time.Duration(i+1)):
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
return nil, lastErr
}
func (c *RedfishConnector) collectCriticalCollectionMembersSequential(
ctx context.Context,
client *http.Client,
req Request,
baseURL string,
collectionDoc map[string]interface{},
rawTree map[string]interface{},
fetchErrs map[string]string,
) (map[string]interface{}, bool) {
memberPaths := redfishCollectionMemberRefs(collectionDoc)
if len(memberPaths) == 0 {
return nil, false
}
retryableMissing := make([]string, 0, len(memberPaths))
unknownMissing := make([]string, 0, len(memberPaths))
for _, memberPath := range memberPaths {
memberPath = normalizeRedfishPath(memberPath)
if memberPath == "" {
continue
}
if _, exists := rawTree[memberPath]; exists {
continue
}
if msg, hasErr := fetchErrs[memberPath]; hasErr {
if !isRetryableRedfishFetchError(fmt.Errorf("%s", msg)) {
continue
}
retryableMissing = append(retryableMissing, memberPath)
continue
}
unknownMissing = append(unknownMissing, memberPath)
}
candidates := append(retryableMissing, unknownMissing...)
if len(candidates) == 0 {
return nil, false
}
if maxMembers := redfishCriticalMemberRecoveryMax(); maxMembers > 0 && len(candidates) > maxMembers {
candidates = candidates[:maxMembers]
}
out := make(map[string]interface{}, len(candidates))
for _, memberPath := range candidates {
doc, err := c.getJSONWithRetry(ctx, client, req, baseURL, memberPath, redfishCriticalMemberRetryAttempts(), redfishCriticalRetryBackoff())
if err != nil {
continue
}
out[memberPath] = doc
}
return out, true
}
func (c *RedfishConnector) recoverCriticalRedfishDocsPlanB(ctx context.Context, client *http.Client, req Request, baseURL string, criticalPaths []string, rawTree map[string]interface{}, fetchErrs map[string]string, tuning redfishprofile.AcquisitionTuning, emit ProgressFn) int {
planBStart := time.Now()
timings := newRedfishPathTimingCollector(4)
var targets []string
seenTargets := make(map[string]struct{})
skippedDiagnosticTargets := 0
addTarget := func(path string) {
path = normalizeRedfishPath(path)
if path == "" {
return
}
if !shouldIncludeCriticalPlanBPath(req, path) {
skippedDiagnosticTargets++
return
}
if _, ok := seenTargets[path]; ok {
return
}
seenTargets[path] = struct{}{}
targets = append(targets, path)
}
for _, p := range criticalPaths {
p = normalizeRedfishPath(p)
if p == "" {
continue
}
if _, ok := rawTree[p]; ok {
continue
}
errMsg, hasErr := fetchErrs[p]
if hasErr && !isRetryableRedfishFetchError(fmt.Errorf("%s", errMsg)) {
continue
}
addTarget(p)
}
// If a critical collection document was fetched, but some of its members
// failed during the initial crawl (common for /Drives on partially loaded BMCs),
// retry those member resources in plan-B too.
for _, p := range criticalPaths {
p = normalizeRedfishPath(p)
if p == "" {
continue
}
docAny, ok := rawTree[p]
if !ok {
continue
}
doc, ok := docAny.(map[string]interface{})
if !ok {
continue
}
for _, memberPath := range redfishCollectionMemberRefs(doc) {
if _, exists := rawTree[memberPath]; exists {
continue
}
errMsg, hasErr := fetchErrs[memberPath]
if hasErr && !isRetryableRedfishFetchError(fmt.Errorf("%s", errMsg)) {
continue
}
addTarget(memberPath)
}
}
// Re-probe critical hardware collections that were successfully fetched but
// returned no members. This happens when the BMC hasn't finished enumerating
// hardware at collection time (e.g. PCIeDevices or NetworkAdapters empty right
// after power-on). Only hardware inventory collection suffixes are retried.
if tuning.RecoveryPolicy.EnableEmptyCriticalCollectionRetry {
for _, p := range criticalPaths {
p = normalizeRedfishPath(p)
if p == "" {
continue
}
if _, queued := seenTargets[p]; queued {
continue
}
docAny, ok := rawTree[p]
if !ok {
continue
}
doc, ok := docAny.(map[string]interface{})
if !ok {
continue
}
if redfishCollectionHasExplicitMembers(doc) {
continue
}
if !isHardwareInventoryCollectionPath(p) {
continue
}
addTarget(p)
}
}
if len(targets) == 0 {
return 0
}
if emit != nil {
if skippedDiagnosticTargets > 0 {
emit(Progress{
Status: "running",
Progress: 97,
Message: fmt.Sprintf("Redfish: extended diagnostics disabled, skipped %d heavy diagnostic endpoints", skippedDiagnosticTargets),
})
}
totalETA := redfishCriticalCooldown() + estimatePlanBETA(len(targets))
emit(Progress{
Status: "running",
Progress: 97,
Message: fmt.Sprintf("Redfish: cooldown before retrying critical endpoints... ETA≈%s", formatETA(totalETA)),
CurrentPhase: "critical_plan_b",
ETASeconds: int(totalETA.Seconds()),
})
}
select {
case <-time.After(redfishCriticalCooldown()):
case <-ctx.Done():
return 0
}
recovered := 0
for i, p := range targets {
if emit != nil {
remaining := len(targets) - i
emit(Progress{
Status: "running",
Progress: 97,
Message: fmt.Sprintf("Redfish: plan-B (%d/%d, ETA≈%s) %s", i+1, len(targets), formatETA(estimatePlanBETA(remaining)), compactProgressPath(p)),
CurrentPhase: "critical_plan_b",
ETASeconds: int(estimatePlanBETA(remaining).Seconds()),
})
}
if i > 0 {
select {
case <-time.After(redfishCriticalSlowGap()):
case <-ctx.Done():
return recovered
}
}
reqStart := time.Now()
doc, err := c.getJSONWithRetry(ctx, client, req, baseURL, p, redfishCriticalPlanBAttempts(), redfishCriticalRetryBackoff())
timings.Observe(p, time.Since(reqStart), err != nil)
if err == nil {
rawTree[p] = doc
delete(fetchErrs, p)
recovered++
if tuning.RecoveryPolicy.EnableCriticalCollectionMemberRetry {
if members, ok := c.collectCriticalCollectionMembersSequential(ctx, client, req, baseURL, doc, rawTree, fetchErrs); ok {
for mp, md := range members {
if _, exists := rawTree[mp]; exists {
continue
}
rawTree[mp] = md
delete(fetchErrs, mp)
recovered++
}
}
}
// Numeric slow-probe is expensive; skip it when collection already advertises explicit members.
if shouldSlowProbeCriticalCollection(p, tuning) && !redfishCollectionHasExplicitMembers(doc) {
if children := c.probeDirectRedfishCollectionChildrenSlow(ctx, client, req, baseURL, p); len(children) > 0 {
for cp, cd := range children {
if _, exists := rawTree[cp]; exists {
continue
}
rawTree[cp] = cd
recovered++
}
}
}
continue
}
fetchErrs[p] = err.Error()
// If collection endpoint times out, still try direct child probing for common numeric paths.
if shouldSlowProbeCriticalCollection(p, tuning) {
if children := c.probeDirectRedfishCollectionChildrenSlow(ctx, client, req, baseURL, p); len(children) > 0 {
for cp, cd := range children {
if _, exists := rawTree[cp]; exists {
continue
}
rawTree[cp] = cd
recovered++
}
delete(fetchErrs, p)
}
}
}
if emit != nil {
if summary := timings.Summary(3); summary != "" {
emit(Progress{
Status: "running",
Progress: 97,
Message: fmt.Sprintf("Redfish: plan-B top branches by time: %s", summary),
})
}
emit(Progress{
Status: "running",
Progress: 97,
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 != "" {
slog.Info("redfish-planb-timing", "summary", summary)
}
return recovered
}
func shouldIncludeCriticalPlanBPath(req Request, path string) bool {
if req.DebugPayloads {
return true
}
return !isExtendedDiagnosticCriticalPlanBPath(path)
}
func isExtendedDiagnosticCriticalPlanBPath(path string) bool {
path = normalizeRedfishPath(path)
if path == "" {
return false
}
parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) < 5 || parts[0] != "redfish" || parts[1] != "v1" || parts[2] != "Chassis" {
return false
}
if !strings.HasPrefix(parts[3], "HGX_") {
return false
}
for _, suffix := range []string{
"/Accelerators",
"/Assembly",
"/Drives",
"/NetworkAdapters",
"/PCIeDevices",
} {
if strings.HasSuffix(path, suffix) {
return true
}
}
return false
}
func (c *RedfishConnector) recoverProfilePlanBDocs(ctx context.Context, client *http.Client, req Request, baseURL string, plan redfishprofile.AcquisitionPlan, rawTree map[string]interface{}, emit ProgressFn) int {
if len(plan.PlanBPaths) == 0 || plan.Mode == redfishprofile.ModeFallback || !plan.Tuning.RecoveryPolicy.EnableProfilePlanB {
return 0
}
planBStart := time.Now()
targets := make([]string, 0, len(plan.PlanBPaths))
for _, p := range plan.PlanBPaths {
p = normalizeRedfishPath(p)
if p == "" {
continue
}
if _, exists := rawTree[p]; exists {
continue
}
targets = append(targets, p)
}
if len(targets) == 0 {
return 0
}
if emit != nil {
emit(Progress{
Status: "running",
Progress: 98,
Message: fmt.Sprintf("Redfish: profile plan-B fetching %d endpoints...", len(targets)),
CurrentPhase: "profile_plan_b",
ETASeconds: int(estimateProgressETA(planBStart, 0, len(targets), 2*time.Second).Seconds()),
})
}
recovered := 0
for i, p := range targets {
if emit != nil {
emit(Progress{
Status: "running",
Progress: 98,
Message: fmt.Sprintf("Redfish: profile plan-B (%d/%d) %s", i+1, len(targets), compactProgressPath(p)),
CurrentPhase: "profile_plan_b",
ETASeconds: int(estimateProgressETA(planBStart, i, len(targets), 2*time.Second).Seconds()),
})
}
doc, err := c.getJSONWithRetry(ctx, client, req, baseURL, p, redfishCriticalPlanBAttempts(), redfishCriticalRetryBackoff())
if err != nil {
continue
}
rawTree[p] = doc
recovered++
}
if recovered > 0 {
slog.Info("redfish-profile-planb",
"mode", plan.Mode,
"profiles", strings.Join(plan.Profiles, ","),
"targets", len(targets),
"recovered", recovered,
)
}
if emit != nil {
emit(Progress{
Status: "running",
Progress: 98,
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",
})
}
return recovered
}
func parseBoardInfo(system map[string]interface{}) models.BoardInfo {
return models.BoardInfo{
Manufacturer: normalizeRedfishIdentityField(asString(system["Manufacturer"])),
ProductName: normalizeRedfishIdentityField(firstNonEmpty(
asString(system["Model"]),
asString(system["ProductName"]),
asString(system["Name"]),
)),
SerialNumber: normalizeRedfishIdentityField(asString(system["SerialNumber"])),
PartNumber: normalizeRedfishIdentityField(firstNonEmpty(
asString(system["PartNumber"]),
asString(system["SKU"]),
)),
UUID: normalizeRedfishIdentityField(asString(system["UUID"])),
}
}
func parseBoardInfoWithFallback(system, chassis, fru map[string]interface{}) models.BoardInfo {
board := parseBoardInfo(system)
chassisBoard := parseBoardInfo(chassis)
fruBoard := parseBoardInfoFromFRUDoc(fru)
if board.Manufacturer == "" {
board.Manufacturer = firstNonEmpty(chassisBoard.Manufacturer, fruBoard.Manufacturer)
}
if board.ProductName == "" {
board.ProductName = firstNonEmpty(chassisBoard.ProductName, fruBoard.ProductName)
}
if board.SerialNumber == "" {
board.SerialNumber = firstNonEmpty(chassisBoard.SerialNumber, fruBoard.SerialNumber)
}
if board.PartNumber == "" {
board.PartNumber = firstNonEmpty(chassisBoard.PartNumber, fruBoard.PartNumber)
}
if board.UUID == "" {
board.UUID = chassisBoard.UUID
}
return board
}
func parseBoardInfoFromFRUDoc(doc map[string]interface{}) models.BoardInfo {
if len(doc) == 0 {
return models.BoardInfo{}
}
return models.BoardInfo{
Manufacturer: findFirstNormalizedStringByKeys(doc, "Manufacturer", "BoardManufacturer", "Vendor"),
ProductName: findFirstNormalizedStringByKeys(doc, "ProductName", "BoardName", "PlatformId", "PlatformName", "MachineTypeModel", "Model"),
SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber", "BoardSerialNumber"),
PartNumber: findFirstNormalizedStringByKeys(doc, "PartNumber", "BoardPartNumber", "ProductPartNumber"),
}
}
func findFirstNormalizedStringByKeys(doc map[string]interface{}, keys ...string) string {
if len(doc) == 0 || len(keys) == 0 {
return ""
}
keySet := make(map[string]struct{}, len(keys))
for _, key := range keys {
k := strings.ToLower(strings.TrimSpace(key))
if k != "" {
keySet[k] = struct{}{}
}
}
stack := []any{doc}
for len(stack) > 0 {
last := len(stack) - 1
node := stack[last]
stack = stack[:last]
switch v := node.(type) {
case map[string]interface{}:
for k, raw := range v {
if _, ok := keySet[strings.ToLower(strings.TrimSpace(k))]; ok {
if s, ok := raw.(string); ok {
if normalized := normalizeRedfishIdentityField(s); normalized != "" {
return normalized
}
}
}
switch nested := raw.(type) {
case map[string]interface{}, []interface{}:
stack = append(stack, nested)
}
}
case []interface{}:
for _, item := range v {
switch nested := item.(type) {
case map[string]interface{}, []interface{}:
stack = append(stack, nested)
}
}
}
}
return ""
}
func parseCPUs(docs []map[string]interface{}) []models.CPU {
cpus := make([]models.CPU, 0, len(docs))
socketIdx := 0
for _, doc := range docs {
// Skip non-CPU processors (GPUs, FPGAs, etc.) that some BMCs list in the
// same Processors collection.
if pt := strings.TrimSpace(asString(doc["ProcessorType"])); pt != "" &&
!strings.EqualFold(pt, "CPU") && !strings.EqualFold(pt, "General") {
continue
}
socket := socketIdx
socketIdx++
if s := strings.TrimSpace(asString(doc["Socket"])); s != "" {
// Parse numeric suffix from labels like "CPU0", "Processor 1", etc.
trimmed := strings.TrimLeft(strings.ToUpper(s), "ABCDEFGHIJKLMNOPQRSTUVWXYZ _")
if n, err := strconv.Atoi(trimmed); err == nil {
socket = n
}
}
l1, l2, l3 := parseCPUCachesFromProcessorMemory(doc)
publicSerial := redfishCPUPublicSerial(doc)
serial := normalizeRedfishIdentityField(asString(doc["SerialNumber"]))
if serial == "" && publicSerial == "" {
serial = findFirstNormalizedStringByKeys(doc, "SerialNumber")
}
cpus = append(cpus, models.CPU{
Socket: socket,
Model: firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
Cores: asInt(doc["TotalCores"]),
Threads: asInt(doc["TotalThreads"]),
FrequencyMHz: int(redfishFirstNumeric(doc, "OperatingSpeedMHz", "CurrentSpeedMHz", "FrequencyMHz")),
MaxFreqMHz: int(redfishFirstNumeric(doc, "MaxSpeedMHz", "TurboEnableMaxSpeedMHz", "TurboDisableMaxSpeedMHz")),
PPIN: firstNonEmpty(findFirstNormalizedStringByKeys(doc, "PPIN", "ProtectedIdentificationNumber"), publicSerial),
SerialNumber: serial,
L1CacheKB: l1,
L2CacheKB: l2,
L3CacheKB: l3,
Status: mapStatus(doc["Status"]),
Details: redfishCPUDetails(doc),
})
}
return cpus
}
func redfishCPUPublicSerial(doc map[string]interface{}) string {
oem, _ := doc["Oem"].(map[string]interface{})
public, _ := oem["Public"].(map[string]interface{})
return normalizeRedfishIdentityField(asString(public["SerialNumber"]))
}
// parseCPUCachesFromProcessorMemory reads L1/L2/L3 cache sizes from the
// Redfish ProcessorMemory array (Processor.v1_x spec).
func parseCPUCachesFromProcessorMemory(doc map[string]interface{}) (l1, l2, l3 int) {
mem, _ := doc["ProcessorMemory"].([]interface{})
for _, mAny := range mem {
m, ok := mAny.(map[string]interface{})
if !ok {
continue
}
capMiB := asInt(m["CapacityMiB"])
if capMiB == 0 {
continue
}
capKB := capMiB * 1024
switch strings.ToUpper(strings.TrimSpace(asString(m["MemoryType"]))) {
case "L1CACHE":
l1 = capKB
case "L2CACHE":
l2 = capKB
case "L3CACHE":
l3 = capKB
}
}
return
}
func parseMemory(docs []map[string]interface{}) []models.MemoryDIMM {
out := make([]models.MemoryDIMM, 0, len(docs))
for _, doc := range docs {
slot := firstNonEmpty(
asString(doc["DeviceLocator"]),
redfishLocationLabel(doc["Location"]),
asString(doc["Name"]),
asString(doc["Id"]),
)
present := true
if strings.EqualFold(asString(doc["Status"]), "Absent") {
present = false
}
if status, ok := doc["Status"].(map[string]interface{}); ok {
state := asString(status["State"])
if strings.EqualFold(state, "Absent") || strings.EqualFold(state, "Disabled") {
present = false
}
}
out = append(out, models.MemoryDIMM{
Slot: slot,
Location: slot,
Present: present,
SizeMB: asInt(doc["CapacityMiB"]),
Type: firstNonEmpty(asString(doc["MemoryDeviceType"]), asString(doc["MemoryType"])),
MaxSpeedMHz: asInt(doc["MaxSpeedMHz"]),
CurrentSpeedMHz: asInt(doc["OperatingSpeedMhz"]),
Manufacturer: asString(doc["Manufacturer"]),
SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
PartNumber: asString(doc["PartNumber"]),
Status: mapStatus(doc["Status"]),
Details: redfishMemoryDetails(doc),
})
}
return out
}
func redfishCPUDetails(doc map[string]interface{}) map[string]any {
return redfishCPUDetailsAcrossDocs(doc)
}
func redfishCPUDetailsAcrossDocs(doc map[string]interface{}, supplementalDocs ...map[string]interface{}) map[string]any {
lookupDocs := append([]map[string]interface{}{doc}, supplementalDocs...)
details := make(map[string]any)
addFloatDetail(details, "temperature_c", redfishFirstNumericAcrossDocs(lookupDocs,
"TemperatureCelsius", "TemperatureC", "Temperature", "CurrentTemperature", "temperature",
))
addFloatDetail(details, "power_w", redfishFirstNumericAcrossDocs(lookupDocs,
"PowerConsumedWatts", "PowerWatts", "PowerConsumptionWatts",
))
addBoolDetail(details, "throttled", redfishFirstBoolAcrossDocs(lookupDocs,
"Throttled", "ThermalThrottled", "PerformanceThrottled",
))
addInt64Detail(details, "correctable_error_count", redfishFirstInt64AcrossDocs(lookupDocs,
"CorrectableErrorCount", "CorrectableErrors", "CorrectableECCErrorCount",
))
addInt64Detail(details, "uncorrectable_error_count", redfishFirstInt64AcrossDocs(lookupDocs,
"UncorrectableErrorCount", "UncorrectableErrors", "UncorrectableECCErrorCount",
))
addFloatDetail(details, "life_remaining_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"LifeRemainingPercent", "PredictedLifeLeftPercent",
))
addFloatDetail(details, "life_used_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"LifeUsedPercent", "PercentageLifeUsed",
))
for _, lookupDoc := range lookupDocs {
if microcode, ok := redfishLookupValue(lookupDoc, "MicrocodeVersion"); ok {
if s := strings.TrimSpace(asString(microcode)); s != "" {
details["microcode"] = s
break
}
}
if microcode, ok := redfishLookupValue(lookupDoc, "Microcode"); ok {
if s := strings.TrimSpace(asString(microcode)); s != "" {
details["microcode"] = s
break
}
}
}
if len(details) == 0 {
return nil
}
return details
}
func redfishMemoryDetails(doc map[string]interface{}) map[string]any {
return redfishMemoryDetailsAcrossDocs(doc)
}
func redfishMemoryDetailsAcrossDocs(doc map[string]interface{}, supplementalDocs ...map[string]interface{}) map[string]any {
lookupDocs := append([]map[string]interface{}{doc}, supplementalDocs...)
details := make(map[string]any)
addFloatDetail(details, "temperature_c", redfishFirstNumericAcrossDocs(lookupDocs,
"TemperatureCelsius", "TemperatureC", "Temperature", "CurrentTemperature", "temperature",
))
addInt64Detail(details, "correctable_ecc_error_count", redfishFirstInt64AcrossDocs(lookupDocs,
"CorrectableECCErrorCount", "CorrectableErrorCount", "CorrectableErrors",
))
addInt64Detail(details, "uncorrectable_ecc_error_count", redfishFirstInt64AcrossDocs(lookupDocs,
"UncorrectableECCErrorCount", "UncorrectableErrorCount", "UncorrectableErrors",
))
addFloatDetail(details, "life_remaining_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"LifeRemainingPercent", "PredictedLifeLeftPercent",
))
addFloatDetail(details, "life_used_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"LifeUsedPercent", "PercentageLifeUsed",
))
addFloatDetail(details, "spare_blocks_remaining_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"SpareBlocksRemainingPercent", "SpareBlocksRemainingPct",
))
addBoolDetail(details, "performance_degraded", redfishFirstBoolAcrossDocs(lookupDocs,
"PerformanceDegraded", "Degraded",
))
addBoolDetail(details, "data_loss_detected", redfishFirstBoolAcrossDocs(lookupDocs,
"DataLossDetected", "DataLoss",
))
if len(details) == 0 {
return nil
}
return details
}
func parseDrive(doc map[string]interface{}) models.Storage {
return parseDriveWithSupplementalDocs(doc)
}
func parseDriveWithSupplementalDocs(doc map[string]interface{}, supplementalDocs ...map[string]interface{}) models.Storage {
sizeGB := 0
if capBytes := asInt64(doc["CapacityBytes"]); capBytes > 0 {
sizeGB = int(capBytes / (1024 * 1024 * 1024))
}
if sizeGB == 0 {
sizeGB = asInt(doc["CapacityGB"])
}
if sizeGB == 0 {
sizeGB = asInt(doc["CapacityMiB"]) / 1024
}
storageType := classifyStorageType(doc)
slot := normalizeRAIDDriveSlot(firstNonEmpty(asString(doc["Id"]), asString(doc["Name"])))
return models.Storage{
Slot: slot,
Type: storageType,
Model: firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
SizeGB: sizeGB,
SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
Manufacturer: asString(doc["Manufacturer"]),
Firmware: asString(doc["Revision"]),
Interface: asString(doc["Protocol"]),
Present: true,
Details: redfishDriveDetailsWithSupplementalDocs(doc, supplementalDocs...),
}
}
// isNumericString returns true if s is a non-empty string of only ASCII digits.
func isNumericString(s string) bool {
if s == "" {
return false
}
for _, c := range s {
if c < '0' || c > '9' {
return false
}
}
return true
}
// normalizeRAIDDriveSlot converts Inspur-style RAID drive IDs to canonical BP notation.
// Example: "PCIe8_RAID_Disk_1:0" → "BP0:0" (enclosure_id - 1 = backplane_index)
// Other slot names are returned unchanged.
func normalizeRAIDDriveSlot(slot string) string {
// Pattern: {anything}_RAID_Disk_{enclosure}:{slot}
const marker = "_RAID_Disk_"
idx := strings.Index(slot, marker)
if idx < 0 {
return slot
}
rest := slot[idx+len(marker):] // e.g. "1:0"
colonIdx := strings.Index(rest, ":")
if colonIdx < 0 {
return slot
}
encStr := rest[:colonIdx]
slotStr := rest[colonIdx+1:]
enc, err := strconv.Atoi(encStr)
if err != nil || enc < 1 {
return slot
}
return fmt.Sprintf("BP%d:%s", enc-1, slotStr)
}
func parseStorageVolume(doc map[string]interface{}, controller string) models.StorageVolume {
sizeGB := 0
capBytes := asInt64(doc["CapacityBytes"])
if capBytes > 0 {
sizeGB = int(capBytes / (1024 * 1024 * 1024))
}
if sizeGB == 0 {
sizeGB = asInt(doc["CapacityGB"])
}
raidLevel := firstNonEmpty(asString(doc["RAIDType"]), asString(doc["VolumeType"]))
if raidLevel == "" {
if v, ok := doc["Oem"].(map[string]interface{}); ok {
if smc, ok := v["Supermicro"].(map[string]interface{}); ok {
raidLevel = firstNonEmpty(raidLevel, asString(smc["RAIDType"]), asString(smc["VolumeType"]))
}
}
}
return models.StorageVolume{
ID: asString(doc["Id"]),
Name: firstNonEmpty(asString(doc["Name"]), asString(doc["Id"])),
Controller: strings.TrimSpace(controller),
RAIDLevel: raidLevel,
SizeGB: sizeGB,
CapacityBytes: capBytes,
Status: mapStatus(doc["Status"]),
Bootable: asBool(doc["Bootable"]),
Encrypted: asBool(doc["Encrypted"]),
}
}
func redfishVolumeCapabilitiesDoc(doc map[string]interface{}) bool {
if len(doc) == 0 {
return false
}
if strings.Contains(strings.ToLower(strings.TrimSpace(asString(doc["@odata.type"]))), "collectioncapabilities") {
return true
}
path := strings.ToLower(normalizeRedfishPath(asString(doc["@odata.id"])))
if strings.HasSuffix(path, "/volumes/capabilities") {
return true
}
id := strings.TrimSpace(asString(doc["Id"]))
name := strings.ToLower(strings.TrimSpace(asString(doc["Name"])))
return strings.EqualFold(id, "Capabilities") || strings.Contains(name, "capabilities for volumecollection")
}
func parseNIC(doc map[string]interface{}) models.NetworkAdapter {
vendorID := asHexOrInt(doc["VendorId"])
deviceID := asHexOrInt(doc["DeviceId"])
model := firstNonEmpty(asString(doc["Model"]), asString(doc["Name"]))
if isMissingOrRawPCIModel(model) {
if resolved := pciids.DeviceName(vendorID, deviceID); resolved != "" {
model = resolved
}
}
vendor := asString(doc["Manufacturer"])
if strings.TrimSpace(vendor) == "" {
vendor = pciids.VendorName(vendorID)
}
location := redfishLocationLabel(doc["Location"])
slot := firstNonEmpty(redfishLocationLabel(doc["Location"]), asString(doc["Id"]), asString(doc["Name"]))
var firmware string
var portCount int
var linkWidth int
var maxLinkWidth int
var linkSpeed string
var maxLinkSpeed string
if controllers, ok := doc["Controllers"].([]interface{}); ok && len(controllers) > 0 {
totalPortCount := 0
for _, ctrlAny := range controllers {
ctrl, ok := ctrlAny.(map[string]interface{})
if !ok {
continue
}
ctrlLocation := redfishLocationLabel(ctrl["Location"])
location = firstNonEmpty(location, ctrlLocation)
if isWeakRedfishNICSlotLabel(slot) {
slot = firstNonEmpty(ctrlLocation, slot)
}
if normalizeRedfishIdentityField(firmware) == "" {
firmware = findFirstNormalizedStringByKeys(ctrl, "FirmwarePackageVersion", "FirmwareVersion")
}
if caps, ok := ctrl["ControllerCapabilities"].(map[string]interface{}); ok {
totalPortCount += sanitizeNetworkPortCount(asInt(caps["NetworkPortCount"]))
}
if pcieIf, ok := ctrl["PCIeInterface"].(map[string]interface{}); ok && linkWidth == 0 && maxLinkWidth == 0 && linkSpeed == "" && maxLinkSpeed == "" {
linkWidth = asInt(pcieIf["LanesInUse"])
maxLinkWidth = firstNonZeroInt(asInt(pcieIf["MaxLanes"]), asInt(pcieIf["Maxlanes"]))
linkSpeed = firstNonEmpty(asString(pcieIf["PCIeType"]), asString(pcieIf["CurrentLinkSpeedGTs"]), asString(pcieIf["CurrentLinkSpeed"]))
maxLinkSpeed = firstNonEmpty(asString(pcieIf["MaxPCIeType"]), asString(pcieIf["MaxLinkSpeedGTs"]), asString(pcieIf["MaxLinkSpeed"]))
}
}
portCount = sanitizeNetworkPortCount(totalPortCount)
}
return models.NetworkAdapter{
Slot: slot,
Location: location,
Present: !strings.EqualFold(mapStatus(doc["Status"]), "Absent"),
BDF: sanitizeRedfishBDF(asString(doc["BDF"])),
Model: strings.TrimSpace(model),
Vendor: strings.TrimSpace(vendor),
VendorID: vendorID,
DeviceID: deviceID,
SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
PartNumber: asString(doc["PartNumber"]),
Firmware: firmware,
PortCount: portCount,
LinkWidth: linkWidth,
LinkSpeed: linkSpeed,
MaxLinkWidth: maxLinkWidth,
MaxLinkSpeed: maxLinkSpeed,
Status: mapStatus(doc["Status"]),
Details: redfishPCIeDetails(doc, nil),
}
}
func isWeakRedfishNICSlotLabel(slot string) bool {
slot = strings.TrimSpace(slot)
if slot == "" {
return true
}
lower := strings.ToLower(slot)
if isNumericString(slot) {
return true
}
if strings.EqualFold(slot, "nic") || strings.HasPrefix(lower, "nic") && !strings.Contains(lower, "slot") {
return true
}
if strings.HasPrefix(lower, "devtype") {
return true
}
return false
}
func networkAdapterPCIeDevicePaths(doc map[string]interface{}) []string {
var out []string
if controllers, ok := doc["Controllers"].([]interface{}); ok {
for _, ctrlAny := range controllers {
ctrl, ok := ctrlAny.(map[string]interface{})
if !ok {
continue
}
links, ok := ctrl["Links"].(map[string]interface{})
if !ok {
continue
}
refs, ok := links["PCIeDevices"].([]interface{})
if !ok {
continue
}
for _, refAny := range refs {
ref, ok := refAny.(map[string]interface{})
if !ok {
continue
}
if p := asString(ref["@odata.id"]); p != "" {
out = append(out, p)
}
}
}
}
return out
}
func enrichNICFromPCIe(nic *models.NetworkAdapter, pcieDoc map[string]interface{}, functionDocs []map[string]interface{}, supplementalDocs []map[string]interface{}) {
if nic == nil {
return
}
pcieSlot := redfishLocationLabel(pcieDoc["Slot"])
if pcieSlot == "" {
pcieSlot = redfishLocationLabel(pcieDoc["Location"])
}
if isWeakRedfishNICSlotLabel(nic.Slot) && pcieSlot != "" {
nic.Slot = pcieSlot
}
if strings.TrimSpace(nic.Location) == "" && pcieSlot != "" {
nic.Location = pcieSlot
}
if strings.TrimSpace(nic.BDF) == "" {
nic.BDF = firstNonEmpty(asString(pcieDoc["BDF"]), buildBDFfromOemPublic(pcieDoc))
}
if nic.VendorID == 0 {
nic.VendorID = asHexOrInt(pcieDoc["VendorId"])
}
if nic.DeviceID == 0 {
nic.DeviceID = asHexOrInt(pcieDoc["DeviceId"])
}
if nic.LinkWidth == 0 {
nic.LinkWidth = asInt(pcieDoc["CurrentLinkWidth"])
}
if nic.MaxLinkWidth == 0 {
nic.MaxLinkWidth = asInt(pcieDoc["MaxLinkWidth"])
}
if strings.TrimSpace(nic.LinkSpeed) == "" {
nic.LinkSpeed = firstNonEmpty(asString(pcieDoc["CurrentLinkSpeedGTs"]), asString(pcieDoc["CurrentLinkSpeed"]))
}
if strings.TrimSpace(nic.MaxLinkSpeed) == "" {
nic.MaxLinkSpeed = firstNonEmpty(asString(pcieDoc["MaxLinkSpeedGTs"]), asString(pcieDoc["MaxLinkSpeed"]))
}
if nic.LinkWidth == 0 || nic.MaxLinkWidth == 0 || nic.LinkSpeed == "" || nic.MaxLinkSpeed == "" {
redfishEnrichFromOEMxFusionPCIeLink(pcieDoc, &nic.LinkWidth, &nic.MaxLinkWidth, &nic.LinkSpeed, &nic.MaxLinkSpeed)
}
if normalizeRedfishIdentityField(nic.SerialNumber) == "" {
nic.SerialNumber = findFirstNormalizedStringByKeys(pcieDoc, "SerialNumber")
}
if normalizeRedfishIdentityField(nic.PartNumber) == "" {
nic.PartNumber = findFirstNormalizedStringByKeys(pcieDoc, "PartNumber", "ProductPartNumber")
}
if normalizeRedfishIdentityField(nic.Firmware) == "" {
nic.Firmware = findFirstNormalizedStringByKeys(pcieDoc, "FirmwareVersion", "FirmwarePackageVersion")
}
for _, fn := range functionDocs {
if strings.TrimSpace(nic.BDF) == "" {
nic.BDF = sanitizeRedfishBDF(asString(fn["FunctionId"]))
}
if nic.VendorID == 0 {
nic.VendorID = asHexOrInt(fn["VendorId"])
}
if nic.DeviceID == 0 {
nic.DeviceID = asHexOrInt(fn["DeviceId"])
}
if nic.LinkWidth == 0 {
nic.LinkWidth = asInt(fn["CurrentLinkWidth"])
}
if nic.MaxLinkWidth == 0 {
nic.MaxLinkWidth = asInt(fn["MaxLinkWidth"])
}
if strings.TrimSpace(nic.LinkSpeed) == "" {
nic.LinkSpeed = firstNonEmpty(asString(fn["CurrentLinkSpeedGTs"]), asString(fn["CurrentLinkSpeed"]))
}
if strings.TrimSpace(nic.MaxLinkSpeed) == "" {
nic.MaxLinkSpeed = firstNonEmpty(asString(fn["MaxLinkSpeedGTs"]), asString(fn["MaxLinkSpeed"]))
}
if nic.LinkWidth == 0 || nic.MaxLinkWidth == 0 || nic.LinkSpeed == "" || nic.MaxLinkSpeed == "" {
redfishEnrichFromOEMxFusionPCIeLink(fn, &nic.LinkWidth, &nic.MaxLinkWidth, &nic.LinkSpeed, &nic.MaxLinkSpeed)
}
if normalizeRedfishIdentityField(nic.SerialNumber) == "" {
nic.SerialNumber = findFirstNormalizedStringByKeys(fn, "SerialNumber")
}
if normalizeRedfishIdentityField(nic.PartNumber) == "" {
nic.PartNumber = findFirstNormalizedStringByKeys(fn, "PartNumber", "ProductPartNumber")
}
if normalizeRedfishIdentityField(nic.Firmware) == "" {
nic.Firmware = findFirstNormalizedStringByKeys(fn, "FirmwareVersion", "FirmwarePackageVersion")
}
}
if strings.TrimSpace(nic.Vendor) == "" {
nic.Vendor = pciids.VendorName(nic.VendorID)
}
if isMissingOrRawPCIModel(nic.Model) {
if resolved := pciids.DeviceName(nic.VendorID, nic.DeviceID); resolved != "" {
nic.Model = resolved
}
}
nic.Details = mergeGenericDetails(nic.Details, redfishPCIeDetailsWithSupplementalDocs(pcieDoc, functionDocs, supplementalDocs))
}
func parsePSU(doc map[string]interface{}, idx int) models.PSU {
return parsePSUWithSupplementalDocs(doc, idx)
}
func parsePSUWithSupplementalDocs(doc map[string]interface{}, idx int, supplementalDocs ...map[string]interface{}) models.PSU {
status := mapStatus(doc["Status"])
present := true
if statusMap, ok := doc["Status"].(map[string]interface{}); ok {
state := asString(statusMap["State"])
if strings.EqualFold(state, "Absent") || strings.EqualFold(state, "Disabled") {
present = false
}
}
slot := firstNonEmpty(
asString(doc["MemberId"]),
asString(doc["Id"]),
asString(doc["Name"]),
)
if slot == "" {
slot = fmt.Sprintf("PSU%d", idx)
}
// Normalize numeric-only slots ("0", "1") to "PSU0", "PSU1" for consistency
// with BMC log parsers (Inspur, Dell etc.) that use the PSU prefix.
if isNumericString(slot) {
slot = "PSU" + slot
}
return models.PSU{
Slot: slot,
Present: present,
Model: firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
Vendor: asString(doc["Manufacturer"]),
WattageW: redfishPSUNominalWattage(doc),
SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
PartNumber: asString(doc["PartNumber"]),
Firmware: asString(doc["FirmwareVersion"]),
Status: status,
InputType: asString(doc["LineInputVoltageType"]),
InputPowerW: asInt(doc["PowerInputWatts"]),
OutputPowerW: asInt(doc["LastPowerOutputWatts"]),
InputVoltage: asFloat(doc["LineInputVoltage"]),
Details: redfishPSUDetailsWithSupplementalDocs(doc, supplementalDocs...),
}
}
func redfishPSUNominalWattage(doc map[string]interface{}) int {
if ranges, ok := doc["InputRanges"].([]interface{}); ok {
best := 0
for _, rawRange := range ranges {
rangeDoc, ok := rawRange.(map[string]interface{})
if !ok {
continue
}
if wattage := asInt(rangeDoc["OutputWattage"]); wattage > best {
best = wattage
}
}
if best > 0 {
return best
}
}
return asInt(doc["PowerCapacityWatts"])
}
func redfishDriveDetails(doc map[string]interface{}) map[string]any {
return redfishDriveDetailsWithSupplementalDocs(doc)
}
func redfishDriveDetailsWithSupplementalDocs(doc map[string]interface{}, supplementalDocs ...map[string]interface{}) map[string]any {
details := make(map[string]any)
lookupDocs := append([]map[string]interface{}{doc}, supplementalDocs...)
addFloatDetail(details, "temperature_c", redfishFirstNumericAcrossDocs(lookupDocs,
"TemperatureCelsius", "TemperatureC", "Temperature", "CurrentTemperature", "temperature",
))
addInt64Detail(details, "power_on_hours", redfishFirstInt64AcrossDocs(lookupDocs,
"PowerOnHours", "PowerOnHour",
))
addInt64Detail(details, "power_cycles", redfishFirstInt64AcrossDocs(lookupDocs,
"PowerCycles", "PowerCycleCount",
))
addInt64Detail(details, "unsafe_shutdowns", redfishFirstInt64AcrossDocs(lookupDocs,
"UnsafeShutdowns", "UnsafeShutdownCount",
))
addInt64Detail(details, "media_errors", redfishFirstInt64AcrossDocs(lookupDocs,
"MediaErrors", "MediaErrorCount",
))
addInt64Detail(details, "error_log_entries", redfishFirstInt64AcrossDocs(lookupDocs,
"ErrorLogEntries", "ErrorLogEntryCount",
))
addInt64Detail(details, "written_bytes", redfishFirstInt64AcrossDocs(lookupDocs,
"WrittenBytes", "BytesWritten",
))
addInt64Detail(details, "read_bytes", redfishFirstInt64AcrossDocs(lookupDocs,
"ReadBytes", "BytesRead",
))
addFloatDetail(details, "life_remaining_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"PredictedMediaLifeLeftPercent", "LifeRemainingPercent", "PercentageDriveLifeUsedInverse", "PercentLifeRemaining",
))
addFloatDetail(details, "life_used_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"PercentageDriveLifeUsed", "LifeUsedPercent", "PercentageUsed", "PercentLifeUsed",
))
addFloatDetail(details, "available_spare_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"AvailableSparePercent", "AvailableSpare", "PercentAvailableSpare",
))
addInt64Detail(details, "reallocated_sectors", redfishFirstInt64AcrossDocs(lookupDocs,
"ReallocatedSectors", "ReallocatedSectorCount",
))
addInt64Detail(details, "current_pending_sectors", redfishFirstInt64AcrossDocs(lookupDocs,
"CurrentPendingSectors", "CurrentPendingSectorCount",
))
addInt64Detail(details, "offline_uncorrectable", redfishFirstInt64AcrossDocs(lookupDocs,
"OfflineUncorrectable", "OfflineUncorrectableSectorCount",
))
if len(details) == 0 {
return nil
}
return details
}
func redfishPSUDetails(doc map[string]interface{}) map[string]any {
return redfishPSUDetailsWithSupplementalDocs(doc)
}
func redfishPSUDetailsWithSupplementalDocs(doc map[string]interface{}, supplementalDocs ...map[string]interface{}) map[string]any {
details := make(map[string]any)
lookupDocs := append([]map[string]interface{}{doc}, supplementalDocs...)
addFloatDetail(details, "temperature_c", redfishFirstNumericAcrossDocs(lookupDocs,
"TemperatureCelsius", "TemperatureC", "Temperature",
))
addFloatDetail(details, "life_remaining_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"LifeRemainingPercent", "PredictedLifeLeftPercent",
))
addFloatDetail(details, "life_used_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"LifeUsedPercent", "PercentageLifeUsed",
))
if len(details) == 0 {
return nil
}
return details
}
func redfishPCIeDetails(doc map[string]interface{}, functionDocs []map[string]interface{}) map[string]any {
return redfishPCIeDetailsWithSupplementalDocs(doc, functionDocs, nil)
}
func redfishPCIeDetailsWithSupplementalDocs(doc map[string]interface{}, functionDocs []map[string]interface{}, supplementalDocs []map[string]interface{}) map[string]any {
lookupDocs := make([]map[string]interface{}, 0, 1+len(functionDocs)+len(supplementalDocs))
lookupDocs = append(lookupDocs, doc)
lookupDocs = append(lookupDocs, functionDocs...)
lookupDocs = append(lookupDocs, supplementalDocs...)
details := make(map[string]any)
temperatureC := redfishFirstNumericAcrossDocs(lookupDocs,
"TemperatureCelsius", "TemperatureC", "Temperature",
)
if temperatureC == 0 {
temperatureC = redfishSupplementalThermalMetricForPCIe(doc, supplementalDocs)
}
addFloatDetail(details, "temperature_c", temperatureC)
addFloatDetail(details, "power_w", redfishFirstNumericAcrossDocs(lookupDocs,
"PowerConsumedWatts", "PowerWatts", "PowerConsumptionWatts",
))
addFloatDetail(details, "life_remaining_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"LifeRemainingPercent", "PredictedLifeLeftPercent",
))
addFloatDetail(details, "life_used_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"LifeUsedPercent", "PercentageLifeUsed",
))
addInt64Detail(details, "ecc_corrected_total", redfishFirstInt64AcrossDocs(lookupDocs,
"ECCCorrectedTotal", "CorrectableECCErrorCount", "CorrectableErrorCount",
))
addInt64Detail(details, "ecc_uncorrected_total", redfishFirstInt64AcrossDocs(lookupDocs,
"ECCUncorrectedTotal", "UncorrectableECCErrorCount", "UncorrectableErrorCount",
))
addBoolDetail(details, "hw_slowdown", redfishFirstBoolAcrossDocs(lookupDocs,
"HWSlowdown", "HardwareSlowdown",
))
addFloatDetail(details, "battery_charge_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"BatteryChargePercent", "BatteryChargePct",
))
addFloatDetail(details, "battery_health_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"BatteryHealthPercent", "BatteryHealthPct",
))
addFloatDetail(details, "battery_temperature_c", redfishFirstNumericAcrossDocs(lookupDocs,
"BatteryTemperatureCelsius", "BatteryTemperatureC",
))
addFloatDetail(details, "battery_voltage_v", redfishFirstNumericAcrossDocs(lookupDocs,
"BatteryVoltage", "BatteryVoltageV",
))
addBoolDetail(details, "battery_replace_required", redfishFirstBoolAcrossDocs(lookupDocs,
"BatteryReplaceRequired", "ReplaceBattery",
))
addFloatDetail(details, "sfp_temperature_c", redfishFirstNumericAcrossDocs(lookupDocs,
"SFPTemperatureCelsius", "SFPTemperatureC", "TransceiverTemperatureCelsius",
))
addFloatDetail(details, "sfp_tx_power_dbm", redfishFirstNumericAcrossDocs(lookupDocs,
"SFPTXPowerDBm", "SFPTransmitPowerDBm", "TxPowerDBm",
))
addFloatDetail(details, "sfp_rx_power_dbm", redfishFirstNumericAcrossDocs(lookupDocs,
"SFPRXPowerDBm", "SFPReceivePowerDBm", "RxPowerDBm",
))
addFloatDetail(details, "sfp_voltage_v", redfishFirstNumericAcrossDocs(lookupDocs,
"SFPVoltageV", "TransceiverVoltageV",
))
addFloatDetail(details, "sfp_bias_ma", redfishFirstNumericAcrossDocs(lookupDocs,
"SFPBiasMA", "BiasCurrentMA", "LaserBiasCurrentMA",
))
if len(details) == 0 {
return nil
}
return details
}
func redfishSupplementalThermalMetricForPCIe(doc map[string]interface{}, supplementalDocs []map[string]interface{}) float64 {
if len(supplementalDocs) == 0 {
return 0
}
deviceNames := redfishPCIeSupplementalDeviceNames(doc)
if len(deviceNames) == 0 {
return 0
}
for _, supplemental := range supplementalDocs {
readings, ok := supplemental["TemperatureReadingsCelsius"].([]interface{})
if !ok || len(readings) == 0 {
continue
}
for _, readingAny := range readings {
reading, ok := readingAny.(map[string]interface{})
if !ok {
continue
}
deviceName := strings.TrimSpace(asString(reading["DeviceName"]))
if deviceName == "" || !matchesAnyFold(deviceName, deviceNames) {
continue
}
if value := asFloat(reading["Reading"]); value != 0 {
return value
}
}
}
return 0
}
func redfishPCIeSupplementalDeviceNames(doc map[string]interface{}) []string {
names := make([]string, 0, 3)
for _, raw := range []string{
asString(doc["Id"]),
asString(doc["Name"]),
asString(doc["Model"]),
} {
name := strings.TrimSpace(raw)
if name == "" {
continue
}
if !matchesAnyFold(name, names) {
names = append(names, name)
}
}
return names
}
func matchesAnyFold(value string, candidates []string) bool {
for _, candidate := range candidates {
if strings.EqualFold(strings.TrimSpace(value), strings.TrimSpace(candidate)) {
return true
}
}
return false
}
func looksLikeNVSwitchPCIeDoc(doc map[string]interface{}) bool {
joined := strings.ToLower(strings.TrimSpace(strings.Join([]string{
asString(doc["Id"]),
asString(doc["Name"]),
asString(doc["Model"]),
asString(doc["Manufacturer"]),
}, " ")))
return strings.Contains(joined, "nvswitch")
}
func chassisPathForPCIeDoc(docPath string) string {
docPath = normalizeRedfishPath(docPath)
if !strings.Contains(docPath, "/PCIeDevices/") {
return ""
}
idx := strings.Index(docPath, "/PCIeDevices/")
if idx <= 0 {
return ""
}
chassisPath := docPath[:idx]
if !strings.HasPrefix(chassisPath, "/redfish/v1/Chassis/") {
return ""
}
return chassisPath
}
func redfishFirstNumeric(doc map[string]interface{}, keys ...string) float64 {
for _, key := range keys {
if v, ok := redfishLookupValue(doc, key); ok {
if f := asFloat(v); f != 0 {
return f
}
}
}
return 0
}
func redfishFirstNumericAcrossDocs(docs []map[string]interface{}, keys ...string) float64 {
for _, doc := range docs {
if v := redfishFirstNumeric(doc, keys...); v != 0 {
return v
}
}
return 0
}
func redfishFirstNumericWithFunctions(doc map[string]interface{}, functionDocs []map[string]interface{}, keys ...string) float64 {
if v := redfishFirstNumeric(doc, keys...); v != 0 {
return v
}
for _, fn := range functionDocs {
if v := redfishFirstNumeric(fn, keys...); v != 0 {
return v
}
}
return 0
}
func redfishFirstInt64(doc map[string]interface{}, keys ...string) int64 {
for _, key := range keys {
if v, ok := redfishLookupValue(doc, key); ok {
if n := asInt64(v); n != 0 {
return n
}
if n := int64(asInt(v)); n != 0 {
return n
}
}
}
return 0
}
func redfishFirstInt64AcrossDocs(docs []map[string]interface{}, keys ...string) int64 {
for _, doc := range docs {
if v := redfishFirstInt64(doc, keys...); v != 0 {
return v
}
}
return 0
}
func redfishFirstInt64WithFunctions(doc map[string]interface{}, functionDocs []map[string]interface{}, keys ...string) int64 {
if v := redfishFirstInt64(doc, keys...); v != 0 {
return v
}
for _, fn := range functionDocs {
if v := redfishFirstInt64(fn, keys...); v != 0 {
return v
}
}
return 0
}
func redfishFirstBoolAcrossDocs(docs []map[string]interface{}, keys ...string) *bool {
for _, doc := range docs {
if v := redfishFirstBool(doc, keys...); v != nil {
return v
}
}
return nil
}
func redfishFirstString(doc map[string]interface{}, keys ...string) string {
for _, key := range keys {
if v, ok := redfishLookupValue(doc, key); ok {
if s := strings.TrimSpace(asString(v)); s != "" {
return s
}
}
}
return ""
}
func redfishFirstStringAcrossDocs(docs []map[string]interface{}, keys ...string) string {
for _, doc := range docs {
if v := redfishFirstString(doc, keys...); v != "" {
return v
}
}
return ""
}
func redfishFirstLocationAcrossDocs(docs []map[string]interface{}, keys ...string) string {
for _, doc := range docs {
for _, key := range keys {
if v, ok := redfishLookupValue(doc, key); ok {
if loc := redfishLocationLabel(v); loc != "" {
return loc
}
}
}
}
return ""
}
func redfishLookupValue(doc map[string]interface{}, key string) (any, bool) {
if doc == nil || strings.TrimSpace(key) == "" {
return nil, false
}
if v, ok := doc[key]; ok {
return v, true
}
if oem, ok := doc["Oem"].(map[string]interface{}); ok {
if v, ok := redfishLookupNestedValue(oem, key); ok {
return v, true
}
}
return nil, false
}
func redfishFirstBool(doc map[string]interface{}, keys ...string) *bool {
for _, key := range keys {
if v, ok := redfishLookupValue(doc, key); ok {
if b, ok := asBoolPtr(v); ok {
return &b
}
}
}
return nil
}
func redfishFirstBoolWithFunctions(doc map[string]interface{}, functionDocs []map[string]interface{}, keys ...string) *bool {
if v := redfishFirstBool(doc, keys...); v != nil {
return v
}
for _, fn := range functionDocs {
if v := redfishFirstBool(fn, keys...); v != nil {
return v
}
}
return nil
}
func redfishLookupNestedValue(doc map[string]interface{}, key string) (any, bool) {
if doc == nil {
return nil, false
}
if v, ok := doc[key]; ok {
return v, true
}
for _, value := range doc {
nested, ok := value.(map[string]interface{})
if !ok {
continue
}
if v, ok := redfishLookupNestedValue(nested, key); ok {
return v, true
}
}
return nil, false
}
func addFloatDetail(dst map[string]any, key string, value float64) {
if value == 0 {
return
}
dst[key] = value
}
func addInt64Detail(dst map[string]any, key string, value int64) {
if value == 0 {
return
}
dst[key] = value
}
func addBoolDetail(dst map[string]any, key string, value *bool) {
if value == nil {
return
}
dst[key] = *value
}
func asBoolPtr(v any) (bool, bool) {
switch x := v.(type) {
case bool:
return x, true
case string:
switch strings.ToLower(strings.TrimSpace(x)) {
case "true", "yes", "enabled", "1":
return true, true
case "false", "no", "disabled", "0":
return false, true
}
case float64:
return x != 0, true
case int:
return x != 0, true
case int64:
return x != 0, true
}
return false, false
}
func parseGPU(doc map[string]interface{}, functionDocs []map[string]interface{}, idx int) models.GPU {
return parseGPUWithSupplementalDocs(doc, functionDocs, nil, idx)
}
func parseGPUWithSupplementalDocs(doc map[string]interface{}, functionDocs []map[string]interface{}, supplementalDocs []map[string]interface{}, idx int) models.GPU {
slot := firstNonEmpty(
redfishLocationLabel(doc["Slot"]),
redfishLocationLabel(doc["Location"]),
redfishLocationLabel(doc["PhysicalLocation"]),
asString(doc["Name"]),
asString(doc["Id"]),
)
if slot == "" {
slot = fmt.Sprintf("GPU%d", idx)
}
gpu := models.GPU{
Slot: slot,
Location: firstNonEmpty(redfishLocationLabel(doc["Location"]), redfishLocationLabel(doc["PhysicalLocation"])),
Model: firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
Manufacturer: asString(doc["Manufacturer"]),
SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
PartNumber: asString(doc["PartNumber"]),
Firmware: asString(doc["FirmwareVersion"]),
Status: mapStatus(doc["Status"]),
Details: redfishPCIeDetailsWithSupplementalDocs(doc, functionDocs, supplementalDocs),
}
if bdf := sanitizeRedfishBDF(asString(doc["BDF"])); bdf != "" {
gpu.BDF = bdf
}
if gpu.BDF == "" {
gpu.BDF = buildBDFfromOemPublic(doc)
}
if gpu.VendorID == 0 {
gpu.VendorID = asHexOrInt(doc["VendorId"])
}
if gpu.DeviceID == 0 {
gpu.DeviceID = asHexOrInt(doc["DeviceId"])
}
if pcieIf, ok := doc["PCIeInterface"].(map[string]interface{}); ok {
if gpu.CurrentLinkWidth == 0 {
gpu.CurrentLinkWidth = asInt(pcieIf["LanesInUse"])
}
if gpu.MaxLinkWidth == 0 {
gpu.MaxLinkWidth = firstNonZeroInt(asInt(pcieIf["MaxLanes"]), asInt(pcieIf["Maxlanes"]))
}
if gpu.CurrentLinkSpeed == "" {
gpu.CurrentLinkSpeed = firstNonEmpty(asString(pcieIf["PCIeType"]), asString(pcieIf["CurrentLinkSpeedGTs"]), asString(pcieIf["CurrentLinkSpeed"]))
}
if gpu.MaxLinkSpeed == "" {
gpu.MaxLinkSpeed = firstNonEmpty(asString(pcieIf["MaxPCIeType"]), asString(pcieIf["MaxLinkSpeedGTs"]), asString(pcieIf["MaxLinkSpeed"]))
}
}
for _, fn := range functionDocs {
if gpu.BDF == "" {
gpu.BDF = sanitizeRedfishBDF(asString(fn["FunctionId"]))
}
if gpu.VendorID == 0 {
gpu.VendorID = asHexOrInt(fn["VendorId"])
}
if gpu.DeviceID == 0 {
gpu.DeviceID = asHexOrInt(fn["DeviceId"])
}
if gpu.MaxLinkWidth == 0 {
gpu.MaxLinkWidth = asInt(fn["MaxLinkWidth"])
}
if gpu.CurrentLinkWidth == 0 {
gpu.CurrentLinkWidth = asInt(fn["CurrentLinkWidth"])
}
if gpu.MaxLinkSpeed == "" {
gpu.MaxLinkSpeed = firstNonEmpty(asString(fn["MaxLinkSpeedGTs"]), asString(fn["MaxLinkSpeed"]))
}
if gpu.CurrentLinkSpeed == "" {
gpu.CurrentLinkSpeed = firstNonEmpty(asString(fn["CurrentLinkSpeedGTs"]), asString(fn["CurrentLinkSpeed"]))
}
if gpu.CurrentLinkWidth == 0 || gpu.MaxLinkWidth == 0 || gpu.CurrentLinkSpeed == "" || gpu.MaxLinkSpeed == "" {
redfishEnrichFromOEMxFusionPCIeLink(fn, &gpu.CurrentLinkWidth, &gpu.MaxLinkWidth, &gpu.CurrentLinkSpeed, &gpu.MaxLinkSpeed)
}
}
if isMissingOrRawPCIModel(gpu.Model) {
if resolved := pciids.DeviceName(gpu.VendorID, gpu.DeviceID); resolved != "" {
gpu.Model = resolved
}
}
if strings.TrimSpace(gpu.Manufacturer) == "" {
gpu.Manufacturer = pciids.VendorName(gpu.VendorID)
}
return gpu
}
func parsePCIeDevice(doc map[string]interface{}, functionDocs []map[string]interface{}) models.PCIeDevice {
return parsePCIeDeviceWithSupplementalDocs(doc, functionDocs, nil)
}
func parsePCIeDeviceWithSupplementalDocs(doc map[string]interface{}, functionDocs []map[string]interface{}, supplementalDocs []map[string]interface{}) models.PCIeDevice {
supplementalSlot := redfishFirstLocationAcrossDocs(supplementalDocs, "Slot", "Location", "PhysicalLocation")
dev := models.PCIeDevice{
Slot: firstNonEmpty(redfishLocationLabel(doc["Slot"]), redfishLocationLabel(doc["Location"]), supplementalSlot, asString(doc["Name"]), asString(doc["Id"])),
BDF: sanitizeRedfishBDF(asString(doc["BDF"])),
DeviceClass: asString(doc["DeviceType"]),
Manufacturer: asString(doc["Manufacturer"]),
PartNumber: asString(doc["PartNumber"]),
SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
VendorID: asHexOrInt(doc["VendorId"]),
DeviceID: asHexOrInt(doc["DeviceId"]),
Details: redfishPCIeDetailsWithSupplementalDocs(doc, functionDocs, supplementalDocs),
}
if strings.TrimSpace(dev.BDF) == "" {
dev.BDF = buildBDFfromOemPublic(doc)
}
for _, fn := range functionDocs {
if dev.BDF == "" {
dev.BDF = sanitizeRedfishBDF(asString(fn["FunctionId"]))
}
if dev.DeviceClass == "" || isGenericPCIeClassLabel(dev.DeviceClass) {
dev.DeviceClass = firstNonEmpty(asString(fn["DeviceClass"]), asString(fn["ClassCode"]))
}
if dev.VendorID == 0 {
dev.VendorID = asHexOrInt(fn["VendorId"])
}
if dev.DeviceID == 0 {
dev.DeviceID = asHexOrInt(fn["DeviceId"])
}
if dev.LinkWidth == 0 {
dev.LinkWidth = asInt(fn["CurrentLinkWidth"])
}
if dev.MaxLinkWidth == 0 {
dev.MaxLinkWidth = asInt(fn["MaxLinkWidth"])
}
if dev.LinkSpeed == "" {
dev.LinkSpeed = firstNonEmpty(asString(fn["CurrentLinkSpeedGTs"]), asString(fn["CurrentLinkSpeed"]))
}
if dev.MaxLinkSpeed == "" {
dev.MaxLinkSpeed = firstNonEmpty(asString(fn["MaxLinkSpeedGTs"]), asString(fn["MaxLinkSpeed"]))
}
if dev.LinkWidth == 0 || dev.MaxLinkWidth == 0 || dev.LinkSpeed == "" || dev.MaxLinkSpeed == "" {
redfishEnrichFromOEMxFusionPCIeLink(fn, &dev.LinkWidth, &dev.MaxLinkWidth, &dev.LinkSpeed, &dev.MaxLinkSpeed)
}
}
if dev.DeviceClass == "" || isGenericPCIeClassLabel(dev.DeviceClass) {
dev.DeviceClass = firstNonEmpty(redfishFirstStringAcrossDocs(supplementalDocs, "DeviceType"), dev.DeviceClass)
}
if dev.DeviceClass == "" {
dev.DeviceClass = "PCIe device"
}
if isGenericPCIeClassLabel(dev.DeviceClass) {
if resolved := pciids.DeviceName(dev.VendorID, dev.DeviceID); resolved != "" {
dev.DeviceClass = resolved
}
}
if isGenericPCIeClassLabel(dev.DeviceClass) {
dev.DeviceClass = "PCIe device"
}
if strings.TrimSpace(dev.Manufacturer) == "" {
dev.Manufacturer = firstNonEmpty(
redfishFirstStringAcrossDocs(supplementalDocs, "Manufacturer"),
pciids.VendorName(dev.VendorID),
)
}
if strings.TrimSpace(dev.PartNumber) == "" {
dev.PartNumber = firstNonEmpty(
redfishFirstStringAcrossDocs(supplementalDocs, "ProductPartNumber", "PartNumber"),
pciids.DeviceName(dev.VendorID, dev.DeviceID),
)
}
if normalizeRedfishIdentityField(dev.SerialNumber) == "" {
dev.SerialNumber = redfishFirstStringAcrossDocs(supplementalDocs, "SerialNumber")
}
return dev
}
func parsePCIeFunction(doc map[string]interface{}, idx int) models.PCIeDevice {
return parsePCIeFunctionWithSupplementalDocs(doc, nil, idx)
}
func parsePCIeFunctionWithSupplementalDocs(doc map[string]interface{}, supplementalDocs []map[string]interface{}, idx int) models.PCIeDevice {
slot := firstNonEmpty(redfishLocationLabel(doc["Location"]), asString(doc["Id"]), asString(doc["Name"]))
if slot == "" {
slot = fmt.Sprintf("PCIeFn%d", idx)
}
dev := models.PCIeDevice{
Slot: slot,
BDF: sanitizeRedfishBDF(asString(doc["BDF"])),
VendorID: asHexOrInt(doc["VendorId"]),
DeviceID: asHexOrInt(doc["DeviceId"]),
DeviceClass: firstNonEmpty(asString(doc["DeviceClass"]), asString(doc["ClassCode"]), "PCIe device"),
Manufacturer: asString(doc["Manufacturer"]),
SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
LinkWidth: asInt(doc["CurrentLinkWidth"]),
LinkSpeed: firstNonEmpty(asString(doc["CurrentLinkSpeedGTs"]), asString(doc["CurrentLinkSpeed"])),
MaxLinkWidth: asInt(doc["MaxLinkWidth"]),
MaxLinkSpeed: firstNonEmpty(asString(doc["MaxLinkSpeedGTs"]), asString(doc["MaxLinkSpeed"])),
Details: redfishPCIeDetailsWithSupplementalDocs(doc, nil, supplementalDocs),
}
if dev.BDF == "" {
dev.BDF = firstNonEmpty(buildBDFfromOemPublic(doc), sanitizeRedfishBDF(asString(doc["FunctionId"])))
}
if isGenericPCIeClassLabel(dev.DeviceClass) {
if resolved := pciids.DeviceName(dev.VendorID, dev.DeviceID); resolved != "" {
dev.DeviceClass = resolved
}
}
if strings.TrimSpace(dev.Manufacturer) == "" {
dev.Manufacturer = pciids.VendorName(dev.VendorID)
}
if strings.TrimSpace(dev.PartNumber) == "" {
dev.PartNumber = pciids.DeviceName(dev.VendorID, dev.DeviceID)
}
return dev
}
func isMissingOrRawPCIModel(model string) bool {
model = strings.TrimSpace(model)
if model == "" {
return true
}
l := strings.ToLower(model)
if l == "unknown" || l == "n/a" || l == "na" || l == "none" {
return true
}
if isGenericRedfishInventoryName(l) {
return true
}
if strings.HasPrefix(l, "0x") && len(l) <= 6 {
return true
}
if len(model) <= 4 {
isHex := true
for _, c := range l {
if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
isHex = false
break
}
}
if isHex {
return true
}
}
return false
}
func isGenericRedfishInventoryName(value string) bool {
value = strings.ToLower(strings.TrimSpace(value))
switch {
case value == "":
return false
case value == "networkadapter", strings.HasPrefix(value, "networkadapter_"), strings.HasPrefix(value, "networkadapter "):
return true
case value == "pciedevice", strings.HasPrefix(value, "pciedevice_"), strings.HasPrefix(value, "pciedevice "):
return true
case value == "pciefunction", strings.HasPrefix(value, "pciefunction_"), strings.HasPrefix(value, "pciefunction "):
return true
case value == "ethernetinterface", strings.HasPrefix(value, "ethernetinterface_"), strings.HasPrefix(value, "ethernetinterface "):
return true
case value == "networkport", strings.HasPrefix(value, "networkport_"), strings.HasPrefix(value, "networkport "):
return true
default:
return false
}
}
// isUnidentifiablePCIeDevice returns true for PCIe topology entries that carry no
// useful inventory information: generic class (SingleFunction/MultiFunction), no
// resolved model or serial, and no PCI vendor/device IDs for future resolution.
// These are typically PCH bridges, root ports, or other bus infrastructure that
// some BMCs (e.g. MSI) enumerate exhaustively in their PCIeDevices collection.
func isUnidentifiablePCIeDevice(dev models.PCIeDevice) bool {
if !isGenericPCIeClassLabel(dev.DeviceClass) {
return false
}
if normalizeRedfishIdentityField(dev.PartNumber) != "" {
return false
}
if normalizeRedfishIdentityField(dev.SerialNumber) != "" {
return false
}
if dev.VendorID > 0 || dev.DeviceID > 0 {
return false
}
return true
}
func isGenericPCIeClassLabel(v string) bool {
switch strings.ToLower(strings.TrimSpace(v)) {
case "", "pcie device", "display", "display controller", "vga", "3d controller", "network", "network controller", "storage", "storage controller", "other", "unknown", "singlefunction", "multifunction", "simulated":
return true
default:
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(v)), "0x")
}
}
func redfishPCIeMatchesChassisDeviceDoc(doc, deviceDoc map[string]interface{}) bool {
if len(doc) == 0 || len(deviceDoc) == 0 || redfishChassisDeviceDocLooksEmpty(deviceDoc) {
return false
}
docSerial := normalizeRedfishIdentityField(findFirstNormalizedStringByKeys(doc, "SerialNumber"))
deviceSerial := normalizeRedfishIdentityField(findFirstNormalizedStringByKeys(deviceDoc, "SerialNumber"))
if docSerial != "" && deviceSerial != "" && strings.EqualFold(docSerial, deviceSerial) {
return true
}
docTokens := redfishPCIeMatchTokens(doc)
deviceTokens := redfishPCIeMatchTokens(deviceDoc)
if len(docTokens) == 0 || len(deviceTokens) == 0 {
return false
}
for _, token := range docTokens {
for _, candidate := range deviceTokens {
if strings.EqualFold(token, candidate) {
return true
}
}
}
return false
}
func redfishPCIeMatchTokens(doc map[string]interface{}) []string {
if len(doc) == 0 {
return nil
}
rawValues := []string{
asString(doc["Name"]),
asString(doc["Model"]),
asString(doc["PartNumber"]),
asString(doc["ProductPartNumber"]),
}
out := make([]string, 0, len(rawValues))
seen := make(map[string]struct{}, len(rawValues))
for _, raw := range rawValues {
value := normalizeRedfishIdentityField(raw)
if value == "" {
continue
}
key := strings.ToLower(value)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, value)
}
return out
}
func redfishChassisDeviceDocLooksEmpty(doc map[string]interface{}) bool {
name := strings.ToLower(strings.TrimSpace(asString(doc["Name"])))
if strings.HasPrefix(name, "empty slot") {
return true
}
if strings.ToLower(strings.TrimSpace(asString(doc["DeviceType"]))) != "unknown" {
return false
}
return normalizeRedfishIdentityField(asString(doc["PartNumber"])) == "" &&
normalizeRedfishIdentityField(asString(doc["ProductPartNumber"])) == "" &&
normalizeRedfishIdentityField(findFirstNormalizedStringByKeys(doc, "SerialNumber")) == ""
}
func buildBDFfromOemPublic(doc map[string]interface{}) string {
if len(doc) == 0 {
return ""
}
oem, ok := doc["Oem"].(map[string]interface{})
if !ok {
return ""
}
public, ok := oem["Public"].(map[string]interface{})
if !ok {
return ""
}
bus := asHexOrInt(public["BusNumber"])
dev := asHexOrInt(public["DeviceNumber"])
fn := asHexOrInt(public["FunctionNumber"])
if bus < 0 || dev < 0 || fn < 0 {
return ""
}
segment := asHexOrInt(public["Segment"])
if segment < 0 {
segment = 0
}
// Require at least bus + dev numbers to avoid inventing meaningless BDFs.
if bus == 0 && dev == 0 && fn == 0 {
return ""
}
return fmt.Sprintf("%04x:%02x:%02x.%x", segment, bus, dev, fn)
}
// redfishEnrichFromOEMxFusionPCIeLink fills in missing PCIe link width/speed
// from the xFusion OEM namespace. xFusion reports link width as a string like
// "X8" in Oem.xFusion.LinkWidth / Oem.xFusion.LinkWidthAbility, and link speed
// as a string like "Gen4 (16.0GT/s)" in Oem.xFusion.LinkSpeed /
// Oem.xFusion.LinkSpeedAbility. These fields appear on PCIeFunction docs.
func redfishEnrichFromOEMxFusionPCIeLink(doc map[string]interface{}, linkWidth, maxLinkWidth *int, linkSpeed, maxLinkSpeed *string) {
oem, _ := doc["Oem"].(map[string]interface{})
if oem == nil {
return
}
xf, _ := oem["xFusion"].(map[string]interface{})
if xf == nil {
return
}
if *linkWidth == 0 {
*linkWidth = parseXFusionLinkWidth(asString(xf["LinkWidth"]))
}
if *maxLinkWidth == 0 {
*maxLinkWidth = parseXFusionLinkWidth(asString(xf["LinkWidthAbility"]))
}
if strings.TrimSpace(*linkSpeed) == "" {
*linkSpeed = strings.TrimSpace(asString(xf["LinkSpeed"]))
}
if strings.TrimSpace(*maxLinkSpeed) == "" {
*maxLinkSpeed = strings.TrimSpace(asString(xf["LinkSpeedAbility"]))
}
}
// parseXFusionLinkWidth converts an xFusion link-width string like "X8" or
// "x16" to the integer lane count. Returns 0 for unrecognised values.
func parseXFusionLinkWidth(s string) int {
s = strings.TrimSpace(s)
if s == "" {
return 0
}
s = strings.TrimPrefix(strings.ToUpper(s), "X")
v := asInt(s)
if v <= 0 {
return 0
}
return v
}
// firstNonZeroInt returns the first argument that is non-zero.
func firstNonZeroInt(vals ...int) int {
for _, v := range vals {
if v != 0 {
return v
}
}
return 0
}
func normalizeRedfishIdentityField(v string) string {
v = strings.TrimSpace(v)
if v == "" {
return ""
}
switch strings.ToLower(v) {
case "n/a", "na", "none", "null", "unknown", "0":
return ""
default:
return v
}
}
func gpuDedupKey(gpu models.GPU) string {
if serial := normalizeRedfishIdentityField(gpu.SerialNumber); serial != "" {
return serial
}
if bdf := strings.TrimSpace(gpu.BDF); bdf != "" {
return bdf
}
return firstNonEmpty(strings.TrimSpace(gpu.Slot)+"|"+strings.TrimSpace(gpu.Model), strings.TrimSpace(gpu.Slot))
}
func gpuDocDedupKey(doc map[string]interface{}, gpu models.GPU) string {
// Prefer stable GPU identifiers (serial, BDF) over path so that the same
// physical GPU exposed under multiple Chassis PCIeDevice trees (e.g. Supermicro
// HGX: Chassis/1/PCIeDevices/GPU1 and Chassis/HGX_GPU_SXM_1/PCIeDevices/GPU_SXM_1)
// is correctly deduplicated.
//
// Only stable identifiers (serial, BDF) are used for cross-path dedup.
// When neither is present we fall back to path, so two genuinely distinct GPUs
// that happen to share the same model name (e.g. in GraphicsControllers) are
// not incorrectly collapsed into one.
if serial := normalizeRedfishIdentityField(gpu.SerialNumber); serial != "" {
return serial
}
if bdf := strings.TrimSpace(gpu.BDF); bdf != "" {
return bdf
}
if path := normalizeRedfishPath(asString(doc["@odata.id"])); path != "" {
return "path:" + path
}
return ""
}
func shouldSkipGenericGPUDuplicate(existing []models.GPU, candidate models.GPU) bool {
if len(existing) == 0 {
return false
}
if normalizeRedfishIdentityField(candidate.SerialNumber) != "" || strings.TrimSpace(candidate.BDF) != "" {
return false
}
slot := strings.TrimSpace(candidate.Slot)
model := strings.TrimSpace(candidate.Model)
if slot == "" || model == "" {
return false
}
// Typical GraphicsControllers fallback on some BMCs reports only model/name
// as slot and lacks stable identifiers. If we already have concrete GPUs of the
// same model/manufacturer from PCIe inventory, this candidate is a duplicate.
if !strings.EqualFold(slot, model) {
return false
}
for _, gpu := range existing {
if !strings.EqualFold(strings.TrimSpace(gpu.Model), model) {
continue
}
existingMfr := strings.TrimSpace(gpu.Manufacturer)
candidateMfr := strings.TrimSpace(candidate.Manufacturer)
if existingMfr != "" && candidateMfr != "" && !strings.EqualFold(existingMfr, candidateMfr) {
continue
}
if normalizeRedfishIdentityField(gpu.SerialNumber) != "" || strings.TrimSpace(gpu.BDF) != "" {
return true
}
}
return false
}
func dropModelOnlyGPUPlaceholders(items []models.GPU) []models.GPU {
if len(items) < 2 {
return items
}
// Merge serial from generic GraphicsControllers placeholders (slot ~= model)
// into concrete PCIe rows (with BDF) when mapping is unambiguous.
mergedPlaceholder := make(map[int]struct{})
usedConcrete := make(map[int]struct{})
unresolvedByGroup := make(map[string][]int)
for i := range items {
serial := normalizeRedfishIdentityField(items[i].SerialNumber)
if serial == "" || strings.TrimSpace(items[i].BDF) != "" || !isModelOnlyGPUPlaceholder(items[i]) {
continue
}
candidates := matchingConcreteGPUIndexes(items, i, usedConcrete)
candidate := -1
if len(candidates) == 1 {
candidate = candidates[0]
}
if candidate >= 0 {
mergeGPUPlaceholderIntoConcrete(&items[candidate], items[i])
usedConcrete[candidate] = struct{}{}
mergedPlaceholder[i] = struct{}{}
continue
}
group := gpuModelVendorKey(items[i])
if group == "" {
continue
}
unresolvedByGroup[group] = append(unresolvedByGroup[group], i)
}
// Fallback mapping by order for ambiguous groups (e.g. same model x8).
for group, placeholders := range unresolvedByGroup {
donors := make([]int, 0, len(placeholders))
for j := range items {
if _, used := usedConcrete[j]; used {
continue
}
if !isConcreteGPUDonor(items[j]) {
continue
}
if gpuModelVendorKey(items[j]) != group {
continue
}
if normalizeRedfishIdentityField(items[j].SerialNumber) != "" {
continue
}
donors = append(donors, j)
}
limit := len(placeholders)
if len(donors) < limit {
limit = len(donors)
}
for k := 0; k < limit; k++ {
pi := placeholders[k]
di := donors[k]
if normalizeRedfishIdentityField(items[pi].SerialNumber) == "" {
continue
}
mergeGPUPlaceholderIntoConcrete(&items[di], items[pi])
usedConcrete[di] = struct{}{}
mergedPlaceholder[pi] = struct{}{}
}
}
concreteByModel := make(map[string]struct{}, len(items))
for _, gpu := range items {
modelKey := strings.ToLower(strings.TrimSpace(gpu.Model))
if modelKey == "" {
continue
}
if normalizeRedfishIdentityField(gpu.SerialNumber) != "" || strings.TrimSpace(gpu.BDF) != "" {
concreteByModel[modelKey] = struct{}{}
}
}
if len(concreteByModel) == 0 {
return items
}
out := make([]models.GPU, 0, len(items))
for i, gpu := range items {
modelKey := strings.ToLower(strings.TrimSpace(gpu.Model))
if _, hasConcrete := concreteByModel[modelKey]; hasConcrete &&
strings.TrimSpace(gpu.BDF) == "" &&
isModelOnlyGPUPlaceholder(gpu) &&
(normalizeRedfishIdentityField(gpu.SerialNumber) == "" || hasMergedPlaceholderIndex(mergedPlaceholder, i)) {
continue
}
out = append(out, gpu)
}
return out
}
func isModelOnlyGPUPlaceholder(gpu models.GPU) bool {
slot := strings.TrimSpace(gpu.Slot)
model := strings.TrimSpace(gpu.Model)
if slot == "" || model == "" {
return false
}
return strings.EqualFold(slot, model) || strings.HasPrefix(strings.ToUpper(slot), "GPU")
}
func isConcreteGPUDonor(gpu models.GPU) bool {
if strings.TrimSpace(gpu.BDF) == "" {
return false
}
return !isModelOnlyGPUPlaceholder(gpu)
}
func gpuModelVendorKey(gpu models.GPU) string {
model := strings.ToLower(strings.TrimSpace(gpu.Model))
if model == "" {
return ""
}
mfr := strings.ToLower(strings.TrimSpace(gpu.Manufacturer))
return model + "|" + mfr
}
func matchingConcreteGPUIndexes(items []models.GPU, placeholderIdx int, usedConcrete map[int]struct{}) []int {
out := make([]int, 0, 2)
ph := items[placeholderIdx]
for j := range items {
if j == placeholderIdx {
continue
}
if _, used := usedConcrete[j]; used {
continue
}
if !isConcreteGPUDonor(items[j]) {
continue
}
if !strings.EqualFold(strings.TrimSpace(items[j].Model), strings.TrimSpace(ph.Model)) {
continue
}
otherMfr := strings.TrimSpace(items[j].Manufacturer)
phMfr := strings.TrimSpace(ph.Manufacturer)
if phMfr != "" && otherMfr != "" && !strings.EqualFold(phMfr, otherMfr) {
continue
}
if normalizeRedfishIdentityField(items[j].SerialNumber) != "" {
continue
}
out = append(out, j)
}
return out
}
func mergeGPUPlaceholderIntoConcrete(concrete *models.GPU, placeholder models.GPU) {
if concrete == nil {
return
}
if normalizeRedfishIdentityField(concrete.SerialNumber) == "" {
if serial := normalizeRedfishIdentityField(placeholder.SerialNumber); serial != "" {
concrete.SerialNumber = serial
}
}
if strings.TrimSpace(concrete.UUID) == "" && strings.TrimSpace(placeholder.UUID) != "" {
concrete.UUID = placeholder.UUID
}
if strings.TrimSpace(concrete.PartNumber) == "" && strings.TrimSpace(placeholder.PartNumber) != "" {
concrete.PartNumber = placeholder.PartNumber
}
if strings.TrimSpace(concrete.Firmware) == "" && strings.TrimSpace(placeholder.Firmware) != "" {
concrete.Firmware = placeholder.Firmware
}
if strings.TrimSpace(concrete.Status) == "" && strings.TrimSpace(placeholder.Status) != "" {
concrete.Status = placeholder.Status
}
}
func hasMergedPlaceholderIndex(indexes map[int]struct{}, idx int) bool {
_, ok := indexes[idx]
return ok
}
func looksLikeGPU(doc map[string]interface{}, functionDocs []map[string]interface{}) bool {
// "Display Device" is how MSI labels H100 secondary display/audio controller
// functions — these are not compute GPUs and should be excluded.
if strings.EqualFold(strings.TrimSpace(asString(doc["Description"])), "Display Device") {
return false
}
// NVSwitch is an NVIDIA NVLink interconnect switch, not a compute GPU.
if strings.Contains(strings.ToLower(strings.TrimSpace(asString(doc["Model"]))), "nvswitch") {
return false
}
deviceType := strings.ToLower(asString(doc["DeviceType"]))
if strings.Contains(deviceType, "gpu") || strings.Contains(deviceType, "graphics") || strings.Contains(deviceType, "accelerator") {
return true
}
if strings.Contains(deviceType, "network") {
return false
}
if oem, ok := doc["Oem"].(map[string]interface{}); ok {
if public, ok := oem["Public"].(map[string]interface{}); ok {
if dc := strings.ToLower(asString(public["DeviceClass"])); strings.Contains(dc, "network") {
return false
}
}
}
modelText := strings.ToLower(strings.Join([]string{
asString(doc["Name"]),
asString(doc["Model"]),
asString(doc["Manufacturer"]),
}, " "))
gpuHints := []string{"gpu", "nvidia", "tesla", "a100", "h100", "l40", "rtx", "radeon", "instinct"}
for _, hint := range gpuHints {
if strings.Contains(modelText, hint) {
return true
}
}
for _, fn := range functionDocs {
classCode := strings.ToLower(strings.TrimPrefix(asString(fn["ClassCode"]), "0x"))
if strings.HasPrefix(classCode, "03") || strings.HasPrefix(classCode, "12") {
return true
}
}
return false
}
// isVirtualStorageDrive returns true for BMC-virtual drives that should not
// appear in hardware inventory (e.g. AMI virtual CD/USB sticks with 0 capacity).
func isVirtualStorageDrive(doc map[string]interface{}) bool {
if strings.EqualFold(asString(doc["Protocol"]), "USB") && asInt64(doc["CapacityBytes"]) == 0 {
return true
}
mfr := strings.ToUpper(strings.TrimSpace(asString(doc["Manufacturer"])))
model := strings.ToUpper(strings.TrimSpace(asString(doc["Model"])))
name := strings.ToUpper(strings.TrimSpace(asString(doc["Name"])))
joined := strings.Join([]string{mfr, model, name}, " ")
if strings.Contains(mfr, "AMI") && strings.Contains(joined, "VIRTUAL") {
return true
}
for _, marker := range []string{
"VIRTUAL CDROM",
"VIRTUAL CD/DVD",
"VIRTUAL FLOPPY",
"VIRTUAL FDD",
"VIRTUAL USB",
"VIRTUAL MEDIA",
} {
if strings.Contains(joined, marker) {
return true
}
}
if strings.Contains(mfr, "AMERICAN MEGATRENDS") && (strings.Contains(joined, "CDROM") || strings.Contains(joined, "FLOPPY") || strings.Contains(joined, "FDD")) {
return true
}
return false
}
// isAbsentDriveDoc returns true when the drive document represents an empty bay
// with no installed media (Status.State == "Absent"). These should be excluded
// from the storage inventory.
func isAbsentDriveDoc(doc map[string]interface{}) bool {
if status, ok := doc["Status"].(map[string]interface{}); ok {
return strings.EqualFold(asString(status["State"]), "Absent")
}
return strings.EqualFold(asString(doc["Status"]), "Absent")
}
func looksLikeDrive(doc map[string]interface{}) bool {
if asString(doc["MediaType"]) != "" {
return true
}
if asString(doc["Protocol"]) != "" && (asInt(doc["CapacityGB"]) > 0 || asInt(doc["CapacityBytes"]) > 0) {
return true
}
if asString(doc["Type"]) != "" && (asString(doc["Model"]) != "" || asInt(doc["CapacityGB"]) > 0 || asInt(doc["CapacityBytes"]) > 0) {
return true
}
return false
}
func classifyStorageType(doc map[string]interface{}) string {
protocol := strings.ToUpper(asString(doc["Protocol"]))
if strings.Contains(protocol, "NVME") {
return "NVMe"
}
media := strings.ToUpper(asString(doc["MediaType"]))
if media == "SSD" {
return "SSD"
}
if media == "HDD" || media == "HDDT" {
return "HDD"
}
nameModel := strings.ToUpper(strings.Join([]string{
asString(doc["Name"]),
asString(doc["Model"]),
asString(doc["Description"]),
}, " "))
if strings.Contains(nameModel, "NVME") {
return "NVMe"
}
if strings.Contains(nameModel, "SSD") {
return "SSD"
}
if strings.Contains(nameModel, "HDD") {
return "HDD"
}
if protocol != "" {
return protocol
}
return firstNonEmpty(asString(doc["Type"]), "Storage")
}
func looksLikeVolume(doc map[string]interface{}) bool {
if redfishVolumeCapabilitiesDoc(doc) {
return false
}
if asString(doc["RAIDType"]) != "" || asString(doc["VolumeType"]) != "" {
return true
}
if strings.Contains(strings.ToLower(asString(doc["@odata.type"])), "volume") && (asInt64(doc["CapacityBytes"]) > 0 || asString(doc["Name"]) != "") {
return true
}
return false
}
func dedupeStorage(items []models.Storage) []models.Storage {
if len(items) <= 1 {
return items
}
// Pass 1: drop exact duplicates by identity and keep the richer variant.
out := dedupeStorageByIdentityPreferRich(items)
if len(out) <= 1 {
return out
}
// Pass 2: replace placeholder slots with rich drive data (slot is preserved).
merged, consumedDonors := mergeStoragePlaceholders(out)
if len(consumedDonors) > 0 {
compacted := make([]models.Storage, 0, len(merged)-len(consumedDonors))
for i, item := range merged {
if _, consumed := consumedDonors[i]; consumed {
continue
}
compacted = append(compacted, item)
}
out = compacted
} else {
out = merged
}
// Pass 3: final identity dedupe after placeholder merge.
return dedupeStorageByIdentityPreferRich(out)
}
func dedupeStorageByIdentityPreferRich(items []models.Storage) []models.Storage {
if len(items) == 0 {
return nil
}
out := make([]models.Storage, 0, len(items))
seen := make(map[string]int, len(items))
for _, item := range items {
key := storageIdentityKey(item)
if key == "" {
continue
}
if idx, ok := seen[key]; ok {
out[idx] = richerStorageEntry(out[idx], item)
continue
}
seen[key] = len(out)
out = append(out, item)
}
return out
}
func storageIdentityKey(item models.Storage) string {
if serial := normalizeRedfishIdentityField(item.SerialNumber); serial != "" {
return "sn:" + serial
}
slot := strings.TrimSpace(item.Slot)
model := strings.TrimSpace(item.Model)
if slot == "" && model == "" {
return ""
}
return "slotmodel:" + slot + "|" + model
}
func richerStorageEntry(a, b models.Storage) models.Storage {
if storageRichnessScore(b) > storageRichnessScore(a) {
b.Details = mergeGenericDetails(b.Details, a.Details)
return b
}
a.Details = mergeGenericDetails(a.Details, b.Details)
return a
}
func storageRichnessScore(item models.Storage) int {
score := 0
if normalizeRedfishIdentityField(item.SerialNumber) != "" {
score += 100
}
if item.SizeGB > 0 {
score += 40
}
if normalizedStorageModel(item) != "" {
score += 20
}
if normalizeRedfishIdentityField(item.Manufacturer) != "" {
score += 10
}
if normalizeRedfishIdentityField(item.Firmware) != "" {
score += 8
}
if strings.TrimSpace(item.Interface) != "" {
score += 5
}
if strings.TrimSpace(item.Description) != "" {
score += 3
}
if item.Present {
score++
}
return score
}
func normalizedStorageModel(item models.Storage) string {
model := normalizeRedfishIdentityField(item.Model)
if model == "" {
return ""
}
slot := strings.TrimSpace(item.Slot)
if slot != "" && strings.EqualFold(model, slot) {
return ""
}
return model
}
func isStoragePlaceholder(item models.Storage) bool {
if normalizeRedfishIdentityField(item.SerialNumber) != "" {
return false
}
if item.SizeGB > 0 {
return false
}
if normalizedStorageModel(item) != "" {
return false
}
if normalizeRedfishIdentityField(item.Manufacturer) != "" {
return false
}
if normalizeRedfishIdentityField(item.Firmware) != "" {
return false
}
if strings.TrimSpace(item.Description) != "" {
return false
}
return true
}
func isRichStorageDonor(item models.Storage) bool {
if isStoragePlaceholder(item) {
return false
}
return normalizeRedfishIdentityField(item.SerialNumber) != "" ||
item.SizeGB > 0 ||
normalizedStorageModel(item) != "" ||
normalizeRedfishIdentityField(item.Manufacturer) != "" ||
normalizeRedfishIdentityField(item.Firmware) != ""
}
func mergeStoragePlaceholders(items []models.Storage) ([]models.Storage, map[int]struct{}) {
if len(items) <= 1 {
return items, nil
}
out := make([]models.Storage, len(items))
copy(out, items)
placeholderIdx := make([]int, 0, len(out))
donorIdx := make([]int, 0, len(out))
for i, item := range out {
if isStoragePlaceholder(item) {
placeholderIdx = append(placeholderIdx, i)
continue
}
if isRichStorageDonor(item) {
donorIdx = append(donorIdx, i)
}
}
if len(placeholderIdx) == 0 || len(donorIdx) == 0 {
return out, nil
}
consumed := make(map[int]struct{}, len(donorIdx))
for _, pi := range placeholderIdx {
di := findStorageDonorIndex(out, donorIdx, consumed, out[pi].Type)
if di < 0 {
continue
}
out[pi] = mergeStorageIntoPlaceholder(out[pi], out[di])
consumed[di] = struct{}{}
}
if len(consumed) == 0 {
return out, nil
}
return out, consumed
}
func findStorageDonorIndex(items []models.Storage, donors []int, consumed map[int]struct{}, placeholderType string) int {
placeholderType = strings.TrimSpace(strings.ToUpper(placeholderType))
if placeholderType != "" {
for _, idx := range donors {
if _, used := consumed[idx]; used {
continue
}
if strings.TrimSpace(strings.ToUpper(items[idx].Type)) == placeholderType {
return idx
}
}
}
for _, idx := range donors {
if _, used := consumed[idx]; !used {
return idx
}
}
return -1
}
func mergeStorageIntoPlaceholder(placeholder, donor models.Storage) models.Storage {
out := placeholder
if strings.TrimSpace(out.Type) == "" {
out.Type = donor.Type
}
if normalizedStorageModel(out) == "" && normalizedStorageModel(donor) != "" {
out.Model = donor.Model
}
if out.SizeGB <= 0 && donor.SizeGB > 0 {
out.SizeGB = donor.SizeGB
}
if normalizeRedfishIdentityField(out.SerialNumber) == "" && normalizeRedfishIdentityField(donor.SerialNumber) != "" {
out.SerialNumber = donor.SerialNumber
}
if normalizeRedfishIdentityField(out.Manufacturer) == "" && normalizeRedfishIdentityField(donor.Manufacturer) != "" {
out.Manufacturer = donor.Manufacturer
}
if normalizeRedfishIdentityField(out.Firmware) == "" && normalizeRedfishIdentityField(donor.Firmware) != "" {
out.Firmware = donor.Firmware
}
if strings.TrimSpace(out.Interface) == "" && strings.TrimSpace(donor.Interface) != "" {
out.Interface = donor.Interface
}
if strings.TrimSpace(out.Location) == "" && strings.TrimSpace(donor.Location) != "" {
out.Location = donor.Location
}
if out.BackplaneID == 0 && donor.BackplaneID != 0 {
out.BackplaneID = donor.BackplaneID
}
if strings.TrimSpace(out.Status) == "" && strings.TrimSpace(donor.Status) != "" {
out.Status = donor.Status
}
if strings.TrimSpace(out.Description) == "" && strings.TrimSpace(donor.Description) != "" {
out.Description = donor.Description
}
if !out.Present {
out.Present = donor.Present
}
return out
}
func dedupeNetworkAdapters(items []models.NetworkAdapter) []models.NetworkAdapter {
if len(items) <= 1 {
return items
}
out := make([]models.NetworkAdapter, 0, len(items))
bySerial := make(map[string]int, len(items))
bySlotModel := make(map[string]int, len(items))
bySlot := make(map[string]int, len(items))
for _, item := range items {
serialKey := normalizeRedfishIdentityField(item.SerialNumber)
slotModelKey := networkAdapterSlotModelKey(item)
slotKey := strings.TrimSpace(item.Slot)
idx := -1
if serialKey != "" {
if existing, ok := bySerial[serialKey]; ok {
idx = existing
}
}
if idx < 0 && slotModelKey != "" {
if existing, ok := bySlotModel[slotModelKey]; ok {
idx = existing
}
}
if idx < 0 && slotKey != "" {
if existing, ok := bySlot[slotKey]; ok {
idx = existing
}
}
if idx >= 0 {
out[idx] = mergeNetworkAdapterEntries(out[idx], item)
} else {
idx = len(out)
out = append(out, item)
}
merged := out[idx]
if serial := normalizeRedfishIdentityField(merged.SerialNumber); serial != "" {
bySerial[serial] = idx
}
if slotModel := networkAdapterSlotModelKey(merged); slotModel != "" {
bySlotModel[slotModel] = idx
}
if slot := strings.TrimSpace(merged.Slot); slot != "" {
bySlot[slot] = idx
}
}
return out
}
func networkAdapterSlotModelKey(nic models.NetworkAdapter) string {
slot := strings.TrimSpace(nic.Slot)
model := normalizeNetworkAdapterModel(nic)
if slot == "" && model == "" {
return ""
}
return slot + "|" + model
}
func normalizeNetworkAdapterModel(nic models.NetworkAdapter) string {
model := normalizeRedfishIdentityField(nic.Model)
if model == "" {
return ""
}
if isMissingOrRawPCIModel(model) {
return ""
}
slot := strings.TrimSpace(nic.Slot)
if slot != "" && strings.EqualFold(slot, model) {
return ""
}
return model
}
func networkAdapterRichnessScore(nic models.NetworkAdapter) int {
score := 0
if normalizeRedfishIdentityField(nic.SerialNumber) != "" {
score += 80
}
if normalizeNetworkAdapterModel(nic) != "" {
score += 20
}
if normalizeRedfishIdentityField(nic.Vendor) != "" {
score += 10
}
if normalizeRedfishIdentityField(nic.Firmware) != "" {
score += 8
}
if looksLikeCanonicalBDF(strings.TrimSpace(nic.BDF)) {
score += 10
}
if normalizeRedfishIdentityField(nic.PartNumber) != "" {
score += 6
}
if nic.VendorID > 0 {
score += 5
}
if nic.DeviceID > 0 {
score += 5
}
if nic.PortCount > 0 {
score += 4
}
if nic.LinkWidth > 0 || nic.MaxLinkWidth > 0 {
score += 4
}
if strings.TrimSpace(nic.LinkSpeed) != "" || strings.TrimSpace(nic.MaxLinkSpeed) != "" {
score += 4
}
if len(nic.MACAddresses) > 0 {
score += 4
}
if strings.TrimSpace(nic.Location) != "" {
score += 2
}
if nic.Present {
score++
}
return score
}
func mergeNetworkAdapterEntries(a, b models.NetworkAdapter) models.NetworkAdapter {
base, donor := a, b
if networkAdapterRichnessScore(donor) > networkAdapterRichnessScore(base) {
base, donor = donor, base
}
out := base
out.PortCount = sanitizeNetworkPortCount(out.PortCount)
donor.PortCount = sanitizeNetworkPortCount(donor.PortCount)
if strings.TrimSpace(out.Slot) == "" && strings.TrimSpace(donor.Slot) != "" {
out.Slot = donor.Slot
}
if strings.TrimSpace(out.Location) == "" && strings.TrimSpace(donor.Location) != "" {
out.Location = donor.Location
}
if strings.TrimSpace(out.BDF) == "" && strings.TrimSpace(donor.BDF) != "" {
out.BDF = donor.BDF
}
if normalizeNetworkAdapterModel(out) == "" && normalizeNetworkAdapterModel(donor) != "" {
out.Model = donor.Model
}
if strings.TrimSpace(out.Description) == "" && strings.TrimSpace(donor.Description) != "" {
out.Description = donor.Description
}
if normalizeRedfishIdentityField(out.Vendor) == "" && normalizeRedfishIdentityField(donor.Vendor) != "" {
out.Vendor = donor.Vendor
}
if out.VendorID == 0 && donor.VendorID != 0 {
out.VendorID = donor.VendorID
}
if out.DeviceID == 0 && donor.DeviceID != 0 {
out.DeviceID = donor.DeviceID
}
if normalizeRedfishIdentityField(out.SerialNumber) == "" && normalizeRedfishIdentityField(donor.SerialNumber) != "" {
out.SerialNumber = donor.SerialNumber
}
if normalizeRedfishIdentityField(out.PartNumber) == "" && normalizeRedfishIdentityField(donor.PartNumber) != "" {
out.PartNumber = donor.PartNumber
}
if normalizeRedfishIdentityField(out.Firmware) == "" && normalizeRedfishIdentityField(donor.Firmware) != "" {
out.Firmware = donor.Firmware
}
if out.PortCount == 0 && donor.PortCount > 0 {
out.PortCount = donor.PortCount
}
if strings.TrimSpace(out.PortType) == "" && strings.TrimSpace(donor.PortType) != "" {
out.PortType = donor.PortType
}
if out.LinkWidth == 0 && donor.LinkWidth > 0 {
out.LinkWidth = donor.LinkWidth
}
if strings.TrimSpace(out.LinkSpeed) == "" && strings.TrimSpace(donor.LinkSpeed) != "" {
out.LinkSpeed = donor.LinkSpeed
}
if out.MaxLinkWidth == 0 && donor.MaxLinkWidth > 0 {
out.MaxLinkWidth = donor.MaxLinkWidth
}
if strings.TrimSpace(out.MaxLinkSpeed) == "" && strings.TrimSpace(donor.MaxLinkSpeed) != "" {
out.MaxLinkSpeed = donor.MaxLinkSpeed
}
if strings.TrimSpace(out.Status) == "" && strings.TrimSpace(donor.Status) != "" {
out.Status = donor.Status
}
out.Present = out.Present || donor.Present
if len(donor.MACAddresses) > 0 {
out.MACAddresses = dedupeStrings(append(append([]string{}, out.MACAddresses...), donor.MACAddresses...))
}
out.Details = mergeGenericDetails(out.Details, donor.Details)
return out
}
const maxReasonableNetworkPortCount = 256
func sanitizeNetworkPortCount(v int) int {
if v <= 0 || v > maxReasonableNetworkPortCount {
return 0
}
return v
}
func dedupePCIeDevices(items []models.PCIeDevice) []models.PCIeDevice {
if len(items) <= 1 {
return items
}
out := make([]models.PCIeDevice, 0, len(items))
byPrimary := make(map[string]int, len(items))
byLoose := make(map[string]int, len(items))
for _, item := range items {
primaryKey := pcieDeviceDedupKey(item)
looseKey := pcieDeviceLooseKey(item)
idx := -1
if primaryKey != "" {
if existing, ok := byPrimary[primaryKey]; ok {
idx = existing
}
}
if idx < 0 && looseKey != "" {
if existing, ok := byLoose[looseKey]; ok {
idx = existing
}
}
if idx >= 0 {
out[idx] = mergePCIeDeviceEntries(out[idx], item)
} else {
idx = len(out)
out = append(out, item)
}
merged := out[idx]
if k := pcieDeviceDedupKey(merged); k != "" {
byPrimary[k] = idx
}
if k := pcieDeviceLooseKey(merged); k != "" {
byLoose[k] = idx
}
}
return out
}
func pcieDeviceLooseKey(dev models.PCIeDevice) string {
return firstNonEmpty(
strings.TrimSpace(dev.Slot)+"|"+strings.TrimSpace(dev.PartNumber)+"|"+strings.TrimSpace(dev.DeviceClass),
strings.TrimSpace(dev.Slot)+"|"+strings.TrimSpace(dev.DeviceClass),
strings.TrimSpace(dev.PartNumber)+"|"+strings.TrimSpace(dev.DeviceClass),
strings.TrimSpace(dev.Description)+"|"+strings.TrimSpace(dev.DeviceClass),
)
}
func pcieDeviceRichnessScore(dev models.PCIeDevice) int {
score := 0
if bdf := strings.TrimSpace(dev.BDF); looksLikeCanonicalBDF(bdf) {
score += 120
}
if normalizeRedfishIdentityField(dev.SerialNumber) != "" {
score += 80
}
if normalizeRedfishIdentityField(dev.PartNumber) != "" {
score += 20
}
if normalizeRedfishIdentityField(dev.Manufacturer) != "" {
score += 10
}
if dev.VendorID > 0 {
score += 8
}
if dev.DeviceID > 0 {
score += 8
}
if !isGenericPCIeClassLabel(dev.DeviceClass) {
score += 8
}
if dev.LinkWidth > 0 || dev.MaxLinkWidth > 0 {
score += 6
}
if strings.TrimSpace(dev.LinkSpeed) != "" || strings.TrimSpace(dev.MaxLinkSpeed) != "" {
score += 6
}
if strings.TrimSpace(dev.Description) != "" {
score += 3
}
if strings.TrimSpace(dev.Slot) != "" {
score += 2
}
return score
}
func mergePCIeDeviceEntries(a, b models.PCIeDevice) models.PCIeDevice {
base, donor := a, b
if pcieDeviceRichnessScore(donor) > pcieDeviceRichnessScore(base) {
base, donor = donor, base
}
out := base
if strings.TrimSpace(out.Slot) == "" && strings.TrimSpace(donor.Slot) != "" {
out.Slot = donor.Slot
}
if strings.TrimSpace(out.Description) == "" && strings.TrimSpace(donor.Description) != "" {
out.Description = donor.Description
}
if out.VendorID == 0 && donor.VendorID != 0 {
out.VendorID = donor.VendorID
}
if out.DeviceID == 0 && donor.DeviceID != 0 {
out.DeviceID = donor.DeviceID
}
if strings.TrimSpace(out.BDF) == "" && strings.TrimSpace(donor.BDF) != "" {
out.BDF = donor.BDF
}
if isGenericPCIeClassLabel(out.DeviceClass) && !isGenericPCIeClassLabel(donor.DeviceClass) {
out.DeviceClass = donor.DeviceClass
}
if normalizeRedfishIdentityField(out.Manufacturer) == "" && normalizeRedfishIdentityField(donor.Manufacturer) != "" {
out.Manufacturer = donor.Manufacturer
}
if out.LinkWidth == 0 && donor.LinkWidth > 0 {
out.LinkWidth = donor.LinkWidth
}
if strings.TrimSpace(out.LinkSpeed) == "" && strings.TrimSpace(donor.LinkSpeed) != "" {
out.LinkSpeed = donor.LinkSpeed
}
if out.MaxLinkWidth == 0 && donor.MaxLinkWidth > 0 {
out.MaxLinkWidth = donor.MaxLinkWidth
}
if strings.TrimSpace(out.MaxLinkSpeed) == "" && strings.TrimSpace(donor.MaxLinkSpeed) != "" {
out.MaxLinkSpeed = donor.MaxLinkSpeed
}
if normalizeRedfishIdentityField(out.PartNumber) == "" && normalizeRedfishIdentityField(donor.PartNumber) != "" {
out.PartNumber = donor.PartNumber
}
if normalizeRedfishIdentityField(out.SerialNumber) == "" && normalizeRedfishIdentityField(donor.SerialNumber) != "" {
out.SerialNumber = donor.SerialNumber
}
if strings.TrimSpace(out.Status) == "" && strings.TrimSpace(donor.Status) != "" {
out.Status = donor.Status
}
if len(donor.MACAddresses) > 0 {
out.MACAddresses = dedupeStrings(append(append([]string{}, out.MACAddresses...), donor.MACAddresses...))
}
out.Details = mergeGenericDetails(out.Details, donor.Details)
return out
}
func psuIdentityKeys(psu models.PSU) []string {
keys := make([]string, 0, 3)
if serial := normalizeRedfishIdentityField(psu.SerialNumber); serial != "" {
keys = append(keys, "sn:"+serial)
}
slot := strings.TrimSpace(psu.Slot)
model := strings.TrimSpace(psu.Model)
if slot != "" && model != "" {
keys = append(keys, "slotmodel:"+slot+"|"+model)
}
if slot != "" {
keys = append(keys, "slot:"+slot)
}
if len(keys) == 0 && model != "" {
keys = append(keys, "model:"+model)
}
return keys
}
func psuRichnessScore(psu models.PSU) int {
score := 0
if normalizeRedfishIdentityField(psu.SerialNumber) != "" {
score += 100
}
if normalizeRedfishIdentityField(psu.Model) != "" {
score += 20
}
if psu.WattageW > 0 {
score += 20
}
if normalizeRedfishIdentityField(psu.Vendor) != "" {
score += 8
}
if normalizeRedfishIdentityField(psu.PartNumber) != "" {
score += 8
}
if normalizeRedfishIdentityField(psu.Firmware) != "" {
score += 8
}
if psu.InputPowerW > 0 || psu.OutputPowerW > 0 {
score += 6
}
if psu.InputVoltage > 0 {
score += 4
}
if psu.Present {
score++
}
return score
}
func mergePSUEntries(a, b models.PSU) models.PSU {
base, donor := a, b
if psuRichnessScore(donor) > psuRichnessScore(base) {
base, donor = donor, base
}
out := base
if strings.TrimSpace(out.Slot) == "" && strings.TrimSpace(donor.Slot) != "" {
out.Slot = donor.Slot
}
out.Present = out.Present || donor.Present
if normalizeRedfishIdentityField(out.Model) == "" && normalizeRedfishIdentityField(donor.Model) != "" {
out.Model = donor.Model
}
if strings.TrimSpace(out.Description) == "" && strings.TrimSpace(donor.Description) != "" {
out.Description = donor.Description
}
if normalizeRedfishIdentityField(out.Vendor) == "" && normalizeRedfishIdentityField(donor.Vendor) != "" {
out.Vendor = donor.Vendor
}
if out.WattageW == 0 && donor.WattageW > 0 {
out.WattageW = donor.WattageW
}
if normalizeRedfishIdentityField(out.SerialNumber) == "" && normalizeRedfishIdentityField(donor.SerialNumber) != "" {
out.SerialNumber = donor.SerialNumber
}
if normalizeRedfishIdentityField(out.PartNumber) == "" && normalizeRedfishIdentityField(donor.PartNumber) != "" {
out.PartNumber = donor.PartNumber
}
if normalizeRedfishIdentityField(out.Firmware) == "" && normalizeRedfishIdentityField(donor.Firmware) != "" {
out.Firmware = donor.Firmware
}
if strings.TrimSpace(out.Status) == "" && strings.TrimSpace(donor.Status) != "" {
out.Status = donor.Status
}
if strings.TrimSpace(out.InputType) == "" && strings.TrimSpace(donor.InputType) != "" {
out.InputType = donor.InputType
}
if out.InputPowerW == 0 && donor.InputPowerW > 0 {
out.InputPowerW = donor.InputPowerW
}
if out.OutputPowerW == 0 && donor.OutputPowerW > 0 {
out.OutputPowerW = donor.OutputPowerW
}
if out.InputVoltage == 0 && donor.InputVoltage > 0 {
out.InputVoltage = donor.InputVoltage
}
if out.OutputVoltage == 0 && donor.OutputVoltage > 0 {
out.OutputVoltage = donor.OutputVoltage
}
if out.TemperatureC == 0 && donor.TemperatureC > 0 {
out.TemperatureC = donor.TemperatureC
}
out.Details = mergeGenericDetails(out.Details, donor.Details)
return out
}
func mergeGenericDetails(primary, secondary map[string]any) map[string]any {
if len(secondary) == 0 {
return primary
}
if primary == nil {
primary = make(map[string]any, len(secondary))
}
for key, value := range secondary {
if _, ok := primary[key]; !ok {
primary[key] = value
}
}
return primary
}
func dedupeStorageVolumes(items []models.StorageVolume) []models.StorageVolume {
seen := make(map[string]struct{}, len(items))
out := make([]models.StorageVolume, 0, len(items))
for _, v := range items {
key := firstNonEmpty(strings.TrimSpace(v.ID), strings.TrimSpace(v.Name), strings.TrimSpace(v.Controller)+"|"+fmt.Sprintf("%d", v.CapacityBytes))
if key == "" {
continue
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, v)
}
return out
}
func storageControllerFromPath(path string) string {
p := normalizeRedfishPath(path)
parts := strings.Split(p, "/")
for i := 0; i < len(parts)-1; i++ {
if parts[i] == "Storage" && i+1 < len(parts) {
return parts[i+1]
}
}
return ""
}
func parseFirmware(system, bios, manager, networkProtocol map[string]interface{}) []models.FirmwareInfo {
var out []models.FirmwareInfo
appendFW := func(name, version string) {
version = strings.TrimSpace(version)
if version == "" {
return
}
out = append(out, models.FirmwareInfo{DeviceName: name, Version: version})
}
appendFW("BIOS", asString(system["BiosVersion"]))
appendFW("BIOS", asString(bios["Version"]))
appendFW("BMC", asString(manager["FirmwareVersion"]))
return out
}
func mapStatus(statusAny interface{}) string {
if statusAny == nil {
return ""
}
if statusMap, ok := statusAny.(map[string]interface{}); ok {
health := asString(statusMap["Health"])
state := asString(statusMap["State"])
return firstNonEmpty(health, state)
}
return asString(statusAny)
}
func asString(v interface{}) string {
switch value := v.(type) {
case nil:
return ""
case string:
return strings.TrimSpace(value)
case json.Number:
return value.String()
default:
return strings.TrimSpace(fmt.Sprintf("%v", value))
}
}
func asInt(v interface{}) int {
switch value := v.(type) {
case nil:
return 0
case int:
return value
case int64:
return int(value)
case float64:
return int(value)
case json.Number:
if i, err := value.Int64(); err == nil {
return int(i)
}
if f, err := value.Float64(); err == nil {
return int(f)
}
case string:
if value == "" {
return 0
}
if i, err := strconv.Atoi(value); err == nil {
return i
}
}
return 0
}
func asInt64(v interface{}) int64 {
switch value := v.(type) {
case nil:
return 0
case int:
return int64(value)
case int64:
return value
case float64:
return int64(value)
case json.Number:
if i, err := value.Int64(); err == nil {
return i
}
if f, err := value.Float64(); err == nil {
return int64(f)
}
case string:
if value == "" {
return 0
}
if i, err := strconv.ParseInt(value, 10, 64); err == nil {
return i
}
}
return 0
}
func asBool(v interface{}) bool {
switch t := v.(type) {
case bool:
return t
case string:
return strings.EqualFold(strings.TrimSpace(t), "true")
default:
return false
}
}
func asFloat(v interface{}) float64 {
switch value := v.(type) {
case nil:
return 0
case float64:
return value
case int:
return float64(value)
case int64:
return float64(value)
case json.Number:
if f, err := value.Float64(); err == nil {
return f
}
case string:
if value == "" {
return 0
}
if f, err := strconv.ParseFloat(value, 64); err == nil {
return f
}
}
return 0
}
func asHexOrInt(v interface{}) int {
switch value := v.(type) {
case nil:
return 0
case int:
return value
case int64:
return int(value)
case float64:
return int(value)
case json.Number:
if i, err := value.Int64(); err == nil {
return int(i)
}
if f, err := value.Float64(); err == nil {
return int(f)
}
case string:
s := strings.TrimSpace(value)
s = strings.TrimPrefix(strings.ToLower(s), "0x")
if s == "" {
return 0
}
if i, err := strconv.ParseInt(s, 16, 64); err == nil {
return int(i)
}
if i, err := strconv.Atoi(s); err == nil {
return i
}
}
return 0
}
func firstNonEmpty(values ...string) string {
for _, v := range values {
if strings.TrimSpace(v) != "" {
return strings.TrimSpace(v)
}
}
return ""
}
func joinPath(base, suffix string) string {
base = strings.TrimRight(base, "/")
if suffix == "" {
return base
}
if strings.HasPrefix(suffix, "/") {
return base + suffix
}
return base + "/" + suffix
}
func firstPathOrDefault(paths []string, fallback string) string {
if len(paths) == 0 {
return fallback
}
return paths[0]
}
func normalizeRedfishPath(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
if i := strings.Index(raw, "#"); i >= 0 {
raw = raw[:i]
}
if strings.HasPrefix(raw, "http://") || strings.HasPrefix(raw, "https://") {
u, err := url.Parse(raw)
if err != nil {
return ""
}
raw = u.Path
}
if !strings.HasPrefix(raw, "/") {
raw = "/" + raw
}
if !strings.HasPrefix(raw, "/redfish/") {
return ""
}
return raw
}
func redfishCollectionMemberRefs(collection map[string]interface{}) []string {
if len(collection) == 0 {
return nil
}
var out []string
seen := make(map[string]struct{})
addRefs := func(raw any) {
refs, ok := raw.([]interface{})
if !ok || len(refs) == 0 {
return
}
for _, refAny := range refs {
ref, ok := refAny.(map[string]interface{})
if !ok {
continue
}
memberPath := normalizeRedfishPath(asString(ref["@odata.id"]))
if memberPath == "" {
continue
}
if _, exists := seen[memberPath]; exists {
continue
}
seen[memberPath] = struct{}{}
out = append(out, memberPath)
}
}
addRefs(collection["Members"])
if oem, ok := collection["Oem"].(map[string]interface{}); ok {
if public, ok := oem["Public"].(map[string]interface{}); ok {
addRefs(public["Members"])
}
}
return out
}
func extractODataIDs(v interface{}) []string {
var refs []string
var walk func(any)
walk = func(node any) {
switch typed := node.(type) {
case map[string]interface{}:
for k, child := range typed {
if k == "@odata.id" {
if ref := asString(child); ref != "" {
refs = append(refs, ref)
}
continue
}
walk(child)
}
case []interface{}:
for _, child := range typed {
walk(child)
}
}
}
walk(v)
return refs
}
func redfishTopRoot(path string) string {
path = strings.TrimPrefix(path, "/")
parts := strings.Split(path, "/")
if len(parts) < 3 {
return "root"
}
return parts[2]
}
func topRoots(counts map[string]int, limit int) []string {
if len(counts) == 0 {
return []string{"n/a"}
}
type rootCount struct {
root string
count int
}
items := make([]rootCount, 0, len(counts))
for root, count := range counts {
items = append(items, rootCount{root: root, count: count})
}
sort.Slice(items, func(i, j int) bool {
return items[i].count > items[j].count
})
if len(items) > limit {
items = items[:limit]
}
out := make([]string, 0, len(items))
for _, item := range items {
out = append(out, fmt.Sprintf("%s(%d)", item.root, item.count))
}
return out
}
func redfishLocationLabel(v interface{}) string {
switch typed := v.(type) {
case nil:
return ""
case string:
return strings.TrimSpace(typed)
case map[string]interface{}:
// Common shapes:
// Slot.Location.PartLocation.ServiceLabel
// Location.PartLocation.ServiceLabel
// PartLocation.ServiceLabel
if nested := redfishLocationLabel(typed["Location"]); nested != "" {
return nested
}
if nested := redfishLocationLabel(typed["PartLocation"]); nested != "" {
return nested
}
serviceLabel := asString(typed["ServiceLabel"])
locationType := asString(typed["LocationType"])
ordinal := asString(typed["LocationOrdinalValue"])
if serviceLabel != "" {
return serviceLabel
}
if locationType != "" && ordinal != "" {
return fmt.Sprintf("%s %s", locationType, ordinal)
}
if locationType != "" {
return locationType
}
if ordinal != "" {
return "Slot " + ordinal
}
return ""
default:
// Avoid fmt.Sprint(map[]) style garbage for complex objects in UI/export.
return ""
}
}
type redfishPathTiming struct {
Path string
Duration time.Duration
Requests int
Errors int
}
type redfishPathTimingCollector struct {
depth int
mu sync.Mutex
byKey map[string]redfishPathTiming
}
func newRedfishPathTimingCollector(depth int) *redfishPathTimingCollector {
if depth < 1 {
depth = 1
}
return &redfishPathTimingCollector{
depth: depth,
byKey: make(map[string]redfishPathTiming),
}
}
func (c *redfishPathTimingCollector) Observe(path string, d time.Duration, failed bool) {
if c == nil {
return
}
key := redfishBranchPathForTiming(path, c.depth)
if key == "" {
return
}
c.mu.Lock()
item := c.byKey[key]
item.Path = key
item.Duration += d
item.Requests++
if failed {
item.Errors++
}
c.byKey[key] = item
c.mu.Unlock()
}
func (c *redfishPathTimingCollector) Summary(limit int) string {
if c == nil || limit == 0 {
return ""
}
c.mu.Lock()
items := make([]redfishPathTiming, 0, len(c.byKey))
for _, item := range c.byKey {
items = append(items, item)
}
c.mu.Unlock()
if len(items) == 0 {
return ""
}
sort.Slice(items, func(i, j int) bool {
if items[i].Duration == items[j].Duration {
if items[i].Requests == items[j].Requests {
return items[i].Path < items[j].Path
}
return items[i].Requests > items[j].Requests
}
return items[i].Duration > items[j].Duration
})
if limit < 0 || limit > len(items) {
limit = len(items)
}
parts := make([]string, 0, limit)
for i := 0; i < limit; i++ {
item := items[i]
parts = append(parts, fmt.Sprintf("%s=%s(req=%d,err=%d)", item.Path, item.Duration.Round(time.Millisecond), item.Requests, item.Errors))
}
return strings.Join(parts, "; ")
}
func redfishBranchPathForTiming(path string, depth int) string {
normalized := normalizeRedfishPath(path)
if normalized == "" {
return ""
}
parts := strings.Split(strings.Trim(normalized, "/"), "/")
if len(parts) < 2 || parts[0] != "redfish" || parts[1] != "v1" {
return normalized
}
if depth < 1 {
depth = 1
}
maxParts := 2 + depth
if len(parts) > maxParts {
parts = parts[:maxParts]
}
return "/" + strings.Join(parts, "/")
}
func compactProgressPath(p string) string {
const maxLen = 72
if len(p) <= maxLen {
return p
}
return "..." + p[len(p)-maxLen+3:]
}
func boolPointerValue(v *bool) interface{} {
if v == nil {
return nil
}
return *v
}
func redfishSnapshotMaxDocuments(tuning redfishprofile.AcquisitionTuning) int {
// Default is intentionally high enough to capture vendor-specific PCIe/GPU trees
// on modern HGX-class systems while staying within memory budgets of a typical
// developer workstation.
const (
def = 100000
min = 1200
max = 500000
)
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_SNAPSHOT_MAX_DOCS")); v != "" {
if n, err := strconv.Atoi(v); err == nil {
if n < min {
return min
}
if n > max {
return max
}
return n
}
}
if tuning.SnapshotMaxDocuments > 0 {
if tuning.SnapshotMaxDocuments < min {
return min
}
if tuning.SnapshotMaxDocuments > max {
return max
}
return tuning.SnapshotMaxDocuments
}
return def
}
func formatActiveModuleLog(modules []ModuleActivation) string {
if len(modules) == 0 {
return "-"
}
parts := make([]string, 0, len(modules))
for _, module := range modules {
parts = append(parts, fmt.Sprintf("%s(%d)", module.Name, module.Score))
}
return strings.Join(parts, ",")
}
func formatModuleScoreLog(scores []ModuleScore) string {
if len(scores) == 0 {
return "-"
}
parts := make([]string, 0, len(scores))
for _, score := range scores {
state := "inactive"
if score.Active {
state = "active"
}
parts = append(parts, fmt.Sprintf("%s=%d[%s]", score.Name, score.Score, state))
}
return strings.Join(parts, ",")
}
func newRedfishRequestTelemetry() *redfishRequestTelemetry {
return &redfishRequestTelemetry{
overall: redfishPhaseTelemetry{
durations: make([]time.Duration, 0, 128),
},
byPhase: make(map[string]*redfishPhaseTelemetry),
}
}
func withRedfishTelemetryPhase(ctx context.Context, phase string) context.Context {
if strings.TrimSpace(phase) == "" {
return ctx
}
return context.WithValue(ctx, redfishTelemetryPhaseContextKey{}, phase)
}
func recordRedfishTelemetry(ctx context.Context, duration time.Duration, failed bool) {
telemetry, _ := ctx.Value(redfishTelemetryContextKey{}).(*redfishRequestTelemetry)
if telemetry == nil {
return
}
phase, _ := ctx.Value(redfishTelemetryPhaseContextKey{}).(string)
telemetry.Observe(phase, duration, failed)
}
func (t *redfishRequestTelemetry) Observe(phase string, duration time.Duration, failed bool) {
if t == nil {
return
}
t.mu.Lock()
defer t.mu.Unlock()
observeRedfishPhaseTelemetry(&t.overall, duration, failed)
phase = strings.TrimSpace(phase)
if phase == "" {
return
}
phaseTelemetry := t.byPhase[phase]
if phaseTelemetry == nil {
phaseTelemetry = &redfishPhaseTelemetry{durations: make([]time.Duration, 0, 64)}
t.byPhase[phase] = phaseTelemetry
}
observeRedfishPhaseTelemetry(phaseTelemetry, duration, failed)
}
func (t *redfishRequestTelemetry) Snapshot() redfishTelemetrySummary {
if t == nil {
return redfishTelemetrySummary{}
}
t.mu.Lock()
defer t.mu.Unlock()
return snapshotRedfishPhaseTelemetry(&t.overall)
}
func (t *redfishRequestTelemetry) PhaseSnapshots() map[string]redfishTelemetrySummary {
if t == nil {
return nil
}
t.mu.Lock()
defer t.mu.Unlock()
out := make(map[string]redfishTelemetrySummary, len(t.byPhase))
for phase, telemetry := range t.byPhase {
out[phase] = snapshotRedfishPhaseTelemetry(telemetry)
}
return out
}
func observeRedfishPhaseTelemetry(t *redfishPhaseTelemetry, duration time.Duration, failed bool) {
if t == nil {
return
}
t.requests++
if failed {
t.errors++
}
t.durations = append(t.durations, duration)
if len(t.durations) > 512 {
t.durations = t.durations[len(t.durations)-512:]
}
t.lastAvg, t.lastP95 = summarizeRedfishDurations(t.durations)
}
func snapshotRedfishPhaseTelemetry(t *redfishPhaseTelemetry) redfishTelemetrySummary {
if t == nil {
return redfishTelemetrySummary{}
}
out := redfishTelemetrySummary{
Requests: t.requests,
Errors: t.errors,
Avg: t.lastAvg,
P95: t.lastP95,
}
if t.requests > 0 {
out.ErrorRate = float64(t.errors) / float64(t.requests)
}
return out
}
func summarizeRedfishDurations(durations []time.Duration) (time.Duration, time.Duration) {
if len(durations) == 0 {
return 0, 0
}
total := time.Duration(0)
items := append([]time.Duration(nil), durations...)
for _, duration := range items {
total += duration
}
sort.Slice(items, func(i, j int) bool { return items[i] < items[j] })
p95Idx := (len(items)*95 - 1) / 100
if p95Idx < 0 {
p95Idx = 0
}
if p95Idx >= len(items) {
p95Idx = len(items) - 1
}
return total / time.Duration(len(items)), items[p95Idx]
}
func adaptRedfishAcquisitionTuning(tuning redfishprofile.AcquisitionTuning, summary redfishTelemetrySummary) (redfishprofile.AcquisitionTuning, bool) {
if summary.Requests < 4 {
return tuning, false
}
policy := tuning.RatePolicy
shouldThrottle := false
if policy.ThrottleP95LatencyMS > 0 && summary.P95 >= time.Duration(policy.ThrottleP95LatencyMS)*time.Millisecond {
shouldThrottle = true
}
if policy.DisablePrefetchOnErrors && summary.ErrorRate >= 0.20 {
shouldThrottle = true
if tuning.PrefetchEnabled == nil {
tuning.PrefetchEnabled = new(bool)
}
*tuning.PrefetchEnabled = false
}
if !shouldThrottle {
return tuning, false
}
if tuning.SnapshotWorkers == 0 {
tuning.SnapshotWorkers = redfishSnapshotWorkers(redfishprofile.AcquisitionTuning{})
}
if tuning.SnapshotWorkers > 1 {
tuning.SnapshotWorkers = maxInt(policy.MinSnapshotWorkers, tuning.SnapshotWorkers/2)
}
if tuning.PrefetchWorkers > 1 {
tuning.PrefetchWorkers = maxInt(policy.MinPrefetchWorkers, tuning.PrefetchWorkers/2)
}
return tuning, true
}
func redfishPhaseTelemetryPayload(phases map[string]redfishTelemetrySummary) map[string]any {
if len(phases) == 0 {
return nil
}
keys := make([]string, 0, len(phases))
for phase := range phases {
keys = append(keys, phase)
}
sort.Strings(keys)
out := make(map[string]any, len(keys))
for _, phase := range keys {
summary := phases[phase]
out[phase] = map[string]any{
"requests": summary.Requests,
"errors": summary.Errors,
"error_rate": summary.ErrorRate,
"avg_ms": summary.Avg.Milliseconds(),
"p95_ms": summary.P95.Milliseconds(),
}
}
return out
}
func redfishPhaseTelemetryLogLines(phases map[string]redfishTelemetrySummary) []string {
if len(phases) == 0 {
return nil
}
keys := make([]string, 0, len(phases))
for phase := range phases {
keys = append(keys, phase)
}
sort.Strings(keys)
lines := make([]string, 0, len(keys))
for _, phase := range keys {
summary := phases[phase]
lines = append(lines, fmt.Sprintf("%s req=%d err=%d err_rate=%.2f avg=%dms p95=%dms",
phase,
summary.Requests,
summary.Errors,
summary.ErrorRate,
summary.Avg.Milliseconds(),
summary.P95.Milliseconds(),
))
}
return lines
}
func buildCollectPhaseTelemetry(phases map[string]redfishTelemetrySummary) []PhaseTelemetry {
if len(phases) == 0 {
return nil
}
keys := make([]string, 0, len(phases))
for phase := range phases {
keys = append(keys, phase)
}
sort.Strings(keys)
out := make([]PhaseTelemetry, 0, len(keys))
for _, phase := range keys {
summary := phases[phase]
out = append(out, PhaseTelemetry{
Phase: phase,
Requests: summary.Requests,
Errors: summary.Errors,
ErrorRate: summary.ErrorRate,
AvgMS: summary.Avg.Milliseconds(),
P95MS: summary.P95.Milliseconds(),
})
}
return out
}
func shouldReportSnapshotFetchError(err error) bool {
if err == nil {
return false
}
msg := err.Error()
if strings.HasPrefix(msg, "status 404 ") ||
strings.HasPrefix(msg, "status 405 ") ||
strings.HasPrefix(msg, "status 410 ") ||
strings.HasPrefix(msg, "status 501 ") {
return false
}
return true
}
func minInt32(a, b int32) int32 {
if a < b {
return a
}
return b
}
func maxInt(values ...int) int {
if len(values) == 0 {
return 0
}
max := values[0]
for _, v := range values[1:] {
if v > max {
max = v
}
}
return max
}
func estimateSnapshotETA(start time.Time, processed, seen, queueLen, workers int, requestTimeout time.Duration) time.Duration {
remaining := maxInt(seen-processed, queueLen, 0)
if remaining == 0 {
return 0
}
if workers <= 0 {
workers = 1
}
if requestTimeout <= 0 {
requestTimeout = 10 * time.Second
}
timeoutBased := time.Duration(float64(requestTimeout) * float64(remaining) / float64(workers))
if processed <= 0 {
return timeoutBased
}
elapsed := time.Since(start)
if elapsed <= 0 {
return timeoutBased
}
rateBased := time.Duration(float64(elapsed) * float64(remaining) / float64(processed))
if rateBased <= 0 {
return timeoutBased
}
// Blend observed throughput with configured per-request timeout to keep ETA stable
// and still bounded by timeout assumptions on slower Redfish branches.
return (rateBased + timeoutBased) / 2
}
func estimatePlanBETA(targets int) time.Duration {
if targets <= 0 {
return 0
}
attempts := redfishCriticalPlanBAttempts()
if attempts < 1 {
attempts = 1
}
timeoutPart := time.Duration(attempts) * redfishCriticalRequestTimeout()
backoffPart := time.Duration(attempts-1) * redfishCriticalRetryBackoff()
gapPart := redfishCriticalSlowGap()
perTarget := timeoutPart + backoffPart + gapPart
return time.Duration(targets) * perTarget
}
func estimateProgressETA(start time.Time, processed, total int, fallbackPerItem time.Duration) time.Duration {
if total <= 0 || processed >= total {
return 0
}
remaining := total - processed
if processed <= 0 {
if fallbackPerItem <= 0 {
fallbackPerItem = time.Second
}
return time.Duration(remaining) * fallbackPerItem
}
elapsed := time.Since(start)
if elapsed <= 0 {
return 0
}
return time.Duration(float64(elapsed) * float64(remaining) / float64(processed))
}
func formatETA(d time.Duration) string {
if d <= 0 {
return "<1s"
}
if d < time.Second {
return "<1s"
}
if d < time.Minute {
return fmt.Sprintf("%ds", int(d.Round(time.Second).Seconds()))
}
totalSec := int(d.Round(time.Second).Seconds())
hours := totalSec / 3600
minutes := (totalSec % 3600) / 60
seconds := totalSec % 60
if hours > 0 {
return fmt.Sprintf("%dh%02dm%02ds", hours, minutes, seconds)
}
return fmt.Sprintf("%dm%02ds", minutes, seconds)
}