Implement the full architectural plan: unified ingest.Service entry point for archive and Redfish payloads, modular redfishprofile package with composable profiles (generic, ami-family, msi, supermicro, dell, hgx-topology), score-based profile matching with fallback expansion mode, and profile-driven acquisition/analysis plans. Vendor-specific logic moved out of common executors and into profile hooks. GPU chassis lookup strategies and known storage recovery collections (IntelVROC/HA-RAID/MRVL) now live in ResolvedAnalysisPlan, populated by profiles at analysis time. Replay helpers read from the plan; no hardcoded path lists remain in generic code. Also splits redfish_replay.go into domain modules (gpu, storage, inventory, fru, profiles) and adds full fixture/matcher/directive test coverage including Dell, AMI, unknown-vendor fallback, and deterministic ordering. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
160 lines
4.4 KiB
Go
160 lines
4.4 KiB
Go
package collector
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"git.mchus.pro/mchus/logpile/internal/models"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
// collectAssemblyFRU reads Chassis/*/Assembly documents and returns FRU entries
|
|
// for subcomponents (backplanes, PSUs, DIMMs, etc.) that carry meaningful
|
|
// serial or part numbers. Entries already present in dedicated collections
|
|
// (PSUs, DIMMs) are included here as well so that all FRU data is available
|
|
// in one place; deduplication by serial is performed.
|
|
func (r redfishSnapshotReader) collectAssemblyFRU(chassisPaths []string) []models.FRUInfo {
|
|
seen := make(map[string]struct{})
|
|
var out []models.FRUInfo
|
|
|
|
add := func(fru models.FRUInfo) {
|
|
key := strings.ToUpper(strings.TrimSpace(fru.SerialNumber))
|
|
if key == "" {
|
|
key = strings.ToUpper(strings.TrimSpace(fru.Description + "|" + fru.PartNumber))
|
|
}
|
|
if key == "" || key == "|" {
|
|
return
|
|
}
|
|
if _, ok := seen[key]; ok {
|
|
return
|
|
}
|
|
seen[key] = struct{}{}
|
|
out = append(out, fru)
|
|
}
|
|
|
|
for _, chassisPath := range chassisPaths {
|
|
doc, err := r.getJSON(joinPath(chassisPath, "/Assembly"))
|
|
if err != nil || len(doc) == 0 {
|
|
continue
|
|
}
|
|
assemblies, _ := doc["Assemblies"].([]interface{})
|
|
for _, aAny := range assemblies {
|
|
a, ok := aAny.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
name := strings.TrimSpace(firstNonEmpty(asString(a["Name"]), asString(a["Description"])))
|
|
model := strings.TrimSpace(asString(a["Model"]))
|
|
partNumber := strings.TrimSpace(asString(a["PartNumber"]))
|
|
serial := extractAssemblySerial(a)
|
|
|
|
if serial == "" && partNumber == "" {
|
|
continue
|
|
}
|
|
add(models.FRUInfo{
|
|
Description: name,
|
|
ProductName: model,
|
|
SerialNumber: serial,
|
|
PartNumber: partNumber,
|
|
})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// extractAssemblySerial tries to find a serial number in an Assembly entry.
|
|
// Standard Redfish Assembly has no top-level SerialNumber; vendors put it in Oem.
|
|
func extractAssemblySerial(a map[string]interface{}) string {
|
|
if s := strings.TrimSpace(asString(a["SerialNumber"])); s != "" {
|
|
return s
|
|
}
|
|
oem, _ := a["Oem"].(map[string]interface{})
|
|
for _, v := range oem {
|
|
subtree, ok := v.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, v2 := range subtree {
|
|
node, ok := v2.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
if s := strings.TrimSpace(asString(node["SerialNumber"])); s != "" {
|
|
return s
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|