diff --git a/internal/exporter/reanimator_converter.go b/internal/exporter/reanimator_converter.go index b279ed7..e412c82 100644 --- a/internal/exporter/reanimator_converter.go +++ b/internal/exporter/reanimator_converter.go @@ -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 -} diff --git a/internal/exporter/reanimator_models.go b/internal/exporter/reanimator_models.go index 230286f..1d0d4f2 100644 --- a/internal/exporter/reanimator_models.go +++ b/internal/exporter/reanimator_models.go @@ -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 diff --git a/internal/parser/vendors/inspur/component.go b/internal/parser/vendors/inspur/component.go index e8eba77..d35632d 100644 --- a/internal/parser/vendors/inspur/component.go +++ b/internal/parser/vendors/inspur/component.go @@ -19,10 +19,9 @@ func ParseComponentLog(content []byte, hw *models.HardwareConfig) { text := string(content) - // Parse RESTful CPU info — fallback when asset.json is absent - if len(hw.CPUs) == 0 { - parseCPUInfo(text, hw) - } + // Parse RESTful CPU info: fills in hw.CPUs when asset.json didn't provide any, + // and enriches asset.json-derived entries (e.g. status) when it did. + parseCPUInfo(text, hw) // Parse RESTful Memory info (detailed memory data) parseMemoryInfo(text, hw) @@ -148,23 +147,39 @@ func parseCPUInfo(text string, hw *models.HardwareConfig) { return } + existingBySocket := make(map[int]int, len(hw.CPUs)) + for i, existing := range hw.CPUs { + existingBySocket[existing.Socket] = i + } + for _, proc := range cpuInfo.Processors { if proc.ProcStatus != 1 && proc.ConfigStatus != 1 { continue } - hw.CPUs = append(hw.CPUs, models.CPU{ - Socket: proc.ProcID, - 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 := strings.TrimSpace(proc.Status) + if idx, ok := existingBySocket[proc.ProcID]; ok { + // asset.json already provided this CPU (no status field there) — + // enrich it with status from the RESTful CPU info instead of + // appending a duplicate entry. + if strings.TrimSpace(hw.CPUs[idx].Status) == "" { + hw.CPUs[idx].Status = status + } + } else { + hw.CPUs = append(hw.CPUs, models.CPU{ + Socket: proc.ProcID, + 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 != "" { hw.Firmware = append(hw.Firmware, models.FirmwareInfo{ DeviceName: fmt.Sprintf("CPU%d Microcode", proc.ProcID), diff --git a/internal/parser/vendors/inspur/pcie.go b/internal/parser/vendors/inspur/pcie.go index 158b919..a2f3df1 100644 --- a/internal/parser/vendors/inspur/pcie.go +++ b/internal/parser/vendors/inspur/pcie.go @@ -189,6 +189,7 @@ func ParsePCIeDevices(content []byte) []models.PCIeDevice { MaxLinkSpeed: maxSpeed, PartNumber: partNumber, SerialNumber: strings.TrimSpace(pcie.SerialNum), + Status: pcieRESTStatus(pcie.Status), } devices = append(devices, device) @@ -197,6 +198,17 @@ func ParsePCIeDevices(content []byte) []models.PCIeDevice { 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]+$`) func sanitizePCIeDeviceName(name string) string {