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 }