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"))
|
||||
|
||||
@@ -72,6 +72,7 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
||||
psus := r.collectPSUs(chassisPaths)
|
||||
pcieDevices := r.collectPCIeDevices(systemPaths, chassisPaths)
|
||||
gpus := r.collectGPUs(systemPaths, chassisPaths)
|
||||
gpus = r.collectGPUsFromProcessors(systemPaths, chassisPaths, gpus)
|
||||
nics := r.collectNICs(chassisPaths)
|
||||
r.enrichNICsFromNetworkInterfaces(&nics, systemPaths)
|
||||
thresholdSensors := r.collectThresholdSensors(chassisPaths)
|
||||
@@ -86,13 +87,15 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
||||
firmware = dedupeFirmwareInfo(append(firmware, r.collectFirmwareInventory()...))
|
||||
boardInfo := parseBoardInfoWithFallback(systemDoc, chassisDoc, fruDoc)
|
||||
applyBoardInfoFallbackFromDocs(&boardInfo, boardFallbackDocs)
|
||||
boardInfo.BMCMACAddress = r.collectBMCMAC(managerPaths)
|
||||
assemblyFRU := r.collectAssemblyFRU(chassisPaths)
|
||||
collectedAt, sourceTimezone := inferRedfishCollectionTime(managerDoc, rawPayloads)
|
||||
|
||||
result := &models.AnalysisResult{
|
||||
CollectedAt: collectedAt,
|
||||
SourceTimezone: sourceTimezone,
|
||||
Events: append(append(append(make([]models.Event, 0, len(discreteEvents)+len(healthEvents)+len(driveFetchWarningEvents)+1), healthEvents...), discreteEvents...), driveFetchWarningEvents...),
|
||||
FRU: make([]models.FRUInfo, 0),
|
||||
FRU: assemblyFRU,
|
||||
Sensors: dedupeSensorReadings(append(append(thresholdSensors, thermalSensors...), powerSensors...)),
|
||||
RawPayloads: cloneRawPayloads(rawPayloads),
|
||||
Hardware: &models.HardwareConfig{
|
||||
@@ -274,7 +277,12 @@ func (r redfishSnapshotReader) collectFirmwareInventory() []models.FirmwareInfo
|
||||
asString(doc["Name"]),
|
||||
asString(doc["Id"]),
|
||||
)
|
||||
if strings.TrimSpace(name) == "" {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
// BMCImageN entries are redundant backup slot labels; skip them.
|
||||
if strings.HasPrefix(strings.ToLower(name), "bmcimage") {
|
||||
continue
|
||||
}
|
||||
out = append(out, models.FirmwareInfo{DeviceName: name, Version: version})
|
||||
@@ -977,7 +985,9 @@ func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storag
|
||||
driveDocs, err := r.getCollectionMembers(driveCollectionPath)
|
||||
if err == nil {
|
||||
for _, driveDoc := range driveDocs {
|
||||
out = append(out, parseDrive(driveDoc))
|
||||
if !isVirtualStorageDrive(driveDoc) {
|
||||
out = append(out, parseDrive(driveDoc))
|
||||
}
|
||||
}
|
||||
if len(driveDocs) == 0 {
|
||||
for _, driveDoc := range r.probeDirectDiskBayChildren(driveCollectionPath) {
|
||||
@@ -1002,7 +1012,9 @@ func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storag
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, parseDrive(driveDoc))
|
||||
if !isVirtualStorageDrive(driveDoc) {
|
||||
out = append(out, parseDrive(driveDoc))
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -1154,6 +1166,10 @@ func (r redfishSnapshotReader) collectNICs(chassisPaths []string) []models.Netwo
|
||||
functionDocs := r.getLinkedPCIeFunctions(pcieDoc)
|
||||
enrichNICFromPCIe(&nic, pcieDoc, functionDocs)
|
||||
}
|
||||
// Collect MACs from NetworkDeviceFunctions when not found via PCIe path.
|
||||
if len(nic.MACAddresses) == 0 {
|
||||
r.enrichNICMACsFromNetworkDeviceFunctions(&nic, doc)
|
||||
}
|
||||
nics = append(nics, nic)
|
||||
}
|
||||
}
|
||||
@@ -1268,3 +1284,237 @@ func stringsTrimTrailingSlash(s string) string {
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// collectBMCMAC returns the MAC address of the first active BMC management
|
||||
// interface found in Managers/*/EthernetInterfaces. Returns empty string if
|
||||
// no MAC is available.
|
||||
func (r redfishSnapshotReader) collectBMCMAC(managerPaths []string) string {
|
||||
for _, managerPath := range managerPaths {
|
||||
members, err := r.getCollectionMembers(joinPath(managerPath, "/EthernetInterfaces"))
|
||||
if err != nil || len(members) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, doc := range members {
|
||||
mac := strings.TrimSpace(firstNonEmpty(
|
||||
asString(doc["PermanentMACAddress"]),
|
||||
asString(doc["MACAddress"]),
|
||||
))
|
||||
if mac == "" || strings.EqualFold(mac, "00:00:00:00:00:00") {
|
||||
continue
|
||||
}
|
||||
return strings.ToUpper(mac)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Some implementations expose it at top level.
|
||||
if s := strings.TrimSpace(asString(a["SerialNumber"])); s != "" {
|
||||
return s
|
||||
}
|
||||
// Dig into Oem for vendor-specific structures (e.g. Huawei COMMONb).
|
||||
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 ""
|
||||
}
|
||||
|
||||
// enrichNICMACsFromNetworkDeviceFunctions reads the NetworkDeviceFunctions
|
||||
// collection linked from a NetworkAdapter document and populates the NIC's
|
||||
// MACAddresses from each function's Ethernet.PermanentMACAddress / MACAddress.
|
||||
// Called when PCIe-path enrichment does not produce any MACs.
|
||||
func (r redfishSnapshotReader) enrichNICMACsFromNetworkDeviceFunctions(nic *models.NetworkAdapter, adapterDoc map[string]interface{}) {
|
||||
ndfCol, ok := adapterDoc["NetworkDeviceFunctions"].(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
colPath := asString(ndfCol["@odata.id"])
|
||||
if colPath == "" {
|
||||
return
|
||||
}
|
||||
funcDocs, err := r.getCollectionMembers(colPath)
|
||||
if err != nil || len(funcDocs) == 0 {
|
||||
return
|
||||
}
|
||||
for _, fn := range funcDocs {
|
||||
eth, _ := fn["Ethernet"].(map[string]interface{})
|
||||
if eth == nil {
|
||||
continue
|
||||
}
|
||||
mac := strings.TrimSpace(firstNonEmpty(
|
||||
asString(eth["PermanentMACAddress"]),
|
||||
asString(eth["MACAddress"]),
|
||||
))
|
||||
if mac == "" {
|
||||
continue
|
||||
}
|
||||
nic.MACAddresses = dedupeStrings(append(nic.MACAddresses, strings.ToUpper(mac)))
|
||||
}
|
||||
if len(funcDocs) > 0 && nic.PortCount == 0 {
|
||||
nic.PortCount = sanitizeNetworkPortCount(len(funcDocs))
|
||||
}
|
||||
}
|
||||
|
||||
// collectGPUsFromProcessors finds GPUs that some BMCs (e.g. MSI) expose as
|
||||
// Processor entries with ProcessorType=GPU rather than as PCIe devices.
|
||||
// It supplements the existing gpus slice (already found via PCIe path),
|
||||
// skipping entries already present by UUID or SerialNumber.
|
||||
// Serial numbers are looked up from Chassis members named after each GPU Id.
|
||||
func (r redfishSnapshotReader) collectGPUsFromProcessors(systemPaths, chassisPaths []string, existing []models.GPU) []models.GPU {
|
||||
// Build a lookup: chassis member ID → chassis doc (for serial numbers).
|
||||
chassisByID := make(map[string]map[string]interface{})
|
||||
for _, cp := range chassisPaths {
|
||||
doc, err := r.getJSON(cp)
|
||||
if err != nil || len(doc) == 0 {
|
||||
continue
|
||||
}
|
||||
id := strings.TrimSpace(asString(doc["Id"]))
|
||||
if id != "" {
|
||||
chassisByID[strings.ToUpper(id)] = doc
|
||||
}
|
||||
}
|
||||
|
||||
// Build dedup sets from existing GPUs.
|
||||
seenUUID := make(map[string]struct{})
|
||||
seenSerial := make(map[string]struct{})
|
||||
for _, g := range existing {
|
||||
if u := strings.ToUpper(strings.TrimSpace(g.UUID)); u != "" {
|
||||
seenUUID[u] = struct{}{}
|
||||
}
|
||||
if s := strings.ToUpper(strings.TrimSpace(g.SerialNumber)); s != "" {
|
||||
seenSerial[s] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
out := append([]models.GPU{}, existing...)
|
||||
idx := len(existing) + 1
|
||||
|
||||
for _, systemPath := range systemPaths {
|
||||
procDocs, err := r.getCollectionMembers(joinPath(systemPath, "/Processors"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, doc := range procDocs {
|
||||
if !strings.EqualFold(strings.TrimSpace(asString(doc["ProcessorType"])), "GPU") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Resolve serial from Chassis/<Id>.
|
||||
gpuID := strings.TrimSpace(asString(doc["Id"]))
|
||||
serial := ""
|
||||
if chassisDoc, ok := chassisByID[strings.ToUpper(gpuID)]; ok {
|
||||
serial = strings.TrimSpace(asString(chassisDoc["SerialNumber"]))
|
||||
}
|
||||
|
||||
uuid := strings.TrimSpace(asString(doc["UUID"]))
|
||||
uuidKey := strings.ToUpper(uuid)
|
||||
serialKey := strings.ToUpper(serial)
|
||||
|
||||
if uuidKey != "" {
|
||||
if _, dup := seenUUID[uuidKey]; dup {
|
||||
continue
|
||||
}
|
||||
seenUUID[uuidKey] = struct{}{}
|
||||
}
|
||||
if serialKey != "" {
|
||||
if _, dup := seenSerial[serialKey]; dup {
|
||||
continue
|
||||
}
|
||||
seenSerial[serialKey] = struct{}{}
|
||||
}
|
||||
|
||||
slotLabel := firstNonEmpty(
|
||||
redfishLocationLabel(doc["Location"]),
|
||||
redfishLocationLabel(doc["PhysicalLocation"]),
|
||||
)
|
||||
if slotLabel == "" && gpuID != "" {
|
||||
slotLabel = gpuID
|
||||
}
|
||||
if slotLabel == "" {
|
||||
slotLabel = fmt.Sprintf("GPU%d", idx)
|
||||
}
|
||||
|
||||
out = append(out, models.GPU{
|
||||
Slot: slotLabel,
|
||||
Model: firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
|
||||
Manufacturer: asString(doc["Manufacturer"]),
|
||||
PartNumber: asString(doc["PartNumber"]),
|
||||
SerialNumber: serial,
|
||||
UUID: uuid,
|
||||
Status: mapStatus(doc["Status"]),
|
||||
})
|
||||
idx++
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -385,10 +385,10 @@ func mergeCanonicalDevice(primary, secondary models.HardwareDevice) models.Hardw
|
||||
fillFloat(&primary.InputVoltage, secondary.InputVoltage)
|
||||
fillInt(&primary.TemperatureC, secondary.TemperatureC)
|
||||
fillString(&primary.Status, secondary.Status)
|
||||
if primary.StatusCheckedAt.IsZero() && !secondary.StatusCheckedAt.IsZero() {
|
||||
if primary.StatusCheckedAt == nil && secondary.StatusCheckedAt != nil {
|
||||
primary.StatusCheckedAt = secondary.StatusCheckedAt
|
||||
}
|
||||
if primary.StatusChangedAt.IsZero() && !secondary.StatusChangedAt.IsZero() {
|
||||
if primary.StatusChangedAt == nil && secondary.StatusChangedAt != nil {
|
||||
primary.StatusChangedAt = secondary.StatusChangedAt
|
||||
}
|
||||
if primary.StatusAtCollect == nil && secondary.StatusAtCollect != nil {
|
||||
@@ -1130,8 +1130,8 @@ type convertedStatusMeta struct {
|
||||
|
||||
func buildStatusMeta(
|
||||
currentStatus string,
|
||||
checkedAt time.Time,
|
||||
changedAt time.Time,
|
||||
checkedAt *time.Time,
|
||||
changedAt *time.Time,
|
||||
statusAtCollection *models.StatusAtCollection,
|
||||
history []models.StatusHistoryEntry,
|
||||
errorDescription string,
|
||||
@@ -1145,7 +1145,7 @@ func buildStatusMeta(
|
||||
|
||||
convertedHistory := make([]ReanimatorStatusHistoryEntry, 0, len(history))
|
||||
for _, h := range history {
|
||||
changed := formatOptionalRFC3339(h.ChangedAt)
|
||||
changed := formatOptionalRFC3339(&h.ChangedAt)
|
||||
if changed == "" {
|
||||
continue
|
||||
}
|
||||
@@ -1166,7 +1166,7 @@ func buildStatusMeta(
|
||||
}
|
||||
|
||||
if statusAtCollection != nil {
|
||||
at := formatOptionalRFC3339(statusAtCollection.At)
|
||||
at := formatOptionalRFC3339(&statusAtCollection.At)
|
||||
if at != "" && strings.TrimSpace(statusAtCollection.Status) != "" {
|
||||
meta.StatusAtCollection = &ReanimatorStatusAtCollection{
|
||||
Status: normalizeStatus(statusAtCollection.Status, true),
|
||||
@@ -1191,8 +1191,8 @@ func buildStatusMeta(
|
||||
return meta
|
||||
}
|
||||
|
||||
func formatOptionalRFC3339(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
func formatOptionalRFC3339(t *time.Time) string {
|
||||
if t == nil || t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.UTC().Format(time.RFC3339)
|
||||
|
||||
@@ -148,8 +148,8 @@ type HardwareDevice struct {
|
||||
TemperatureC int `json:"temperature_c,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
|
||||
StatusCheckedAt time.Time `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt time.Time `json:"status_changed_at,omitempty"`
|
||||
StatusCheckedAt *time.Time `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt *time.Time `json:"status_changed_at,omitempty"`
|
||||
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
||||
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
@@ -167,13 +167,14 @@ type FirmwareInfo struct {
|
||||
|
||||
// BoardInfo represents motherboard/system information
|
||||
type BoardInfo struct {
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
ProductName string `json:"product_name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
SerialNumber string `json:"serial_number,omitempty"`
|
||||
PartNumber string `json:"part_number,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
UUID string `json:"uuid,omitempty"`
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
ProductName string `json:"product_name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
SerialNumber string `json:"serial_number,omitempty"`
|
||||
PartNumber string `json:"part_number,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
UUID string `json:"uuid,omitempty"`
|
||||
BMCMACAddress string `json:"bmc_mac_address,omitempty"`
|
||||
}
|
||||
|
||||
// CPU represents processor information
|
||||
@@ -193,8 +194,8 @@ type CPU struct {
|
||||
SerialNumber string `json:"serial_number,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
|
||||
StatusCheckedAt time.Time `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt time.Time `json:"status_changed_at,omitempty"`
|
||||
StatusCheckedAt *time.Time `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt *time.Time `json:"status_changed_at,omitempty"`
|
||||
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
||||
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
@@ -217,8 +218,8 @@ type MemoryDIMM struct {
|
||||
Status string `json:"status,omitempty"`
|
||||
Ranks int `json:"ranks,omitempty"`
|
||||
|
||||
StatusCheckedAt time.Time `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt time.Time `json:"status_changed_at,omitempty"`
|
||||
StatusCheckedAt *time.Time `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt *time.Time `json:"status_changed_at,omitempty"`
|
||||
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
||||
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
@@ -240,8 +241,8 @@ type Storage struct {
|
||||
BackplaneID int `json:"backplane_id,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
|
||||
StatusCheckedAt time.Time `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt time.Time `json:"status_changed_at,omitempty"`
|
||||
StatusCheckedAt *time.Time `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt *time.Time `json:"status_changed_at,omitempty"`
|
||||
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
||||
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
@@ -278,8 +279,8 @@ type PCIeDevice struct {
|
||||
MACAddresses []string `json:"mac_addresses,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
|
||||
StatusCheckedAt time.Time `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt time.Time `json:"status_changed_at,omitempty"`
|
||||
StatusCheckedAt *time.Time `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt *time.Time `json:"status_changed_at,omitempty"`
|
||||
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
||||
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
@@ -314,8 +315,8 @@ type PSU struct {
|
||||
OutputVoltage float64 `json:"output_voltage,omitempty"`
|
||||
TemperatureC int `json:"temperature_c,omitempty"`
|
||||
|
||||
StatusCheckedAt time.Time `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt time.Time `json:"status_changed_at,omitempty"`
|
||||
StatusCheckedAt *time.Time `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt *time.Time `json:"status_changed_at,omitempty"`
|
||||
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
||||
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
@@ -352,8 +353,8 @@ type GPU struct {
|
||||
CurrentLinkSpeed string `json:"current_link_speed,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
|
||||
StatusCheckedAt time.Time `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt time.Time `json:"status_changed_at,omitempty"`
|
||||
StatusCheckedAt *time.Time `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt *time.Time `json:"status_changed_at,omitempty"`
|
||||
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
||||
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
@@ -377,8 +378,8 @@ type NetworkAdapter struct {
|
||||
MACAddresses []string `json:"mac_addresses,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
|
||||
StatusCheckedAt time.Time `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt time.Time `json:"status_changed_at,omitempty"`
|
||||
StatusCheckedAt *time.Time `json:"status_checked_at,omitempty"`
|
||||
StatusChangedAt *time.Time `json:"status_changed_at,omitempty"`
|
||||
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
||||
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
|
||||
8
internal/parser/vendors/inspur/gpu_status.go
vendored
8
internal/parser/vendors/inspur/gpu_status.go
vendored
@@ -70,11 +70,13 @@ func applyGPUStatusFromEvents(hw *models.HardwareConfig, events []models.Event)
|
||||
ChangedAt: e.Timestamp,
|
||||
Details: strings.TrimSpace(e.Description),
|
||||
})
|
||||
gpu.StatusChangedAt = e.Timestamp
|
||||
ts := e.Timestamp
|
||||
gpu.StatusChangedAt = &ts
|
||||
currentStatus[idx] = newStatus
|
||||
}
|
||||
|
||||
gpu.StatusCheckedAt = e.Timestamp
|
||||
ts := e.Timestamp
|
||||
gpu.StatusCheckedAt = &ts
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +87,7 @@ func applyGPUStatusFromEvents(hw *models.HardwareConfig, events []models.Event)
|
||||
} else {
|
||||
gpu.ErrorDescription = ""
|
||||
}
|
||||
if gpu.StatusCheckedAt.IsZero() && strings.TrimSpace(gpu.Status) == "" {
|
||||
if gpu.StatusCheckedAt == nil && strings.TrimSpace(gpu.Status) == "" {
|
||||
gpu.Status = "OK"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ func ApplyGPUAndNVSwitchCheckTimes(result *models.AnalysisResult, times componen
|
||||
if ts.IsZero() {
|
||||
continue
|
||||
}
|
||||
gpu.StatusCheckedAt = ts
|
||||
gpu.StatusCheckedAt = &ts
|
||||
status := strings.TrimSpace(gpu.Status)
|
||||
if status == "" {
|
||||
status = "Unknown"
|
||||
@@ -261,7 +261,7 @@ func ApplyGPUAndNVSwitchCheckTimes(result *models.AnalysisResult, times componen
|
||||
continue
|
||||
}
|
||||
|
||||
dev.StatusCheckedAt = ts
|
||||
dev.StatusCheckedAt = &ts
|
||||
status := strings.TrimSpace(dev.Status)
|
||||
if status == "" {
|
||||
status = "Unknown"
|
||||
|
||||
@@ -72,20 +72,20 @@ func TestApplyGPUAndNVSwitchCheckTimes(t *testing.T) {
|
||||
NVSwitchBySlot: map[string]time.Time{"NVSWITCH0": nvsTs},
|
||||
})
|
||||
|
||||
if got := result.Hardware.GPUs[0].StatusCheckedAt; !got.Equal(gpuTs) {
|
||||
t.Fatalf("expected gpu status_checked_at %s, got %s", gpuTs.Format(time.RFC3339), got.Format(time.RFC3339))
|
||||
if got := result.Hardware.GPUs[0].StatusCheckedAt; got == nil || !got.Equal(gpuTs) {
|
||||
t.Fatalf("expected gpu status_checked_at %s, got %v", gpuTs.Format(time.RFC3339), got)
|
||||
}
|
||||
if result.Hardware.GPUs[0].StatusAtCollect == nil || !result.Hardware.GPUs[0].StatusAtCollect.At.Equal(gpuTs) {
|
||||
t.Fatalf("expected gpu status_at_collection.at %s", gpuTs.Format(time.RFC3339))
|
||||
}
|
||||
if got := result.Hardware.PCIeDevices[0].StatusCheckedAt; !got.Equal(nvsTs) {
|
||||
t.Fatalf("expected nvswitch status_checked_at %s, got %s", nvsTs.Format(time.RFC3339), got.Format(time.RFC3339))
|
||||
if got := result.Hardware.PCIeDevices[0].StatusCheckedAt; got == nil || !got.Equal(nvsTs) {
|
||||
t.Fatalf("expected nvswitch status_checked_at %s, got %v", nvsTs.Format(time.RFC3339), got)
|
||||
}
|
||||
if result.Hardware.PCIeDevices[0].StatusAtCollect == nil || !result.Hardware.PCIeDevices[0].StatusAtCollect.At.Equal(nvsTs) {
|
||||
t.Fatalf("expected nvswitch status_at_collection.at %s", nvsTs.Format(time.RFC3339))
|
||||
}
|
||||
if !result.Hardware.PCIeDevices[1].StatusCheckedAt.IsZero() {
|
||||
t.Fatalf("expected non-nvswitch device status_checked_at to stay zero")
|
||||
if result.Hardware.PCIeDevices[1].StatusCheckedAt != nil {
|
||||
t.Fatalf("expected non-nvswitch device status_checked_at to stay nil")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -568,10 +568,10 @@ func mergeDevices(primary, secondary models.HardwareDevice) models.HardwareDevic
|
||||
fillFloat(&primary.InputVoltage, secondary.InputVoltage)
|
||||
fillInt(&primary.TemperatureC, secondary.TemperatureC)
|
||||
fillString(&primary.Status, secondary.Status)
|
||||
if primary.StatusCheckedAt.IsZero() && !secondary.StatusCheckedAt.IsZero() {
|
||||
if primary.StatusCheckedAt == nil && secondary.StatusCheckedAt != nil {
|
||||
primary.StatusCheckedAt = secondary.StatusCheckedAt
|
||||
}
|
||||
if primary.StatusChangedAt.IsZero() && !secondary.StatusChangedAt.IsZero() {
|
||||
if primary.StatusChangedAt == nil && secondary.StatusChangedAt != nil {
|
||||
primary.StatusChangedAt = secondary.StatusChangedAt
|
||||
}
|
||||
if primary.StatusAtCollect == nil && secondary.StatusAtCollect != nil {
|
||||
|
||||
Reference in New Issue
Block a user