Files
logpile/internal/parser/vendors/inspur/component_dbus.go
T
Mikhail ChusavitinandClaude Sonnet 5 5677c49998 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>
2026-07-23 12:24:17 +03:00

205 lines
5.8 KiB
Go

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
}