Add raw export reanalyze flow for Redfish snapshots
This commit is contained in:
@@ -542,15 +542,15 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht
|
||||
enqueue(ref)
|
||||
}
|
||||
}
|
||||
n := atomic.AddInt32(&processed, 1)
|
||||
if err != nil {
|
||||
c.debugSnapshotf("worker=%d fetch error path=%s err=%v", workerID, current, err)
|
||||
if emit != nil {
|
||||
emit(Progress{
|
||||
Status: "running",
|
||||
Progress: 92 + int(minInt32(n/200, 6)),
|
||||
Message: fmt.Sprintf("Redfish snapshot: ошибка на %s", compactProgressPath(current)),
|
||||
})
|
||||
n := atomic.AddInt32(&processed, 1)
|
||||
if err != nil {
|
||||
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: ошибка на %s", compactProgressPath(current)),
|
||||
})
|
||||
}
|
||||
}
|
||||
if emit != nil && n%40 == 0 {
|
||||
@@ -894,14 +894,20 @@ func parsePSU(doc map[string]interface{}, idx int) models.PSU {
|
||||
}
|
||||
|
||||
func parseGPU(doc map[string]interface{}, functionDocs []map[string]interface{}, idx int) models.GPU {
|
||||
slot := firstNonEmpty(asString(doc["Slot"]), asString(doc["Name"]), asString(doc["Id"]))
|
||||
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(asString(doc["Location"]), asString(doc["PhysicalLocation"])),
|
||||
Location: firstNonEmpty(redfishLocationLabel(doc["Location"]), redfishLocationLabel(doc["PhysicalLocation"])),
|
||||
Model: firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
|
||||
Manufacturer: asString(doc["Manufacturer"]),
|
||||
SerialNumber: asString(doc["SerialNumber"]),
|
||||
@@ -958,7 +964,7 @@ func parseGPU(doc map[string]interface{}, functionDocs []map[string]interface{},
|
||||
|
||||
func parsePCIeDevice(doc map[string]interface{}, functionDocs []map[string]interface{}) models.PCIeDevice {
|
||||
dev := models.PCIeDevice{
|
||||
Slot: firstNonEmpty(asString(doc["Slot"]), asString(doc["Name"]), asString(doc["Id"])),
|
||||
Slot: firstNonEmpty(redfishLocationLabel(doc["Slot"]), redfishLocationLabel(doc["Location"]), asString(doc["Name"]), asString(doc["Id"])),
|
||||
BDF: asString(doc["BDF"]),
|
||||
DeviceClass: firstNonEmpty(asString(doc["DeviceType"]), asString(doc["PCIeType"])),
|
||||
Manufacturer: asString(doc["Manufacturer"]),
|
||||
@@ -1013,7 +1019,7 @@ func parsePCIeDevice(doc map[string]interface{}, functionDocs []map[string]inter
|
||||
}
|
||||
|
||||
func parsePCIeFunction(doc map[string]interface{}, idx int) models.PCIeDevice {
|
||||
slot := firstNonEmpty(asString(doc["Id"]), asString(doc["Name"]))
|
||||
slot := firstNonEmpty(redfishLocationLabel(doc["Location"]), asString(doc["Id"]), asString(doc["Name"]))
|
||||
if slot == "" {
|
||||
slot = fmt.Sprintf("PCIeFn%d", idx)
|
||||
}
|
||||
@@ -1367,6 +1373,9 @@ func normalizeRedfishPath(raw string) string {
|
||||
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)
|
||||
@@ -1444,6 +1453,45 @@ func topRoots(counts map[string]int, limit int) []string {
|
||||
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 ""
|
||||
}
|
||||
}
|
||||
|
||||
func compactProgressPath(p string) string {
|
||||
const maxLen = 72
|
||||
if len(p) <= maxLen {
|
||||
@@ -1452,6 +1500,20 @@ func compactProgressPath(p string) string {
|
||||
return "..." + p[len(p)-maxLen+3:]
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
// ReplayRedfishFromRawPayloads rebuilds AnalysisResult from saved Redfish raw payloads.
|
||||
// It expects rawPayloads["redfish_tree"] to contain a map[path]document snapshot.
|
||||
func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (*models.AnalysisResult, error) {
|
||||
if len(rawPayloads) == 0 {
|
||||
return nil, fmt.Errorf("missing raw_payloads")
|
||||
}
|
||||
treeAny, ok := rawPayloads["redfish_tree"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("raw_payloads.redfish_tree is missing")
|
||||
}
|
||||
tree, ok := treeAny.(map[string]interface{})
|
||||
if !ok || len(tree) == 0 {
|
||||
return nil, fmt.Errorf("raw_payloads.redfish_tree has invalid format")
|
||||
}
|
||||
|
||||
r := redfishSnapshotReader{tree: tree}
|
||||
if emit != nil {
|
||||
emit(Progress{Status: "running", Progress: 10, Message: "Redfish snapshot: replay service root..."})
|
||||
}
|
||||
if _, err := r.getJSON("/redfish/v1"); err != nil {
|
||||
return nil, fmt.Errorf("redfish service root: %w", err)
|
||||
}
|
||||
|
||||
systemPaths := r.discoverMemberPaths("/redfish/v1/Systems", "/redfish/v1/Systems/1")
|
||||
chassisPaths := r.discoverMemberPaths("/redfish/v1/Chassis", "/redfish/v1/Chassis/1")
|
||||
managerPaths := r.discoverMemberPaths("/redfish/v1/Managers", "/redfish/v1/Managers/1")
|
||||
primarySystem := firstPathOrDefault(systemPaths, "/redfish/v1/Systems/1")
|
||||
primaryManager := firstPathOrDefault(managerPaths, "/redfish/v1/Managers/1")
|
||||
|
||||
if emit != nil {
|
||||
emit(Progress{Status: "running", Progress: 30, Message: "Redfish snapshot: replay system..."})
|
||||
}
|
||||
systemDoc, err := r.getJSON(primarySystem)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("system info: %w", err)
|
||||
}
|
||||
biosDoc, _ := r.getJSON(joinPath(primarySystem, "/Bios"))
|
||||
secureBootDoc, _ := r.getJSON(joinPath(primarySystem, "/SecureBoot"))
|
||||
|
||||
if emit != nil {
|
||||
emit(Progress{Status: "running", Progress: 55, Message: "Redfish snapshot: replay CPU/RAM/Storage..."})
|
||||
}
|
||||
processors, _ := r.getCollectionMembers(joinPath(primarySystem, "/Processors"))
|
||||
memory, _ := r.getCollectionMembers(joinPath(primarySystem, "/Memory"))
|
||||
storageDevices := r.collectStorage(primarySystem)
|
||||
|
||||
if emit != nil {
|
||||
emit(Progress{Status: "running", Progress: 80, Message: "Redfish snapshot: replay network/BMC..."})
|
||||
}
|
||||
psus := r.collectPSUs(chassisPaths)
|
||||
pcieDevices := r.collectPCIeDevices(systemPaths, chassisPaths)
|
||||
gpus := r.collectGPUs(systemPaths, chassisPaths)
|
||||
nics := r.collectNICs(chassisPaths)
|
||||
managerDoc, _ := r.getJSON(primaryManager)
|
||||
networkProtocolDoc, _ := r.getJSON(joinPath(primaryManager, "/NetworkProtocol"))
|
||||
|
||||
result := &models.AnalysisResult{
|
||||
Events: make([]models.Event, 0),
|
||||
FRU: make([]models.FRUInfo, 0),
|
||||
Sensors: make([]models.SensorReading, 0),
|
||||
RawPayloads: map[string]any{
|
||||
"redfish_tree": tree,
|
||||
},
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: parseBoardInfo(systemDoc),
|
||||
CPUs: parseCPUs(processors),
|
||||
Memory: parseMemory(memory),
|
||||
Storage: storageDevices,
|
||||
PCIeDevices: pcieDevices,
|
||||
GPUs: gpus,
|
||||
PowerSupply: psus,
|
||||
NetworkAdapters: nics,
|
||||
Firmware: parseFirmware(systemDoc, biosDoc, managerDoc, secureBootDoc, networkProtocolDoc),
|
||||
},
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type redfishSnapshotReader struct {
|
||||
tree map[string]interface{}
|
||||
}
|
||||
|
||||
func (r redfishSnapshotReader) getJSON(requestPath string) (map[string]interface{}, error) {
|
||||
p := normalizeRedfishPath(requestPath)
|
||||
if doc, ok := r.tree[p]; ok {
|
||||
if m, ok := doc.(map[string]interface{}); ok {
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
if p != "/" {
|
||||
if doc, ok := r.tree[stringsTrimTrailingSlash(p)]; ok {
|
||||
if m, ok := doc.(map[string]interface{}); ok {
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
if doc, ok := r.tree[p+"/"]; ok {
|
||||
if m, ok := doc.(map[string]interface{}); ok {
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("snapshot path not found: %s", requestPath)
|
||||
}
|
||||
|
||||
func (r redfishSnapshotReader) getCollectionMembers(collectionPath string) ([]map[string]interface{}, error) {
|
||||
collection, err := r.getJSON(collectionPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refs, ok := collection["Members"].([]interface{})
|
||||
if !ok || len(refs) == 0 {
|
||||
return []map[string]interface{}{}, nil
|
||||
}
|
||||
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
|
||||
}
|
||||
doc, err := r.getJSON(memberPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, doc)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r redfishSnapshotReader) discoverMemberPaths(collectionPath, fallbackPath string) []string {
|
||||
collection, err := r.getJSON(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 (r redfishSnapshotReader) getLinkedPCIeFunctions(doc map[string]interface{}) []map[string]interface{} {
|
||||
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 := r.getJSON(memberPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, memberDoc)
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
if pcieFunctions, ok := doc["PCIeFunctions"].(map[string]interface{}); ok {
|
||||
if collectionPath := asString(pcieFunctions["@odata.id"]); collectionPath != "" {
|
||||
memberDocs, err := r.getCollectionMembers(collectionPath)
|
||||
if err == nil {
|
||||
return memberDocs
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storage {
|
||||
var out []models.Storage
|
||||
storageMembers, _ := r.getCollectionMembers(joinPath(systemPath, "/Storage"))
|
||||
for _, member := range storageMembers {
|
||||
if driveCollection, ok := member["Drives"].(map[string]interface{}); ok {
|
||||
if driveCollectionPath := asString(driveCollection["@odata.id"]); driveCollectionPath != "" {
|
||||
driveDocs, err := r.getCollectionMembers(driveCollectionPath)
|
||||
if err == nil {
|
||||
for _, driveDoc := range driveDocs {
|
||||
out = append(out, parseDrive(driveDoc))
|
||||
}
|
||||
}
|
||||
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 := r.getJSON(odata)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, parseDrive(driveDoc))
|
||||
}
|
||||
continue
|
||||
}
|
||||
if looksLikeDrive(member) {
|
||||
out = append(out, parseDrive(member))
|
||||
}
|
||||
}
|
||||
|
||||
simpleStorageMembers, _ := r.getCollectionMembers(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) {
|
||||
continue
|
||||
}
|
||||
out = append(out, parseDrive(devDoc))
|
||||
}
|
||||
}
|
||||
|
||||
chassisPaths := r.discoverMemberPaths("/redfish/v1/Chassis", "/redfish/v1/Chassis/1")
|
||||
for _, chassisPath := range chassisPaths {
|
||||
driveDocs, err := r.getCollectionMembers(joinPath(chassisPath, "/Drives"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, driveDoc := range driveDocs {
|
||||
if !looksLikeDrive(driveDoc) {
|
||||
continue
|
||||
}
|
||||
out = append(out, parseDrive(driveDoc))
|
||||
}
|
||||
}
|
||||
return dedupeStorage(out)
|
||||
}
|
||||
|
||||
func (r redfishSnapshotReader) collectNICs(chassisPaths []string) []models.NetworkAdapter {
|
||||
var nics []models.NetworkAdapter
|
||||
seen := make(map[string]struct{})
|
||||
for _, chassisPath := range chassisPaths {
|
||||
adapterDocs, err := r.getCollectionMembers(joinPath(chassisPath, "/NetworkAdapters"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, doc := range adapterDocs {
|
||||
nic := parseNIC(doc)
|
||||
key := firstNonEmpty(nic.SerialNumber, nic.Slot+"|"+nic.Model)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
nics = append(nics, nic)
|
||||
}
|
||||
}
|
||||
return nics
|
||||
}
|
||||
|
||||
func (r redfishSnapshotReader) collectPSUs(chassisPaths []string) []models.PSU {
|
||||
var out []models.PSU
|
||||
seen := make(map[string]struct{})
|
||||
idx := 1
|
||||
for _, chassisPath := range chassisPaths {
|
||||
if powerDoc, err := r.getJSON(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
|
||||
}
|
||||
psu := parsePSU(doc, idx)
|
||||
idx++
|
||||
key := firstNonEmpty(psu.SerialNumber, psu.Slot+"|"+psu.Model)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, psu)
|
||||
}
|
||||
}
|
||||
}
|
||||
memberDocs, err := r.getCollectionMembers(joinPath(chassisPath, "/PowerSubsystem/PowerSupplies"))
|
||||
if err != nil || len(memberDocs) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, doc := range memberDocs {
|
||||
psu := parsePSU(doc, idx)
|
||||
idx++
|
||||
key := firstNonEmpty(psu.SerialNumber, psu.Slot+"|"+psu.Model)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, psu)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r redfishSnapshotReader) collectGPUs(systemPaths, chassisPaths []string) []models.GPU {
|
||||
collections := make([]string, 0, len(systemPaths)*2+len(chassisPaths))
|
||||
for _, systemPath := range systemPaths {
|
||||
collections = append(collections, joinPath(systemPath, "/PCIeDevices"))
|
||||
collections = append(collections, joinPath(systemPath, "/Accelerators"))
|
||||
}
|
||||
for _, chassisPath := range chassisPaths {
|
||||
collections = append(collections, joinPath(chassisPath, "/PCIeDevices"))
|
||||
}
|
||||
var out []models.GPU
|
||||
seen := make(map[string]struct{})
|
||||
idx := 1
|
||||
for _, collectionPath := range collections {
|
||||
memberDocs, err := r.getCollectionMembers(collectionPath)
|
||||
if err != nil || len(memberDocs) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, doc := range memberDocs {
|
||||
functionDocs := r.getLinkedPCIeFunctions(doc)
|
||||
if !looksLikeGPU(doc, functionDocs) {
|
||||
continue
|
||||
}
|
||||
gpu := parseGPU(doc, functionDocs, idx)
|
||||
idx++
|
||||
key := firstNonEmpty(gpu.SerialNumber, gpu.BDF, gpu.Slot+"|"+gpu.Model)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, gpu)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r redfishSnapshotReader) collectPCIeDevices(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
|
||||
seen := make(map[string]struct{})
|
||||
for _, collectionPath := range collections {
|
||||
memberDocs, err := r.getCollectionMembers(collectionPath)
|
||||
if err != nil || len(memberDocs) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, doc := range memberDocs {
|
||||
functionDocs := r.getLinkedPCIeFunctions(doc)
|
||||
dev := parsePCIeDevice(doc, functionDocs)
|
||||
key := firstNonEmpty(dev.SerialNumber, dev.BDF, dev.Slot+"|"+dev.DeviceClass)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, dev)
|
||||
}
|
||||
}
|
||||
for _, systemPath := range systemPaths {
|
||||
functionDocs, err := r.getCollectionMembers(joinPath(systemPath, "/PCIeFunctions"))
|
||||
if err != nil || len(functionDocs) == 0 {
|
||||
continue
|
||||
}
|
||||
for idx, fn := range functionDocs {
|
||||
dev := parsePCIeFunction(fn, idx+1)
|
||||
key := firstNonEmpty(dev.BDF, dev.SerialNumber, dev.Slot+"|"+dev.DeviceClass)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, dev)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func stringsTrimTrailingSlash(s string) string {
|
||||
for len(s) > 1 && s[len(s)-1] == '/' {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -200,3 +200,41 @@ func TestRedfishConnectorCollect(t *testing.T) {
|
||||
t.Fatalf("expected non-empty redfish_tree, got %#v", treeAny)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePCIeDeviceSlot_FromNestedRedfishSlotLocation(t *testing.T) {
|
||||
doc := map[string]interface{}{
|
||||
"Id": "NIC1",
|
||||
"Slot": map[string]interface{}{
|
||||
"Lanes": 16,
|
||||
"Location": map[string]interface{}{
|
||||
"PartLocation": map[string]interface{}{
|
||||
"LocationOrdinalValue": 1,
|
||||
"LocationType": "Slot",
|
||||
"ServiceLabel": "PCIe Slot 1 (1)",
|
||||
},
|
||||
},
|
||||
"PCIeType": "Gen5",
|
||||
"SlotType": "FullLength",
|
||||
},
|
||||
}
|
||||
|
||||
got := parsePCIeDevice(doc, nil)
|
||||
if got.Slot != "PCIe Slot 1 (1)" {
|
||||
t.Fatalf("unexpected slot: %q", got.Slot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePCIeDeviceSlot_EmptyMapFallsBackToID(t *testing.T) {
|
||||
doc := map[string]interface{}{
|
||||
"Id": "NIC42",
|
||||
"Slot": map[string]interface{}{},
|
||||
}
|
||||
|
||||
got := parsePCIeDevice(doc, nil)
|
||||
if got.Slot != "NIC42" {
|
||||
t.Fatalf("unexpected slot fallback: %q", got.Slot)
|
||||
}
|
||||
if got.Slot == "map[]" {
|
||||
t.Fatalf("slot should not stringify empty map")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user