Improve Lenovo XCC inventory enrichment

This commit is contained in:
Mikhail Chusavitin
2026-04-29 16:38:30 +03:00
parent aba7a54990
commit cf9cf5d0cf
2 changed files with 314 additions and 12 deletions
+174 -12
View File
@@ -100,9 +100,11 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
}
if f := findByPath(files, "tmp/inventory_ipmi_fru.log"); f != nil {
result.FRU = parseFRU(f.Content)
enrichBoardFromFRU(result)
}
if f := findByPath(files, "tmp/inventory_ipmi_sensor.log"); f != nil {
result.Sensors = parseSensors(f.Content)
result.Hardware.PowerSupply = enrichPSUsFromSensors(result.Hardware.PowerSupply, result.Sensors)
}
for _, f := range findEventFiles(files) {
result.Events = append(result.Events, parseEvents(f.Content)...)
@@ -341,9 +343,13 @@ func parseBasicSysInfo(content []byte, result *models.AnalysisResult) {
item := doc.Items[0]
result.Hardware.BoardInfo = models.BoardInfo{
ProductName: strings.TrimSpace(item.MachineTypeModel),
SerialNumber: strings.TrimSpace(item.SerialNumber),
UUID: strings.TrimSpace(item.UUID),
ProductName: cleanXCCValue(item.MachineTypeModel),
SerialNumber: cleanXCCValue(item.SerialNumber),
UUID: cleanXCCValue(item.UUID),
}
if host := cleanXCCValue(item.MachineName); host != "" {
result.TargetHost = host
}
if t, err := parseXCCTime(item.CurrentTime); err == nil {
@@ -464,17 +470,21 @@ func parseDisks(content []byte) []models.Storage {
stateStr := strings.TrimSpace(d.StateStr)
present := !strings.EqualFold(stateStr, "absent") &&
!strings.EqualFold(stateStr, "not present")
status := mapDiskHealthStatus(d.HealthStatus, stateStr)
disk := models.Storage{
Slot: fmt.Sprintf("%d", d.SlotNo),
Type: strings.TrimSpace(d.Media),
Model: strings.TrimSpace(d.ProductName),
Model: cleanXCCValue(d.ProductName),
SizeGB: sizeGB,
SerialNumber: strings.TrimSpace(d.SerialNo),
Manufacturer: strings.TrimSpace(d.Manufacture),
Firmware: strings.TrimSpace(d.FWVersion),
SerialNumber: cleanXCCValue(d.SerialNo),
Manufacturer: cleanXCCValue(d.Manufacture),
Firmware: cleanXCCValue(d.FWVersion),
Interface: strings.TrimSpace(d.Interface),
Present: present,
Status: stateStr,
Status: status,
}
if d.Temperature > 0 {
disk.Details = map[string]any{"temperature_c": d.Temperature}
}
if d.RemainLife >= 0 && d.RemainLife <= 100 {
v := d.RemainLife
@@ -551,13 +561,18 @@ func parsePSUs(content []byte) []models.PSU {
var out []models.PSU
for _, item := range doc.Items {
for _, p := range item.Power {
model := cleanXCCValue(p.FRUNumber)
if model == "" {
model = cleanXCCValue(p.PartNumber)
}
psu := models.PSU{
Slot: fmt.Sprintf("%d", p.Name),
Present: true,
Model: model,
WattageW: p.RatedPower,
SerialNumber: strings.TrimSpace(p.SerialNumber),
PartNumber: strings.TrimSpace(p.PartNumber),
Vendor: strings.TrimSpace(p.ManufID),
SerialNumber: cleanXCCValue(p.SerialNumber),
PartNumber: cleanXCCValue(p.PartNumber),
Vendor: cleanXCCValue(p.ManufID),
Status: strings.TrimSpace(p.Status),
}
out = append(out, psu)
@@ -611,11 +626,13 @@ func parseSensors(content []byte) []models.SensorReading {
if name == "" {
continue
}
unit := strings.TrimSpace(s.Unit)
sr := models.SensorReading{
Name: name,
RawValue: strings.TrimSpace(s.Value),
Unit: strings.TrimSpace(s.Unit),
Unit: unit,
Status: strings.TrimSpace(s.Status),
Type: classifySensorType(name, unit),
}
if v, err := strconv.ParseFloat(sr.RawValue, 64); err == nil {
sr.Value = v
@@ -646,6 +663,151 @@ func parseEvents(content []byte) []models.Event {
return out
}
// --- Cross-reference enrichment ---
// enrichBoardFromFRU sets BoardInfo.Manufacturer from the system board FRU entry
// when it is not already populated. Mirrors bee's board parsing from dmidecode type 1.
func enrichBoardFromFRU(result *models.AnalysisResult) {
if result.Hardware.BoardInfo.Manufacturer != "" {
return
}
for _, fru := range result.FRU {
desc := strings.ToLower(fru.Description)
if !strings.Contains(desc, "system board") &&
!strings.Contains(desc, "planar") &&
!strings.Contains(desc, "backplane") {
continue
}
if mfg := cleanXCCValue(fru.Manufacturer); mfg != "" {
result.Hardware.BoardInfo.Manufacturer = mfg
return
}
}
}
// psuSensorSlot extracts a 1-based PSU slot number from a sensor name.
// Recognises patterns: "PSU1 ...", "PSU 2 ...", "Power Supply 1 ...", "PWS1 ..."
var psuSensorSlotPattern = regexp.MustCompile(`(?i)(?:PSU|Power\s+Supply|PWS)\s*(\d+)`)
// enrichPSUsFromSensors cross-references sensor readings into PSU InputPowerW /
// OutputPowerW / InputVoltage. Mirrors bee's enrichPSUsWithTelemetry approach.
func enrichPSUsFromSensors(psus []models.PSU, sensors []models.SensorReading) []models.PSU {
if len(psus) == 0 || len(sensors) == 0 {
return psus
}
for i := range psus {
slot, err := strconv.Atoi(psus[i].Slot)
if err != nil {
continue
}
for _, s := range sensors {
m := psuSensorSlotPattern.FindStringSubmatch(s.Name)
if len(m) < 2 {
continue
}
sensorSlot, err := strconv.Atoi(m[1])
if err != nil || sensorSlot != slot {
continue
}
nameLower := strings.ToLower(s.Name)
switch {
case isPSUInputPower(nameLower):
psus[i].InputPowerW = int(s.Value)
case isPSUOutputPower(nameLower):
psus[i].OutputPowerW = int(s.Value)
case isPSUInputVoltage(nameLower):
psus[i].InputVoltage = s.Value
}
}
}
return psus
}
func isPSUInputPower(name string) bool {
return strings.Contains(name, "input power") ||
strings.Contains(name, "input watts") ||
strings.Contains(name, "_pin") ||
strings.Contains(name, " pin")
}
func isPSUOutputPower(name string) bool {
return strings.Contains(name, "output power") ||
strings.Contains(name, "output watts") ||
strings.Contains(name, "_pout") ||
strings.Contains(name, " pout")
}
func isPSUInputVoltage(name string) bool {
return strings.Contains(name, "input voltage") ||
strings.Contains(name, "ac voltage") ||
strings.Contains(name, "_vin") ||
strings.Contains(name, " vin")
}
// mapDiskHealthStatus maps an XCC disk healthStatus integer to a canonical status
// string. Mirrors bee's mapRAIDDriveStatus logic.
// XCC codes: 1=Warning, 2=Normal, 3=Critical, 4=PredictiveFailure; 0=Unknown.
func mapDiskHealthStatus(code int, stateStr string) string {
switch code {
case 2:
return "OK"
case 1, 4:
return "Warning"
case 3:
return "Critical"
default:
if stateStr != "" {
return stateStr
}
return "Unknown"
}
}
// classifySensorType returns a sensor category based on bee's classification logic:
// fan / temperature / power / voltage / current / other.
func classifySensorType(name, unit string) string {
u := strings.ToLower(strings.TrimSpace(unit))
switch u {
case "rpm":
return "fan"
case "c", "celsius", "°c":
return "temperature"
case "w", "watts":
return "power"
case "v", "volts":
return "voltage"
case "a", "amps":
return "current"
}
n := strings.ToLower(name)
switch {
case strings.Contains(n, "fan"):
return "fan"
case strings.Contains(n, "temp"):
return "temperature"
case strings.Contains(n, "power") || strings.Contains(n, " pwr"):
return "power"
case strings.Contains(n, "volt") || strings.Contains(n, " vin") || strings.Contains(n, " vout"):
return "voltage"
case strings.Contains(n, "curr") || strings.Contains(n, " amp"):
return "current"
default:
return "other"
}
}
// cleanXCCValue strips XCC placeholder strings, returning "" for non-values.
// Mirrors bee's cleanDMIValue for IPMI/XCC context.
func cleanXCCValue(v string) string {
v = strings.TrimSpace(v)
switch strings.ToLower(v) {
case "", "n/a", "na", "none", "unknown", "not available",
"not applicable", "not present", "not specified", "-":
return ""
}
return v
}
// --- Helpers ---
func xccSeverity(s, message string) models.Severity {