redfish: MSI support, fix zero dates, BMC MAC, Assembly FRU, crawler cleanup
- Add MSI CG480-S5063 (H100 SXM5) support:
- collectGPUsFromProcessors: find GPUs via Processors/ProcessorType=GPU,
resolve serials from Chassis/<GpuId>
- looksLikeGPU: skip Description="Display Device" PCIe sidecars
- isVirtualStorageDrive: filter AMI virtual USB drives (0-byte)
- enrichNICMACsFromNetworkDeviceFunctions: pull MACs for MSI NICs
- parseCPUs: filter by ProcessorType, parse Socket, L1/L2/L3 from ProcessorMemory
- parseMemory: Location.PartLocation.ServiceLabel slot fallback
- shouldCrawlPath: block /SubProcessors subtrees
- Fix status_checked_at/status_changed_at serializing as 0001-01-01:
change all StatusCheckedAt/StatusChangedAt fields to *time.Time
- Redfish crawler cleanup:
- Block non-inventory branches: AccountService, CertificateService,
EventService, Registries, SessionService, TaskService, manager config paths,
OperatingConfigs, BootOptions, HostPostCode, Bios/Settings, OEM KVM paths
- Add Assembly to critical endpoints (FRU data)
- Remove BootOptions from priority seeds
- collectBMCMAC: read BMC MAC from Managers/*/EthernetInterfaces
- collectAssemblyFRU: extract FRU serial/part from Chassis/*/Assembly
- Firmware: remove NetworkProtocol noise, fix SecureBoot field,
filter BMCImageN redundant backup slots
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1427,6 +1427,7 @@ func redfishCriticalEndpoints(systemPaths, chassisPaths, managerPaths []string)
|
||||
add(joinPath(p, "/PCIeDevices"))
|
||||
add(joinPath(p, "/Accelerators"))
|
||||
add(joinPath(p, "/Drives"))
|
||||
add(joinPath(p, "/Assembly"))
|
||||
}
|
||||
for _, p := range managerPaths {
|
||||
add(p)
|
||||
@@ -1916,6 +1917,58 @@ func shouldCrawlPath(path string) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Non-inventory top-level service branches.
|
||||
for _, prefix := range []string{
|
||||
"/redfish/v1/AccountService",
|
||||
"/redfish/v1/CertificateService",
|
||||
"/redfish/v1/EventService",
|
||||
"/redfish/v1/Registries",
|
||||
"/redfish/v1/SessionService",
|
||||
"/redfish/v1/TaskService",
|
||||
} {
|
||||
if strings.HasPrefix(normalized, prefix) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Manager-specific configuration paths (not hardware inventory).
|
||||
if strings.Contains(normalized, "/Managers/") {
|
||||
for _, part := range []string{
|
||||
"/FirewallRules",
|
||||
"/KvmService",
|
||||
"/LldpService",
|
||||
"/SecurityService",
|
||||
"/SmtpService",
|
||||
"/SnmpService",
|
||||
"/SyslogService",
|
||||
"/VirtualMedia",
|
||||
"/VncService",
|
||||
"/Certificates",
|
||||
} {
|
||||
if strings.Contains(normalized, part) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
// Per-CPU operating frequency configurations — not hardware inventory.
|
||||
if strings.HasSuffix(normalized, "/OperatingConfigs") {
|
||||
return false
|
||||
}
|
||||
// Per-core/thread sub-processors — inventory is captured at the top processor level.
|
||||
if strings.Contains(normalized, "/SubProcessors") {
|
||||
return false
|
||||
}
|
||||
// Non-inventory system endpoints.
|
||||
for _, part := range []string{
|
||||
"/BootOptions",
|
||||
"/HostPostCode",
|
||||
"/Bios/Settings",
|
||||
"/GetServerAllUSBStatus",
|
||||
"/Oem/Public/KVM",
|
||||
} {
|
||||
if strings.Contains(normalized, part) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
heavyParts := []string{
|
||||
"/JsonSchemas",
|
||||
"/LogServices/",
|
||||
@@ -2435,24 +2488,76 @@ func findFirstNormalizedStringByKeys(doc map[string]interface{}, keys ...string)
|
||||
|
||||
func parseCPUs(docs []map[string]interface{}) []models.CPU {
|
||||
cpus := make([]models.CPU, 0, len(docs))
|
||||
for idx, doc := range docs {
|
||||
socketIdx := 0
|
||||
for _, doc := range docs {
|
||||
// Skip non-CPU processors (GPUs, FPGAs, etc.) that some BMCs list in the
|
||||
// same Processors collection.
|
||||
if pt := strings.TrimSpace(asString(doc["ProcessorType"])); pt != "" &&
|
||||
!strings.EqualFold(pt, "CPU") && !strings.EqualFold(pt, "General") {
|
||||
continue
|
||||
}
|
||||
socket := socketIdx
|
||||
socketIdx++
|
||||
if s := strings.TrimSpace(asString(doc["Socket"])); s != "" {
|
||||
// Parse numeric suffix from labels like "CPU0", "Processor 1", etc.
|
||||
trimmed := strings.TrimLeft(strings.ToUpper(s), "ABCDEFGHIJKLMNOPQRSTUVWXYZ _")
|
||||
if n, err := strconv.Atoi(trimmed); err == nil {
|
||||
socket = n
|
||||
}
|
||||
}
|
||||
l1, l2, l3 := parseCPUCachesFromProcessorMemory(doc)
|
||||
cpus = append(cpus, models.CPU{
|
||||
Socket: idx,
|
||||
Socket: socket,
|
||||
Model: firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
|
||||
Cores: asInt(doc["TotalCores"]),
|
||||
Threads: asInt(doc["TotalThreads"]),
|
||||
FrequencyMHz: asInt(doc["OperatingSpeedMHz"]),
|
||||
MaxFreqMHz: asInt(doc["MaxSpeedMHz"]),
|
||||
SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
|
||||
L1CacheKB: l1,
|
||||
L2CacheKB: l2,
|
||||
L3CacheKB: l3,
|
||||
Status: mapStatus(doc["Status"]),
|
||||
})
|
||||
}
|
||||
return cpus
|
||||
}
|
||||
|
||||
// parseCPUCachesFromProcessorMemory reads L1/L2/L3 cache sizes from the
|
||||
// Redfish ProcessorMemory array (Processor.v1_x spec).
|
||||
func parseCPUCachesFromProcessorMemory(doc map[string]interface{}) (l1, l2, l3 int) {
|
||||
mem, _ := doc["ProcessorMemory"].([]interface{})
|
||||
for _, mAny := range mem {
|
||||
m, ok := mAny.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
capMiB := asInt(m["CapacityMiB"])
|
||||
if capMiB == 0 {
|
||||
continue
|
||||
}
|
||||
capKB := capMiB * 1024
|
||||
switch strings.ToUpper(strings.TrimSpace(asString(m["MemoryType"]))) {
|
||||
case "L1CACHE":
|
||||
l1 = capKB
|
||||
case "L2CACHE":
|
||||
l2 = capKB
|
||||
case "L3CACHE":
|
||||
l3 = capKB
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parseMemory(docs []map[string]interface{}) []models.MemoryDIMM {
|
||||
out := make([]models.MemoryDIMM, 0, len(docs))
|
||||
for _, doc := range docs {
|
||||
slot := firstNonEmpty(asString(doc["DeviceLocator"]), asString(doc["Name"]), asString(doc["Id"]))
|
||||
slot := firstNonEmpty(
|
||||
asString(doc["DeviceLocator"]),
|
||||
redfishLocationLabel(doc["Location"]),
|
||||
asString(doc["Name"]),
|
||||
asString(doc["Id"]),
|
||||
)
|
||||
present := true
|
||||
if strings.EqualFold(asString(doc["Status"]), "Absent") {
|
||||
present = false
|
||||
@@ -3154,6 +3259,12 @@ func hasMergedPlaceholderIndex(indexes map[int]struct{}, idx int) bool {
|
||||
}
|
||||
|
||||
func looksLikeGPU(doc map[string]interface{}, functionDocs []map[string]interface{}) bool {
|
||||
// "Display Device" is how MSI labels H100 secondary display/audio controller
|
||||
// functions — these are not compute GPUs and should be excluded.
|
||||
if strings.EqualFold(strings.TrimSpace(asString(doc["Description"])), "Display Device") {
|
||||
return false
|
||||
}
|
||||
|
||||
deviceType := strings.ToLower(asString(doc["DeviceType"]))
|
||||
if strings.Contains(deviceType, "gpu") || strings.Contains(deviceType, "graphics") || strings.Contains(deviceType, "accelerator") {
|
||||
return true
|
||||
@@ -3192,6 +3303,20 @@ func looksLikeGPU(doc map[string]interface{}, functionDocs []map[string]interfac
|
||||
return false
|
||||
}
|
||||
|
||||
// isVirtualStorageDrive returns true for BMC-virtual drives that should not
|
||||
// appear in hardware inventory (e.g. AMI virtual CD/USB sticks with 0 capacity).
|
||||
func isVirtualStorageDrive(doc map[string]interface{}) bool {
|
||||
if strings.EqualFold(asString(doc["Protocol"]), "USB") && asInt64(doc["CapacityBytes"]) == 0 {
|
||||
return true
|
||||
}
|
||||
mfr := strings.ToUpper(strings.TrimSpace(asString(doc["Manufacturer"])))
|
||||
model := strings.ToUpper(strings.TrimSpace(asString(doc["Model"])))
|
||||
if strings.Contains(mfr, "AMI") && strings.Contains(model, "VIRTUAL") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func looksLikeDrive(doc map[string]interface{}) bool {
|
||||
if asString(doc["MediaType"]) != "" {
|
||||
return true
|
||||
@@ -3951,8 +4076,7 @@ func parseFirmware(system, bios, manager, secureBoot, networkProtocol map[string
|
||||
appendFW("BIOS", asString(system["BiosVersion"]))
|
||||
appendFW("BIOS", asString(bios["Version"]))
|
||||
appendFW("BMC", asString(manager["FirmwareVersion"]))
|
||||
appendFW("SecureBoot", asString(secureBoot["SecureBootCurrentBoot"]))
|
||||
appendFW("NetworkProtocol", asString(networkProtocol["Id"]))
|
||||
appendFW("SecureBoot", asString(secureBoot["SecureBootMode"]))
|
||||
|
||||
return out
|
||||
}
|
||||
@@ -4441,7 +4565,6 @@ func redfishSnapshotPrioritySeeds(systemPaths, chassisPaths, managerPaths []stri
|
||||
add(joinPath(p, "/Memory"))
|
||||
add(joinPath(p, "/EthernetInterfaces"))
|
||||
add(joinPath(p, "/NetworkInterfaces"))
|
||||
add(joinPath(p, "/BootOptions"))
|
||||
add(joinPath(p, "/PCIeDevices"))
|
||||
add(joinPath(p, "/PCIeFunctions"))
|
||||
add(joinPath(p, "/Accelerators"))
|
||||
|
||||
Reference in New Issue
Block a user