From b89580c24d713d86c8fe7e902b52341ec0ef38a9 Mon Sep 17 00:00:00 2001 From: Michael Chus Date: Sun, 19 Apr 2026 18:39:21 +0300 Subject: [PATCH] Fix PSU power chart: use name-based SDR matching instead of entity ID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MSI servers place PSU_POWER_IN/OUT sensors on entity 3.0, not 10.N (the IPMI "Power Supply" entity). The old parser filtered by entity ID and found nothing, so the dashboard fell back to DCMI which reports roughly half the actual draw. Now delegates to collector.PSUSlotsFromSDR — the same name-based matching already used in the Power Fit benchmark. Co-Authored-By: Claude Sonnet 4.6 --- audit/internal/platform/live_metrics.go | 81 ++++++++++--------------- 1 file changed, 32 insertions(+), 49 deletions(-) diff --git a/audit/internal/platform/live_metrics.go b/audit/internal/platform/live_metrics.go index 783e7ef..b9228b1 100644 --- a/audit/internal/platform/live_metrics.go +++ b/audit/internal/platform/live_metrics.go @@ -1,8 +1,10 @@ package platform import ( + "bee/audit/internal/collector" "bufio" "encoding/json" + "fmt" "os" "os/exec" "sort" @@ -339,63 +341,44 @@ func compactAmbientTempName(chip, name string) string { } // samplePSUPower reads per-PSU input power via IPMI SDR. -// It parses `ipmitool sdr elist full` output looking for Power Supply entity -// sensors (entity ID "10.N") that report a value in Watts. +// Uses collector.PSUSlotsFromSDR (name-based matching) which works across +// vendors where PSU sensors may not carry entity ID "10.N". // Returns nil when IPMI is unavailable or no PSU Watt sensors exist. func samplePSUPower() []PSUReading { - out, err := exec.Command("ipmitool", "sdr", "elist", "full").Output() + out, err := exec.Command("ipmitool", "sdr").Output() if err != nil || len(out) == 0 { return nil } - // map slot → reading (keep highest-watt value per slot in case of duplicates) - type entry struct { - name string - powerW float64 - } - bySlot := map[int]entry{} - for _, line := range strings.Split(string(out), "\n") { - parts := strings.Split(line, "|") - if len(parts) < 5 { - continue - } - entityID := strings.TrimSpace(parts[3]) // e.g. "10.1" - if !strings.HasPrefix(entityID, "10.") { - continue // not a Power Supply entity - } - slotStr := strings.TrimPrefix(entityID, "10.") - slot, err := strconv.Atoi(slotStr) - if err != nil { - continue - } - valueField := strings.TrimSpace(parts[4]) // e.g. "740.00 Watts" - if !strings.Contains(strings.ToLower(valueField), "watts") { - continue - } - valueFields := strings.Fields(valueField) - if len(valueFields) < 2 { - continue - } - w, err := strconv.ParseFloat(valueFields[0], 64) - if err != nil || w <= 0 { - continue - } - sensorName := strings.TrimSpace(parts[0]) - if existing, ok := bySlot[slot]; !ok || w > existing.powerW { - bySlot[slot] = entry{name: sensorName, powerW: w} - } - } - if len(bySlot) == 0 { + slots := collector.PSUSlotsFromSDR(string(out)) + if len(slots) == 0 { return nil } - slots := make([]int, 0, len(bySlot)) - for s := range bySlot { - slots = append(slots, s) + // Collect slot keys and sort for stable output. + keys := make([]int, 0, len(slots)) + for k := range slots { + n, err := strconv.Atoi(k) + if err == nil { + keys = append(keys, n) + } } - sort.Ints(slots) - psus := make([]PSUReading, 0, len(slots)) - for _, s := range slots { - e := bySlot[s] - psus = append(psus, PSUReading{Slot: s, Name: e.name, PowerW: e.powerW}) + sort.Ints(keys) + psus := make([]PSUReading, 0, len(keys)) + for _, k := range keys { + entry := slots[strconv.Itoa(k)] + // Prefer AC input power; fall back to DC output power. + var w float64 + if entry.InputW != nil && *entry.InputW > 0 { + w = *entry.InputW + } else if entry.OutputW != nil && *entry.OutputW > 0 { + w = *entry.OutputW + } + if w <= 0 { + continue + } + psus = append(psus, PSUReading{Slot: k + 1, Name: fmt.Sprintf("PSU%d", k+1), PowerW: w}) + } + if len(psus) == 0 { + return nil } return psus }