add: PSU collector (1.7) via ipmitool fru, skips gracefully without IPMI
This commit is contained in:
@@ -28,8 +28,9 @@ func Run() schema.HardwareIngestRequest {
|
||||
snap.Memory = collectMemory()
|
||||
snap.Storage = collectStorage()
|
||||
snap.PCIeDevices = collectPCIe()
|
||||
snap.PowerSupplies = collectPSUs()
|
||||
|
||||
// remaining collectors added in steps 1.7 – 1.10
|
||||
// remaining collectors added in steps 1.8 – 1.10
|
||||
|
||||
slog.Info("audit completed", "duration", time.Since(start).Round(time.Millisecond))
|
||||
|
||||
|
||||
129
audit/internal/collector/psu.go
Normal file
129
audit/internal/collector/psu.go
Normal file
@@ -0,0 +1,129 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user