export: align reanimator contract v2.7
This commit is contained in:
135
internal/parser/fru_manufactured.go
Normal file
135
internal/parser/fru_manufactured.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
var manufacturedYearWeekPattern = regexp.MustCompile(`^\d{4}-W\d{2}$`)
|
||||
|
||||
// NormalizeManufacturedYearWeek converts common FRU manufacturing date formats
|
||||
// into contract-compatible YYYY-Www values. Unknown or ambiguous inputs return "".
|
||||
func NormalizeManufacturedYearWeek(raw string) string {
|
||||
value := strings.TrimSpace(raw)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
upper := strings.ToUpper(value)
|
||||
if manufacturedYearWeekPattern.MatchString(upper) {
|
||||
return upper
|
||||
}
|
||||
|
||||
layouts := []string{
|
||||
time.RFC3339,
|
||||
"2006-01-02T15:04:05",
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02",
|
||||
"2006/01/02",
|
||||
"01/02/2006 15:04:05",
|
||||
"01/02/2006",
|
||||
"01-02-2006",
|
||||
"Mon Jan 2 15:04:05 2006",
|
||||
"Mon Jan _2 15:04:05 2006",
|
||||
"Jan 2 2006",
|
||||
"Jan _2 2006",
|
||||
}
|
||||
for _, layout := range layouts {
|
||||
if ts, err := time.Parse(layout, value); err == nil {
|
||||
year, week := ts.ISOWeek()
|
||||
return formatYearWeek(year, week)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func formatYearWeek(year, week int) string {
|
||||
if year <= 0 || week <= 0 || week > 53 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%04d-W%02d", year, week)
|
||||
}
|
||||
|
||||
// ApplyManufacturedYearWeekFromFRU attaches normalized manufactured_year_week to
|
||||
// component details by exact serial-number match. Board-level FRU entries are not
|
||||
// expanded to components.
|
||||
func ApplyManufacturedYearWeekFromFRU(frus []models.FRUInfo, hw *models.HardwareConfig) {
|
||||
if hw == nil || len(frus) == 0 {
|
||||
return
|
||||
}
|
||||
bySerial := make(map[string]string, len(frus))
|
||||
for _, fru := range frus {
|
||||
serial := normalizeFRUSerial(fru.SerialNumber)
|
||||
yearWeek := NormalizeManufacturedYearWeek(fru.MfgDate)
|
||||
if serial == "" || yearWeek == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := bySerial[serial]; exists {
|
||||
continue
|
||||
}
|
||||
bySerial[serial] = yearWeek
|
||||
}
|
||||
if len(bySerial) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for i := range hw.CPUs {
|
||||
attachYearWeek(&hw.CPUs[i].Details, bySerial[normalizeFRUSerial(hw.CPUs[i].SerialNumber)])
|
||||
}
|
||||
for i := range hw.Memory {
|
||||
attachYearWeek(&hw.Memory[i].Details, bySerial[normalizeFRUSerial(hw.Memory[i].SerialNumber)])
|
||||
}
|
||||
for i := range hw.Storage {
|
||||
attachYearWeek(&hw.Storage[i].Details, bySerial[normalizeFRUSerial(hw.Storage[i].SerialNumber)])
|
||||
}
|
||||
for i := range hw.PCIeDevices {
|
||||
attachYearWeek(&hw.PCIeDevices[i].Details, bySerial[normalizeFRUSerial(hw.PCIeDevices[i].SerialNumber)])
|
||||
}
|
||||
for i := range hw.GPUs {
|
||||
attachYearWeek(&hw.GPUs[i].Details, bySerial[normalizeFRUSerial(hw.GPUs[i].SerialNumber)])
|
||||
}
|
||||
for i := range hw.NetworkAdapters {
|
||||
attachYearWeek(&hw.NetworkAdapters[i].Details, bySerial[normalizeFRUSerial(hw.NetworkAdapters[i].SerialNumber)])
|
||||
}
|
||||
for i := range hw.PowerSupply {
|
||||
attachYearWeek(&hw.PowerSupply[i].Details, bySerial[normalizeFRUSerial(hw.PowerSupply[i].SerialNumber)])
|
||||
}
|
||||
}
|
||||
|
||||
func attachYearWeek(details *map[string]any, yearWeek string) {
|
||||
if yearWeek == "" {
|
||||
return
|
||||
}
|
||||
if *details == nil {
|
||||
*details = map[string]any{}
|
||||
}
|
||||
if existing, ok := (*details)["manufactured_year_week"]; ok && strings.TrimSpace(toString(existing)) != "" {
|
||||
return
|
||||
}
|
||||
(*details)["manufactured_year_week"] = yearWeek
|
||||
}
|
||||
|
||||
func normalizeFRUSerial(v string) string {
|
||||
s := strings.TrimSpace(v)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
switch strings.ToUpper(s) {
|
||||
case "N/A", "NA", "NULL", "UNKNOWN", "-", "0":
|
||||
return ""
|
||||
default:
|
||||
return strings.ToUpper(s)
|
||||
}
|
||||
}
|
||||
|
||||
func toString(v any) string {
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
return x
|
||||
default:
|
||||
return strings.TrimSpace(fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
65
internal/parser/fru_manufactured_test.go
Normal file
65
internal/parser/fru_manufactured_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
func TestNormalizeManufacturedYearWeek(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"2024-W07", "2024-W07"},
|
||||
{"2024-02-13", "2024-W07"},
|
||||
{"02/13/2024", "2024-W07"},
|
||||
{"Tue Feb 13 12:00:00 2024", "2024-W07"},
|
||||
{"", ""},
|
||||
{"not-a-date", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := NormalizeManufacturedYearWeek(tt.in); got != tt.want {
|
||||
t.Fatalf("NormalizeManufacturedYearWeek(%q) = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyManufacturedYearWeekFromFRU_AttachesByExactSerial(t *testing.T) {
|
||||
hw := &models.HardwareConfig{
|
||||
PowerSupply: []models.PSU{
|
||||
{
|
||||
Slot: "PSU0",
|
||||
SerialNumber: "PSU-SN-001",
|
||||
},
|
||||
},
|
||||
Storage: []models.Storage{
|
||||
{
|
||||
Slot: "OB01",
|
||||
SerialNumber: "DISK-SN-001",
|
||||
},
|
||||
},
|
||||
}
|
||||
fru := []models.FRUInfo{
|
||||
{
|
||||
Description: "PSU0_FRU (ID 30)",
|
||||
SerialNumber: "PSU-SN-001",
|
||||
MfgDate: "2024-02-13",
|
||||
},
|
||||
{
|
||||
Description: "Builtin FRU Device (ID 0)",
|
||||
SerialNumber: "BOARD-SN-001",
|
||||
MfgDate: "2024-02-01",
|
||||
},
|
||||
}
|
||||
|
||||
ApplyManufacturedYearWeekFromFRU(fru, hw)
|
||||
|
||||
if got := hw.PowerSupply[0].Details["manufactured_year_week"]; got != "2024-W07" {
|
||||
t.Fatalf("expected PSU year week 2024-W07, got %#v", hw.PowerSupply[0].Details)
|
||||
}
|
||||
if hw.Storage[0].Details != nil {
|
||||
t.Fatalf("expected unmatched storage serial to stay untouched, got %#v", hw.Storage[0].Details)
|
||||
}
|
||||
}
|
||||
2
internal/parser/vendors/h3c/parser.go
vendored
2
internal/parser/vendors/h3c/parser.go
vendored
@@ -216,6 +216,7 @@ func parseH3CG5(files []parser.ExtractedFile) *models.AnalysisResult {
|
||||
}
|
||||
result.Hardware.Storage = dedupeStorage(result.Hardware.Storage)
|
||||
result.Hardware.Volumes = dedupeVolumes(result.Hardware.Volumes)
|
||||
parser.ApplyManufacturedYearWeekFromFRU(result.FRU, result.Hardware)
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -286,6 +287,7 @@ func parseH3CG6(files []parser.ExtractedFile) *models.AnalysisResult {
|
||||
}
|
||||
result.Hardware.Storage = dedupeStorage(result.Hardware.Storage)
|
||||
result.Hardware.Volumes = dedupeVolumes(result.Hardware.Volumes)
|
||||
parser.ApplyManufacturedYearWeekFromFRU(result.FRU, result.Hardware)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
33
internal/parser/vendors/inspur/event_logs_test.go
vendored
Normal file
33
internal/parser/vendors/inspur/event_logs_test.go
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
package inspur
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseIDLLog_UsesBMCSourceForEventLogs(t *testing.T) {
|
||||
content := []byte(`|2025-12-02T17:54:27+08:00|MEMORY|Assert|Warning|0C180401|CPU1_C4D0 Memory Device Disabled - Assert|`)
|
||||
|
||||
events := ParseIDLLog(content)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Source != "BMC" {
|
||||
t.Fatalf("expected IDL events to use BMC source, got %#v", events[0])
|
||||
}
|
||||
if events[0].SensorName != "CPU1_C4D0" {
|
||||
t.Fatalf("expected extracted DIMM component ref, got %#v", events[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSyslog_UsesHostSourceAndProcessAsSensorName(t *testing.T) {
|
||||
content := []byte(`<13>2026-03-15T14:03:11+00:00 host123 systemd[1]: Started Example Service`)
|
||||
|
||||
events := ParseSyslog(content, "syslog/info")
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Source != "syslog" {
|
||||
t.Fatalf("expected syslog source, got %#v", events[0])
|
||||
}
|
||||
if events[0].SensorName != "systemd[1]" {
|
||||
t.Fatalf("expected process name in sensor/component slot, got %#v", events[0])
|
||||
}
|
||||
}
|
||||
@@ -165,7 +165,10 @@ func TestParseIDLLog_ParsesStructuredJSONLine(t *testing.T) {
|
||||
if events[0].ID != "17FFB002" {
|
||||
t.Fatalf("expected event ID 17FFB002, got %q", events[0].ID)
|
||||
}
|
||||
if events[0].Source != "PCIE" {
|
||||
t.Fatalf("expected source PCIE, got %q", events[0].Source)
|
||||
if events[0].Source != "BMC" {
|
||||
t.Fatalf("expected BMC source for IDL event, got %q", events[0].Source)
|
||||
}
|
||||
if events[0].SensorType != "pcie" {
|
||||
t.Fatalf("expected component type pcie, got %#v", events[0])
|
||||
}
|
||||
}
|
||||
|
||||
2
internal/parser/vendors/inspur/idl.go
vendored
2
internal/parser/vendors/inspur/idl.go
vendored
@@ -60,7 +60,7 @@ func ParseIDLLog(content []byte) []models.Event {
|
||||
events = append(events, models.Event{
|
||||
ID: eventID,
|
||||
Timestamp: ts,
|
||||
Source: component,
|
||||
Source: "BMC",
|
||||
SensorType: strings.ToLower(component),
|
||||
SensorName: sensorName,
|
||||
EventType: eventType,
|
||||
|
||||
1
internal/parser/vendors/inspur/parser.go
vendored
1
internal/parser/vendors/inspur/parser.go
vendored
@@ -217,6 +217,7 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
||||
// Apply RAID disk serials from audit.log (authoritative: last non-NULL SN change).
|
||||
// These override redis/component.log serials which may be stale after disk replacement.
|
||||
applyRAIDSlotSerials(result.Hardware, raidSlotSerials)
|
||||
parser.ApplyManufacturedYearWeekFromFRU(result.FRU, result.Hardware)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
4
internal/parser/vendors/inspur/syslog.go
vendored
4
internal/parser/vendors/inspur/syslog.go
vendored
@@ -48,9 +48,9 @@ func ParseSyslog(content []byte, sourcePath string) []models.Event {
|
||||
event := models.Event{
|
||||
ID: generateEventID(sourcePath, lineNum),
|
||||
Timestamp: timestamp,
|
||||
Source: matches[4],
|
||||
Source: "syslog",
|
||||
SensorType: "syslog",
|
||||
SensorName: matches[3],
|
||||
SensorName: matches[4],
|
||||
Description: matches[5],
|
||||
Severity: severity,
|
||||
RawData: line,
|
||||
|
||||
Reference in New Issue
Block a user