Add Inspur Group OEM Redfish profile
This commit is contained in:
@@ -100,6 +100,13 @@ Live Redfish collection must expose profile-match diagnostics:
|
|||||||
- the collect page should render active modules as chips from structured status data, not by
|
- the collect page should render active modules as chips from structured status data, not by
|
||||||
parsing log lines
|
parsing log lines
|
||||||
|
|
||||||
|
Profile matching may use stable platform grammar signals in addition to vendor strings:
|
||||||
|
- discovered member/resource naming from lightweight discovery collections
|
||||||
|
- firmware inventory member IDs
|
||||||
|
- OEM action names and linked target paths embedded in discovery documents
|
||||||
|
- replay-only snapshot hints such as OEM assembly/type markers when they are present in
|
||||||
|
`raw_payloads.redfish_tree`
|
||||||
|
|
||||||
On replay, profile-derived analysis directives may enable vendor-specific inventory linking
|
On replay, profile-derived analysis directives may enable vendor-specific inventory linking
|
||||||
helpers such as processor-GPU fallback, chassis-ID alias resolution, and bounded storage recovery.
|
helpers such as processor-GPU fallback, chassis-ID alias resolution, and bounded storage recovery.
|
||||||
Replay should now resolve a structured analysis plan inside `redfishprofile/`, analogous to the
|
Replay should now resolve a structured analysis plan inside `redfishprofile/`, analogous to the
|
||||||
|
|||||||
@@ -918,3 +918,34 @@ hardware change.
|
|||||||
- Hardware event history (last 7 days) visible in Reanimator `EventLogs` section.
|
- Hardware event history (last 7 days) visible in Reanimator `EventLogs` section.
|
||||||
- No impact on existing inventory pipeline or offline archive replay (archives without `redfish_log_entries` key silently skip parsing).
|
- No impact on existing inventory pipeline or offline archive replay (archives without `redfish_log_entries` key silently skip parsing).
|
||||||
- Adds extra HTTP requests during live collection (sequential, after tree-walk completes).
|
- Adds extra HTTP requests during live collection (sequential, after tree-walk completes).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ADL-036 — Redfish profile matching may use platform grammar hints beyond vendor strings
|
||||||
|
|
||||||
|
**Date:** 2026-03-25
|
||||||
|
**Context:**
|
||||||
|
Some BMCs expose unusable `Manufacturer` / `Model` values (`NULL`, placeholders, or generic SoC
|
||||||
|
names) while still exposing a stable platform-specific Redfish grammar: repeated member names,
|
||||||
|
firmware inventory IDs, OEM action names, and target-path quirks. Matching only on vendor
|
||||||
|
strings forced such systems into fallback mode even when the platform shape was consistent.
|
||||||
|
|
||||||
|
**Decision:**
|
||||||
|
- Extend `redfishprofile.MatchSignals` with doc-derived hint tokens collected from discovery docs
|
||||||
|
and replay snapshots.
|
||||||
|
- Allow profile matchers to score on stable platform grammar such as:
|
||||||
|
- collection member naming (`outboardPCIeCard*`, drive slot grammars)
|
||||||
|
- firmware inventory member IDs
|
||||||
|
- OEM action/type markers and linked target paths
|
||||||
|
- During live collection, gather only lightweight extra hint collections needed for matching
|
||||||
|
(`NetworkInterfaces`, `NetworkAdapters`, `Drives`, `UpdateService/FirmwareInventory`), not slow
|
||||||
|
deep inventory branches.
|
||||||
|
- Keep such profiles out of fallback aggregation unless they are proven safe as broad additive
|
||||||
|
hints.
|
||||||
|
|
||||||
|
**Consequences:**
|
||||||
|
- Platform-family profiles can activate even when vendor strings are absent or set to `NULL`.
|
||||||
|
- Matching logic becomes more robust for OEM BMC implementations that differ mainly by Redfish
|
||||||
|
grammar rather than by explicit vendor strings.
|
||||||
|
- Live collection gains a small amount of extra discovery I/O to harvest stable member IDs, but
|
||||||
|
avoids slow deep probes such as `Assembly` just for profile selection.
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit Progre
|
|||||||
snapshotClient := c.httpClientWithTimeout(req, redfishSnapshotRequestTimeout())
|
snapshotClient := c.httpClientWithTimeout(req, redfishSnapshotRequestTimeout())
|
||||||
prefetchClient := c.httpClientWithTimeout(req, redfishPrefetchRequestTimeout())
|
prefetchClient := c.httpClientWithTimeout(req, redfishPrefetchRequestTimeout())
|
||||||
criticalClient := c.httpClientWithTimeout(req, redfishCriticalRequestTimeout())
|
criticalClient := c.httpClientWithTimeout(req, redfishCriticalRequestTimeout())
|
||||||
|
hintClient := c.httpClientWithTimeout(req, 4*time.Second)
|
||||||
|
|
||||||
if emit != nil {
|
if emit != nil {
|
||||||
emit(Progress{Status: "running", Progress: 10, Message: "Redfish: подключение к BMC..."})
|
emit(Progress{Status: "running", Progress: 10, Message: "Redfish: подключение к BMC..."})
|
||||||
@@ -178,7 +179,8 @@ func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit Progre
|
|||||||
chassisDoc, _ := c.getJSON(discoveryCtx, snapshotClient, req, baseURL, primaryChassis)
|
chassisDoc, _ := c.getJSON(discoveryCtx, snapshotClient, req, baseURL, primaryChassis)
|
||||||
managerDoc, _ := c.getJSON(discoveryCtx, snapshotClient, req, baseURL, primaryManager)
|
managerDoc, _ := c.getJSON(discoveryCtx, snapshotClient, req, baseURL, primaryManager)
|
||||||
resourceHints := append(append([]string{}, systemPaths...), append(chassisPaths, managerPaths...)...)
|
resourceHints := append(append([]string{}, systemPaths...), append(chassisPaths, managerPaths...)...)
|
||||||
signals := redfishprofile.CollectSignals(serviceRootDoc, systemDoc, chassisDoc, managerDoc, resourceHints)
|
hintDocs := c.collectProfileHintDocs(discoveryCtx, hintClient, req, baseURL, primarySystem, primaryChassis)
|
||||||
|
signals := redfishprofile.CollectSignals(serviceRootDoc, systemDoc, chassisDoc, managerDoc, resourceHints, hintDocs...)
|
||||||
matchResult := redfishprofile.MatchProfiles(signals)
|
matchResult := redfishprofile.MatchProfiles(signals)
|
||||||
acquisitionPlan := redfishprofile.BuildAcquisitionPlan(signals)
|
acquisitionPlan := redfishprofile.BuildAcquisitionPlan(signals)
|
||||||
telemetrySummary := telemetry.Snapshot()
|
telemetrySummary := telemetry.Snapshot()
|
||||||
@@ -1557,6 +1559,33 @@ func (c *RedfishConnector) discoverMemberPaths(ctx context.Context, client *http
|
|||||||
return nil
|
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) {
|
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)
|
maxDocuments := redfishSnapshotMaxDocuments(tuning)
|
||||||
workers := redfishSnapshotWorkers(tuning)
|
workers := redfishSnapshotWorkers(tuning)
|
||||||
@@ -3476,14 +3505,20 @@ func parseCPUs(docs []map[string]interface{}) []models.CPU {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
l1, l2, l3 := parseCPUCachesFromProcessorMemory(doc)
|
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{
|
cpus = append(cpus, models.CPU{
|
||||||
Socket: socket,
|
Socket: socket,
|
||||||
Model: firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
|
Model: firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
|
||||||
Cores: asInt(doc["TotalCores"]),
|
Cores: asInt(doc["TotalCores"]),
|
||||||
Threads: asInt(doc["TotalThreads"]),
|
Threads: asInt(doc["TotalThreads"]),
|
||||||
FrequencyMHz: asInt(doc["OperatingSpeedMHz"]),
|
FrequencyMHz: int(redfishFirstNumeric(doc, "OperatingSpeedMHz", "CurrentSpeedMHz", "FrequencyMHz")),
|
||||||
MaxFreqMHz: asInt(doc["MaxSpeedMHz"]),
|
MaxFreqMHz: int(redfishFirstNumeric(doc, "MaxSpeedMHz", "TurboEnableMaxSpeedMHz", "TurboDisableMaxSpeedMHz")),
|
||||||
SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
|
PPIN: firstNonEmpty(findFirstNormalizedStringByKeys(doc, "PPIN", "ProtectedIdentificationNumber"), publicSerial),
|
||||||
|
SerialNumber: serial,
|
||||||
L1CacheKB: l1,
|
L1CacheKB: l1,
|
||||||
L2CacheKB: l2,
|
L2CacheKB: l2,
|
||||||
L3CacheKB: l3,
|
L3CacheKB: l3,
|
||||||
@@ -3494,6 +3529,12 @@ func parseCPUs(docs []map[string]interface{}) []models.CPU {
|
|||||||
return cpus
|
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
|
// parseCPUCachesFromProcessorMemory reads L1/L2/L3 cache sizes from the
|
||||||
// Redfish ProcessorMemory array (Processor.v1_x spec).
|
// Redfish ProcessorMemory array (Processor.v1_x spec).
|
||||||
func parseCPUCachesFromProcessorMemory(doc map[string]interface{}) (l1, l2, l3 int) {
|
func parseCPUCachesFromProcessorMemory(doc map[string]interface{}) (l1, l2, l3 int) {
|
||||||
@@ -3942,7 +3983,7 @@ func parsePSUWithSupplementalDocs(doc map[string]interface{}, idx int, supplemen
|
|||||||
Present: present,
|
Present: present,
|
||||||
Model: firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
|
Model: firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
|
||||||
Vendor: asString(doc["Manufacturer"]),
|
Vendor: asString(doc["Manufacturer"]),
|
||||||
WattageW: asInt(doc["PowerCapacityWatts"]),
|
WattageW: redfishPSUNominalWattage(doc),
|
||||||
SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
|
SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
|
||||||
PartNumber: asString(doc["PartNumber"]),
|
PartNumber: asString(doc["PartNumber"]),
|
||||||
Firmware: asString(doc["FirmwareVersion"]),
|
Firmware: asString(doc["FirmwareVersion"]),
|
||||||
@@ -3955,6 +3996,25 @@ func parsePSUWithSupplementalDocs(doc map[string]interface{}, idx int, supplemen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
func redfishDriveDetails(doc map[string]interface{}) map[string]any {
|
||||||
return redfishDriveDetailsWithSupplementalDocs(doc)
|
return redfishDriveDetailsWithSupplementalDocs(doc)
|
||||||
}
|
}
|
||||||
@@ -5781,7 +5841,6 @@ func parseFirmware(system, bios, manager, networkProtocol map[string]interface{}
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func mapStatus(statusAny interface{}) string {
|
func mapStatus(statusAny interface{}) string {
|
||||||
if statusAny == nil {
|
if statusAny == nil {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
|||||||
if emit != nil {
|
if emit != nil {
|
||||||
emit(Progress{Status: "running", Progress: 10, Message: "Redfish snapshot: replay service root..."})
|
emit(Progress{Status: "running", Progress: 10, Message: "Redfish snapshot: replay service root..."})
|
||||||
}
|
}
|
||||||
serviceRootDoc, err := r.getJSON("/redfish/v1")
|
if _, err := r.getJSON("/redfish/v1"); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Printf("redfish replay: service root /redfish/v1 missing from snapshot, continuing with defaults: %v", err)
|
log.Printf("redfish replay: service root /redfish/v1 missing from snapshot, continuing with defaults: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,8 +60,7 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
|||||||
fruDoc = chassisFRUDoc
|
fruDoc = chassisFRUDoc
|
||||||
}
|
}
|
||||||
boardFallbackDocs := r.collectBoardFallbackDocs(systemPaths, chassisPaths)
|
boardFallbackDocs := r.collectBoardFallbackDocs(systemPaths, chassisPaths)
|
||||||
resourceHints := append(append([]string{}, systemPaths...), append(chassisPaths, managerPaths...)...)
|
profileSignals := redfishprofile.CollectSignalsFromTree(tree)
|
||||||
profileSignals := redfishprofile.CollectSignals(serviceRootDoc, systemDoc, chassisDoc, managerDoc, resourceHints)
|
|
||||||
profileMatch := redfishprofile.MatchProfiles(profileSignals)
|
profileMatch := redfishprofile.MatchProfiles(profileSignals)
|
||||||
analysisPlan := redfishprofile.ResolveAnalysisPlan(profileMatch, tree, redfishprofile.DiscoveredResources{
|
analysisPlan := redfishprofile.ResolveAnalysisPlan(profileMatch, tree, redfishprofile.DiscoveredResources{
|
||||||
SystemPaths: systemPaths,
|
SystemPaths: systemPaths,
|
||||||
@@ -107,11 +105,11 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
|||||||
result := &models.AnalysisResult{
|
result := &models.AnalysisResult{
|
||||||
CollectedAt: collectedAt,
|
CollectedAt: collectedAt,
|
||||||
InventoryLastModifiedAt: inventoryLastModifiedAt,
|
InventoryLastModifiedAt: inventoryLastModifiedAt,
|
||||||
SourceTimezone: sourceTimezone,
|
SourceTimezone: sourceTimezone,
|
||||||
Events: append(append(append(append(make([]models.Event, 0, len(discreteEvents)+len(healthEvents)+len(driveFetchWarningEvents)+len(logEntryEvents)+1), healthEvents...), discreteEvents...), driveFetchWarningEvents...), logEntryEvents...),
|
Events: append(append(append(append(make([]models.Event, 0, len(discreteEvents)+len(healthEvents)+len(driveFetchWarningEvents)+len(logEntryEvents)+1), healthEvents...), discreteEvents...), driveFetchWarningEvents...), logEntryEvents...),
|
||||||
FRU: assemblyFRU,
|
FRU: assemblyFRU,
|
||||||
Sensors: dedupeSensorReadings(append(append(thresholdSensors, thermalSensors...), powerSensors...)),
|
Sensors: dedupeSensorReadings(append(append(thresholdSensors, thermalSensors...), powerSensors...)),
|
||||||
RawPayloads: cloneRawPayloads(rawPayloads),
|
RawPayloads: cloneRawPayloads(rawPayloads),
|
||||||
Hardware: &models.HardwareConfig{
|
Hardware: &models.HardwareConfig{
|
||||||
BoardInfo: boardInfo,
|
BoardInfo: boardInfo,
|
||||||
CPUs: processors,
|
CPUs: processors,
|
||||||
@@ -123,7 +121,7 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
|||||||
PowerSupply: psus,
|
PowerSupply: psus,
|
||||||
NetworkAdapters: nics,
|
NetworkAdapters: nics,
|
||||||
Firmware: firmware,
|
Firmware: firmware,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
match := profileMatch
|
match := profileMatch
|
||||||
for _, profile := range match.Profiles {
|
for _, profile := range match.Profiles {
|
||||||
@@ -277,7 +275,6 @@ func redfishFetchErrorsFromRawPayloads(rawPayloads map[string]any) map[string]st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func buildDriveFetchWarningEvents(rawPayloads map[string]any) []models.Event {
|
func buildDriveFetchWarningEvents(rawPayloads map[string]any) []models.Event {
|
||||||
errs := redfishFetchErrorsFromRawPayloads(rawPayloads)
|
errs := redfishFetchErrorsFromRawPayloads(rawPayloads)
|
||||||
if len(errs) == 0 {
|
if len(errs) == 0 {
|
||||||
|
|||||||
@@ -938,7 +938,9 @@ func TestParseComponents_UseNestedSerialNumberFallback(t *testing.T) {
|
|||||||
"Manufacturer": "vendor0",
|
"Manufacturer": "vendor0",
|
||||||
"SerialNumber": "N/A",
|
"SerialNumber": "N/A",
|
||||||
"Oem": map[string]interface{}{
|
"Oem": map[string]interface{}{
|
||||||
"SerialNumber": "SN-OK-001",
|
"VendorX": map[string]interface{}{
|
||||||
|
"SerialNumber": "SN-OK-001",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -978,6 +980,38 @@ func TestParseComponents_UseNestedSerialNumberFallback(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseCPU_UsesPublicSerialAsPPINAndCurrentSpeedMHz(t *testing.T) {
|
||||||
|
cpus := parseCPUs([]map[string]interface{}{
|
||||||
|
{
|
||||||
|
"Id": "CPU0",
|
||||||
|
"Model": "Intel Xeon",
|
||||||
|
"TotalCores": 48,
|
||||||
|
"TotalThreads": 96,
|
||||||
|
"MaxSpeedMHz": 4000,
|
||||||
|
"OperatingSpeedMHz": 0,
|
||||||
|
"Oem": map[string]interface{}{
|
||||||
|
"Public": map[string]interface{}{
|
||||||
|
"SerialNumber": "6FB5241E81CECDFD",
|
||||||
|
"CurrentSpeedMHz": 2700,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(cpus) != 1 {
|
||||||
|
t.Fatalf("expected one CPU, got %d", len(cpus))
|
||||||
|
}
|
||||||
|
if cpus[0].PPIN != "6FB5241E81CECDFD" {
|
||||||
|
t.Fatalf("expected PPIN from Oem.Public.SerialNumber, got %+v", cpus[0])
|
||||||
|
}
|
||||||
|
if cpus[0].SerialNumber != "" {
|
||||||
|
t.Fatalf("expected empty CPU serial number when only Public serial exists, got %+v", cpus[0])
|
||||||
|
}
|
||||||
|
if cpus[0].FrequencyMHz != 2700 {
|
||||||
|
t.Fatalf("expected CPU frequency from Oem.Public.CurrentSpeedMHz, got %+v", cpus[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseCPUAndMemory_CollectOemDetails(t *testing.T) {
|
func TestParseCPUAndMemory_CollectOemDetails(t *testing.T) {
|
||||||
cpus := parseCPUs([]map[string]interface{}{
|
cpus := parseCPUs([]map[string]interface{}{
|
||||||
{
|
{
|
||||||
@@ -1687,7 +1721,7 @@ func TestReplayCollectStorage_UsesKnownControllerRecoveryWhenEnabled(t *testing.
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
got := r.collectStorage("/redfish/v1/Systems/1", redfishprofile.ResolvedAnalysisPlan{
|
got := r.collectStorage("/redfish/v1/Systems/1", redfishprofile.ResolvedAnalysisPlan{
|
||||||
Directives: redfishprofile.AnalysisDirectives{EnableKnownStorageControllerRecovery: true},
|
Directives: redfishprofile.AnalysisDirectives{EnableKnownStorageControllerRecovery: true},
|
||||||
KnownStorageDriveCollections: []string{"/Storage/IntelVROC/Drives"},
|
KnownStorageDriveCollections: []string{"/Storage/IntelVROC/Drives"},
|
||||||
})
|
})
|
||||||
if len(got) != 1 {
|
if len(got) != 1 {
|
||||||
@@ -2357,6 +2391,20 @@ func TestAppendPSU_MergesRicherDuplicate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRedfishPSUNominalWattage_PrefersInputRangeOutputWattage(t *testing.T) {
|
||||||
|
doc := map[string]interface{}{
|
||||||
|
"PowerCapacityWatts": 22600,
|
||||||
|
"InputRanges": []interface{}{
|
||||||
|
map[string]interface{}{"OutputWattage": 2700},
|
||||||
|
map[string]interface{}{"OutputWattage": 3200},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := redfishPSUNominalWattage(doc); got != 3200 {
|
||||||
|
t.Fatalf("redfishPSUNominalWattage() = %d, want 3200", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestReplayCollectGPUs_DropsModelOnlyPlaceholderWhenConcreteDiscoveredLater(t *testing.T) {
|
func TestReplayCollectGPUs_DropsModelOnlyPlaceholderWhenConcreteDiscoveredLater(t *testing.T) {
|
||||||
r := redfishSnapshotReader{tree: map[string]interface{}{
|
r := redfishSnapshotReader{tree: map[string]interface{}{
|
||||||
"/redfish/v1/Systems/1/GraphicsControllers": map[string]interface{}{
|
"/redfish/v1/Systems/1/GraphicsControllers": map[string]interface{}{
|
||||||
@@ -2677,7 +2725,7 @@ func TestCollectGPUsFromProcessors_SupermicroHGXUsesChassisAliasSerial(t *testin
|
|||||||
|
|
||||||
gpus := r.collectGPUs(systemPaths, chassisPaths, testAnalysisPlan(redfishprofile.AnalysisDirectives{EnableGenericGraphicsControllerDedup: true}))
|
gpus := r.collectGPUs(systemPaths, chassisPaths, testAnalysisPlan(redfishprofile.AnalysisDirectives{EnableGenericGraphicsControllerDedup: true}))
|
||||||
gpus = r.collectGPUsFromProcessors(systemPaths, chassisPaths, gpus, redfishprofile.ResolvedAnalysisPlan{
|
gpus = r.collectGPUsFromProcessors(systemPaths, chassisPaths, gpus, redfishprofile.ResolvedAnalysisPlan{
|
||||||
Directives: redfishprofile.AnalysisDirectives{EnableProcessorGPUFallback: true, EnableProcessorGPUChassisAlias: true},
|
Directives: redfishprofile.AnalysisDirectives{EnableProcessorGPUFallback: true, EnableProcessorGPUChassisAlias: true},
|
||||||
ProcessorGPUChassisLookupModes: []string{"hgx-alias"},
|
ProcessorGPUChassisLookupModes: []string{"hgx-alias"},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2715,7 +2763,7 @@ func TestCollectGPUsFromProcessors_MSIUsesIndexedChassisLookup(t *testing.T) {
|
|||||||
[]string{"/redfish/v1/Chassis/GPU1"},
|
[]string{"/redfish/v1/Chassis/GPU1"},
|
||||||
nil,
|
nil,
|
||||||
redfishprofile.ResolvedAnalysisPlan{
|
redfishprofile.ResolvedAnalysisPlan{
|
||||||
Directives: redfishprofile.AnalysisDirectives{EnableProcessorGPUFallback: true, EnableMSIProcessorGPUChassisLookup: true},
|
Directives: redfishprofile.AnalysisDirectives{EnableProcessorGPUFallback: true, EnableMSIProcessorGPUChassisLookup: true},
|
||||||
ProcessorGPUChassisLookupModes: []string{"msi-index"},
|
ProcessorGPUChassisLookupModes: []string{"msi-index"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package redfishprofile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
outboardCardHintRe = regexp.MustCompile(`/outboardPCIeCard\d+(?:/|$)`)
|
||||||
|
obDriveHintRe = regexp.MustCompile(`/Drives/OB\d+$`)
|
||||||
|
fpDriveHintRe = regexp.MustCompile(`/Drives/FP00HDD\d+$`)
|
||||||
|
vrFirmwareHintRe = regexp.MustCompile(`^CPU\d+_PVCC.*_VR$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
var inspurGroupOEMFirmwareHints = map[string]struct{}{
|
||||||
|
"Front_HDD_CPLD0": {},
|
||||||
|
"MainBoard0CPLD": {},
|
||||||
|
"MainBoardCPLD": {},
|
||||||
|
"PDBBoardCPLD": {},
|
||||||
|
"SCMCPLD": {},
|
||||||
|
"SWBoardCPLD": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
func inspurGroupOEMPlatformsProfile() Profile {
|
||||||
|
return staticProfile{
|
||||||
|
name: "inspur-group-oem-platforms",
|
||||||
|
priority: 25,
|
||||||
|
safeForFallback: false,
|
||||||
|
matchFn: func(s MatchSignals) int {
|
||||||
|
topologyScore := 0
|
||||||
|
boardScore := 0
|
||||||
|
chassisOutboard := matchedPathTokens(s.ResourceHints, "/redfish/v1/Chassis/", outboardCardHintRe)
|
||||||
|
systemOutboard := matchedPathTokens(s.ResourceHints, "/redfish/v1/Systems/", outboardCardHintRe)
|
||||||
|
obDrives := matchedPathTokens(s.ResourceHints, "", obDriveHintRe)
|
||||||
|
fpDrives := matchedPathTokens(s.ResourceHints, "", fpDriveHintRe)
|
||||||
|
firmwareNames, vrFirmwareNames := inspurGroupOEMFirmwareMatches(s.ResourceHints)
|
||||||
|
|
||||||
|
if len(chassisOutboard) > 0 {
|
||||||
|
topologyScore += 20
|
||||||
|
}
|
||||||
|
if len(systemOutboard) > 0 {
|
||||||
|
topologyScore += 10
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case len(obDrives) > 0 && len(fpDrives) > 0:
|
||||||
|
topologyScore += 15
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case len(firmwareNames) >= 2:
|
||||||
|
boardScore += 15
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case len(vrFirmwareNames) >= 2:
|
||||||
|
boardScore += 10
|
||||||
|
}
|
||||||
|
if anySignalContains(s, "COMMONbAssembly") {
|
||||||
|
boardScore += 12
|
||||||
|
}
|
||||||
|
if anySignalContains(s, "EnvironmentMetrcs") {
|
||||||
|
boardScore += 8
|
||||||
|
}
|
||||||
|
if anySignalContains(s, "GetServerAllUSBStatus") {
|
||||||
|
boardScore += 8
|
||||||
|
}
|
||||||
|
if topologyScore == 0 || boardScore == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return min(topologyScore+boardScore, 100)
|
||||||
|
},
|
||||||
|
extendAcquisition: func(plan *AcquisitionPlan, _ MatchSignals) {
|
||||||
|
addPlanNote(plan, "Inspur Group OEM platform fingerprint matched")
|
||||||
|
},
|
||||||
|
applyAnalysisDirectives: func(d *AnalysisDirectives, _ MatchSignals) {
|
||||||
|
d.EnableGenericGraphicsControllerDedup = true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchedPathTokens(paths []string, requiredPrefix string, re *regexp.Regexp) []string {
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
for _, rawPath := range paths {
|
||||||
|
path := normalizePath(rawPath)
|
||||||
|
if path == "" || (requiredPrefix != "" && !strings.HasPrefix(path, requiredPrefix)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
token := re.FindString(path)
|
||||||
|
if token == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
token = strings.Trim(token, "/")
|
||||||
|
if token == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[token] = struct{}{}
|
||||||
|
}
|
||||||
|
out := make([]string, 0, len(seen))
|
||||||
|
for token := range seen {
|
||||||
|
out = append(out, token)
|
||||||
|
}
|
||||||
|
return dedupeSorted(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func inspurGroupOEMFirmwareMatches(paths []string) ([]string, []string) {
|
||||||
|
firmwareNames := make(map[string]struct{})
|
||||||
|
vrNames := make(map[string]struct{})
|
||||||
|
for _, rawPath := range paths {
|
||||||
|
path := normalizePath(rawPath)
|
||||||
|
if !strings.HasPrefix(path, "/redfish/v1/UpdateService/FirmwareInventory/") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(path[strings.LastIndex(path, "/")+1:])
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := inspurGroupOEMFirmwareHints[name]; ok {
|
||||||
|
firmwareNames[name] = struct{}{}
|
||||||
|
}
|
||||||
|
if vrFirmwareHintRe.MatchString(name) {
|
||||||
|
vrNames[name] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mapKeysSorted(firmwareNames), mapKeysSorted(vrNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
func anySignalContains(signals MatchSignals, needle string) bool {
|
||||||
|
needle = strings.TrimSpace(needle)
|
||||||
|
if needle == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, signal := range signals.ResourceHints {
|
||||||
|
if strings.Contains(signal, needle) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, signal := range signals.DocHints {
|
||||||
|
if strings.Contains(signal, needle) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapKeysSorted(items map[string]struct{}) []string {
|
||||||
|
out := make([]string, 0, len(items))
|
||||||
|
for item := range items {
|
||||||
|
out = append(out, item)
|
||||||
|
}
|
||||||
|
return dedupeSorted(out)
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package redfishprofile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCollectSignalsFromTree_InspurGroupOEMPlatformsSelectsMatchedMode(t *testing.T) {
|
||||||
|
tree := map[string]interface{}{
|
||||||
|
"/redfish/v1": map[string]interface{}{
|
||||||
|
"@odata.id": "/redfish/v1",
|
||||||
|
},
|
||||||
|
"/redfish/v1/Systems": map[string]interface{}{
|
||||||
|
"Members": []interface{}{
|
||||||
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/redfish/v1/Systems/1": map[string]interface{}{
|
||||||
|
"@odata.id": "/redfish/v1/Systems/1",
|
||||||
|
"Oem": map[string]interface{}{
|
||||||
|
"Public": map[string]interface{}{
|
||||||
|
"USB": map[string]interface{}{
|
||||||
|
"@odata.id": "/redfish/v1/Systems/1/Oem/Public/GetServerAllUSBStatus",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"NetworkInterfaces": map[string]interface{}{
|
||||||
|
"@odata.id": "/redfish/v1/Systems/1/NetworkInterfaces",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/redfish/v1/Systems/1/NetworkInterfaces": map[string]interface{}{
|
||||||
|
"Members": []interface{}{
|
||||||
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/NetworkInterfaces/outboardPCIeCard0"},
|
||||||
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/NetworkInterfaces/outboardPCIeCard1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/redfish/v1/Chassis": map[string]interface{}{
|
||||||
|
"Members": []interface{}{
|
||||||
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/redfish/v1/Chassis/1": map[string]interface{}{
|
||||||
|
"@odata.id": "/redfish/v1/Chassis/1",
|
||||||
|
"Actions": map[string]interface{}{
|
||||||
|
"Oem": map[string]interface{}{
|
||||||
|
"Public": map[string]interface{}{
|
||||||
|
"NvGpuPowerLimitWatts": map[string]interface{}{
|
||||||
|
"target": "/redfish/v1/Chassis/1/GPU/EnvironmentMetrcs",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Drives": map[string]interface{}{
|
||||||
|
"@odata.id": "/redfish/v1/Chassis/1/Drives",
|
||||||
|
},
|
||||||
|
"NetworkAdapters": map[string]interface{}{
|
||||||
|
"@odata.id": "/redfish/v1/Chassis/1/NetworkAdapters",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/redfish/v1/Chassis/1/Drives": map[string]interface{}{
|
||||||
|
"Members": []interface{}{
|
||||||
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/Drives/OB01"},
|
||||||
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/Drives/FP00HDD00"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/redfish/v1/Chassis/1/NetworkAdapters": map[string]interface{}{
|
||||||
|
"Members": []interface{}{
|
||||||
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/NetworkAdapters/outboardPCIeCard0"},
|
||||||
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/NetworkAdapters/outboardPCIeCard1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/redfish/v1/Chassis/1/Assembly": map[string]interface{}{
|
||||||
|
"Assemblies": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"Oem": map[string]interface{}{
|
||||||
|
"COMMONb": map[string]interface{}{
|
||||||
|
"COMMONbAssembly": map[string]interface{}{
|
||||||
|
"@odata.type": "#COMMONbAssembly.v1_0_0.COMMONbAssembly",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/redfish/v1/Managers": map[string]interface{}{
|
||||||
|
"Members": []interface{}{
|
||||||
|
map[string]interface{}{"@odata.id": "/redfish/v1/Managers/1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/redfish/v1/Managers/1": map[string]interface{}{
|
||||||
|
"Actions": map[string]interface{}{
|
||||||
|
"Oem": map[string]interface{}{
|
||||||
|
"#PublicManager.ExportConfFile": map[string]interface{}{
|
||||||
|
"target": "/redfish/v1/Managers/1/Actions/Oem/Public/ExportConfFile",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/redfish/v1/UpdateService/FirmwareInventory": map[string]interface{}{
|
||||||
|
"Members": []interface{}{
|
||||||
|
map[string]interface{}{"@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/Front_HDD_CPLD0"},
|
||||||
|
map[string]interface{}{"@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/SCMCPLD"},
|
||||||
|
map[string]interface{}{"@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/CPU0_PVCCD_HV_VR"},
|
||||||
|
map[string]interface{}{"@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/CPU1_PVCCIN_VR"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
signals := CollectSignalsFromTree(tree)
|
||||||
|
match := MatchProfiles(signals)
|
||||||
|
|
||||||
|
if match.Mode != ModeMatched {
|
||||||
|
t.Fatalf("expected matched mode, got %q", match.Mode)
|
||||||
|
}
|
||||||
|
assertProfileSelected(t, match, "inspur-group-oem-platforms")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectSignalsFromTree_InspurGroupOEMPlatformsDoesNotFalsePositiveOnExampleRawExports(t *testing.T) {
|
||||||
|
examples := []string{
|
||||||
|
"2026-03-18 (G5500 V7) - 210619KUGGXGS2000015.zip",
|
||||||
|
"2026-03-11 (SYS-821GE-TNHR) - A514359X5C08846.zip",
|
||||||
|
"2026-03-15 (CG480-S5063) - P5T0006091.zip",
|
||||||
|
"2026-03-18 (CG290-S3063) - PAT0011258.zip",
|
||||||
|
"2024-04-25 (AS -4124GQ-TNMI) - S490387X4418273.zip",
|
||||||
|
}
|
||||||
|
for _, name := range examples {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
tree := loadRawExportTreeFromExampleZip(t, name)
|
||||||
|
match := MatchProfiles(CollectSignalsFromTree(tree))
|
||||||
|
assertProfileNotSelected(t, match, "inspur-group-oem-platforms")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadRawExportTreeFromExampleZip(t *testing.T, name string) map[string]interface{} {
|
||||||
|
t.Helper()
|
||||||
|
path := filepath.Join("..", "..", "..", "example", name)
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open example zip %s: %v", path, err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
info, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("stat example zip %s: %v", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
zr, err := zip.NewReader(f, info.Size())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read example zip %s: %v", path, err)
|
||||||
|
}
|
||||||
|
for _, file := range zr.File {
|
||||||
|
if file.Name != "raw_export.json" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rc, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open %s in %s: %v", file.Name, path, err)
|
||||||
|
}
|
||||||
|
defer rc.Close()
|
||||||
|
var payload struct {
|
||||||
|
Source struct {
|
||||||
|
RawPayloads struct {
|
||||||
|
RedfishTree map[string]interface{} `json:"redfish_tree"`
|
||||||
|
} `json:"raw_payloads"`
|
||||||
|
} `json:"source"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(rc).Decode(&payload); err != nil {
|
||||||
|
t.Fatalf("decode raw_export.json from %s: %v", path, err)
|
||||||
|
}
|
||||||
|
if len(payload.Source.RawPayloads.RedfishTree) == 0 {
|
||||||
|
t.Fatalf("example %s has empty redfish_tree", path)
|
||||||
|
}
|
||||||
|
return payload.Source.RawPayloads.RedfishTree
|
||||||
|
}
|
||||||
|
t.Fatalf("raw_export.json not found in %s", path)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -55,6 +55,7 @@ func BuiltinProfiles() []Profile {
|
|||||||
msiProfile(),
|
msiProfile(),
|
||||||
supermicroProfile(),
|
supermicroProfile(),
|
||||||
dellProfile(),
|
dellProfile(),
|
||||||
|
inspurGroupOEMPlatformsProfile(),
|
||||||
hgxProfile(),
|
hgxProfile(),
|
||||||
xfusionProfile(),
|
xfusionProfile(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,14 @@ package redfishprofile
|
|||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
func CollectSignals(serviceRootDoc, systemDoc, chassisDoc, managerDoc map[string]interface{}, resourceHints []string) MatchSignals {
|
func CollectSignals(serviceRootDoc, systemDoc, chassisDoc, managerDoc map[string]interface{}, resourceHints []string, hintDocs ...map[string]interface{}) MatchSignals {
|
||||||
|
resourceHints = append([]string{}, resourceHints...)
|
||||||
|
docHints := make([]string, 0)
|
||||||
|
for _, doc := range append([]map[string]interface{}{serviceRootDoc, systemDoc, chassisDoc, managerDoc}, hintDocs...) {
|
||||||
|
embeddedPaths, embeddedHints := collectDocSignalHints(doc)
|
||||||
|
resourceHints = append(resourceHints, embeddedPaths...)
|
||||||
|
docHints = append(docHints, embeddedHints...)
|
||||||
|
}
|
||||||
signals := MatchSignals{
|
signals := MatchSignals{
|
||||||
ServiceRootVendor: lookupString(serviceRootDoc, "Vendor"),
|
ServiceRootVendor: lookupString(serviceRootDoc, "Vendor"),
|
||||||
ServiceRootProduct: lookupString(serviceRootDoc, "Product"),
|
ServiceRootProduct: lookupString(serviceRootDoc, "Product"),
|
||||||
@@ -13,6 +20,7 @@ func CollectSignals(serviceRootDoc, systemDoc, chassisDoc, managerDoc map[string
|
|||||||
ChassisModel: lookupString(chassisDoc, "Model"),
|
ChassisModel: lookupString(chassisDoc, "Model"),
|
||||||
ManagerManufacturer: lookupString(managerDoc, "Manufacturer"),
|
ManagerManufacturer: lookupString(managerDoc, "Manufacturer"),
|
||||||
ResourceHints: resourceHints,
|
ResourceHints: resourceHints,
|
||||||
|
DocHints: docHints,
|
||||||
}
|
}
|
||||||
signals.OEMNamespaces = dedupeSorted(append(
|
signals.OEMNamespaces = dedupeSorted(append(
|
||||||
oemNamespaces(serviceRootDoc),
|
oemNamespaces(serviceRootDoc),
|
||||||
@@ -50,6 +58,7 @@ func CollectSignalsFromTree(tree map[string]interface{}) MatchSignals {
|
|||||||
managerPath := memberPath("/redfish/v1/Managers", "/redfish/v1/Managers/1")
|
managerPath := memberPath("/redfish/v1/Managers", "/redfish/v1/Managers/1")
|
||||||
|
|
||||||
resourceHints := make([]string, 0, len(tree))
|
resourceHints := make([]string, 0, len(tree))
|
||||||
|
hintDocs := make([]map[string]interface{}, 0, len(tree))
|
||||||
for path := range tree {
|
for path := range tree {
|
||||||
path = strings.TrimSpace(path)
|
path = strings.TrimSpace(path)
|
||||||
if path == "" {
|
if path == "" {
|
||||||
@@ -57,6 +66,13 @@ func CollectSignalsFromTree(tree map[string]interface{}) MatchSignals {
|
|||||||
}
|
}
|
||||||
resourceHints = append(resourceHints, path)
|
resourceHints = append(resourceHints, path)
|
||||||
}
|
}
|
||||||
|
for _, v := range tree {
|
||||||
|
doc, ok := v.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hintDocs = append(hintDocs, doc)
|
||||||
|
}
|
||||||
|
|
||||||
return CollectSignals(
|
return CollectSignals(
|
||||||
getDoc("/redfish/v1"),
|
getDoc("/redfish/v1"),
|
||||||
@@ -64,9 +80,72 @@ func CollectSignalsFromTree(tree map[string]interface{}) MatchSignals {
|
|||||||
getDoc(chassisPath),
|
getDoc(chassisPath),
|
||||||
getDoc(managerPath),
|
getDoc(managerPath),
|
||||||
resourceHints,
|
resourceHints,
|
||||||
|
hintDocs...,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func collectDocSignalHints(doc map[string]interface{}) ([]string, []string) {
|
||||||
|
if len(doc) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
paths := make([]string, 0)
|
||||||
|
hints := make([]string, 0)
|
||||||
|
var walk func(any)
|
||||||
|
walk = func(v any) {
|
||||||
|
switch x := v.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
for rawKey, child := range x {
|
||||||
|
key := strings.TrimSpace(rawKey)
|
||||||
|
if key != "" {
|
||||||
|
hints = append(hints, key)
|
||||||
|
}
|
||||||
|
if s, ok := child.(string); ok {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s != "" {
|
||||||
|
switch key {
|
||||||
|
case "@odata.id", "target":
|
||||||
|
paths = append(paths, s)
|
||||||
|
case "@odata.type":
|
||||||
|
hints = append(hints, s)
|
||||||
|
default:
|
||||||
|
if isInterestingSignalString(s) {
|
||||||
|
hints = append(hints, s)
|
||||||
|
if strings.HasPrefix(s, "/") {
|
||||||
|
paths = append(paths, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(child)
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
for _, child := range x {
|
||||||
|
walk(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(doc)
|
||||||
|
return paths, hints
|
||||||
|
}
|
||||||
|
|
||||||
|
func isInterestingSignalString(s string) bool {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(s, "/"):
|
||||||
|
return true
|
||||||
|
case strings.HasPrefix(s, "#"):
|
||||||
|
return true
|
||||||
|
case strings.Contains(s, "COMMONb"):
|
||||||
|
return true
|
||||||
|
case strings.Contains(s, "EnvironmentMetrcs"):
|
||||||
|
return true
|
||||||
|
case strings.Contains(s, "GetServerAllUSBStatus"):
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func lookupString(doc map[string]interface{}, key string) string {
|
func lookupString(doc map[string]interface{}, key string) string {
|
||||||
if len(doc) == 0 {
|
if len(doc) == 0 {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type MatchSignals struct {
|
|||||||
ManagerManufacturer string
|
ManagerManufacturer string
|
||||||
OEMNamespaces []string
|
OEMNamespaces []string
|
||||||
ResourceHints []string
|
ResourceHints []string
|
||||||
|
DocHints []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type AcquisitionPlan struct {
|
type AcquisitionPlan struct {
|
||||||
@@ -110,12 +111,12 @@ type AnalysisDirectives struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ResolvedAnalysisPlan struct {
|
type ResolvedAnalysisPlan struct {
|
||||||
Match MatchResult
|
Match MatchResult
|
||||||
Directives AnalysisDirectives
|
Directives AnalysisDirectives
|
||||||
Notes []string
|
Notes []string
|
||||||
ProcessorGPUChassisLookupModes []string
|
ProcessorGPUChassisLookupModes []string
|
||||||
KnownStorageDriveCollections []string
|
KnownStorageDriveCollections []string
|
||||||
KnownStorageVolumeCollections []string
|
KnownStorageVolumeCollections []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Profile interface {
|
type Profile interface {
|
||||||
@@ -146,6 +147,7 @@ type ProfileScore struct {
|
|||||||
func normalizeSignals(signals MatchSignals) MatchSignals {
|
func normalizeSignals(signals MatchSignals) MatchSignals {
|
||||||
signals.OEMNamespaces = dedupeSorted(signals.OEMNamespaces)
|
signals.OEMNamespaces = dedupeSorted(signals.OEMNamespaces)
|
||||||
signals.ResourceHints = dedupeSorted(signals.ResourceHints)
|
signals.ResourceHints = dedupeSorted(signals.ResourceHints)
|
||||||
|
signals.DocHints = dedupeSorted(signals.DocHints)
|
||||||
return signals
|
return signals
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user