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 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__Speed (RPM); PWM duty cycle objects are // named Pwm_ (percent). FAN_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 }