fix(parser): support Inspur onekeylog per-file component/*.txt D-Bus layout
Some onekeylog BMC firmware variants split the combined component.log into per-file D-Bus GetAll transcripts under component/ (e.g. PowerSupplyInfo.txt, FanInfo.txt), which the inspur parser did not read, leaving PSU and fan data empty. Add a GETALL block parser and wire it as a fallback for PSU and fan telemetry when component.log is absent; document the layout and known gaps in bible-local. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
4ce0251ce4
commit
5677c49998
+204
@@ -0,0 +1,204 @@
|
||||
package inspur
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
// Some onekeylog BMC firmware variants do not produce a combined component.log.
|
||||
// Instead each component is dumped as its own file under component/ (e.g.
|
||||
// PowerSupplyInfo.txt, FanInfo.txt) using a raw D-Bus GetAll transcript:
|
||||
//
|
||||
// -------------------GETALL PSU0 OBJect-------------------------
|
||||
// Get service Name:: xyz.openbmc_project.PSUSensor
|
||||
// Get firmware Name :: PSU0
|
||||
// Get PSU0 ConfigData::{"type":"a{sv}" "data":[{"Manufacturer" "type":"s" "data":"APLUSPOWER"
|
||||
// "Model" "type":"s" "data":"AP-CA1300F12B7"
|
||||
// ...
|
||||
// "Present" "type":"b" "data":true}}]}
|
||||
//
|
||||
// This is not valid JSON (bare tab-separated key/type/data triples), so it needs
|
||||
// its own line-oriented extraction rather than encoding/json.
|
||||
|
||||
var dbusGetAllObjectHeaderRe = regexp.MustCompile(`-{5,}GETALL\s+(\S+)\s+OBJect-{5,}`)
|
||||
var psuSlotNameRe = regexp.MustCompile(`(?i)^PSU\d+$`)
|
||||
var fanSpeedNameRe = regexp.MustCompile(`(?i)^FAN\d+_\d+_Speed$`)
|
||||
var fanPwmNameRe = regexp.MustCompile(`(?i)^Pwm_\d+$`)
|
||||
var dbusFieldRe = regexp.MustCompile(`"(\w+)"\t"type":"[a-zA-Z]+"\t"data":("(?:[^"\\]|\\.)*"|true|false|-?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?)`)
|
||||
|
||||
// parseDBusGetAllObjects extracts one field map per "GETALL <name> OBJect" block
|
||||
// found in a component/*.txt transcript. The same object name can recur across
|
||||
// multiple command sections (e.g. Pwm_0 shows up both under the FanPWM sensor
|
||||
// query with a Value reading, and again under FanControl with only a Target
|
||||
// setpoint), so fields are unioned across occurrences, first-seen wins per key.
|
||||
func parseDBusGetAllObjects(text string) map[string]map[string]string {
|
||||
headers := dbusGetAllObjectHeaderRe.FindAllStringSubmatchIndex(text, -1)
|
||||
if len(headers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
objects := make(map[string]map[string]string, len(headers))
|
||||
for i, h := range headers {
|
||||
name := text[h[2]:h[3]]
|
||||
start := h[1]
|
||||
end := len(text)
|
||||
if i+1 < len(headers) {
|
||||
end = headers[i+1][0]
|
||||
}
|
||||
|
||||
fields, ok := objects[name]
|
||||
if !ok {
|
||||
fields = make(map[string]string)
|
||||
objects[name] = fields
|
||||
}
|
||||
for _, m := range dbusFieldRe.FindAllStringSubmatch(text[start:end], -1) {
|
||||
if _, exists := fields[m[1]]; !exists {
|
||||
fields[m[1]] = unquoteDBusValue(m[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
return objects
|
||||
}
|
||||
|
||||
func unquoteDBusValue(raw string) string {
|
||||
if len(raw) >= 2 && raw[0] == '"' && raw[len(raw)-1] == '"' {
|
||||
if unquoted, err := strconv.Unquote(raw); err == nil {
|
||||
return unquoted
|
||||
}
|
||||
return strings.Trim(raw, `"`)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func dbusFieldInt(s string) int {
|
||||
return int(dbusFieldFloat(s))
|
||||
}
|
||||
|
||||
// ParseComponentDirPowerSupply parses component/PowerSupplyInfo.txt, the per-file
|
||||
// PSU transcript used by onekeylog variants that lack a combined component.log.
|
||||
func ParseComponentDirPowerSupply(content []byte, hw *models.HardwareConfig) {
|
||||
if hw == nil {
|
||||
return
|
||||
}
|
||||
|
||||
objects := parseDBusGetAllObjects(string(content))
|
||||
if len(objects) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var merged []models.PSU
|
||||
seen := make(map[string]int)
|
||||
for _, existing := range hw.PowerSupply {
|
||||
key := inspurPSUKey(existing)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
seen[key] = len(merged)
|
||||
merged = append(merged, existing)
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(objects))
|
||||
for name := range objects {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
for _, name := range names {
|
||||
fields := objects[name]
|
||||
// "Total_Power_Sum" is also tagged Type=PowerSupply but is an aggregate
|
||||
// pseudo-object with no serial/model; only real PSUn slots are individual units.
|
||||
if fields["Type"] != "PowerSupply" || !psuSlotNameRe.MatchString(name) {
|
||||
continue
|
||||
}
|
||||
|
||||
item := models.PSU{
|
||||
Slot: name,
|
||||
Present: fields["Present"] == "true",
|
||||
Model: strings.TrimSpace(fields["Model"]),
|
||||
Vendor: strings.TrimSpace(fields["Manufacturer"]),
|
||||
WattageW: dbusFieldInt(fields["RatedPower"]),
|
||||
SerialNumber: strings.TrimSpace(fields["SerialNumber"]),
|
||||
PartNumber: strings.TrimSpace(fields["PartNumber"]),
|
||||
Firmware: strings.TrimSpace(fields["Version"]),
|
||||
}
|
||||
|
||||
key := inspurPSUKey(item)
|
||||
if idx, ok := seen[key]; ok {
|
||||
mergeInspurPSU(&merged[idx], item)
|
||||
continue
|
||||
}
|
||||
if key != "" {
|
||||
seen[key] = len(merged)
|
||||
}
|
||||
merged = append(merged, item)
|
||||
}
|
||||
|
||||
hw.PowerSupply = merged
|
||||
}
|
||||
|
||||
// ParseComponentDirFan parses component/FanInfo.txt, the per-file fan transcript
|
||||
// used by onekeylog variants that lack a combined component.log. Tachometer
|
||||
// objects are named FAN<n>_<rotor>_Speed (RPM); PWM duty cycle objects are
|
||||
// named Pwm_<n> (percent). FAN<n>_Status objects carry no telemetry, only
|
||||
// chassis associations, and are skipped.
|
||||
func ParseComponentDirFan(content []byte) []models.SensorReading {
|
||||
objects := parseDBusGetAllObjects(string(content))
|
||||
if len(objects) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(objects))
|
||||
for name := range objects {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
out := make([]models.SensorReading, 0, len(names))
|
||||
for _, name := range names {
|
||||
fields := objects[name]
|
||||
value := fields["Value"]
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case fanSpeedNameRe.MatchString(name):
|
||||
status := "OK"
|
||||
if fields["Functional"] == "false" || fields["Available"] == "false" {
|
||||
status = "Critical"
|
||||
}
|
||||
out = append(out, models.SensorReading{
|
||||
Name: name,
|
||||
Type: "fan_speed",
|
||||
Value: dbusFieldFloat(value),
|
||||
Unit: "RPM",
|
||||
RawValue: fmt.Sprintf("rpm=%s", value),
|
||||
Status: status,
|
||||
})
|
||||
case fanPwmNameRe.MatchString(name):
|
||||
out = append(out, models.SensorReading{
|
||||
Name: name,
|
||||
Type: "fan_pwm",
|
||||
Value: dbusFieldFloat(value),
|
||||
Unit: "%",
|
||||
RawValue: fmt.Sprintf("pct=%s", value),
|
||||
Status: "OK",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func dbusFieldFloat(s string) float64 {
|
||||
f, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return f
|
||||
}
|
||||
+19
-1
@@ -16,7 +16,7 @@ import (
|
||||
|
||||
// parserVersion - version of this parser module
|
||||
// IMPORTANT: Increment this version when making changes to parser logic!
|
||||
const parserVersion = "2.1"
|
||||
const parserVersion = "2.2"
|
||||
|
||||
func init() {
|
||||
parser.Register(&Parser{})
|
||||
@@ -59,6 +59,14 @@ func (p *Parser) Detect(files []parser.ExtractedFile) int {
|
||||
if strings.Contains(path, "component/component.log") {
|
||||
confidence += 15
|
||||
}
|
||||
// Per-file component/ variant (no combined component.log), e.g. dumps
|
||||
// named dump_<serial>_<timestamp>/ rather than onekeylog/.
|
||||
if strings.HasSuffix(path, "onekeylog_dreport.log") {
|
||||
confidence += 25
|
||||
}
|
||||
if strings.Contains(path, "component/powersupplyinfo.txt") {
|
||||
confidence += 15
|
||||
}
|
||||
|
||||
// Check for asset.json with Inspur-specific structure
|
||||
if strings.HasSuffix(path, "asset.json") {
|
||||
@@ -183,6 +191,16 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
||||
Description: desc,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Some onekeylog BMC variants split component.log into per-file
|
||||
// D-Bus transcripts under component/ instead of one combined log.
|
||||
if f := parser.FindFileByName(files, "PowerSupplyInfo.txt"); f != nil {
|
||||
ParseComponentDirPowerSupply(f.Content, result.Hardware)
|
||||
}
|
||||
if f := parser.FindFileByName(files, "FanInfo.txt"); f != nil {
|
||||
fanSensors := ParseComponentDirFan(f.Content)
|
||||
result.Sensors = mergeSensorReadings(result.Sensors, fanSensors)
|
||||
}
|
||||
}
|
||||
|
||||
// Enrich runtime component data from Redis snapshot (serials, FW, telemetry),
|
||||
|
||||
Reference in New Issue
Block a user