fix(exporter): drop dead bmc_event_summary field, fix status loss for inspur CPU/GPU
bmc_event_summary was a derived Assert/Deassert summary added in 4409594
alongside real fixes for GPU fault handling. It's not part of the Reanimator
hardware-ingest contract (event_logs is the only accepted log channel) and
was silently dropped on import — pure dead weight, removed.
Three related status bugs surfaced while auditing converted exports against
the contract, all specific to Inspur/onekeylog dumps:
- CPU status from RESTful CPU info was parsed but never assigned to
models.CPU, and was skipped entirely whenever asset.json already supplied
a CPU list (its own inventory has no status field) — CPUs always exported
as Unknown even when the source reported OK.
- PCIe device status (RESTful "status": 1) was parsed but never mapped onto
models.PCIeDevice, so RESTful-only devices always lost status.
- For GPUs specifically, asset.go emits two device records per physical GPU
(a generic pcie_devices entry enriched with real status, and a separate
gpus entry with a resolved model name but no status). dedupePCIe picks a
single winner by quality score, and a better model name outweighed having
a real status — the winner kept "Unknown" even when a losing duplicate had
the real value. dedupePCIe now backfills status onto the winner from a
losing duplicate when the winner's is Unknown.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
0867123a91
commit
42cfa3aa94
@@ -43,17 +43,16 @@ func ConvertToReanimator(result *models.AnalysisResult) (*ReanimatorExport, erro
|
|||||||
TargetHost: targetHost,
|
TargetHost: targetHost,
|
||||||
CollectedAt: collectedAt,
|
CollectedAt: collectedAt,
|
||||||
Hardware: ReanimatorHardware{
|
Hardware: ReanimatorHardware{
|
||||||
Board: convertBoard(result.Hardware.BoardInfo),
|
Board: convertBoard(result.Hardware.BoardInfo),
|
||||||
Firmware: dedupeFirmware(convertFirmware(result.Hardware.Firmware)),
|
Firmware: dedupeFirmware(convertFirmware(result.Hardware.Firmware)),
|
||||||
CPUs: dedupeCPUs(convertCPUsFromDevices(devices, collectedAt, result.Hardware.BoardInfo.SerialNumber, buildCPUMicrocodeBySocket(result.Hardware.Firmware))),
|
CPUs: dedupeCPUs(convertCPUsFromDevices(devices, collectedAt, result.Hardware.BoardInfo.SerialNumber, buildCPUMicrocodeBySocket(result.Hardware.Firmware))),
|
||||||
Memory: dedupeMemory(convertMemoryFromDevices(devices, collectedAt)),
|
Memory: dedupeMemory(convertMemoryFromDevices(devices, collectedAt)),
|
||||||
Storage: dedupeStorage(convertStorageFromDevices(devices, collectedAt)),
|
Storage: dedupeStorage(convertStorageFromDevices(devices, collectedAt)),
|
||||||
PCIeDevices: dedupePCIe(convertPCIeFromDevices(devices, collectedAt)),
|
PCIeDevices: dedupePCIe(convertPCIeFromDevices(devices, collectedAt)),
|
||||||
PowerSupplies: dedupePSUs(convertPSUsFromDevices(devices, collectedAt)),
|
PowerSupplies: dedupePSUs(convertPSUsFromDevices(devices, collectedAt)),
|
||||||
Sensors: convertSensors(result.Sensors),
|
Sensors: convertSensors(result.Sensors),
|
||||||
BMCEventSummary: buildBMCEventSummary(result.Events, collectedAt),
|
EventLogs: convertEventLogs(result.Events, collectedAt),
|
||||||
EventLogs: convertEventLogs(result.Events, collectedAt),
|
Licenses: dedupeLicenses(convertLicenses(result.Hardware.Licenses, collectedAt)),
|
||||||
Licenses: dedupeLicenses(convertLicenses(result.Hardware.Licenses, collectedAt)),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2195,9 +2194,16 @@ func dedupePCIe(items []ReanimatorPCIe) []ReanimatorPCIe {
|
|||||||
order = append(order, key)
|
order = append(order, key)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
winner, loser := existing, curr
|
||||||
if curr.score > existing.score {
|
if curr.score > existing.score {
|
||||||
byKey[key] = curr
|
winner, loser = curr, existing
|
||||||
}
|
}
|
||||||
|
// The winner is picked by overall quality score, but a losing duplicate
|
||||||
|
// (e.g. a generic pcie_devices entry enriched with real status vs. a
|
||||||
|
// gpus entry with a resolved model name but no status) may still carry
|
||||||
|
// status data the winner lacks. Backfill it rather than dropping it.
|
||||||
|
mergePCIeStatusInto(&winner.item, loser.item)
|
||||||
|
byKey[key] = winner
|
||||||
}
|
}
|
||||||
result := make([]ReanimatorPCIe, 0, len(byKey))
|
result := make([]ReanimatorPCIe, 0, len(byKey))
|
||||||
for _, key := range order {
|
for _, key := range order {
|
||||||
@@ -2206,6 +2212,23 @@ func dedupePCIe(items []ReanimatorPCIe) []ReanimatorPCIe {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mergePCIeStatusInto backfills dst's status fields from src when dst has none.
|
||||||
|
func mergePCIeStatusInto(dst *ReanimatorPCIe, src ReanimatorPCIe) {
|
||||||
|
// Status is already normalized to "Unknown" (never "") by the time items
|
||||||
|
// reach dedup, so emptiness isn't the right check here.
|
||||||
|
if !strings.EqualFold(strings.TrimSpace(dst.Status), "Unknown") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if srcStatus := strings.TrimSpace(src.Status); srcStatus == "" || strings.EqualFold(srcStatus, "Unknown") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dst.Status = src.Status
|
||||||
|
dst.StatusCheckedAt = src.StatusCheckedAt
|
||||||
|
dst.StatusChangedAt = src.StatusChangedAt
|
||||||
|
dst.StatusHistory = src.StatusHistory
|
||||||
|
dst.ErrorDescription = src.ErrorDescription
|
||||||
|
}
|
||||||
|
|
||||||
func pcieDedupKey(item ReanimatorPCIe) string {
|
func pcieDedupKey(item ReanimatorPCIe) string {
|
||||||
slot := strings.ToLower(strings.TrimSpace(item.Slot))
|
slot := strings.ToLower(strings.TrimSpace(item.Slot))
|
||||||
serial := strings.ToLower(strings.TrimSpace(item.SerialNumber))
|
serial := strings.ToLower(strings.TrimSpace(item.SerialNumber))
|
||||||
@@ -2686,75 +2709,3 @@ func inferTargetHost(targetHost, filename string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildBMCEventSummary produces a summary table of Critical/Warning BMC events
|
|
||||||
// with their resolution status derived from Assert/Deassert pairs.
|
|
||||||
func buildBMCEventSummary(events []models.Event, collectedAt string) []ReanimatorBMCEventRow {
|
|
||||||
type assertKey struct {
|
|
||||||
id string
|
|
||||||
desc string
|
|
||||||
}
|
|
||||||
type eventPair struct {
|
|
||||||
assertEvent *models.Event
|
|
||||||
deassertEvent *models.Event
|
|
||||||
}
|
|
||||||
|
|
||||||
pairs := make(map[assertKey]*eventPair)
|
|
||||||
order := make([]assertKey, 0)
|
|
||||||
|
|
||||||
for i := range events {
|
|
||||||
e := &events[i]
|
|
||||||
if e.Severity != models.SeverityCritical && e.Severity != models.SeverityWarning {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
key := assertKey{id: e.ID, desc: e.Description}
|
|
||||||
p, exists := pairs[key]
|
|
||||||
if !exists {
|
|
||||||
p = &eventPair{}
|
|
||||||
pairs[key] = p
|
|
||||||
order = append(order, key)
|
|
||||||
}
|
|
||||||
switch strings.ToLower(e.EventType) {
|
|
||||||
case "deassert":
|
|
||||||
if p.deassertEvent == nil || e.Timestamp.After(p.deassertEvent.Timestamp) {
|
|
||||||
p.deassertEvent = e
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if p.assertEvent == nil || e.Timestamp.Before(p.assertEvent.Timestamp) {
|
|
||||||
p.assertEvent = e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rows := make([]ReanimatorBMCEventRow, 0, len(order))
|
|
||||||
for _, key := range order {
|
|
||||||
p := pairs[key]
|
|
||||||
ref := p.assertEvent
|
|
||||||
if ref == nil {
|
|
||||||
ref = p.deassertEvent
|
|
||||||
}
|
|
||||||
if ref == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
status := "Active"
|
|
||||||
resolvedAt := ""
|
|
||||||
if p.deassertEvent != nil {
|
|
||||||
status = "Resolved"
|
|
||||||
resolvedAt = formatEventLogTime(p.deassertEvent.Timestamp, collectedAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
rows = append(rows, ReanimatorBMCEventRow{
|
|
||||||
Severity: normalizeEventLogSeverity(ref.Severity),
|
|
||||||
Component: strings.ToUpper(strings.TrimSpace(ref.SensorType)),
|
|
||||||
MessageID: strings.TrimSpace(ref.ID),
|
|
||||||
Timestamp: formatEventLogTime(ref.Timestamp, collectedAt),
|
|
||||||
Description: strings.TrimSpace(ref.Description),
|
|
||||||
Status: status,
|
|
||||||
ResolvedAt: resolvedAt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if len(rows) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return rows
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,29 +12,17 @@ type ReanimatorExport struct {
|
|||||||
|
|
||||||
// ReanimatorHardware contains all hardware components
|
// ReanimatorHardware contains all hardware components
|
||||||
type ReanimatorHardware struct {
|
type ReanimatorHardware struct {
|
||||||
Board ReanimatorBoard `json:"board"`
|
Board ReanimatorBoard `json:"board"`
|
||||||
Firmware []ReanimatorFirmware `json:"firmware,omitempty"`
|
Firmware []ReanimatorFirmware `json:"firmware,omitempty"`
|
||||||
CPUs []ReanimatorCPU `json:"cpus,omitempty"`
|
CPUs []ReanimatorCPU `json:"cpus,omitempty"`
|
||||||
Memory []ReanimatorMemory `json:"memory,omitempty"`
|
Memory []ReanimatorMemory `json:"memory,omitempty"`
|
||||||
Storage []ReanimatorStorage `json:"storage,omitempty"`
|
Storage []ReanimatorStorage `json:"storage,omitempty"`
|
||||||
PCIeDevices []ReanimatorPCIe `json:"pcie_devices,omitempty"`
|
PCIeDevices []ReanimatorPCIe `json:"pcie_devices,omitempty"`
|
||||||
PowerSupplies []ReanimatorPSU `json:"power_supplies,omitempty"`
|
PowerSupplies []ReanimatorPSU `json:"power_supplies,omitempty"`
|
||||||
Sensors *ReanimatorSensors `json:"sensors,omitempty"`
|
Sensors *ReanimatorSensors `json:"sensors,omitempty"`
|
||||||
BMCEventSummary []ReanimatorBMCEventRow `json:"bmc_event_summary,omitempty"`
|
EventLogs []ReanimatorEventLog `json:"event_logs,omitempty"`
|
||||||
EventLogs []ReanimatorEventLog `json:"event_logs,omitempty"`
|
PlatformConfig map[string]any `json:"platform_config,omitempty"`
|
||||||
PlatformConfig map[string]any `json:"platform_config,omitempty"`
|
Licenses []ReanimatorLicense `json:"licenses,omitempty"`
|
||||||
Licenses []ReanimatorLicense `json:"licenses,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReanimatorBMCEventRow is one row in the BMC critical/warning event summary table.
|
|
||||||
type ReanimatorBMCEventRow struct {
|
|
||||||
Severity string `json:"severity"`
|
|
||||||
Component string `json:"component"`
|
|
||||||
MessageID string `json:"message_id"`
|
|
||||||
Timestamp string `json:"timestamp"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
ResolvedAt string `json:"resolved_at,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReanimatorBoard represents motherboard/server information
|
// ReanimatorBoard represents motherboard/server information
|
||||||
|
|||||||
+32
-17
@@ -19,10 +19,9 @@ func ParseComponentLog(content []byte, hw *models.HardwareConfig) {
|
|||||||
|
|
||||||
text := string(content)
|
text := string(content)
|
||||||
|
|
||||||
// Parse RESTful CPU info — fallback when asset.json is absent
|
// Parse RESTful CPU info: fills in hw.CPUs when asset.json didn't provide any,
|
||||||
if len(hw.CPUs) == 0 {
|
// and enriches asset.json-derived entries (e.g. status) when it did.
|
||||||
parseCPUInfo(text, hw)
|
parseCPUInfo(text, hw)
|
||||||
}
|
|
||||||
|
|
||||||
// Parse RESTful Memory info (detailed memory data)
|
// Parse RESTful Memory info (detailed memory data)
|
||||||
parseMemoryInfo(text, hw)
|
parseMemoryInfo(text, hw)
|
||||||
@@ -148,23 +147,39 @@ func parseCPUInfo(text string, hw *models.HardwareConfig) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
existingBySocket := make(map[int]int, len(hw.CPUs))
|
||||||
|
for i, existing := range hw.CPUs {
|
||||||
|
existingBySocket[existing.Socket] = i
|
||||||
|
}
|
||||||
|
|
||||||
for _, proc := range cpuInfo.Processors {
|
for _, proc := range cpuInfo.Processors {
|
||||||
if proc.ProcStatus != 1 && proc.ConfigStatus != 1 {
|
if proc.ProcStatus != 1 && proc.ConfigStatus != 1 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
hw.CPUs = append(hw.CPUs, models.CPU{
|
status := strings.TrimSpace(proc.Status)
|
||||||
Socket: proc.ProcID,
|
if idx, ok := existingBySocket[proc.ProcID]; ok {
|
||||||
Model: strings.TrimSpace(proc.ProcName),
|
// asset.json already provided this CPU (no status field there) —
|
||||||
Cores: proc.CoreCount,
|
// enrich it with status from the RESTful CPU info instead of
|
||||||
Threads: proc.ThreadCount,
|
// appending a duplicate entry.
|
||||||
FrequencyMHz: proc.ProcSpeed,
|
if strings.TrimSpace(hw.CPUs[idx].Status) == "" {
|
||||||
MaxFreqMHz: proc.MaxSpeedMHz,
|
hw.CPUs[idx].Status = status
|
||||||
L1CacheKB: proc.L1Cache,
|
}
|
||||||
L2CacheKB: proc.L2Cache,
|
} else {
|
||||||
L3CacheKB: proc.L3Cache,
|
hw.CPUs = append(hw.CPUs, models.CPU{
|
||||||
TDP: proc.TDP,
|
Socket: proc.ProcID,
|
||||||
PPIN: proc.PPIN,
|
Model: strings.TrimSpace(proc.ProcName),
|
||||||
})
|
Cores: proc.CoreCount,
|
||||||
|
Threads: proc.ThreadCount,
|
||||||
|
FrequencyMHz: proc.ProcSpeed,
|
||||||
|
MaxFreqMHz: proc.MaxSpeedMHz,
|
||||||
|
L1CacheKB: proc.L1Cache,
|
||||||
|
L2CacheKB: proc.L2Cache,
|
||||||
|
L3CacheKB: proc.L3Cache,
|
||||||
|
TDP: proc.TDP,
|
||||||
|
PPIN: proc.PPIN,
|
||||||
|
Status: status,
|
||||||
|
})
|
||||||
|
}
|
||||||
if proc.MicroCode != "" {
|
if proc.MicroCode != "" {
|
||||||
hw.Firmware = append(hw.Firmware, models.FirmwareInfo{
|
hw.Firmware = append(hw.Firmware, models.FirmwareInfo{
|
||||||
DeviceName: fmt.Sprintf("CPU%d Microcode", proc.ProcID),
|
DeviceName: fmt.Sprintf("CPU%d Microcode", proc.ProcID),
|
||||||
|
|||||||
+12
@@ -189,6 +189,7 @@ func ParsePCIeDevices(content []byte) []models.PCIeDevice {
|
|||||||
MaxLinkSpeed: maxSpeed,
|
MaxLinkSpeed: maxSpeed,
|
||||||
PartNumber: partNumber,
|
PartNumber: partNumber,
|
||||||
SerialNumber: strings.TrimSpace(pcie.SerialNum),
|
SerialNumber: strings.TrimSpace(pcie.SerialNum),
|
||||||
|
Status: pcieRESTStatus(pcie.Status),
|
||||||
}
|
}
|
||||||
|
|
||||||
devices = append(devices, device)
|
devices = append(devices, device)
|
||||||
@@ -197,6 +198,17 @@ func ParsePCIeDevices(content []byte) []models.PCIeDevice {
|
|||||||
return devices
|
return devices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pcieRESTStatus maps the RESTful PCIE Device info "status" flag (1 = OK) to
|
||||||
|
// the shared status vocabulary. Only the observed OK case is mapped — the
|
||||||
|
// meaning of other values isn't confirmed in the source, so it's left
|
||||||
|
// unknown rather than guessed.
|
||||||
|
func pcieRESTStatus(status int) string {
|
||||||
|
if status == 1 {
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
var rawHexDeviceNameRegex = regexp.MustCompile(`(?i)^0x[0-9a-f]+$`)
|
var rawHexDeviceNameRegex = regexp.MustCompile(`(?i)^0x[0-9a-f]+$`)
|
||||||
|
|
||||||
func sanitizePCIeDeviceName(name string) string {
|
func sanitizePCIeDeviceName(name string) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user