diff --git a/internal/collector/redfish.go b/internal/collector/redfish.go index 8c5f9d9..a80d2db 100644 --- a/internal/collector/redfish.go +++ b/internal/collector/redfish.go @@ -689,9 +689,21 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht // Some Supermicro BMCs expose NVMe disks at direct Disk.Bay endpoints even when the // Drives collection returns Members: []. Probe those paths so raw export can be replayed. + driveCollections := make([]string, 0) for path := range out { - if !strings.HasSuffix(normalizeRedfishPath(path), "/Drives") { - continue + if strings.HasSuffix(normalizeRedfishPath(path), "/Drives") { + driveCollections = append(driveCollections, normalizeRedfishPath(path)) + } + } + sort.Strings(driveCollections) + nvmeProbeStart := time.Now() + for i, path := range driveCollections { + 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), коллекция=%s", i+1, len(driveCollections), formatETA(estimateProgressETA(nvmeProbeStart, i, len(driveCollections), 2*time.Second)), compactProgressPath(path)), + }) } for _, bayPath := range directDiskBayCandidates(path) { doc, err := c.getJSON(ctx, client, req, baseURL, bayPath) @@ -707,14 +719,38 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht } // Some BMCs under-report collection Members for sensors/PSU subresources but still serve // direct numeric child endpoints. Probe common collections to maximize raw snapshot fidelity. + postProbeCollections := make([]string, 0) for path := range out { + if shouldPostProbeCollectionPath(path) { + postProbeCollections = append(postProbeCollections, normalizeRedfishPath(path)) + } + } + sort.Strings(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 коллекций (%d/%d, ETA≈%s), текущая=%s", i+1, len(postProbeCollections), formatETA(estimateProgressETA(postProbeStart, i, len(postProbeCollections), 3*time.Second)), compactProgressPath(path)), + }) + } for childPath, doc := range c.probeDirectRedfishCollectionChildren(ctx, client, req, baseURL, path) { if _, exists := out[childPath]; exists { continue } out[childPath] = doc + addedPostProbe++ } } + if emit != nil && addedPostProbe > 0 { + emit(Progress{ + Status: "running", + Progress: 98, + Message: fmt.Sprintf("Redfish snapshot: post-probe добавлено %d документов", addedPostProbe), + }) + } if emit != nil { emit(Progress{ @@ -887,6 +923,31 @@ func directNumericProbePlan(collectionPath string) (maxItems, startIndex, missBu } } +func shouldPostProbeCollectionPath(path string) bool { + path = normalizeRedfishPath(path) + // 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"), + 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 looksLikeRedfishResource(doc map[string]interface{}) bool { if len(doc) == 0 { return false @@ -1145,6 +1206,13 @@ func shouldCrawlPath(path string) bool { return false } normalized := normalizeRedfishPath(path) + if strings.Contains(normalized, "/Chassis/") && + strings.Contains(normalized, "/PCIeDevices/") && + strings.Contains(normalized, "/PCIeFunctions/") { + // Chassis-level PCIeFunctions links are frequently noisy/slow on some BMCs + // and duplicate data we already collect from PCIe devices/functions elsewhere. + return false + } if strings.Contains(normalized, "/Memory/") { after := strings.SplitN(normalized, "/Memory/", 2) if len(after) == 2 && strings.Count(after[1], "/") >= 1 { @@ -2753,6 +2821,24 @@ func estimatePlanBETA(targets int) time.Duration { 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" diff --git a/internal/collector/redfish_test.go b/internal/collector/redfish_test.go index 649075f..a6d9db1 100644 --- a/internal/collector/redfish_test.go +++ b/internal/collector/redfish_test.go @@ -779,4 +779,22 @@ func TestShouldCrawlPath_MemorySubresourcesAreSkipped(t *testing.T) { if shouldCrawlPath("/redfish/v1/Systems/1/Memory/CPU0_C0D0/MemoryMetrics") { t.Fatalf("expected DIMM metrics subresource to be skipped") } + if shouldCrawlPath("/redfish/v1/Chassis/1/PCIeDevices/0/PCIeFunctions/1") { + t.Fatalf("expected noisy chassis pciefunctions branch to be skipped") + } +} + +func TestShouldPostProbeCollectionPath(t *testing.T) { + if !shouldPostProbeCollectionPath("/redfish/v1/Chassis/1/Sensors") { + t.Fatalf("expected sensors collection to be post-probed") + } + if !shouldPostProbeCollectionPath("/redfish/v1/Systems/1/Storage/RAID/Drives") { + t.Fatalf("expected drives collection to be post-probed") + } + if shouldPostProbeCollectionPath("/redfish/v1/Chassis/1/Boards/BOARD1") { + t.Fatalf("expected board member resource to be skipped from post-probe") + } + if shouldPostProbeCollectionPath("/redfish/v1/Chassis/1/Assembly/Oem/COMMONb/COMMONbAssembly/1") { + t.Fatalf("expected assembly member resource to be skipped from post-probe") + } }