diff --git a/.gitignore b/.gitignore index 00d652f..1b34e7b 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,8 @@ go.work.sum dist/ # Release artifacts +release/ +releases/ releases/**/SHA256SUMS.txt releases/**/*.tar.gz releases/**/*.zip diff --git a/internal/collector/redfish.go b/internal/collector/redfish.go index 5f7f7c2..05cb8e0 100644 --- a/internal/collector/redfish.go +++ b/internal/collector/redfish.go @@ -678,6 +678,16 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht c.debugSnapshotf("snapshot nvme bay probe hit path=%s", bayPath) } } + // 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. + for path := range out { + for childPath, doc := range c.probeDirectRedfishCollectionChildren(ctx, client, req, baseURL, path) { + if _, exists := out[childPath]; exists { + continue + } + out[childPath] = doc + } + } if emit != nil { emit(Progress{ @@ -738,6 +748,106 @@ func directDiskBayCandidates(drivesCollectionPath string) []string { 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 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 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 +} + func redfishLinkRefs(doc map[string]interface{}, topKey, nestedKey string) []string { top, ok := doc[topKey].(map[string]interface{}) if !ok { @@ -1856,27 +1966,44 @@ func redfishSnapshotPrioritySeeds(systemPaths, chassisPaths, managerPaths []stri add(joinPath(p, "/SecureBoot")) add(joinPath(p, "/Processors")) add(joinPath(p, "/Memory")) + add(joinPath(p, "/EthernetInterfaces")) + add(joinPath(p, "/NetworkInterfaces")) + add(joinPath(p, "/BootOptions")) + add(joinPath(p, "/Certificates")) add(joinPath(p, "/PCIeDevices")) add(joinPath(p, "/PCIeFunctions")) add(joinPath(p, "/Accelerators")) add(joinPath(p, "/Storage")) + add(joinPath(p, "/SimpleStorage")) add(joinPath(p, "/Storage/IntelVROC")) add(joinPath(p, "/Storage/IntelVROC/Drives")) add(joinPath(p, "/Storage/IntelVROC/Volumes")) } for _, p := range chassisPaths { add(p) + add(joinPath(p, "/Sensors")) + add(joinPath(p, "/Thermal")) + add(joinPath(p, "/EnvironmentMetrics")) add(joinPath(p, "/PCIeDevices")) add(joinPath(p, "/PCIeSlots")) add(joinPath(p, "/NetworkAdapters")) + add(joinPath(p, "/Drives")) + add(joinPath(p, "/Temperatures")) + add(joinPath(p, "/Fans")) + add(joinPath(p, "/Voltages")) add(joinPath(p, "/PowerSubsystem")) add(joinPath(p, "/PowerSubsystem/PowerSupplies")) + add(joinPath(p, "/PowerSubsystem/Voltages")) add(joinPath(p, "/ThermalSubsystem")) add(joinPath(p, "/ThermalSubsystem/Fans")) + add(joinPath(p, "/ThermalSubsystem/Temperatures")) add(joinPath(p, "/Power")) } for _, p := range managerPaths { add(p) + add(joinPath(p, "/EthernetInterfaces")) + add(joinPath(p, "/NetworkProtocol/HTTPS/Certificates")) + add(joinPath(p, "/LogServices")) add(joinPath(p, "/NetworkProtocol")) } return out