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, "/PCIeDevices"))
|
||||||
add(joinPath(p, "/Accelerators"))
|
add(joinPath(p, "/Accelerators"))
|
||||||
add(joinPath(p, "/Drives"))
|
add(joinPath(p, "/Drives"))
|
||||||
|
add(joinPath(p, "/Assembly"))
|
||||||
}
|
}
|
||||||
for _, p := range managerPaths {
|
for _, p := range managerPaths {
|
||||||
add(p)
|
add(p)
|
||||||
@@ -1916,6 +1917,58 @@ func shouldCrawlPath(path string) bool {
|
|||||||
return false
|
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{
|
heavyParts := []string{
|
||||||
"/JsonSchemas",
|
"/JsonSchemas",
|
||||||
"/LogServices/",
|
"/LogServices/",
|
||||||
@@ -2435,24 +2488,76 @@ func findFirstNormalizedStringByKeys(doc map[string]interface{}, keys ...string)
|
|||||||
|
|
||||||
func parseCPUs(docs []map[string]interface{}) []models.CPU {
|
func parseCPUs(docs []map[string]interface{}) []models.CPU {
|
||||||
cpus := make([]models.CPU, 0, len(docs))
|
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{
|
cpus = append(cpus, models.CPU{
|
||||||
Socket: idx,
|
Socket: socket,
|
||||||
Model: firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
|
Model: firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
|
||||||
Cores: asInt(doc["TotalCores"]),
|
Cores: asInt(doc["TotalCores"]),
|
||||||
Threads: asInt(doc["TotalThreads"]),
|
Threads: asInt(doc["TotalThreads"]),
|
||||||
FrequencyMHz: asInt(doc["OperatingSpeedMHz"]),
|
FrequencyMHz: asInt(doc["OperatingSpeedMHz"]),
|
||||||
MaxFreqMHz: asInt(doc["MaxSpeedMHz"]),
|
MaxFreqMHz: asInt(doc["MaxSpeedMHz"]),
|
||||||
SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
|
SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
|
||||||
|
L1CacheKB: l1,
|
||||||
|
L2CacheKB: l2,
|
||||||
|
L3CacheKB: l3,
|
||||||
|
Status: mapStatus(doc["Status"]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return cpus
|
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 {
|
func parseMemory(docs []map[string]interface{}) []models.MemoryDIMM {
|
||||||
out := make([]models.MemoryDIMM, 0, len(docs))
|
out := make([]models.MemoryDIMM, 0, len(docs))
|
||||||
for _, doc := range 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
|
present := true
|
||||||
if strings.EqualFold(asString(doc["Status"]), "Absent") {
|
if strings.EqualFold(asString(doc["Status"]), "Absent") {
|
||||||
present = false
|
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 {
|
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"]))
|
deviceType := strings.ToLower(asString(doc["DeviceType"]))
|
||||||
if strings.Contains(deviceType, "gpu") || strings.Contains(deviceType, "graphics") || strings.Contains(deviceType, "accelerator") {
|
if strings.Contains(deviceType, "gpu") || strings.Contains(deviceType, "graphics") || strings.Contains(deviceType, "accelerator") {
|
||||||
return true
|
return true
|
||||||
@@ -3192,6 +3303,20 @@ func looksLikeGPU(doc map[string]interface{}, functionDocs []map[string]interfac
|
|||||||
return false
|
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 {
|
func looksLikeDrive(doc map[string]interface{}) bool {
|
||||||
if asString(doc["MediaType"]) != "" {
|
if asString(doc["MediaType"]) != "" {
|
||||||
return true
|
return true
|
||||||
@@ -3951,8 +4076,7 @@ func parseFirmware(system, bios, manager, secureBoot, networkProtocol map[string
|
|||||||
appendFW("BIOS", asString(system["BiosVersion"]))
|
appendFW("BIOS", asString(system["BiosVersion"]))
|
||||||
appendFW("BIOS", asString(bios["Version"]))
|
appendFW("BIOS", asString(bios["Version"]))
|
||||||
appendFW("BMC", asString(manager["FirmwareVersion"]))
|
appendFW("BMC", asString(manager["FirmwareVersion"]))
|
||||||
appendFW("SecureBoot", asString(secureBoot["SecureBootCurrentBoot"]))
|
appendFW("SecureBoot", asString(secureBoot["SecureBootMode"]))
|
||||||
appendFW("NetworkProtocol", asString(networkProtocol["Id"]))
|
|
||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
@@ -4441,7 +4565,6 @@ func redfishSnapshotPrioritySeeds(systemPaths, chassisPaths, managerPaths []stri
|
|||||||
add(joinPath(p, "/Memory"))
|
add(joinPath(p, "/Memory"))
|
||||||
add(joinPath(p, "/EthernetInterfaces"))
|
add(joinPath(p, "/EthernetInterfaces"))
|
||||||
add(joinPath(p, "/NetworkInterfaces"))
|
add(joinPath(p, "/NetworkInterfaces"))
|
||||||
add(joinPath(p, "/BootOptions"))
|
|
||||||
add(joinPath(p, "/PCIeDevices"))
|
add(joinPath(p, "/PCIeDevices"))
|
||||||
add(joinPath(p, "/PCIeFunctions"))
|
add(joinPath(p, "/PCIeFunctions"))
|
||||||
add(joinPath(p, "/Accelerators"))
|
add(joinPath(p, "/Accelerators"))
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
|||||||
psus := r.collectPSUs(chassisPaths)
|
psus := r.collectPSUs(chassisPaths)
|
||||||
pcieDevices := r.collectPCIeDevices(systemPaths, chassisPaths)
|
pcieDevices := r.collectPCIeDevices(systemPaths, chassisPaths)
|
||||||
gpus := r.collectGPUs(systemPaths, chassisPaths)
|
gpus := r.collectGPUs(systemPaths, chassisPaths)
|
||||||
|
gpus = r.collectGPUsFromProcessors(systemPaths, chassisPaths, gpus)
|
||||||
nics := r.collectNICs(chassisPaths)
|
nics := r.collectNICs(chassisPaths)
|
||||||
r.enrichNICsFromNetworkInterfaces(&nics, systemPaths)
|
r.enrichNICsFromNetworkInterfaces(&nics, systemPaths)
|
||||||
thresholdSensors := r.collectThresholdSensors(chassisPaths)
|
thresholdSensors := r.collectThresholdSensors(chassisPaths)
|
||||||
@@ -86,13 +87,15 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
|||||||
firmware = dedupeFirmwareInfo(append(firmware, r.collectFirmwareInventory()...))
|
firmware = dedupeFirmwareInfo(append(firmware, r.collectFirmwareInventory()...))
|
||||||
boardInfo := parseBoardInfoWithFallback(systemDoc, chassisDoc, fruDoc)
|
boardInfo := parseBoardInfoWithFallback(systemDoc, chassisDoc, fruDoc)
|
||||||
applyBoardInfoFallbackFromDocs(&boardInfo, boardFallbackDocs)
|
applyBoardInfoFallbackFromDocs(&boardInfo, boardFallbackDocs)
|
||||||
|
boardInfo.BMCMACAddress = r.collectBMCMAC(managerPaths)
|
||||||
|
assemblyFRU := r.collectAssemblyFRU(chassisPaths)
|
||||||
collectedAt, sourceTimezone := inferRedfishCollectionTime(managerDoc, rawPayloads)
|
collectedAt, sourceTimezone := inferRedfishCollectionTime(managerDoc, rawPayloads)
|
||||||
|
|
||||||
result := &models.AnalysisResult{
|
result := &models.AnalysisResult{
|
||||||
CollectedAt: collectedAt,
|
CollectedAt: collectedAt,
|
||||||
SourceTimezone: sourceTimezone,
|
SourceTimezone: sourceTimezone,
|
||||||
Events: append(append(append(make([]models.Event, 0, len(discreteEvents)+len(healthEvents)+len(driveFetchWarningEvents)+1), healthEvents...), discreteEvents...), driveFetchWarningEvents...),
|
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...)),
|
Sensors: dedupeSensorReadings(append(append(thresholdSensors, thermalSensors...), powerSensors...)),
|
||||||
RawPayloads: cloneRawPayloads(rawPayloads),
|
RawPayloads: cloneRawPayloads(rawPayloads),
|
||||||
Hardware: &models.HardwareConfig{
|
Hardware: &models.HardwareConfig{
|
||||||
@@ -274,7 +277,12 @@ func (r redfishSnapshotReader) collectFirmwareInventory() []models.FirmwareInfo
|
|||||||
asString(doc["Name"]),
|
asString(doc["Name"]),
|
||||||
asString(doc["Id"]),
|
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
|
continue
|
||||||
}
|
}
|
||||||
out = append(out, models.FirmwareInfo{DeviceName: name, Version: version})
|
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)
|
driveDocs, err := r.getCollectionMembers(driveCollectionPath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, driveDoc := range driveDocs {
|
for _, driveDoc := range driveDocs {
|
||||||
out = append(out, parseDrive(driveDoc))
|
if !isVirtualStorageDrive(driveDoc) {
|
||||||
|
out = append(out, parseDrive(driveDoc))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(driveDocs) == 0 {
|
if len(driveDocs) == 0 {
|
||||||
for _, driveDoc := range r.probeDirectDiskBayChildren(driveCollectionPath) {
|
for _, driveDoc := range r.probeDirectDiskBayChildren(driveCollectionPath) {
|
||||||
@@ -1002,7 +1012,9 @@ func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storag
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out = append(out, parseDrive(driveDoc))
|
if !isVirtualStorageDrive(driveDoc) {
|
||||||
|
out = append(out, parseDrive(driveDoc))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1154,6 +1166,10 @@ func (r redfishSnapshotReader) collectNICs(chassisPaths []string) []models.Netwo
|
|||||||
functionDocs := r.getLinkedPCIeFunctions(pcieDoc)
|
functionDocs := r.getLinkedPCIeFunctions(pcieDoc)
|
||||||
enrichNICFromPCIe(&nic, pcieDoc, functionDocs)
|
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)
|
nics = append(nics, nic)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1268,3 +1284,237 @@ func stringsTrimTrailingSlash(s string) string {
|
|||||||
}
|
}
|
||||||
return s
|
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)
|
fillFloat(&primary.InputVoltage, secondary.InputVoltage)
|
||||||
fillInt(&primary.TemperatureC, secondary.TemperatureC)
|
fillInt(&primary.TemperatureC, secondary.TemperatureC)
|
||||||
fillString(&primary.Status, secondary.Status)
|
fillString(&primary.Status, secondary.Status)
|
||||||
if primary.StatusCheckedAt.IsZero() && !secondary.StatusCheckedAt.IsZero() {
|
if primary.StatusCheckedAt == nil && secondary.StatusCheckedAt != nil {
|
||||||
primary.StatusCheckedAt = secondary.StatusCheckedAt
|
primary.StatusCheckedAt = secondary.StatusCheckedAt
|
||||||
}
|
}
|
||||||
if primary.StatusChangedAt.IsZero() && !secondary.StatusChangedAt.IsZero() {
|
if primary.StatusChangedAt == nil && secondary.StatusChangedAt != nil {
|
||||||
primary.StatusChangedAt = secondary.StatusChangedAt
|
primary.StatusChangedAt = secondary.StatusChangedAt
|
||||||
}
|
}
|
||||||
if primary.StatusAtCollect == nil && secondary.StatusAtCollect != nil {
|
if primary.StatusAtCollect == nil && secondary.StatusAtCollect != nil {
|
||||||
@@ -1130,8 +1130,8 @@ type convertedStatusMeta struct {
|
|||||||
|
|
||||||
func buildStatusMeta(
|
func buildStatusMeta(
|
||||||
currentStatus string,
|
currentStatus string,
|
||||||
checkedAt time.Time,
|
checkedAt *time.Time,
|
||||||
changedAt time.Time,
|
changedAt *time.Time,
|
||||||
statusAtCollection *models.StatusAtCollection,
|
statusAtCollection *models.StatusAtCollection,
|
||||||
history []models.StatusHistoryEntry,
|
history []models.StatusHistoryEntry,
|
||||||
errorDescription string,
|
errorDescription string,
|
||||||
@@ -1145,7 +1145,7 @@ func buildStatusMeta(
|
|||||||
|
|
||||||
convertedHistory := make([]ReanimatorStatusHistoryEntry, 0, len(history))
|
convertedHistory := make([]ReanimatorStatusHistoryEntry, 0, len(history))
|
||||||
for _, h := range history {
|
for _, h := range history {
|
||||||
changed := formatOptionalRFC3339(h.ChangedAt)
|
changed := formatOptionalRFC3339(&h.ChangedAt)
|
||||||
if changed == "" {
|
if changed == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1166,7 +1166,7 @@ func buildStatusMeta(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if statusAtCollection != nil {
|
if statusAtCollection != nil {
|
||||||
at := formatOptionalRFC3339(statusAtCollection.At)
|
at := formatOptionalRFC3339(&statusAtCollection.At)
|
||||||
if at != "" && strings.TrimSpace(statusAtCollection.Status) != "" {
|
if at != "" && strings.TrimSpace(statusAtCollection.Status) != "" {
|
||||||
meta.StatusAtCollection = &ReanimatorStatusAtCollection{
|
meta.StatusAtCollection = &ReanimatorStatusAtCollection{
|
||||||
Status: normalizeStatus(statusAtCollection.Status, true),
|
Status: normalizeStatus(statusAtCollection.Status, true),
|
||||||
@@ -1191,8 +1191,8 @@ func buildStatusMeta(
|
|||||||
return meta
|
return meta
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatOptionalRFC3339(t time.Time) string {
|
func formatOptionalRFC3339(t *time.Time) string {
|
||||||
if t.IsZero() {
|
if t == nil || t.IsZero() {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return t.UTC().Format(time.RFC3339)
|
return t.UTC().Format(time.RFC3339)
|
||||||
|
|||||||
@@ -148,8 +148,8 @@ type HardwareDevice struct {
|
|||||||
TemperatureC int `json:"temperature_c,omitempty"`
|
TemperatureC int `json:"temperature_c,omitempty"`
|
||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
|
|
||||||
StatusCheckedAt time.Time `json:"status_checked_at,omitempty"`
|
StatusCheckedAt *time.Time `json:"status_checked_at,omitempty"`
|
||||||
StatusChangedAt time.Time `json:"status_changed_at,omitempty"`
|
StatusChangedAt *time.Time `json:"status_changed_at,omitempty"`
|
||||||
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
||||||
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
||||||
ErrorDescription string `json:"error_description,omitempty"`
|
ErrorDescription string `json:"error_description,omitempty"`
|
||||||
@@ -167,13 +167,14 @@ type FirmwareInfo struct {
|
|||||||
|
|
||||||
// BoardInfo represents motherboard/system information
|
// BoardInfo represents motherboard/system information
|
||||||
type BoardInfo struct {
|
type BoardInfo struct {
|
||||||
Manufacturer string `json:"manufacturer,omitempty"`
|
Manufacturer string `json:"manufacturer,omitempty"`
|
||||||
ProductName string `json:"product_name,omitempty"`
|
ProductName string `json:"product_name,omitempty"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
SerialNumber string `json:"serial_number,omitempty"`
|
SerialNumber string `json:"serial_number,omitempty"`
|
||||||
PartNumber string `json:"part_number,omitempty"`
|
PartNumber string `json:"part_number,omitempty"`
|
||||||
Version string `json:"version,omitempty"`
|
Version string `json:"version,omitempty"`
|
||||||
UUID string `json:"uuid,omitempty"`
|
UUID string `json:"uuid,omitempty"`
|
||||||
|
BMCMACAddress string `json:"bmc_mac_address,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CPU represents processor information
|
// CPU represents processor information
|
||||||
@@ -193,8 +194,8 @@ type CPU struct {
|
|||||||
SerialNumber string `json:"serial_number,omitempty"`
|
SerialNumber string `json:"serial_number,omitempty"`
|
||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
|
|
||||||
StatusCheckedAt time.Time `json:"status_checked_at,omitempty"`
|
StatusCheckedAt *time.Time `json:"status_checked_at,omitempty"`
|
||||||
StatusChangedAt time.Time `json:"status_changed_at,omitempty"`
|
StatusChangedAt *time.Time `json:"status_changed_at,omitempty"`
|
||||||
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
||||||
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
||||||
ErrorDescription string `json:"error_description,omitempty"`
|
ErrorDescription string `json:"error_description,omitempty"`
|
||||||
@@ -217,8 +218,8 @@ type MemoryDIMM struct {
|
|||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
Ranks int `json:"ranks,omitempty"`
|
Ranks int `json:"ranks,omitempty"`
|
||||||
|
|
||||||
StatusCheckedAt time.Time `json:"status_checked_at,omitempty"`
|
StatusCheckedAt *time.Time `json:"status_checked_at,omitempty"`
|
||||||
StatusChangedAt time.Time `json:"status_changed_at,omitempty"`
|
StatusChangedAt *time.Time `json:"status_changed_at,omitempty"`
|
||||||
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
||||||
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
||||||
ErrorDescription string `json:"error_description,omitempty"`
|
ErrorDescription string `json:"error_description,omitempty"`
|
||||||
@@ -240,8 +241,8 @@ type Storage struct {
|
|||||||
BackplaneID int `json:"backplane_id,omitempty"`
|
BackplaneID int `json:"backplane_id,omitempty"`
|
||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
|
|
||||||
StatusCheckedAt time.Time `json:"status_checked_at,omitempty"`
|
StatusCheckedAt *time.Time `json:"status_checked_at,omitempty"`
|
||||||
StatusChangedAt time.Time `json:"status_changed_at,omitempty"`
|
StatusChangedAt *time.Time `json:"status_changed_at,omitempty"`
|
||||||
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
||||||
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
||||||
ErrorDescription string `json:"error_description,omitempty"`
|
ErrorDescription string `json:"error_description,omitempty"`
|
||||||
@@ -278,8 +279,8 @@ type PCIeDevice struct {
|
|||||||
MACAddresses []string `json:"mac_addresses,omitempty"`
|
MACAddresses []string `json:"mac_addresses,omitempty"`
|
||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
|
|
||||||
StatusCheckedAt time.Time `json:"status_checked_at,omitempty"`
|
StatusCheckedAt *time.Time `json:"status_checked_at,omitempty"`
|
||||||
StatusChangedAt time.Time `json:"status_changed_at,omitempty"`
|
StatusChangedAt *time.Time `json:"status_changed_at,omitempty"`
|
||||||
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
||||||
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
||||||
ErrorDescription string `json:"error_description,omitempty"`
|
ErrorDescription string `json:"error_description,omitempty"`
|
||||||
@@ -314,8 +315,8 @@ type PSU struct {
|
|||||||
OutputVoltage float64 `json:"output_voltage,omitempty"`
|
OutputVoltage float64 `json:"output_voltage,omitempty"`
|
||||||
TemperatureC int `json:"temperature_c,omitempty"`
|
TemperatureC int `json:"temperature_c,omitempty"`
|
||||||
|
|
||||||
StatusCheckedAt time.Time `json:"status_checked_at,omitempty"`
|
StatusCheckedAt *time.Time `json:"status_checked_at,omitempty"`
|
||||||
StatusChangedAt time.Time `json:"status_changed_at,omitempty"`
|
StatusChangedAt *time.Time `json:"status_changed_at,omitempty"`
|
||||||
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
||||||
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
||||||
ErrorDescription string `json:"error_description,omitempty"`
|
ErrorDescription string `json:"error_description,omitempty"`
|
||||||
@@ -352,8 +353,8 @@ type GPU struct {
|
|||||||
CurrentLinkSpeed string `json:"current_link_speed,omitempty"`
|
CurrentLinkSpeed string `json:"current_link_speed,omitempty"`
|
||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
|
|
||||||
StatusCheckedAt time.Time `json:"status_checked_at,omitempty"`
|
StatusCheckedAt *time.Time `json:"status_checked_at,omitempty"`
|
||||||
StatusChangedAt time.Time `json:"status_changed_at,omitempty"`
|
StatusChangedAt *time.Time `json:"status_changed_at,omitempty"`
|
||||||
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
||||||
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
||||||
ErrorDescription string `json:"error_description,omitempty"`
|
ErrorDescription string `json:"error_description,omitempty"`
|
||||||
@@ -377,8 +378,8 @@ type NetworkAdapter struct {
|
|||||||
MACAddresses []string `json:"mac_addresses,omitempty"`
|
MACAddresses []string `json:"mac_addresses,omitempty"`
|
||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
|
|
||||||
StatusCheckedAt time.Time `json:"status_checked_at,omitempty"`
|
StatusCheckedAt *time.Time `json:"status_checked_at,omitempty"`
|
||||||
StatusChangedAt time.Time `json:"status_changed_at,omitempty"`
|
StatusChangedAt *time.Time `json:"status_changed_at,omitempty"`
|
||||||
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
StatusAtCollect *StatusAtCollection `json:"status_at_collection,omitempty"`
|
||||||
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
||||||
ErrorDescription string `json:"error_description,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,
|
ChangedAt: e.Timestamp,
|
||||||
Details: strings.TrimSpace(e.Description),
|
Details: strings.TrimSpace(e.Description),
|
||||||
})
|
})
|
||||||
gpu.StatusChangedAt = e.Timestamp
|
ts := e.Timestamp
|
||||||
|
gpu.StatusChangedAt = &ts
|
||||||
currentStatus[idx] = newStatus
|
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 {
|
} else {
|
||||||
gpu.ErrorDescription = ""
|
gpu.ErrorDescription = ""
|
||||||
}
|
}
|
||||||
if gpu.StatusCheckedAt.IsZero() && strings.TrimSpace(gpu.Status) == "" {
|
if gpu.StatusCheckedAt == nil && strings.TrimSpace(gpu.Status) == "" {
|
||||||
gpu.Status = "OK"
|
gpu.Status = "OK"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ func ApplyGPUAndNVSwitchCheckTimes(result *models.AnalysisResult, times componen
|
|||||||
if ts.IsZero() {
|
if ts.IsZero() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
gpu.StatusCheckedAt = ts
|
gpu.StatusCheckedAt = &ts
|
||||||
status := strings.TrimSpace(gpu.Status)
|
status := strings.TrimSpace(gpu.Status)
|
||||||
if status == "" {
|
if status == "" {
|
||||||
status = "Unknown"
|
status = "Unknown"
|
||||||
@@ -261,7 +261,7 @@ func ApplyGPUAndNVSwitchCheckTimes(result *models.AnalysisResult, times componen
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
dev.StatusCheckedAt = ts
|
dev.StatusCheckedAt = &ts
|
||||||
status := strings.TrimSpace(dev.Status)
|
status := strings.TrimSpace(dev.Status)
|
||||||
if status == "" {
|
if status == "" {
|
||||||
status = "Unknown"
|
status = "Unknown"
|
||||||
|
|||||||
@@ -72,20 +72,20 @@ func TestApplyGPUAndNVSwitchCheckTimes(t *testing.T) {
|
|||||||
NVSwitchBySlot: map[string]time.Time{"NVSWITCH0": nvsTs},
|
NVSwitchBySlot: map[string]time.Time{"NVSWITCH0": nvsTs},
|
||||||
})
|
})
|
||||||
|
|
||||||
if got := result.Hardware.GPUs[0].StatusCheckedAt; !got.Equal(gpuTs) {
|
if got := result.Hardware.GPUs[0].StatusCheckedAt; got == nil || !got.Equal(gpuTs) {
|
||||||
t.Fatalf("expected gpu status_checked_at %s, got %s", gpuTs.Format(time.RFC3339), got.Format(time.RFC3339))
|
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) {
|
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))
|
t.Fatalf("expected gpu status_at_collection.at %s", gpuTs.Format(time.RFC3339))
|
||||||
}
|
}
|
||||||
if got := result.Hardware.PCIeDevices[0].StatusCheckedAt; !got.Equal(nvsTs) {
|
if got := result.Hardware.PCIeDevices[0].StatusCheckedAt; got == nil || !got.Equal(nvsTs) {
|
||||||
t.Fatalf("expected nvswitch status_checked_at %s, got %s", nvsTs.Format(time.RFC3339), got.Format(time.RFC3339))
|
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) {
|
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))
|
t.Fatalf("expected nvswitch status_at_collection.at %s", nvsTs.Format(time.RFC3339))
|
||||||
}
|
}
|
||||||
if !result.Hardware.PCIeDevices[1].StatusCheckedAt.IsZero() {
|
if result.Hardware.PCIeDevices[1].StatusCheckedAt != nil {
|
||||||
t.Fatalf("expected non-nvswitch device status_checked_at to stay zero")
|
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)
|
fillFloat(&primary.InputVoltage, secondary.InputVoltage)
|
||||||
fillInt(&primary.TemperatureC, secondary.TemperatureC)
|
fillInt(&primary.TemperatureC, secondary.TemperatureC)
|
||||||
fillString(&primary.Status, secondary.Status)
|
fillString(&primary.Status, secondary.Status)
|
||||||
if primary.StatusCheckedAt.IsZero() && !secondary.StatusCheckedAt.IsZero() {
|
if primary.StatusCheckedAt == nil && secondary.StatusCheckedAt != nil {
|
||||||
primary.StatusCheckedAt = secondary.StatusCheckedAt
|
primary.StatusCheckedAt = secondary.StatusCheckedAt
|
||||||
}
|
}
|
||||||
if primary.StatusChangedAt.IsZero() && !secondary.StatusChangedAt.IsZero() {
|
if primary.StatusChangedAt == nil && secondary.StatusChangedAt != nil {
|
||||||
primary.StatusChangedAt = secondary.StatusChangedAt
|
primary.StatusChangedAt = secondary.StatusChangedAt
|
||||||
}
|
}
|
||||||
if primary.StatusAtCollect == nil && secondary.StatusAtCollect != nil {
|
if primary.StatusAtCollect == nil && secondary.StatusAtCollect != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user