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 }