Add Inspur Group OEM Redfish profile
This commit is contained in:
@@ -147,6 +147,7 @@ func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit Progre
|
||||
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: подключение к BMC..."})
|
||||
@@ -178,7 +179,8 @@ func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit Progre
|
||||
chassisDoc, _ := c.getJSON(discoveryCtx, snapshotClient, req, baseURL, primaryChassis)
|
||||
managerDoc, _ := c.getJSON(discoveryCtx, snapshotClient, req, baseURL, primaryManager)
|
||||
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)
|
||||
acquisitionPlan := redfishprofile.BuildAcquisitionPlan(signals)
|
||||
telemetrySummary := telemetry.Snapshot()
|
||||
@@ -1557,6 +1559,33 @@ func (c *RedfishConnector) discoverMemberPaths(ctx context.Context, client *http
|
||||
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)
|
||||
@@ -3476,14 +3505,20 @@ func parseCPUs(docs []map[string]interface{}) []models.CPU {
|
||||
}
|
||||
}
|
||||
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: asInt(doc["OperatingSpeedMHz"]),
|
||||
MaxFreqMHz: asInt(doc["MaxSpeedMHz"]),
|
||||
SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
|
||||
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,
|
||||
@@ -3494,6 +3529,12 @@ func parseCPUs(docs []map[string]interface{}) []models.CPU {
|
||||
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) {
|
||||
@@ -3942,7 +3983,7 @@ func parsePSUWithSupplementalDocs(doc map[string]interface{}, idx int, supplemen
|
||||
Present: present,
|
||||
Model: firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
|
||||
Vendor: asString(doc["Manufacturer"]),
|
||||
WattageW: asInt(doc["PowerCapacityWatts"]),
|
||||
WattageW: redfishPSUNominalWattage(doc),
|
||||
SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
|
||||
PartNumber: asString(doc["PartNumber"]),
|
||||
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 {
|
||||
return redfishDriveDetailsWithSupplementalDocs(doc)
|
||||
}
|
||||
@@ -5781,7 +5841,6 @@ func parseFirmware(system, bios, manager, networkProtocol map[string]interface{}
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
func mapStatus(statusAny interface{}) string {
|
||||
if statusAny == nil {
|
||||
return ""
|
||||
|
||||
@@ -31,8 +31,7 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
||||
if emit != nil {
|
||||
emit(Progress{Status: "running", Progress: 10, Message: "Redfish snapshot: replay service root..."})
|
||||
}
|
||||
serviceRootDoc, err := r.getJSON("/redfish/v1")
|
||||
if err != nil {
|
||||
if _, err := r.getJSON("/redfish/v1"); err != nil {
|
||||
log.Printf("redfish replay: service root /redfish/v1 missing from snapshot, continuing with defaults: %v", err)
|
||||
}
|
||||
|
||||
@@ -61,8 +60,7 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
||||
fruDoc = chassisFRUDoc
|
||||
}
|
||||
boardFallbackDocs := r.collectBoardFallbackDocs(systemPaths, chassisPaths)
|
||||
resourceHints := append(append([]string{}, systemPaths...), append(chassisPaths, managerPaths...)...)
|
||||
profileSignals := redfishprofile.CollectSignals(serviceRootDoc, systemDoc, chassisDoc, managerDoc, resourceHints)
|
||||
profileSignals := redfishprofile.CollectSignalsFromTree(tree)
|
||||
profileMatch := redfishprofile.MatchProfiles(profileSignals)
|
||||
analysisPlan := redfishprofile.ResolveAnalysisPlan(profileMatch, tree, redfishprofile.DiscoveredResources{
|
||||
SystemPaths: systemPaths,
|
||||
@@ -107,11 +105,11 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
||||
result := &models.AnalysisResult{
|
||||
CollectedAt: collectedAt,
|
||||
InventoryLastModifiedAt: inventoryLastModifiedAt,
|
||||
SourceTimezone: sourceTimezone,
|
||||
Events: append(append(append(append(make([]models.Event, 0, len(discreteEvents)+len(healthEvents)+len(driveFetchWarningEvents)+len(logEntryEvents)+1), healthEvents...), discreteEvents...), driveFetchWarningEvents...), logEntryEvents...),
|
||||
FRU: assemblyFRU,
|
||||
Sensors: dedupeSensorReadings(append(append(thresholdSensors, thermalSensors...), powerSensors...)),
|
||||
RawPayloads: cloneRawPayloads(rawPayloads),
|
||||
SourceTimezone: sourceTimezone,
|
||||
Events: append(append(append(append(make([]models.Event, 0, len(discreteEvents)+len(healthEvents)+len(driveFetchWarningEvents)+len(logEntryEvents)+1), healthEvents...), discreteEvents...), driveFetchWarningEvents...), logEntryEvents...),
|
||||
FRU: assemblyFRU,
|
||||
Sensors: dedupeSensorReadings(append(append(thresholdSensors, thermalSensors...), powerSensors...)),
|
||||
RawPayloads: cloneRawPayloads(rawPayloads),
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: boardInfo,
|
||||
CPUs: processors,
|
||||
@@ -123,7 +121,7 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
||||
PowerSupply: psus,
|
||||
NetworkAdapters: nics,
|
||||
Firmware: firmware,
|
||||
},
|
||||
},
|
||||
}
|
||||
match := profileMatch
|
||||
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 {
|
||||
errs := redfishFetchErrorsFromRawPayloads(rawPayloads)
|
||||
if len(errs) == 0 {
|
||||
|
||||
@@ -938,7 +938,9 @@ func TestParseComponents_UseNestedSerialNumberFallback(t *testing.T) {
|
||||
"Manufacturer": "vendor0",
|
||||
"SerialNumber": "N/A",
|
||||
"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) {
|
||||
cpus := parseCPUs([]map[string]interface{}{
|
||||
{
|
||||
@@ -1687,7 +1721,7 @@ func TestReplayCollectStorage_UsesKnownControllerRecoveryWhenEnabled(t *testing.
|
||||
}}
|
||||
|
||||
got := r.collectStorage("/redfish/v1/Systems/1", redfishprofile.ResolvedAnalysisPlan{
|
||||
Directives: redfishprofile.AnalysisDirectives{EnableKnownStorageControllerRecovery: true},
|
||||
Directives: redfishprofile.AnalysisDirectives{EnableKnownStorageControllerRecovery: true},
|
||||
KnownStorageDriveCollections: []string{"/Storage/IntelVROC/Drives"},
|
||||
})
|
||||
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) {
|
||||
r := redfishSnapshotReader{tree: 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.collectGPUsFromProcessors(systemPaths, chassisPaths, gpus, redfishprofile.ResolvedAnalysisPlan{
|
||||
Directives: redfishprofile.AnalysisDirectives{EnableProcessorGPUFallback: true, EnableProcessorGPUChassisAlias: true},
|
||||
Directives: redfishprofile.AnalysisDirectives{EnableProcessorGPUFallback: true, EnableProcessorGPUChassisAlias: true},
|
||||
ProcessorGPUChassisLookupModes: []string{"hgx-alias"},
|
||||
})
|
||||
|
||||
@@ -2715,7 +2763,7 @@ func TestCollectGPUsFromProcessors_MSIUsesIndexedChassisLookup(t *testing.T) {
|
||||
[]string{"/redfish/v1/Chassis/GPU1"},
|
||||
nil,
|
||||
redfishprofile.ResolvedAnalysisPlan{
|
||||
Directives: redfishprofile.AnalysisDirectives{EnableProcessorGPUFallback: true, EnableMSIProcessorGPUChassisLookup: true},
|
||||
Directives: redfishprofile.AnalysisDirectives{EnableProcessorGPUFallback: true, EnableMSIProcessorGPUChassisLookup: true},
|
||||
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(),
|
||||
supermicroProfile(),
|
||||
dellProfile(),
|
||||
inspurGroupOEMPlatformsProfile(),
|
||||
hgxProfile(),
|
||||
xfusionProfile(),
|
||||
}
|
||||
|
||||
@@ -2,7 +2,14 @@ package redfishprofile
|
||||
|
||||
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{
|
||||
ServiceRootVendor: lookupString(serviceRootDoc, "Vendor"),
|
||||
ServiceRootProduct: lookupString(serviceRootDoc, "Product"),
|
||||
@@ -13,6 +20,7 @@ func CollectSignals(serviceRootDoc, systemDoc, chassisDoc, managerDoc map[string
|
||||
ChassisModel: lookupString(chassisDoc, "Model"),
|
||||
ManagerManufacturer: lookupString(managerDoc, "Manufacturer"),
|
||||
ResourceHints: resourceHints,
|
||||
DocHints: docHints,
|
||||
}
|
||||
signals.OEMNamespaces = dedupeSorted(append(
|
||||
oemNamespaces(serviceRootDoc),
|
||||
@@ -50,6 +58,7 @@ func CollectSignalsFromTree(tree map[string]interface{}) MatchSignals {
|
||||
managerPath := memberPath("/redfish/v1/Managers", "/redfish/v1/Managers/1")
|
||||
|
||||
resourceHints := make([]string, 0, len(tree))
|
||||
hintDocs := make([]map[string]interface{}, 0, len(tree))
|
||||
for path := range tree {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
@@ -57,6 +66,13 @@ func CollectSignalsFromTree(tree map[string]interface{}) MatchSignals {
|
||||
}
|
||||
resourceHints = append(resourceHints, path)
|
||||
}
|
||||
for _, v := range tree {
|
||||
doc, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
hintDocs = append(hintDocs, doc)
|
||||
}
|
||||
|
||||
return CollectSignals(
|
||||
getDoc("/redfish/v1"),
|
||||
@@ -64,9 +80,72 @@ func CollectSignalsFromTree(tree map[string]interface{}) MatchSignals {
|
||||
getDoc(chassisPath),
|
||||
getDoc(managerPath),
|
||||
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 {
|
||||
if len(doc) == 0 {
|
||||
return ""
|
||||
|
||||
@@ -17,6 +17,7 @@ type MatchSignals struct {
|
||||
ManagerManufacturer string
|
||||
OEMNamespaces []string
|
||||
ResourceHints []string
|
||||
DocHints []string
|
||||
}
|
||||
|
||||
type AcquisitionPlan struct {
|
||||
@@ -110,12 +111,12 @@ type AnalysisDirectives struct {
|
||||
}
|
||||
|
||||
type ResolvedAnalysisPlan struct {
|
||||
Match MatchResult
|
||||
Directives AnalysisDirectives
|
||||
Notes []string
|
||||
ProcessorGPUChassisLookupModes []string
|
||||
KnownStorageDriveCollections []string
|
||||
KnownStorageVolumeCollections []string
|
||||
Match MatchResult
|
||||
Directives AnalysisDirectives
|
||||
Notes []string
|
||||
ProcessorGPUChassisLookupModes []string
|
||||
KnownStorageDriveCollections []string
|
||||
KnownStorageVolumeCollections []string
|
||||
}
|
||||
|
||||
type Profile interface {
|
||||
@@ -146,6 +147,7 @@ type ProfileScore struct {
|
||||
func normalizeSignals(signals MatchSignals) MatchSignals {
|
||||
signals.OEMNamespaces = dedupeSorted(signals.OEMNamespaces)
|
||||
signals.ResourceHints = dedupeSorted(signals.ResourceHints)
|
||||
signals.DocHints = dedupeSorted(signals.DocHints)
|
||||
return signals
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user