575 lines
17 KiB
Go
575 lines
17 KiB
Go
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
|
|
}
|