improve redfish collection progress and robust hardware dedup/serial parsing
This commit is contained in:
258
internal/parser/vendors/inspur/component.go
vendored
258
internal/parser/vendors/inspur/component.go
vendored
@@ -46,10 +46,21 @@ func ParseComponentLogEvents(content []byte) []models.Event {
|
||||
// Parse RESTful Memory info for Warning/Error status
|
||||
memEvents := parseMemoryEvents(text)
|
||||
events = append(events, memEvents...)
|
||||
events = append(events, parseFanEvents(text)...)
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
// ParseComponentLogSensors extracts sensor readings from component.log JSON sections.
|
||||
func ParseComponentLogSensors(content []byte) []models.SensorReading {
|
||||
text := string(content)
|
||||
var out []models.SensorReading
|
||||
out = append(out, parseFanSensors(text)...)
|
||||
out = append(out, parseDiskBackplaneSensors(text)...)
|
||||
out = append(out, parsePSUSummarySensors(text)...)
|
||||
return out
|
||||
}
|
||||
|
||||
// MemoryRESTInfo represents the RESTful Memory info structure
|
||||
type MemoryRESTInfo struct {
|
||||
MemModules []struct {
|
||||
@@ -224,6 +235,30 @@ func parseHDDInfo(text string, hw *models.HardwareConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
// Merge into existing inventory first (asset/other sections).
|
||||
for i := range hw.Storage {
|
||||
slot := strings.TrimSpace(hw.Storage[i].Slot)
|
||||
if slot == "" {
|
||||
continue
|
||||
}
|
||||
detail, ok := hddMap[slot]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if hw.Storage[i].SerialNumber == "" {
|
||||
hw.Storage[i].SerialNumber = detail.SN
|
||||
}
|
||||
if hw.Storage[i].Model == "" {
|
||||
hw.Storage[i].Model = detail.Model
|
||||
}
|
||||
if hw.Storage[i].Firmware == "" {
|
||||
hw.Storage[i].Firmware = detail.Firmware
|
||||
}
|
||||
if hw.Storage[i].Manufacturer == "" {
|
||||
hw.Storage[i].Manufacturer = detail.Mfr
|
||||
}
|
||||
}
|
||||
|
||||
// If storage is empty, populate from HDD info
|
||||
if len(hw.Storage) == 0 {
|
||||
for _, hdd := range hddInfo {
|
||||
@@ -255,6 +290,22 @@ func parseHDDInfo(text string, hw *models.HardwareConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
// FanRESTInfo represents the RESTful fan info structure.
|
||||
type FanRESTInfo struct {
|
||||
Fans []struct {
|
||||
ID int `json:"id"`
|
||||
FanName string `json:"fan_name"`
|
||||
Present string `json:"present"`
|
||||
Status string `json:"status"`
|
||||
StatusStr string `json:"status_str"`
|
||||
SpeedRPM int `json:"speed_rpm"`
|
||||
SpeedPercent int `json:"speed_percent"`
|
||||
MaxSpeedRPM int `json:"max_speed_rpm"`
|
||||
FanModel string `json:"fan_model"`
|
||||
} `json:"fans"`
|
||||
FansPower int `json:"fans_power"`
|
||||
}
|
||||
|
||||
// NetworkAdapterRESTInfo represents the RESTful Network Adapter info structure
|
||||
type NetworkAdapterRESTInfo struct {
|
||||
SysAdapters []struct {
|
||||
@@ -335,6 +386,213 @@ func parseNetworkAdapterInfo(text string, hw *models.HardwareConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
func parseFanSensors(text string) []models.SensorReading {
|
||||
re := regexp.MustCompile(`RESTful fan info:\s*(\{[\s\S]*?\})\s*RESTful diskbackplane`)
|
||||
match := re.FindStringSubmatch(text)
|
||||
if match == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
jsonStr := strings.ReplaceAll(match[1], "\n", "")
|
||||
var fanInfo FanRESTInfo
|
||||
if err := json.Unmarshal([]byte(jsonStr), &fanInfo); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]models.SensorReading, 0, len(fanInfo.Fans)+1)
|
||||
for _, fan := range fanInfo.Fans {
|
||||
name := strings.TrimSpace(fan.FanName)
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("FAN%d", fan.ID)
|
||||
}
|
||||
status := normalizeComponentStatus(fan.StatusStr, fan.Status, fan.Present)
|
||||
raw := fmt.Sprintf("rpm=%d pct=%d model=%s max_rpm=%d", fan.SpeedRPM, fan.SpeedPercent, fan.FanModel, fan.MaxSpeedRPM)
|
||||
out = append(out, models.SensorReading{
|
||||
Name: name,
|
||||
Type: "fan_speed",
|
||||
Value: float64(fan.SpeedRPM),
|
||||
Unit: "RPM",
|
||||
RawValue: raw,
|
||||
Status: status,
|
||||
})
|
||||
}
|
||||
|
||||
if fanInfo.FansPower > 0 {
|
||||
out = append(out, models.SensorReading{
|
||||
Name: "Fans_Power",
|
||||
Type: "power",
|
||||
Value: float64(fanInfo.FansPower),
|
||||
Unit: "W",
|
||||
RawValue: fmt.Sprintf("%d", fanInfo.FansPower),
|
||||
Status: "OK",
|
||||
})
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func parseFanEvents(text string) []models.Event {
|
||||
re := regexp.MustCompile(`RESTful fan info:\s*(\{[\s\S]*?\})\s*RESTful diskbackplane`)
|
||||
match := re.FindStringSubmatch(text)
|
||||
if match == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
jsonStr := strings.ReplaceAll(match[1], "\n", "")
|
||||
var fanInfo FanRESTInfo
|
||||
if err := json.Unmarshal([]byte(jsonStr), &fanInfo); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var events []models.Event
|
||||
for _, fan := range fanInfo.Fans {
|
||||
status := normalizeComponentStatus(fan.StatusStr, fan.Status, fan.Present)
|
||||
if isHealthyComponentStatus(status) {
|
||||
continue
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(fan.FanName)
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("FAN%d", fan.ID)
|
||||
}
|
||||
|
||||
severity := models.SeverityWarning
|
||||
lowStatus := strings.ToLower(status)
|
||||
if strings.Contains(lowStatus, "critical") || strings.Contains(lowStatus, "fail") || strings.Contains(lowStatus, "error") {
|
||||
severity = models.SeverityCritical
|
||||
}
|
||||
|
||||
events = append(events, models.Event{
|
||||
ID: fmt.Sprintf("fan_%d_status", fan.ID),
|
||||
Timestamp: time.Now(),
|
||||
Source: "Fan",
|
||||
SensorType: "fan",
|
||||
SensorName: name,
|
||||
EventType: "Fan Status",
|
||||
Severity: severity,
|
||||
Description: fmt.Sprintf("%s reports %s", name, status),
|
||||
RawData: fmt.Sprintf("rpm=%d pct=%d model=%s", fan.SpeedRPM, fan.SpeedPercent, fan.FanModel),
|
||||
})
|
||||
}
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
func parseDiskBackplaneSensors(text string) []models.SensorReading {
|
||||
re := regexp.MustCompile(`RESTful diskbackplane info:\s*(\[[\s\S]*?\])\s*BMC`)
|
||||
match := re.FindStringSubmatch(text)
|
||||
if match == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
jsonStr := strings.ReplaceAll(match[1], "\n", "")
|
||||
var backplaneInfo DiskBackplaneRESTInfo
|
||||
if err := json.Unmarshal([]byte(jsonStr), &backplaneInfo); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]models.SensorReading, 0, len(backplaneInfo))
|
||||
for _, bp := range backplaneInfo {
|
||||
if bp.Present != 1 {
|
||||
continue
|
||||
}
|
||||
name := fmt.Sprintf("Backplane%d_Temp", bp.BackplaneIndex)
|
||||
status := "OK"
|
||||
if bp.Temperature <= 0 {
|
||||
status = "unknown"
|
||||
}
|
||||
raw := fmt.Sprintf("front=%d ports=%d drives=%d cpld=%s", bp.Front, bp.PortCount, bp.DriverCount, bp.CPLDVersion)
|
||||
out = append(out, models.SensorReading{
|
||||
Name: name,
|
||||
Type: "temperature",
|
||||
Value: float64(bp.Temperature),
|
||||
Unit: "C",
|
||||
RawValue: raw,
|
||||
Status: status,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parsePSUSummarySensors(text string) []models.SensorReading {
|
||||
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful Network`)
|
||||
match := re.FindStringSubmatch(text)
|
||||
if match == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
jsonStr := strings.ReplaceAll(match[1], "\n", "")
|
||||
var psuInfo PSURESTInfo
|
||||
if err := json.Unmarshal([]byte(jsonStr), &psuInfo); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]models.SensorReading, 0, len(psuInfo.PowerSupplies)*3+1)
|
||||
if psuInfo.PresentPowerReading > 0 {
|
||||
out = append(out, models.SensorReading{
|
||||
Name: "PSU_Present_Power_Reading",
|
||||
Type: "power",
|
||||
Value: float64(psuInfo.PresentPowerReading),
|
||||
Unit: "W",
|
||||
RawValue: fmt.Sprintf("%d", psuInfo.PresentPowerReading),
|
||||
Status: "OK",
|
||||
})
|
||||
}
|
||||
|
||||
for _, psu := range psuInfo.PowerSupplies {
|
||||
if psu.Present != 1 {
|
||||
continue
|
||||
}
|
||||
status := normalizeComponentStatus(psu.Status)
|
||||
out = append(out, models.SensorReading{
|
||||
Name: fmt.Sprintf("PSU%d_InputPower", psu.ID),
|
||||
Type: "power",
|
||||
Value: float64(psu.PSInPower),
|
||||
Unit: "W",
|
||||
RawValue: fmt.Sprintf("%d", psu.PSInPower),
|
||||
Status: status,
|
||||
})
|
||||
out = append(out, models.SensorReading{
|
||||
Name: fmt.Sprintf("PSU%d_OutputPower", psu.ID),
|
||||
Type: "power",
|
||||
Value: float64(psu.PSOutPower),
|
||||
Unit: "W",
|
||||
RawValue: fmt.Sprintf("%d", psu.PSOutPower),
|
||||
Status: status,
|
||||
})
|
||||
out = append(out, models.SensorReading{
|
||||
Name: fmt.Sprintf("PSU%d_Temp", psu.ID),
|
||||
Type: "temperature",
|
||||
Value: float64(psu.PSUMaxTemp),
|
||||
Unit: "C",
|
||||
RawValue: fmt.Sprintf("%d", psu.PSUMaxTemp),
|
||||
Status: status,
|
||||
})
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeComponentStatus(values ...string) string {
|
||||
for _, v := range values {
|
||||
s := strings.TrimSpace(v)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
return s
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func isHealthyComponentStatus(status string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(status)) {
|
||||
case "", "ok", "normal", "present", "enabled":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var rawDeviceIDLikeRegex = regexp.MustCompile(`(?i)^(?:0x)?[0-9a-f]{3,4}$`)
|
||||
|
||||
func looksLikeRawDeviceID(v string) bool {
|
||||
|
||||
114
internal/parser/vendors/inspur/component_test.go
vendored
114
internal/parser/vendors/inspur/component_test.go
vendored
@@ -50,3 +50,117 @@ RESTful fan`
|
||||
t.Fatalf("expected NIC vendor resolved from pci.ids")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseComponentLogSensors_ExtractsFanBackplaneAndPSUSummary(t *testing.T) {
|
||||
text := `RESTful PSU info:
|
||||
{
|
||||
"power_supplies": [
|
||||
{ "id": 0, "present": 1, "status": "OK", "ps_in_power": 123, "ps_out_power": 110, "psu_max_temperature": 41 }
|
||||
],
|
||||
"present_power_reading": 999
|
||||
}
|
||||
RESTful Network Adapter info:
|
||||
{ "sys_adapters": [] }
|
||||
RESTful fan info:
|
||||
{
|
||||
"fans": [
|
||||
{ "id": 1, "fan_name": "FAN0_F_Speed", "present": "OK", "status": "OK", "status_str": "OK", "speed_rpm": 9200, "speed_percent": 35, "max_speed_rpm": 20000, "fan_model": "6056" }
|
||||
],
|
||||
"fans_power": 33
|
||||
}
|
||||
RESTful diskbackplane info:
|
||||
[
|
||||
{ "port_count": 8, "driver_count": 4, "front": 1, "backplane_index": 0, "present": 1, "cpld_version": "3.1", "temperature": 18 }
|
||||
]
|
||||
BMC`
|
||||
|
||||
sensors := ParseComponentLogSensors([]byte(text))
|
||||
if len(sensors) == 0 {
|
||||
t.Fatalf("expected sensors from component.log, got none")
|
||||
}
|
||||
|
||||
has := func(name string) bool {
|
||||
for _, s := range sensors {
|
||||
if s.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if !has("FAN0_F_Speed") {
|
||||
t.Fatalf("expected FAN0_F_Speed sensor in parsed output")
|
||||
}
|
||||
if !has("Backplane0_Temp") {
|
||||
t.Fatalf("expected Backplane0_Temp sensor in parsed output")
|
||||
}
|
||||
if !has("PSU_Present_Power_Reading") {
|
||||
t.Fatalf("expected PSU_Present_Power_Reading sensor in parsed output")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseComponentLogEvents_FanCriticalStatus(t *testing.T) {
|
||||
text := `RESTful fan info:
|
||||
{
|
||||
"fans": [
|
||||
{ "id": 7, "fan_name": "FAN3_R_Speed", "present": "OK", "status": "Critical", "status_str": "Critical", "speed_rpm": 0, "speed_percent": 0, "max_speed_rpm": 20000, "fan_model": "6056" }
|
||||
],
|
||||
"fans_power": 0
|
||||
}
|
||||
RESTful diskbackplane info:
|
||||
[]
|
||||
BMC`
|
||||
|
||||
events := ParseComponentLogEvents([]byte(text))
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 fan event, got %d", len(events))
|
||||
}
|
||||
if events[0].EventType != "Fan Status" {
|
||||
t.Fatalf("expected Fan Status event type, got %q", events[0].EventType)
|
||||
}
|
||||
if events[0].Severity != models.SeverityCritical {
|
||||
t.Fatalf("expected critical severity, got %q", events[0].Severity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHDDInfo_MergesIntoExistingStorage(t *testing.T) {
|
||||
text := `RESTful HDD info:
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"present": 1,
|
||||
"enable": 1,
|
||||
"SN": "SER123",
|
||||
"model": "Sample SSD",
|
||||
"capacity": 1024,
|
||||
"manufacture": "ACME",
|
||||
"firmware": "1.0.0",
|
||||
"locationstring": "OB01",
|
||||
"capablespeed": 6
|
||||
}
|
||||
]
|
||||
RESTful PSU`
|
||||
|
||||
hw := &models.HardwareConfig{
|
||||
Storage: []models.Storage{
|
||||
{
|
||||
Slot: "OB01",
|
||||
Type: "SSD",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
parseHDDInfo(text, hw)
|
||||
if len(hw.Storage) != 1 {
|
||||
t.Fatalf("expected 1 storage item, got %d", len(hw.Storage))
|
||||
}
|
||||
if hw.Storage[0].SerialNumber != "SER123" {
|
||||
t.Fatalf("expected serial from HDD section, got %q", hw.Storage[0].SerialNumber)
|
||||
}
|
||||
if hw.Storage[0].Model != "Sample SSD" {
|
||||
t.Fatalf("expected model from HDD section, got %q", hw.Storage[0].Model)
|
||||
}
|
||||
if hw.Storage[0].Firmware != "1.0.0" {
|
||||
t.Fatalf("expected firmware from HDD section, got %q", hw.Storage[0].Firmware)
|
||||
}
|
||||
}
|
||||
|
||||
42
internal/parser/vendors/inspur/parser.go
vendored
42
internal/parser/vendors/inspur/parser.go
vendored
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
// parserVersion - version of this parser module
|
||||
// IMPORTANT: Increment this version when making changes to parser logic!
|
||||
const parserVersion = "1.2.1"
|
||||
const parserVersion = "1.3.0"
|
||||
|
||||
func init() {
|
||||
parser.Register(&Parser{})
|
||||
@@ -123,6 +123,11 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
||||
// Extract events from component.log (memory errors, etc.)
|
||||
componentEvents := ParseComponentLogEvents(f.Content)
|
||||
result.Events = append(result.Events, componentEvents...)
|
||||
|
||||
// Extract additional telemetry sensors from component.log sections
|
||||
// (fan RPM, backplane temperature, PSU summary power, etc.).
|
||||
componentSensors := ParseComponentLogSensors(f.Content)
|
||||
result.Sensors = mergeSensorReadings(result.Sensors, componentSensors)
|
||||
}
|
||||
|
||||
// Enrich runtime component data from Redis snapshot (serials, FW, telemetry),
|
||||
@@ -262,3 +267,38 @@ func extractSlotNumberFromGPU(slot string) int {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func mergeSensorReadings(base, extra []models.SensorReading) []models.SensorReading {
|
||||
if len(extra) == 0 {
|
||||
return base
|
||||
}
|
||||
|
||||
out := append([]models.SensorReading{}, base...)
|
||||
seen := make(map[string]struct{}, len(out))
|
||||
for _, s := range out {
|
||||
if key := sensorMergeKey(s); key != "" {
|
||||
seen[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range extra {
|
||||
key := sensorMergeKey(s)
|
||||
if key != "" {
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func sensorMergeKey(s models.SensorReading) string {
|
||||
name := strings.ToLower(strings.TrimSpace(s.Name))
|
||||
if name == "" {
|
||||
return ""
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user