Files
bee/audit/internal/collector/psu.go

130 lines
3.1 KiB
Go

package collector
import (
"bee/audit/internal/schema"
"log/slog"
"os/exec"
"strconv"
"strings"
)
func collectPSUs() []schema.HardwarePowerSupply {
// ipmitool requires /dev/ipmi0 — not available on non-server hardware
out, err := exec.Command("ipmitool", "fru", "print").Output()
if err != nil {
slog.Info("psu: ipmitool unavailable, skipping", "err", err)
return nil
}
psus := parseFRU(string(out))
slog.Info("psu: collected", "count", len(psus))
return psus
}
// parseFRU parses ipmitool fru print output.
// Each FRU record starts with "FRU Device Description : <name> (ID <n>)"
// followed by indented key: value lines.
func parseFRU(output string) []schema.HardwarePowerSupply {
var psus []schema.HardwarePowerSupply
slot := 0
for _, block := range splitFRUBlocks(output) {
psu, ok := parseFRUBlock(block, slot)
if !ok {
continue
}
psus = append(psus, psu)
slot++
}
return psus
}
func splitFRUBlocks(output string) []string {
var blocks []string
var cur strings.Builder
for _, line := range strings.Split(output, "\n") {
if strings.HasPrefix(line, "FRU Device Description") {
if cur.Len() > 0 {
blocks = append(blocks, cur.String())
cur.Reset()
}
}
cur.WriteString(line)
cur.WriteByte('\n')
}
if cur.Len() > 0 {
blocks = append(blocks, cur.String())
}
return blocks
}
func parseFRUBlock(block string, slotIdx int) (schema.HardwarePowerSupply, bool) {
fields := map[string]string{}
header := ""
for _, line := range strings.Split(block, "\n") {
if strings.HasPrefix(line, "FRU Device Description") {
header = line
continue
}
idx := strings.Index(line, " : ")
if idx < 0 {
continue
}
key := strings.TrimSpace(line[:idx])
val := strings.TrimSpace(line[idx+3:])
fields[key] = val
}
// Only process PSU FRU records
headerLower := strings.ToLower(header)
if !strings.Contains(headerLower, "psu") &&
!strings.Contains(headerLower, "power supply") &&
!strings.Contains(headerLower, "power_supply") {
return schema.HardwarePowerSupply{}, false
}
present := true
psu := schema.HardwarePowerSupply{Present: &present}
slotStr := strconv.Itoa(slotIdx)
psu.Slot = &slotStr
if v := cleanDMIValue(fields["Board Product"]); v != "" {
psu.Model = &v
}
if v := cleanDMIValue(fields["Board Mfg"]); v != "" {
psu.Vendor = &v
}
if v := cleanDMIValue(fields["Board Serial"]); v != "" {
psu.SerialNumber = &v
}
if v := cleanDMIValue(fields["Board Part Number"]); v != "" {
psu.PartNumber = &v
}
if v := cleanDMIValue(fields["Board Extra"]); v != "" {
psu.Firmware = &v
}
// wattage: some vendors put it in product name e.g. "PSU 800W"
if psu.Model != nil {
if w := parseWattage(*psu.Model); w > 0 {
psu.WattageW = &w
}
}
status := "OK"
psu.Status = &status
return psu, true
}
// parseWattage extracts wattage from strings like "PSU 800W", "1200W PLATINUM".
func parseWattage(s string) int {
s = strings.ToUpper(s)
for _, part := range strings.Fields(s) {
part = strings.TrimSuffix(part, "W")
if n, err := strconv.Atoi(part); err == nil && n > 0 && n <= 5000 {
return n
}
}
return 0
}