Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3836a34cc | ||
|
|
ba9a52a61a | ||
|
|
27373aa104 | ||
|
|
4f7b5b826a | ||
|
|
dfd64550cf | ||
|
|
9505303d1d | ||
|
|
f2c04cf0e8 |
Submodule internal/chart updated: 2a15bc87f1...8105c7ec08
@@ -16,11 +16,21 @@ type AnalysisResult struct {
|
|||||||
SourceTimezone string `json:"source_timezone,omitempty"` // Source timezone/offset used during collection (e.g. +08:00)
|
SourceTimezone string `json:"source_timezone,omitempty"` // Source timezone/offset used during collection (e.g. +08:00)
|
||||||
CollectedAt time.Time `json:"collected_at,omitempty"` // Collection/upload timestamp
|
CollectedAt time.Time `json:"collected_at,omitempty"` // Collection/upload timestamp
|
||||||
InventoryLastModifiedAt time.Time `json:"inventory_last_modified_at,omitempty"` // Redfish inventory last modified (InventoryData/Status)
|
InventoryLastModifiedAt time.Time `json:"inventory_last_modified_at,omitempty"` // Redfish inventory last modified (InventoryData/Status)
|
||||||
RawPayloads map[string]any `json:"raw_payloads,omitempty"` // Additional source payloads (e.g. Redfish tree)
|
RawPayloads map[string]any `json:"raw_payloads,omitempty"` // Additional source payloads (e.g. Redfish tree)
|
||||||
Events []Event `json:"events"`
|
CollectionErrors []CollectionError `json:"collection_errors,omitempty"` // BMC-reported failures to collect specific sections
|
||||||
FRU []FRUInfo `json:"fru"`
|
Events []Event `json:"events"`
|
||||||
Sensors []SensorReading `json:"sensors"`
|
FRU []FRUInfo `json:"fru"`
|
||||||
Hardware *HardwareConfig `json:"hardware"`
|
Sensors []SensorReading `json:"sensors"`
|
||||||
|
Hardware *HardwareConfig `json:"hardware"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectionError represents a BMC-reported failure to collect a specific data section.
|
||||||
|
// Populated by vendor parsers when the source explicitly returns an error response
|
||||||
|
// instead of structured data (e.g. {"error":"...","code":1458} in Inspur component.log).
|
||||||
|
type CollectionError struct {
|
||||||
|
Section string `json:"section"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Code int `json:"code,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event represents a single log event
|
// Event represents a single log event
|
||||||
|
|||||||
5
internal/parser/vendors/inspur/asset.go
vendored
5
internal/parser/vendors/inspur/asset.go
vendored
@@ -117,7 +117,6 @@ func ParseAssetJSON(content []byte, pcieSlotDeviceNames map[int]string, pcieSlot
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse CPU info
|
// Parse CPU info
|
||||||
seenMicrocode := make(map[string]bool)
|
|
||||||
for i, cpu := range asset.CpuInfo {
|
for i, cpu := range asset.CpuInfo {
|
||||||
config.CPUs = append(config.CPUs, models.CPU{
|
config.CPUs = append(config.CPUs, models.CPU{
|
||||||
Socket: i,
|
Socket: i,
|
||||||
@@ -133,13 +132,11 @@ func ParseAssetJSON(content []byte, pcieSlotDeviceNames map[int]string, pcieSlot
|
|||||||
PPIN: cpu.PPIN,
|
PPIN: cpu.PPIN,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add CPU microcode to firmware list (deduplicated)
|
if cpu.MicroCodeVer != "" {
|
||||||
if cpu.MicroCodeVer != "" && !seenMicrocode[cpu.MicroCodeVer] {
|
|
||||||
config.Firmware = append(config.Firmware, models.FirmwareInfo{
|
config.Firmware = append(config.Firmware, models.FirmwareInfo{
|
||||||
DeviceName: fmt.Sprintf("CPU%d Microcode", i),
|
DeviceName: fmt.Sprintf("CPU%d Microcode", i),
|
||||||
Version: cpu.MicroCodeVer,
|
Version: cpu.MicroCodeVer,
|
||||||
})
|
})
|
||||||
seenMicrocode[cpu.MicroCodeVer] = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
145
internal/parser/vendors/inspur/component.go
vendored
145
internal/parser/vendors/inspur/component.go
vendored
@@ -19,6 +19,11 @@ func ParseComponentLog(content []byte, hw *models.HardwareConfig) {
|
|||||||
|
|
||||||
text := string(content)
|
text := string(content)
|
||||||
|
|
||||||
|
// Parse RESTful CPU info — fallback when asset.json is absent
|
||||||
|
if len(hw.CPUs) == 0 {
|
||||||
|
parseCPUInfo(text, hw)
|
||||||
|
}
|
||||||
|
|
||||||
// Parse RESTful Memory info (detailed memory data)
|
// Parse RESTful Memory info (detailed memory data)
|
||||||
parseMemoryInfo(text, hw)
|
parseMemoryInfo(text, hw)
|
||||||
|
|
||||||
@@ -51,6 +56,52 @@ func ParseComponentLogEvents(content []byte) []models.Event {
|
|||||||
return events
|
return events
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseComponentLogCollectionErrors detects BMC-reported collection failures in component.log.
|
||||||
|
// When a RESTful section returns {"error":"...","code":N} instead of structured data,
|
||||||
|
// the BMC itself failed to collect that subsystem — the parser emits a CollectionError
|
||||||
|
// so the UI can surface it explicitly rather than showing an empty section.
|
||||||
|
func ParseComponentLogCollectionErrors(content []byte) []models.CollectionError {
|
||||||
|
type bmcErrorResponse struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
Code int `json:"code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map of section name (for display) → regex that captures its JSON payload.
|
||||||
|
// Sections that return arrays use \[ ... \]; object sections use \{ ... \}.
|
||||||
|
// We only probe sections that are expected to have structured hardware data.
|
||||||
|
sections := []struct {
|
||||||
|
name string
|
||||||
|
re *regexp.Regexp
|
||||||
|
}{
|
||||||
|
{"HDD", regexp.MustCompile(`RESTful HDD info:\s*(\{[^\n]*\})`)},
|
||||||
|
{"PCIe Devices", regexp.MustCompile(`RESTful PCIE Device info:\s*(\{[^\n]*\})`)},
|
||||||
|
{"Network Adapters", regexp.MustCompile(`RESTful Network Adapter info:\s*(\{[^\n]*\})`)},
|
||||||
|
{"Disk Backplane", regexp.MustCompile(`RESTful diskbackplane info:\s*(\{[^\n]*\})`)},
|
||||||
|
}
|
||||||
|
|
||||||
|
text := string(content)
|
||||||
|
var out []models.CollectionError
|
||||||
|
for _, s := range sections {
|
||||||
|
m := s.re.FindStringSubmatch(text)
|
||||||
|
if m == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var errResp bmcErrorResponse
|
||||||
|
if err := json.Unmarshal([]byte(m[1]), &errResp); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(errResp.Error) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, models.CollectionError{
|
||||||
|
Section: s.name,
|
||||||
|
Message: errResp.Error,
|
||||||
|
Code: errResp.Code,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// ParseComponentLogSensors extracts sensor readings from component.log JSON sections.
|
// ParseComponentLogSensors extracts sensor readings from component.log JSON sections.
|
||||||
func ParseComponentLogSensors(content []byte) []models.SensorReading {
|
func ParseComponentLogSensors(content []byte) []models.SensorReading {
|
||||||
text := string(content)
|
text := string(content)
|
||||||
@@ -61,6 +112,68 @@ func ParseComponentLogSensors(content []byte) []models.SensorReading {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CPURESTInfo represents the RESTful CPU info structure in component.log
|
||||||
|
type CPURESTInfo struct {
|
||||||
|
Processors []struct {
|
||||||
|
ProcID int `json:"proc_id"`
|
||||||
|
CPUID string `json:"PROC_ID"` // uppercase key — prevents case-insensitive collision with proc_id
|
||||||
|
Manufacturer string `json:"Manufacturer"`
|
||||||
|
MaxSpeedMHz int `json:"MaxSpeedMHz"`
|
||||||
|
ConfigStatus int `json:"configStatus"`
|
||||||
|
ProcName string `json:"proc_name"`
|
||||||
|
ProcStatus int `json:"proc_status"`
|
||||||
|
ProcSpeed int `json:"proc_speed"`
|
||||||
|
CoreCount int `json:"proc_core_count"`
|
||||||
|
ThreadCount int `json:"proc_thread_count"`
|
||||||
|
TDP int `json:"proc_tdp"`
|
||||||
|
L1Cache int `json:"proc_l1cache_size"`
|
||||||
|
L2Cache int `json:"proc_l2cache_size"`
|
||||||
|
L3Cache int `json:"proc_l3cache_size"`
|
||||||
|
MicroCode string `json:"micro_code"`
|
||||||
|
PPIN string `json:"ppin"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
} `json:"processors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCPUInfo(text string, hw *models.HardwareConfig) {
|
||||||
|
re := regexp.MustCompile(`RESTful CPU info:\s*(\{[\s\S]*?\})\s*RESTful Memory`)
|
||||||
|
match := re.FindStringSubmatch(text)
|
||||||
|
if match == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonStr := strings.ReplaceAll(match[1], "\n", "")
|
||||||
|
var cpuInfo CPURESTInfo
|
||||||
|
if err := json.Unmarshal([]byte(jsonStr), &cpuInfo); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
if proc.MicroCode != "" {
|
||||||
|
hw.Firmware = append(hw.Firmware, models.FirmwareInfo{
|
||||||
|
DeviceName: fmt.Sprintf("CPU%d Microcode", proc.ProcID),
|
||||||
|
Version: proc.MicroCode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MemoryRESTInfo represents the RESTful Memory info structure
|
// MemoryRESTInfo represents the RESTful Memory info structure
|
||||||
type MemoryRESTInfo struct {
|
type MemoryRESTInfo struct {
|
||||||
MemModules []struct {
|
MemModules []struct {
|
||||||
@@ -112,9 +225,10 @@ func parseMemoryInfo(text string, hw *models.HardwareConfig) {
|
|||||||
}
|
}
|
||||||
for _, mem := range memInfo.MemModules {
|
for _, mem := range memInfo.MemModules {
|
||||||
item := models.MemoryDIMM{
|
item := models.MemoryDIMM{
|
||||||
Slot: mem.MemModSlot,
|
Slot: mem.MemModSlot,
|
||||||
Location: mem.MemModSlot,
|
Location: mem.MemModSlot,
|
||||||
Present: mem.MemModStatus == 1 && mem.MemModSize > 0,
|
// status=1 with a known serial/part is definitely present even if BMC reports size=0
|
||||||
|
Present: mem.MemModStatus == 1 && (mem.MemModSize > 0 || strings.TrimSpace(mem.MemModSerial) != "" || strings.TrimSpace(mem.MemModPartNum) != ""),
|
||||||
SizeMB: mem.MemModSize * 1024, // Convert GB to MB
|
SizeMB: mem.MemModSize * 1024, // Convert GB to MB
|
||||||
Type: mem.MemModType,
|
Type: mem.MemModType,
|
||||||
Technology: strings.TrimSpace(mem.MemModTechnology),
|
Technology: strings.TrimSpace(mem.MemModTechnology),
|
||||||
@@ -136,6 +250,25 @@ func parseMemoryInfo(text string, hw *models.HardwareConfig) {
|
|||||||
}
|
}
|
||||||
merged = append(merged, item)
|
merged = append(merged, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a present DIMM has size=0 (BMC firmware glitch), infer size from
|
||||||
|
// another present DIMM with the same part number in the same batch.
|
||||||
|
partSize := make(map[string]int)
|
||||||
|
for _, m := range merged {
|
||||||
|
if m.Present && m.SizeMB > 0 && strings.TrimSpace(m.PartNumber) != "" {
|
||||||
|
partSize[strings.TrimSpace(m.PartNumber)] = m.SizeMB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range merged {
|
||||||
|
if merged[i].Present && merged[i].SizeMB == 0 {
|
||||||
|
if pn := strings.TrimSpace(merged[i].PartNumber); pn != "" {
|
||||||
|
if sz, ok := partSize[pn]; ok {
|
||||||
|
merged[i].SizeMB = sz
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
hw.Memory = merged
|
hw.Memory = merged
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +296,7 @@ type PSURESTInfo struct {
|
|||||||
|
|
||||||
func parsePSUInfo(text string, hw *models.HardwareConfig) {
|
func parsePSUInfo(text string, hw *models.HardwareConfig) {
|
||||||
// Find RESTful PSU info section
|
// Find RESTful PSU info section
|
||||||
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful Network`)
|
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful (?:PCIE|Network)`)
|
||||||
match := re.FindStringSubmatch(text)
|
match := re.FindStringSubmatch(text)
|
||||||
if match == nil {
|
if match == nil {
|
||||||
return
|
return
|
||||||
@@ -793,7 +926,7 @@ func parseDiskBackplaneSensors(text string) []models.SensorReading {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parsePSUSummarySensors(text string) []models.SensorReading {
|
func parsePSUSummarySensors(text string) []models.SensorReading {
|
||||||
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful Network`)
|
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful (?:PCIE|Network)`)
|
||||||
match := re.FindStringSubmatch(text)
|
match := re.FindStringSubmatch(text)
|
||||||
if match == nil {
|
if match == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -941,7 +1074,7 @@ func extractComponentFirmware(text string, hw *models.HardwareConfig) {
|
|||||||
// Skip extracting from component.log to avoid duplicates
|
// Skip extracting from component.log to avoid duplicates
|
||||||
|
|
||||||
// Extract PSU firmware from RESTful PSU info
|
// Extract PSU firmware from RESTful PSU info
|
||||||
rePSU := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful Network`)
|
rePSU := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful (?:PCIE|Network)`)
|
||||||
if match := rePSU.FindStringSubmatch(text); match != nil {
|
if match := rePSU.FindStringSubmatch(text); match != nil {
|
||||||
jsonStr := strings.ReplaceAll(match[1], "\n", "")
|
jsonStr := strings.ReplaceAll(match[1], "\n", "")
|
||||||
var psuInfo PSURESTInfo
|
var psuInfo PSURESTInfo
|
||||||
|
|||||||
83
internal/parser/vendors/inspur/cpu_mem_fix_test.go
vendored
Normal file
83
internal/parser/vendors/inspur/cpu_mem_fix_test.go
vendored
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package inspur
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
const cpuMemComponentLog = `RESTful version info:
|
||||||
|
[]
|
||||||
|
RESTful CPU info:
|
||||||
|
{ "processors": [ { "proc_id": 0, "PROC_ID": "A6-06-06-00-FF-FB-EB-BF", "InstructionSet": "x86-64", "Manufacturer": "Intel(R) Corporation", "MaxSpeedMHz": 3100, "configStatus": 1, "proc_name": "Intel(R) Xeon(R) Gold 6330 CPU @ 2.00GHz", "proc_status": 1, "proc_speed": 2000, "proc_core_count": 28, "proc_used_core_count": 28, "proc_thread_count": 56, "proc_tdp": 205, "proc_l1cache_size": 80, "proc_l2cache_size": 1280, "proc_l3cache_size": 43008, "micro_code": "0x0D000410", "ppin": "47149E2253E81688", "status": "OK" }, { "proc_id": 1, "PROC_ID": "A6-06-06-00-FF-FB-EB-BF", "InstructionSet": "x86-64", "Manufacturer": "Intel(R) Corporation", "MaxSpeedMHz": 3100, "configStatus": 1, "proc_name": "Intel(R) Xeon(R) Gold 6330 CPU @ 2.00GHz", "proc_status": 1, "proc_speed": 2000, "proc_core_count": 28, "proc_thread_count": 56, "proc_tdp": 205, "proc_l1cache_size": 80, "proc_l2cache_size": 1280, "proc_l3cache_size": 43008, "micro_code": "0x0D000410", "ppin": "475AC1221D41F557", "status": "OK" } ] }
|
||||||
|
RESTful Memory info:
|
||||||
|
{ "mem_modules": [ { "mem_mod_id": 0, "config_status": 1, "mem_mod_slot": "CPU0_C0D0", "mem_mod_status": 1, "mem_mod_size": 32, "mem_mod_type": "DDR4", "mem_mod_technology": "Synchronous", "mem_mod_frequency": 3200, "mem_mod_current_frequency": 2933, "mem_mod_vendor": "Samsung", "mem_mod_part_num": "M393A4K40EB3-CWE", "mem_mod_serial_num": "S1440202433526FC12", "mem_mod_ranks": 2, "status": "OK" }, { "mem_mod_id": 16, "config_status": 1, "mem_mod_slot": "CPU1_C0D0", "mem_mod_status": 1, "mem_mod_size": 0, "mem_mod_type": "DDR4", "mem_mod_technology": "Synchronous", "mem_mod_frequency": 3200, "mem_mod_current_frequency": 2933, "mem_mod_vendor": "Samsung", "mem_mod_part_num": "M393A4K40EB3-CWE", "mem_mod_serial_num": "K0UX000401205D2037", "mem_mod_ranks": 2, "status": "OK" } ], "total_memory_count": 2, "present_memory_count": 2, "mem_total_mem_size": 32 }
|
||||||
|
RESTful HDD info:
|
||||||
|
[]
|
||||||
|
RESTful PSU info:
|
||||||
|
{ "power_supplies": [] }
|
||||||
|
RESTful Network Adapter info:
|
||||||
|
{ "sys_adapters": [] }
|
||||||
|
RESTful fan info:
|
||||||
|
{ "fans": [] }
|
||||||
|
RESTful diskbackplane info:
|
||||||
|
[]
|
||||||
|
BMC done
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestParseCPUInfo_FromComponentLog(t *testing.T) {
|
||||||
|
hw := &models.HardwareConfig{}
|
||||||
|
ParseComponentLog([]byte(cpuMemComponentLog), hw)
|
||||||
|
|
||||||
|
if len(hw.CPUs) != 2 {
|
||||||
|
t.Fatalf("expected 2 CPUs, got %d", len(hw.CPUs))
|
||||||
|
}
|
||||||
|
if !strings.Contains(hw.CPUs[0].Model, "Gold 6330") {
|
||||||
|
t.Errorf("unexpected CPU model: %s", hw.CPUs[0].Model)
|
||||||
|
}
|
||||||
|
if hw.CPUs[0].Cores != 28 {
|
||||||
|
t.Errorf("expected 28 cores, got %d", hw.CPUs[0].Cores)
|
||||||
|
}
|
||||||
|
if hw.CPUs[0].PPIN != "47149E2253E81688" {
|
||||||
|
t.Errorf("unexpected PPIN: %s", hw.CPUs[0].PPIN)
|
||||||
|
}
|
||||||
|
if hw.CPUs[1].PPIN != "475AC1221D41F557" {
|
||||||
|
t.Errorf("unexpected CPU1 PPIN: %s", hw.CPUs[1].PPIN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMemoryInfo_PresentWithZeroSize(t *testing.T) {
|
||||||
|
hw := &models.HardwareConfig{}
|
||||||
|
ParseComponentLog([]byte(cpuMemComponentLog), hw)
|
||||||
|
|
||||||
|
presentCount := 0
|
||||||
|
for _, m := range hw.Memory {
|
||||||
|
if m.Present {
|
||||||
|
presentCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if presentCount != 2 {
|
||||||
|
t.Errorf("expected 2 present DIMMs, got %d", presentCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find CPU1_C0D0 (size=0 but serial present — size should be inferred from same part number)
|
||||||
|
found := false
|
||||||
|
for _, m := range hw.Memory {
|
||||||
|
if m.Slot == "CPU1_C0D0" {
|
||||||
|
found = true
|
||||||
|
if !m.Present {
|
||||||
|
t.Error("CPU1_C0D0 should be Present=true despite size=0")
|
||||||
|
}
|
||||||
|
if m.SerialNumber != "K0UX000401205D2037" {
|
||||||
|
t.Errorf("wrong serial: %s", m.SerialNumber)
|
||||||
|
}
|
||||||
|
if m.SizeMB != 32768 {
|
||||||
|
t.Errorf("expected SizeMB=32768 inferred from part number, got %d", m.SizeMB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Error("CPU1_C0D0 not found in memory list")
|
||||||
|
}
|
||||||
|
}
|
||||||
22
internal/parser/vendors/inspur/parser.go
vendored
22
internal/parser/vendors/inspur/parser.go
vendored
@@ -16,7 +16,7 @@ import (
|
|||||||
|
|
||||||
// parserVersion - version of this parser module
|
// parserVersion - version of this parser module
|
||||||
// IMPORTANT: Increment this version when making changes to parser logic!
|
// IMPORTANT: Increment this version when making changes to parser logic!
|
||||||
const parserVersion = "1.8"
|
const parserVersion = "2.0"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
parser.Register(&Parser{})
|
parser.Register(&Parser{})
|
||||||
@@ -163,6 +163,26 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
|||||||
// (fan RPM, backplane temperature, PSU summary power, etc.).
|
// (fan RPM, backplane temperature, PSU summary power, etc.).
|
||||||
componentSensors := ParseComponentLogSensors(f.Content)
|
componentSensors := ParseComponentLogSensors(f.Content)
|
||||||
result.Sensors = mergeSensorReadings(result.Sensors, componentSensors)
|
result.Sensors = mergeSensorReadings(result.Sensors, componentSensors)
|
||||||
|
|
||||||
|
// Record sections where BMC itself returned an error instead of data,
|
||||||
|
// and mirror each one into the Events stream so they appear in the log viewer.
|
||||||
|
// Source is set to "BMC/<section>" so the viewer can show the specific module.
|
||||||
|
for _, ce := range ParseComponentLogCollectionErrors(f.Content) {
|
||||||
|
result.CollectionErrors = append(result.CollectionErrors, ce)
|
||||||
|
desc := ce.Message
|
||||||
|
if ce.Code != 0 {
|
||||||
|
desc = fmt.Sprintf("%s (code %d)", ce.Message, ce.Code)
|
||||||
|
}
|
||||||
|
result.Events = append(result.Events, models.Event{
|
||||||
|
ID: fmt.Sprintf("bmc_collection_error_%s", strings.ToLower(strings.ReplaceAll(ce.Section, " ", "_"))),
|
||||||
|
Timestamp: time.Time{}, // no timestamp available
|
||||||
|
Source: fmt.Sprintf("BMC/%s", ce.Section),
|
||||||
|
SensorType: "bmc_collection_error",
|
||||||
|
EventType: "Collection Error",
|
||||||
|
Severity: models.SeverityWarning,
|
||||||
|
Description: desc,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enrich runtime component data from Redis snapshot (serials, FW, telemetry),
|
// Enrich runtime component data from Redis snapshot (serials, FW, telemetry),
|
||||||
|
|||||||
1677
internal/parser/vendors/pciids/pci.ids
vendored
1677
internal/parser/vendors/pciids/pci.ids
vendored
File diff suppressed because it is too large
Load Diff
@@ -854,6 +854,28 @@ func (s *Server) handleGetParseErrors(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BMC-reported collection failures surfaced by vendor parsers.
|
||||||
|
if result != nil {
|
||||||
|
for _, ce := range result.CollectionErrors {
|
||||||
|
msg := strings.TrimSpace(ce.Message)
|
||||||
|
if msg == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
detail := ""
|
||||||
|
if ce.Code != 0 {
|
||||||
|
detail = fmt.Sprintf("code %d", ce.Code)
|
||||||
|
}
|
||||||
|
add(parseErrorEntry{
|
||||||
|
Source: "bmc",
|
||||||
|
Category: "bmc_collection_error",
|
||||||
|
Severity: "warning",
|
||||||
|
Path: ce.Section,
|
||||||
|
Message: msg,
|
||||||
|
Detail: detail,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sort.Slice(items, func(i, j int) bool {
|
sort.Slice(items, func(i, j int) bool {
|
||||||
if items[i].Severity != items[j].Severity {
|
if items[i].Severity != items[j].Severity {
|
||||||
// error > warning > info
|
// error > warning > info
|
||||||
|
|||||||
2
third_party/pciids
vendored
2
third_party/pciids
vendored
Submodule third_party/pciids updated: 82b1a68f47...9186887530
@@ -933,3 +933,71 @@ code {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Parse / collection errors panel ───────────────────────────────────── */
|
||||||
|
.parse-errors-section {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parse-errors-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: .55rem .75rem;
|
||||||
|
background: var(--warn-bg);
|
||||||
|
border: 1px solid #f0e0c0;
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: .85rem;
|
||||||
|
color: var(--warn-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.parse-errors-toggle {
|
||||||
|
font-size: .75rem;
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parse-errors-body {
|
||||||
|
border: 1px solid #f0e0c0;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 6px 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parse-errors-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: .82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parse-errors-table th {
|
||||||
|
background: var(--surface-3);
|
||||||
|
padding: .4rem .65rem;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parse-errors-table td {
|
||||||
|
padding: .38rem .65rem;
|
||||||
|
border-bottom: 1px solid var(--border-lite);
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parse-errors-table tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parse-error-row.parse-error-error td:first-child {
|
||||||
|
color: var(--crit-fg);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parse-error-row.parse-error-warning td:first-child {
|
||||||
|
color: #7a5200;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1413,6 +1413,62 @@ async function loadData(vendor, filename) {
|
|||||||
document.getElementById('header-log-meta').classList.remove('hidden');
|
document.getElementById('header-log-meta').classList.remove('hidden');
|
||||||
|
|
||||||
loadAuditViewer();
|
loadAuditViewer();
|
||||||
|
loadParseErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadParseErrors() {
|
||||||
|
const section = document.getElementById('parse-errors-section');
|
||||||
|
const rows = document.getElementById('parse-errors-rows');
|
||||||
|
const title = document.getElementById('parse-errors-title');
|
||||||
|
if (!section || !rows) return;
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/parse-errors');
|
||||||
|
if (!resp.ok) return;
|
||||||
|
data = await resp.json();
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = (data && data.items) ? data.items : [];
|
||||||
|
if (items.length === 0) {
|
||||||
|
section.classList.add('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorCount = items.filter(i => i.severity === 'error').length;
|
||||||
|
const warnCount = items.filter(i => i.severity === 'warning').length;
|
||||||
|
const parts = [];
|
||||||
|
if (errorCount > 0) parts.push(`${errorCount} error${errorCount > 1 ? 's' : ''}`);
|
||||||
|
if (warnCount > 0) parts.push(`${warnCount} warning${warnCount > 1 ? 's' : ''}`);
|
||||||
|
const otherCount = items.length - errorCount - warnCount;
|
||||||
|
if (otherCount > 0) parts.push(`${otherCount} notice${otherCount > 1 ? 's' : ''}`);
|
||||||
|
title.textContent = `Collection diagnostics — ${parts.join(', ')}`;
|
||||||
|
|
||||||
|
rows.innerHTML = '';
|
||||||
|
for (const item of items) {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.className = `parse-error-row parse-error-${item.severity || 'info'}`;
|
||||||
|
tr.innerHTML =
|
||||||
|
`<td>${escapeHtml(item.source || '')}</td>` +
|
||||||
|
`<td>${escapeHtml(item.path || item.category || '')}</td>` +
|
||||||
|
`<td>${escapeHtml(item.message || '')}</td>` +
|
||||||
|
`<td>${escapeHtml(item.detail || '')}</td>`;
|
||||||
|
rows.appendChild(tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
section.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
let parseErrorsCollapsed = false;
|
||||||
|
function toggleParseErrors() {
|
||||||
|
const body = document.getElementById('parse-errors-body');
|
||||||
|
const toggle = document.getElementById('parse-errors-toggle');
|
||||||
|
if (!body) return;
|
||||||
|
parseErrorsCollapsed = !parseErrorsCollapsed;
|
||||||
|
body.style.display = parseErrorsCollapsed ? 'none' : '';
|
||||||
|
toggle.textContent = parseErrorsCollapsed ? '▼' : '▲';
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadAuditViewer() {
|
function loadAuditViewer() {
|
||||||
@@ -1468,6 +1524,15 @@ async function clearData() {
|
|||||||
if (frame) {
|
if (frame) {
|
||||||
frame.src = 'about:blank';
|
frame.src = 'about:blank';
|
||||||
}
|
}
|
||||||
|
const parseErrSection = document.getElementById('parse-errors-section');
|
||||||
|
if (parseErrSection) parseErrSection.classList.add('hidden');
|
||||||
|
const parseErrRows = document.getElementById('parse-errors-rows');
|
||||||
|
if (parseErrRows) parseErrRows.innerHTML = '';
|
||||||
|
parseErrorsCollapsed = false;
|
||||||
|
const parseErrBody = document.getElementById('parse-errors-body');
|
||||||
|
if (parseErrBody) parseErrBody.style.display = '';
|
||||||
|
const parseErrToggle = document.getElementById('parse-errors-toggle');
|
||||||
|
if (parseErrToggle) parseErrToggle.textContent = '▲';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to clear data:', err);
|
console.error('Failed to clear data:', err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,6 +170,25 @@
|
|||||||
</iframe>
|
</iframe>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section id="parse-errors-section" class="parse-errors-section hidden">
|
||||||
|
<div class="parse-errors-header" onclick="toggleParseErrors()">
|
||||||
|
<span id="parse-errors-title">Collection warnings</span>
|
||||||
|
<span id="parse-errors-toggle" class="parse-errors-toggle">▲</span>
|
||||||
|
</div>
|
||||||
|
<div id="parse-errors-body" class="parse-errors-body">
|
||||||
|
<table class="parse-errors-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Source</th>
|
||||||
|
<th>Section</th>
|
||||||
|
<th>Message</th>
|
||||||
|
<th>Detail</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="parse-errors-rows"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user