package collector import ( "fmt" "sort" "strings" "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) 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) 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: cloneRawPayloads(rawPayloads), Hardware: &models.HardwareConfig{ BoardInfo: parseBoardInfo(systemDoc), CPUs: parseCPUs(processors), Memory: parseMemory(memory), Storage: storageDevices, Volumes: storageVolumes, 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 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)*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.BDF, dev.SerialNumber, 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 }