package collector import ( "encoding/json" "fmt" "sort" "strings" "time" "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") primaryChassis := firstPathOrDefault(chassisPaths, "/redfish/v1/Chassis/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) } chassisDoc, _ := r.getJSON(primaryChassis) biosDoc, _ := r.getJSON(joinPath(primarySystem, "/Bios")) secureBootDoc, _ := r.getJSON(joinPath(primarySystem, "/SecureBoot")) systemFRUDoc, _ := r.getJSON(joinPath(primarySystem, "/Oem/Public/FRU")) chassisFRUDoc, _ := r.getJSON(joinPath(primaryChassis, "/Oem/Public/FRU")) fruDoc := systemFRUDoc if len(fruDoc) == 0 { fruDoc = chassisFRUDoc } boardFallbackDocs := r.collectBoardFallbackDocs(systemPaths, chassisPaths) 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) storageVolumes := r.collectStorageVolumes(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) r.enrichNICsFromNetworkInterfaces(&nics, systemPaths) thresholdSensors := r.collectThresholdSensors(chassisPaths) discreteEvents := r.collectDiscreteSensorEvents(chassisPaths) healthEvents := r.collectHealthSummaryEvents(chassisPaths) managerDoc, _ := r.getJSON(primaryManager) networkProtocolDoc, _ := r.getJSON(joinPath(primaryManager, "/NetworkProtocol")) firmware := parseFirmware(systemDoc, biosDoc, managerDoc, secureBootDoc, networkProtocolDoc) firmware = dedupeFirmwareInfo(append(firmware, r.collectFirmwareInventory()...)) boardInfo := parseBoardInfoWithFallback(systemDoc, chassisDoc, fruDoc) applyBoardInfoFallbackFromDocs(&boardInfo, boardFallbackDocs) result := &models.AnalysisResult{ Events: append(append(make([]models.Event, 0, len(discreteEvents)+len(healthEvents)+1), healthEvents...), discreteEvents...), FRU: make([]models.FRUInfo, 0), Sensors: thresholdSensors, RawPayloads: cloneRawPayloads(rawPayloads), Hardware: &models.HardwareConfig{ BoardInfo: boardInfo, CPUs: parseCPUs(processors), Memory: parseMemory(memory), Storage: storageDevices, Volumes: storageVolumes, PCIeDevices: pcieDevices, GPUs: gpus, PowerSupply: psus, NetworkAdapters: nics, Firmware: firmware, }, } appendMissingServerModelWarning(result, systemDoc, joinPath(primarySystem, "/Oem/Public/FRU"), joinPath(primaryChassis, "/Oem/Public/FRU")) return result, nil } func appendMissingServerModelWarning(result *models.AnalysisResult, systemDoc map[string]interface{}, systemFRUPath, chassisFRUPath string) { if result == nil || result.Hardware == nil { return } if strings.TrimSpace(result.Hardware.BoardInfo.ProductName) != "" { return } reasons := make([]string, 0, 3) systemModelRaw := strings.TrimSpace(asString(systemDoc["Model"])) if systemModelRaw != "" && normalizeRedfishIdentityField(systemModelRaw) == "" { reasons = append(reasons, fmt.Sprintf("system model is placeholder: %q", systemModelRaw)) } errs := redfishFetchErrorsFromRawPayloads(result.RawPayloads) if msg := errs[normalizeRedfishPath(systemFRUPath)]; strings.TrimSpace(msg) != "" { reasons = append(reasons, fmt.Sprintf("%s unavailable: %s", systemFRUPath, msg)) } if msg := errs[normalizeRedfishPath(chassisFRUPath)]; strings.TrimSpace(msg) != "" { reasons = append(reasons, fmt.Sprintf("%s unavailable: %s", chassisFRUPath, msg)) } if len(reasons) == 0 { reasons = append(reasons, "no non-placeholder ProductName/Model found in collected Redfish documents") } result.Events = append(result.Events, models.Event{ Timestamp: time.Now(), Source: "Redfish", EventType: "Collection Warning", Severity: models.SeverityWarning, Description: "Server model is missing in collected Redfish data", RawData: strings.Join(reasons, "; "), }) } func redfishFetchErrorsFromRawPayloads(rawPayloads map[string]any) map[string]string { out := make(map[string]string) if len(rawPayloads) == 0 { return out } raw, ok := rawPayloads["redfish_fetch_errors"] if !ok { return out } switch list := raw.(type) { case []map[string]interface{}: return redfishFetchErrorListToMap(list) case []interface{}: normalized := make([]map[string]interface{}, 0, len(list)) for _, item := range list { m, ok := item.(map[string]interface{}) if !ok { continue } normalized = append(normalized, m) } return redfishFetchErrorListToMap(normalized) default: return out } } func (r redfishSnapshotReader) collectFirmwareInventory() []models.FirmwareInfo { docs, err := r.getCollectionMembers("/redfish/v1/UpdateService/FirmwareInventory") if err != nil || len(docs) == 0 { return nil } out := make([]models.FirmwareInfo, 0, len(docs)) for _, doc := range docs { version := firstNonEmpty( asString(doc["Version"]), asString(doc["FirmwareVersion"]), asString(doc["SoftwareVersion"]), ) if strings.TrimSpace(version) == "" { continue } name := firstNonEmpty( asString(doc["DeviceName"]), asString(doc["Name"]), asString(doc["Id"]), ) if strings.TrimSpace(name) == "" { continue } out = append(out, models.FirmwareInfo{DeviceName: name, Version: version}) } return out } func dedupeFirmwareInfo(items []models.FirmwareInfo) []models.FirmwareInfo { seen := make(map[string]struct{}, len(items)) out := make([]models.FirmwareInfo, 0, len(items)) for _, fw := range items { name := strings.TrimSpace(fw.DeviceName) ver := strings.TrimSpace(fw.Version) if name == "" || ver == "" { continue } key := strings.ToLower(name + "|" + ver) if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} out = append(out, models.FirmwareInfo{DeviceName: name, Version: ver}) } return out } func (r redfishSnapshotReader) collectThresholdSensors(chassisPaths []string) []models.SensorReading { out := make([]models.SensorReading, 0) seen := make(map[string]struct{}) for _, chassisPath := range chassisPaths { docs, err := r.getCollectionMembers(joinPath(chassisPath, "/ThresholdSensors")) if err != nil || len(docs) == 0 { continue } for _, doc := range docs { sensor, ok := parseThresholdSensor(doc) if !ok { continue } key := strings.ToLower(strings.TrimSpace(sensor.Name)) if key == "" { key = strings.ToLower(strings.TrimSpace(sensor.Type) + "|" + strings.TrimSpace(sensor.RawValue)) } if _, exists := seen[key]; exists { continue } seen[key] = struct{}{} out = append(out, sensor) } } return out } func parseThresholdSensor(doc map[string]interface{}) (models.SensorReading, bool) { if len(doc) == 0 { return models.SensorReading{}, false } name := firstNonEmpty(asString(doc["Name"]), asString(doc["Id"])) status := mapStatus(doc["Status"]) if status == "" { status = firstNonEmpty(asString(doc["Health"]), asString(doc["State"])) } reading := 0.0 unit := "" rawValue := "" switch { case asString(doc["ReadingCelsius"]) != "": reading = asFloat(doc["ReadingCelsius"]) unit = "C" rawValue = asString(doc["ReadingCelsius"]) case asString(doc["ReadingVolts"]) != "": reading = asFloat(doc["ReadingVolts"]) unit = "V" rawValue = asString(doc["ReadingVolts"]) case asString(doc["ReadingAmps"]) != "": reading = asFloat(doc["ReadingAmps"]) unit = "A" rawValue = asString(doc["ReadingAmps"]) case asString(doc["ReadingWatts"]) != "": reading = asFloat(doc["ReadingWatts"]) unit = "W" rawValue = asString(doc["ReadingWatts"]) case asString(doc["Reading"]) != "": reading = asFloat(doc["Reading"]) unit = asString(doc["ReadingUnits"]) rawValue = asString(doc["Reading"]) } if name == "" && rawValue == "" && status == "" { return models.SensorReading{}, false } return models.SensorReading{ Name: firstNonEmpty(name, "threshold-sensor"), Type: firstNonEmpty(asString(doc["ReadingType"]), asString(doc["SensorType"]), "threshold"), Value: reading, Unit: unit, RawValue: rawValue, Status: status, }, true } func (r redfishSnapshotReader) collectDiscreteSensorEvents(chassisPaths []string) []models.Event { out := make([]models.Event, 0) for _, chassisPath := range chassisPaths { docs, err := r.getCollectionMembers(joinPath(chassisPath, "/DiscreteSensors")) if err != nil || len(docs) == 0 { continue } for _, doc := range docs { name := firstNonEmpty(asString(doc["Name"]), asString(doc["Id"])) status := mapStatus(doc["Status"]) if status == "" { status = firstNonEmpty(asString(doc["Health"]), asString(doc["State"])) } if name == "" || status == "" { continue } normalized := strings.ToLower(strings.TrimSpace(status)) if normalized == "ok" || normalized == "enabled" || normalized == "normal" || normalized == "present" { continue } out = append(out, models.Event{ Timestamp: time.Now(), Source: "Redfish", SensorName: name, EventType: "Discrete Sensor Status", Severity: models.SeverityWarning, Description: fmt.Sprintf("%s reports %s", name, status), RawData: firstNonEmpty(asString(doc["Description"]), status), }) } } return out } func (r redfishSnapshotReader) collectHealthSummaryEvents(chassisPaths []string) []models.Event { out := make([]models.Event, 0) for _, chassisPath := range chassisPaths { doc, err := r.getJSON(joinPath(chassisPath, "/HealthSummary")) if err != nil || len(doc) == 0 { continue } health := firstNonEmpty( mapStatus(doc["Status"]), asString(doc["Health"]), asString(doc["HealthRollup"]), findFirstNormalizedStringByKeys(doc, "Health", "HealthRollup", "OverallHealth"), ) if health == "" { continue } if strings.EqualFold(health, "OK") || strings.EqualFold(health, "Normal") { continue } raw, _ := json.Marshal(doc) out = append(out, models.Event{ Timestamp: time.Now(), Source: "Redfish", EventType: "Health Summary", Severity: models.SeverityWarning, Description: fmt.Sprintf("Chassis health summary reports %s", health), RawData: string(raw), }) } return out } func (r redfishSnapshotReader) enrichNICsFromNetworkInterfaces(nics *[]models.NetworkAdapter, systemPaths []string) { if nics == nil { return } bySlot := make(map[string]int, len(*nics)) for i, nic := range *nics { bySlot[strings.ToLower(strings.TrimSpace(nic.Slot))] = i } for _, systemPath := range systemPaths { ifaces, err := r.getCollectionMembers(joinPath(systemPath, "/NetworkInterfaces")) if err != nil || len(ifaces) == 0 { continue } for _, iface := range ifaces { slot := firstNonEmpty(asString(iface["Id"]), asString(iface["Name"])) if strings.TrimSpace(slot) == "" { continue } idx, ok := bySlot[strings.ToLower(strings.TrimSpace(slot))] if !ok { *nics = append(*nics, models.NetworkAdapter{ Slot: slot, Present: true, Model: firstNonEmpty(asString(iface["Model"]), asString(iface["Name"])), Status: mapStatus(iface["Status"]), }) idx = len(*nics) - 1 bySlot[strings.ToLower(strings.TrimSpace(slot))] = idx } portsPath := redfishLinkedPath(iface, "NetworkPorts") if portsPath == "" { continue } portDocs, err := r.getCollectionMembers(portsPath) if err != nil || len(portDocs) == 0 { continue } macs := append([]string{}, (*nics)[idx].MACAddresses...) for _, p := range portDocs { macs = append(macs, collectNetworkPortMACs(p)...) } (*nics)[idx].MACAddresses = dedupeStrings(macs) if (*nics)[idx].PortCount == 0 { (*nics)[idx].PortCount = len(portDocs) } } } } func collectNetworkPortMACs(doc map[string]interface{}) []string { if len(doc) == 0 { return nil } out := make([]string, 0, 4) if list, ok := doc["AssociatedNetworkAddresses"].([]interface{}); ok { for _, item := range list { if s := strings.TrimSpace(asString(item)); s != "" { out = append(out, s) } } } for _, key := range []string{"MACAddress", "PermanentMACAddress", "CurrentMACAddress"} { if s := strings.TrimSpace(asString(doc[key])); s != "" { out = append(out, s) } } return out } func dedupeStrings(items []string) []string { seen := make(map[string]struct{}, len(items)) out := make([]string, 0, len(items)) for _, item := range items { s := strings.TrimSpace(item) if s == "" { continue } key := strings.ToLower(s) if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} out = append(out, s) } return out } func (r redfishSnapshotReader) collectBoardFallbackDocs(systemPaths, chassisPaths []string) []map[string]interface{} { out := make([]map[string]interface{}, 0) for _, chassisPath := range chassisPaths { for _, suffix := range []string{"/Boards", "/Backplanes"} { path := joinPath(chassisPath, suffix) if docs, err := r.getCollectionMembers(path); err == nil && len(docs) > 0 { out = append(out, docs...) continue } if doc, err := r.getJSON(path); err == nil && len(doc) > 0 { out = append(out, doc) } } } for _, path := range append(append([]string{}, systemPaths...), chassisPaths...) { for _, suffix := range []string{"/Oem/Public", "/Oem/Public/ThermalConfig", "/ThermalConfig"} { docPath := joinPath(path, suffix) if doc, err := r.getJSON(docPath); err == nil && len(doc) > 0 { out = append(out, doc) } } } return out } func applyBoardInfoFallbackFromDocs(board *models.BoardInfo, docs []map[string]interface{}) { if board == nil || len(docs) == 0 { return } for _, doc := range docs { candidate := parseBoardInfoFromFRUDoc(doc) if !isLikelyServerProductName(candidate.ProductName) { continue } if board.Manufacturer == "" { board.Manufacturer = candidate.Manufacturer } if board.ProductName == "" { board.ProductName = candidate.ProductName } if board.SerialNumber == "" { board.SerialNumber = candidate.SerialNumber } if board.PartNumber == "" { board.PartNumber = candidate.PartNumber } if board.Manufacturer != "" && board.ProductName != "" && board.SerialNumber != "" && board.PartNumber != "" { return } } } func isLikelyServerProductName(v string) bool { v = strings.TrimSpace(v) if v == "" { return false } n := strings.ToUpper(v) if strings.Contains(n, "NULL") { return false } componentTokens := []string{ "DIMM", "DDR", "NVME", "SSD", "HDD", "GPU", "NIC", "RAID", "PSU", "FAN", "BACKPLANE", "FRU", } for _, token := range componentTokens { if strings.Contains(n, strings.ToUpper(token)) { return false } } return true } 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 r.fallbackCollectionMembers(collectionPath, err) } refs, ok := collection["Members"].([]interface{}) if !ok || len(refs) == 0 { return r.fallbackCollectionMembers(collectionPath, 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) } if len(out) == 0 { return r.fallbackCollectionMembers(collectionPath, nil) } return out, nil } func (r redfishSnapshotReader) fallbackCollectionMembers(collectionPath string, originalErr error) ([]map[string]interface{}, error) { prefix := strings.TrimSuffix(normalizeRedfishPath(collectionPath), "/") + "/" if prefix == "/" { if originalErr != nil { return nil, originalErr } return []map[string]interface{}{}, nil } paths := make([]string, 0) for key := range r.tree { p := normalizeRedfishPath(key) if !strings.HasPrefix(p, prefix) { continue } rest := strings.TrimPrefix(p, prefix) if rest == "" || strings.Contains(rest, "/") { continue } paths = append(paths, p) } if len(paths) == 0 { if originalErr != nil { return nil, originalErr } return []map[string]interface{}{}, nil } sort.Strings(paths) out := make([]map[string]interface{}, 0, len(paths)) for _, p := range paths { doc, err := r.getJSON(p) if err != nil { continue } out = append(out, doc) } return out, nil } func cloneRawPayloads(src map[string]any) map[string]any { if len(src) == 0 { return nil } dst := make(map[string]any, len(src)) for k, v := range src { dst[k] = v } return dst } 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)) } if len(driveDocs) == 0 { for _, driveDoc := range r.probeDirectDiskBayChildren(driveCollectionPath) { 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)) } for _, enclosurePath := range redfishLinkRefs(member, "Links", "Enclosures") { driveDocs, err := r.getCollectionMembers(joinPath(enclosurePath, "/Drives")) if err == nil { for _, driveDoc := range driveDocs { if looksLikeDrive(driveDoc) { out = append(out, parseDrive(driveDoc)) } } if len(driveDocs) == 0 { for _, driveDoc := range r.probeDirectDiskBayChildren(joinPath(enclosurePath, "/Drives")) { out = append(out, parseDrive(driveDoc)) } } } } } for _, driveDoc := range r.collectKnownStorageMembers(systemPath, []string{ "/Storage/IntelVROC/Drives", "/Storage/IntelVROC/Controllers/1/Drives", }) { if looksLikeDrive(driveDoc) { out = append(out, parseDrive(driveDoc)) } } 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)) } } for _, chassisPath := range chassisPaths { if !isSupermicroNVMeBackplanePath(chassisPath) { continue } for _, driveDoc := range r.probeSupermicroNVMeDiskBays(chassisPath) { if !looksLikeDrive(driveDoc) { continue } out = append(out, parseDrive(driveDoc)) } } return dedupeStorage(out) } func (r redfishSnapshotReader) collectStorageVolumes(systemPath string) []models.StorageVolume { var out []models.StorageVolume storageMembers, _ := r.getCollectionMembers(joinPath(systemPath, "/Storage")) for _, member := range storageMembers { controller := firstNonEmpty(asString(member["Id"]), asString(member["Name"])) volumeCollectionPath := redfishLinkedPath(member, "Volumes") if volumeCollectionPath == "" { continue } volumeDocs, err := r.getCollectionMembers(volumeCollectionPath) if err != nil { continue } for _, volDoc := range volumeDocs { if looksLikeVolume(volDoc) { out = append(out, parseStorageVolume(volDoc, controller)) } } } for _, volDoc := range r.collectKnownStorageMembers(systemPath, []string{ "/Storage/IntelVROC/Volumes", "/Storage/HA-RAID/Volumes", "/Storage/MRVL.HA-RAID/Volumes", }) { if looksLikeVolume(volDoc) { out = append(out, parseStorageVolume(volDoc, storageControllerFromPath(asString(volDoc["@odata.id"])))) } } return dedupeStorageVolumes(out) } func (r redfishSnapshotReader) collectKnownStorageMembers(systemPath string, relativeCollections []string) []map[string]interface{} { var out []map[string]interface{} for _, rel := range relativeCollections { docs, err := r.getCollectionMembers(joinPath(systemPath, rel)) if err != nil || len(docs) == 0 { continue } out = append(out, docs...) } return out } func (r redfishSnapshotReader) probeSupermicroNVMeDiskBays(backplanePath string) []map[string]interface{} { return r.probeDirectDiskBayChildren(joinPath(backplanePath, "/Drives")) } func (r redfishSnapshotReader) probeDirectDiskBayChildren(drivesCollectionPath string) []map[string]interface{} { var out []map[string]interface{} for _, path := range directDiskBayCandidates(drivesCollectionPath) { doc, err := r.getJSON(path) if err != nil || !looksLikeDrive(doc) { continue } out = append(out, doc) } return 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) for _, pciePath := range networkAdapterPCIeDevicePaths(doc) { pcieDoc, err := r.getJSON(pciePath) if err != nil { continue } functionDocs := r.getLinkedPCIeFunctions(pcieDoc) enrichNICFromPCIe(&nic, pcieDoc, functionDocs) } 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 memberDocs, err := r.getCollectionMembers(joinPath(chassisPath, "/PowerSubsystem/PowerSupplies")); err == nil && len(memberDocs) > 0 { for _, doc := range memberDocs { idx = appendPSU(&out, seen, parsePSU(doc, idx), idx) } continue } 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 } idx = appendPSU(&out, seen, parsePSU(doc, idx), idx) } } } } return out } func (r redfishSnapshotReader) collectGPUs(systemPaths, chassisPaths []string) []models.GPU { collections := make([]string, 0, len(systemPaths)*3+len(chassisPaths)*2) for _, systemPath := range systemPaths { collections = append(collections, joinPath(systemPath, "/PCIeDevices")) collections = append(collections, joinPath(systemPath, "/Accelerators")) collections = append(collections, joinPath(systemPath, "/GraphicsControllers")) } for _, chassisPath := range chassisPaths { collections = append(collections, joinPath(chassisPath, "/PCIeDevices")) collections = append(collections, joinPath(chassisPath, "/Accelerators")) } 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++ if shouldSkipGenericGPUDuplicate(out, gpu) { continue } key := gpuDedupKey(gpu) if key == "" { continue } if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} out = append(out, gpu) } } return dropModelOnlyGPUPlaceholders(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 := pcieDeviceDedupKey(dev) 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 := pcieDeviceDedupKey(dev) 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 }