Add raw export reanalyze flow for Redfish snapshots
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user