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:
Mikhail Chusavitin
2026-08-15 12:11:16 +03:00
co-authored by Claude Sonnet 5
parent 0867123a91
commit 42cfa3aa94
4 changed files with 90 additions and 124 deletions
+35 -84
View File
@@ -43,17 +43,16 @@ func ConvertToReanimator(result *models.AnalysisResult) (*ReanimatorExport, erro
TargetHost: targetHost,
CollectedAt: collectedAt,
Hardware: ReanimatorHardware{
Board: convertBoard(result.Hardware.BoardInfo),
Firmware: dedupeFirmware(convertFirmware(result.Hardware.Firmware)),
CPUs: dedupeCPUs(convertCPUsFromDevices(devices, collectedAt, result.Hardware.BoardInfo.SerialNumber, buildCPUMicrocodeBySocket(result.Hardware.Firmware))),
Memory: dedupeMemory(convertMemoryFromDevices(devices, collectedAt)),
Storage: dedupeStorage(convertStorageFromDevices(devices, collectedAt)),
PCIeDevices: dedupePCIe(convertPCIeFromDevices(devices, collectedAt)),
PowerSupplies: dedupePSUs(convertPSUsFromDevices(devices, collectedAt)),
Sensors: convertSensors(result.Sensors),
BMCEventSummary: buildBMCEventSummary(result.Events, collectedAt),
EventLogs: convertEventLogs(result.Events, collectedAt),
Licenses: dedupeLicenses(convertLicenses(result.Hardware.Licenses, collectedAt)),
Board: convertBoard(result.Hardware.BoardInfo),
Firmware: dedupeFirmware(convertFirmware(result.Hardware.Firmware)),
CPUs: dedupeCPUs(convertCPUsFromDevices(devices, collectedAt, result.Hardware.BoardInfo.SerialNumber, buildCPUMicrocodeBySocket(result.Hardware.Firmware))),
Memory: dedupeMemory(convertMemoryFromDevices(devices, collectedAt)),
Storage: dedupeStorage(convertStorageFromDevices(devices, collectedAt)),
PCIeDevices: dedupePCIe(convertPCIeFromDevices(devices, collectedAt)),
PowerSupplies: dedupePSUs(convertPSUsFromDevices(devices, collectedAt)),
Sensors: convertSensors(result.Sensors),
EventLogs: convertEventLogs(result.Events, collectedAt),
Licenses: dedupeLicenses(convertLicenses(result.Hardware.Licenses, collectedAt)),
},
}
@@ -2195,9 +2194,16 @@ func dedupePCIe(items []ReanimatorPCIe) []ReanimatorPCIe {
order = append(order, key)
continue
}
winner, loser := existing, curr
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))
for _, key := range order {
@@ -2206,6 +2212,23 @@ func dedupePCIe(items []ReanimatorPCIe) []ReanimatorPCIe {
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 {
slot := strings.ToLower(strings.TrimSpace(item.Slot))
serial := strings.ToLower(strings.TrimSpace(item.SerialNumber))
@@ -2686,75 +2709,3 @@ func inferTargetHost(targetHost, filename string) string {
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
}
+11 -23
View File
@@ -12,29 +12,17 @@ type ReanimatorExport struct {
// ReanimatorHardware contains all hardware components
type ReanimatorHardware struct {
Board ReanimatorBoard `json:"board"`
Firmware []ReanimatorFirmware `json:"firmware,omitempty"`
CPUs []ReanimatorCPU `json:"cpus,omitempty"`
Memory []ReanimatorMemory `json:"memory,omitempty"`
Storage []ReanimatorStorage `json:"storage,omitempty"`
PCIeDevices []ReanimatorPCIe `json:"pcie_devices,omitempty"`
PowerSupplies []ReanimatorPSU `json:"power_supplies,omitempty"`
Sensors *ReanimatorSensors `json:"sensors,omitempty"`
BMCEventSummary []ReanimatorBMCEventRow `json:"bmc_event_summary,omitempty"`
EventLogs []ReanimatorEventLog `json:"event_logs,omitempty"`
PlatformConfig map[string]any `json:"platform_config,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"`
Board ReanimatorBoard `json:"board"`
Firmware []ReanimatorFirmware `json:"firmware,omitempty"`
CPUs []ReanimatorCPU `json:"cpus,omitempty"`
Memory []ReanimatorMemory `json:"memory,omitempty"`
Storage []ReanimatorStorage `json:"storage,omitempty"`
PCIeDevices []ReanimatorPCIe `json:"pcie_devices,omitempty"`
PowerSupplies []ReanimatorPSU `json:"power_supplies,omitempty"`
Sensors *ReanimatorSensors `json:"sensors,omitempty"`
EventLogs []ReanimatorEventLog `json:"event_logs,omitempty"`
PlatformConfig map[string]any `json:"platform_config,omitempty"`
Licenses []ReanimatorLicense `json:"licenses,omitempty"`
}
// ReanimatorBoard represents motherboard/server information