Implement audit enrichments, TUI workflows, and production ISO scaffold

This commit is contained in:
Mikhail Chusavitin
2026-03-06 11:56:26 +03:00
parent bdfb6a0a79
commit 18b8c69bc5
32 changed files with 3187 additions and 9 deletions

View File

@@ -28,6 +28,11 @@ func Run() schema.HardwareIngestRequest {
snap.Memory = collectMemory()
snap.Storage = collectStorage()
snap.PCIeDevices = collectPCIe()
snap.PCIeDevices = enrichPCIeWithNVIDIA(snap.PCIeDevices, snap.Board.SerialNumber)
snap.PCIeDevices = enrichPCIeWithMellanox(snap.PCIeDevices)
snap.PCIeDevices = enrichPCIeWithNICTelemetry(snap.PCIeDevices)
snap.Storage = enrichStorageWithVROC(snap.Storage, snap.PCIeDevices)
snap.Storage = appendUniqueStorage(snap.Storage, collectRAIDStorage(snap.PCIeDevices))
snap.PowerSupplies = collectPSUs()
// remaining collectors added in steps 1.8 1.10

View File

@@ -0,0 +1,164 @@
package collector
import (
"bee/audit/internal/schema"
"log/slog"
"os"
"os/exec"
"path/filepath"
"strings"
)
const mellanoxVendorID = 0x15b3
var (
mstflintQuery = func(bdf string) (string, error) {
out, err := exec.Command("mstflint", "-d", bdf, "q").Output()
if err != nil {
return "", err
}
return string(out), nil
}
ethtoolInfoQuery = func(iface string) (string, error) {
out, err := exec.Command("ethtool", "-i", iface).Output()
if err != nil {
return "", err
}
return string(out), nil
}
netIfacesByBDF = listNetIfacesByBDF
)
// enrichPCIeWithMellanox enriches Mellanox/NVIDIA Networking devices with
// firmware/serial information from mstflint, with ethtool fallback for firmware.
func enrichPCIeWithMellanox(devs []schema.HardwarePCIeDevice) []schema.HardwarePCIeDevice {
enriched := 0
for i := range devs {
if !isMellanoxDevice(devs[i]) {
continue
}
bdf := ""
if devs[i].BDF != nil {
bdf = normalizePCIeBDF(*devs[i].BDF)
}
if bdf == "" {
continue
}
fw, serial := queryMellanoxFromMstflint(bdf)
if fw == "" {
fw = queryFirmwareFromEthtool(bdf)
}
if fw != "" {
devs[i].Firmware = &fw
}
if serial != "" {
devs[i].SerialNumber = &serial
}
if fw != "" || serial != "" {
enriched++
}
}
slog.Info("mellanox: enriched", "count", enriched)
return devs
}
func isMellanoxDevice(dev schema.HardwarePCIeDevice) bool {
if dev.VendorID != nil && *dev.VendorID == mellanoxVendorID {
return true
}
if dev.Manufacturer != nil {
m := strings.ToLower(*dev.Manufacturer)
if strings.Contains(m, "mellanox") || strings.Contains(m, "nvidia networking") {
return true
}
}
return false
}
func queryMellanoxFromMstflint(bdf string) (firmware, serial string) {
out, err := mstflintQuery(bdf)
if err != nil {
return "", ""
}
return parseMstflintQuery(out)
}
func parseMstflintQuery(raw string) (firmware, serial string) {
for _, line := range strings.Split(raw, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
idx := strings.Index(line, ":")
if idx < 0 {
continue
}
key := strings.ToLower(strings.TrimSpace(line[:idx]))
val := strings.TrimSpace(line[idx+1:])
switch key {
case "fw version":
if val != "" {
firmware = val
}
case "board serial number":
if val != "" {
serial = val
}
}
}
return firmware, serial
}
func queryFirmwareFromEthtool(bdf string) string {
for _, iface := range netIfacesByBDF(bdf) {
out, err := ethtoolInfoQuery(iface)
if err != nil {
continue
}
if fw := parseEthtoolFirmwareInfo(out); fw != "" {
return fw
}
}
return ""
}
func parseEthtoolFirmwareInfo(raw string) string {
for _, line := range strings.Split(raw, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
idx := strings.Index(line, ":")
if idx < 0 {
continue
}
key := strings.ToLower(strings.TrimSpace(line[:idx]))
val := strings.TrimSpace(line[idx+1:])
if key == "firmware-version" && val != "" {
return val
}
}
return ""
}
func listNetIfacesByBDF(bdf string) []string {
path := filepath.Join("/sys/bus/pci/devices", bdf, "net")
entries, err := os.ReadDir(path)
if err != nil {
return nil
}
ifaces := make([]string, 0, len(entries))
for _, e := range entries {
if e.Name() == "" {
continue
}
ifaces = append(ifaces, e.Name())
}
return ifaces
}

View File

@@ -0,0 +1,118 @@
package collector
import (
"bee/audit/internal/schema"
"fmt"
"testing"
)
func TestParseMstflintQuery(t *testing.T) {
raw := `Device #1:
----------
FW Version: 28.39.1002
Board Serial Number: MT1234ABC
`
fw, serial := parseMstflintQuery(raw)
if fw != "28.39.1002" {
t.Fatalf("firmware: got %q", fw)
}
if serial != "MT1234ABC" {
t.Fatalf("serial: got %q", serial)
}
}
func TestParseEthtoolFirmwareInfo(t *testing.T) {
raw := `driver: mlx5_core
version: 6.6.31-0-lts
firmware-version: 28.39.1002 (MT_0000000000)
bus-info: 0000:18:00.0
`
fw := parseEthtoolFirmwareInfo(raw)
if fw != "28.39.1002 (MT_0000000000)" {
t.Fatalf("firmware: got %q", fw)
}
}
func TestEnrichPCIeWithMellanox_mstflint(t *testing.T) {
origMst := mstflintQuery
origEth := ethtoolInfoQuery
origIfaces := netIfacesByBDF
t.Cleanup(func() {
mstflintQuery = origMst
ethtoolInfoQuery = origEth
netIfacesByBDF = origIfaces
})
mstflintQuery = func(bdf string) (string, error) {
if bdf != "0000:18:00.0" {
t.Fatalf("unexpected bdf: %s", bdf)
}
return "FW Version: 28.39.1002\nBoard Serial Number: SN-MST-001\n", nil
}
ethtoolInfoQuery = func(string) (string, error) {
t.Fatal("ethtool should not be called when mstflint succeeds")
return "", nil
}
netIfacesByBDF = func(string) []string { return nil }
vendorID := mellanoxVendorID
bdf := "0000:18:00.0"
manufacturer := "Mellanox Technologies"
devs := []schema.HardwarePCIeDevice{{
VendorID: &vendorID,
BDF: &bdf,
Manufacturer: &manufacturer,
}}
out := enrichPCIeWithMellanox(devs)
if out[0].Firmware == nil || *out[0].Firmware != "28.39.1002" {
t.Fatalf("firmware: got %v", out[0].Firmware)
}
if out[0].SerialNumber == nil || *out[0].SerialNumber != "SN-MST-001" {
t.Fatalf("serial: got %v", out[0].SerialNumber)
}
}
func TestEnrichPCIeWithMellanox_fallbackEthtool(t *testing.T) {
origMst := mstflintQuery
origEth := ethtoolInfoQuery
origIfaces := netIfacesByBDF
t.Cleanup(func() {
mstflintQuery = origMst
ethtoolInfoQuery = origEth
netIfacesByBDF = origIfaces
})
mstflintQuery = func(string) (string, error) {
return "", fmt.Errorf("mstflint not found")
}
netIfacesByBDF = func(bdf string) []string {
if bdf != "0000:18:00.0" {
t.Fatalf("unexpected bdf: %s", bdf)
}
return []string{"eth0"}
}
ethtoolInfoQuery = func(iface string) (string, error) {
if iface != "eth0" {
t.Fatalf("unexpected iface: %s", iface)
}
return "driver: mlx5_core\nfirmware-version: 28.40.1000\n", nil
}
vendorID := mellanoxVendorID
bdf := "0000:18:00.0"
manufacturer := "NVIDIA Networking"
devs := []schema.HardwarePCIeDevice{{
VendorID: &vendorID,
BDF: &bdf,
Manufacturer: &manufacturer,
}}
out := enrichPCIeWithMellanox(devs)
if out[0].Firmware == nil || *out[0].Firmware != "28.40.1000" {
t.Fatalf("firmware: got %v", out[0].Firmware)
}
if out[0].SerialNumber != nil {
t.Fatalf("serial should stay nil without mstflint, got %v", out[0].SerialNumber)
}
}

View File

@@ -0,0 +1,172 @@
package collector
import (
"bee/audit/internal/schema"
"log/slog"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
)
var (
ethtoolModuleQuery = func(iface string) (string, error) {
out, err := raidToolQuery("ethtool", "-m", iface)
if err != nil {
return "", err
}
return string(out), nil
}
readNetStatFile = func(iface, key string) (int64, error) {
path := filepath.Join("/sys/class/net", iface, "statistics", key)
raw, err := os.ReadFile(path)
if err != nil {
return 0, err
}
v, err := strconv.ParseInt(strings.TrimSpace(string(raw)), 10, 64)
if err != nil {
return 0, err
}
return v, nil
}
)
func enrichPCIeWithNICTelemetry(devs []schema.HardwarePCIeDevice) []schema.HardwarePCIeDevice {
enriched := 0
for i := range devs {
if !isNICDevice(devs[i]) || devs[i].BDF == nil {
continue
}
bdf := normalizePCIeBDF(*devs[i].BDF)
if bdf == "" {
continue
}
ifaces := netIfacesByBDF(bdf)
if len(ifaces) == 0 {
continue
}
iface := ifaces[0]
if devs[i].Firmware == nil {
if out, err := ethtoolInfoQuery(iface); err == nil {
if fw := parseEthtoolFirmwareInfo(out); fw != "" {
devs[i].Firmware = &fw
}
}
}
if devs[i].Telemetry == nil {
devs[i].Telemetry = map[string]any{}
}
injectNICPacketStats(devs[i].Telemetry, iface)
if out, err := ethtoolModuleQuery(iface); err == nil {
injectSFPDOMTelemetry(devs[i].Telemetry, out)
}
if len(devs[i].Telemetry) == 0 {
devs[i].Telemetry = nil
} else {
enriched++
}
}
slog.Info("nic: telemetry enriched", "count", enriched)
return devs
}
func isNICDevice(dev schema.HardwarePCIeDevice) bool {
if dev.DeviceClass == nil {
return false
}
c := strings.ToLower(strings.TrimSpace(*dev.DeviceClass))
return strings.Contains(c, "ethernet controller") ||
strings.Contains(c, "network controller") ||
strings.Contains(c, "infiniband controller")
}
func injectNICPacketStats(dst map[string]any, iface string) {
for _, key := range []string{"rx_packets", "tx_packets", "rx_errors", "tx_errors"} {
if v, err := readNetStatFile(iface, key); err == nil {
dst[key] = v
}
}
}
func injectSFPDOMTelemetry(dst map[string]any, raw string) {
parsed := parseSFPDOM(raw)
for k, v := range parsed {
dst[k] = v
}
}
var floatRe = regexp.MustCompile(`[-+]?[0-9]*\.?[0-9]+`)
func parseSFPDOM(raw string) map[string]any {
out := map[string]any{}
for _, line := range strings.Split(raw, "\n") {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
idx := strings.Index(trimmed, ":")
if idx < 0 {
continue
}
key := strings.ToLower(strings.TrimSpace(trimmed[:idx]))
val := strings.TrimSpace(trimmed[idx+1:])
switch {
case strings.Contains(key, "module temperature"):
if f, ok := firstFloat(val); ok {
out["sfp_temperature_c"] = f
}
case strings.Contains(key, "laser output power"):
if f, ok := dbmValue(val); ok {
out["sfp_tx_power_dbm"] = f
}
case strings.Contains(key, "receiver signal"):
if f, ok := dbmValue(val); ok {
out["sfp_rx_power_dbm"] = f
}
case strings.Contains(key, "module voltage"):
if f, ok := firstFloat(val); ok {
out["sfp_voltage_v"] = f
}
case strings.Contains(key, "laser bias current"):
if f, ok := firstFloat(val); ok {
out["sfp_bias_ma"] = f
}
}
}
return out
}
func firstFloat(raw string) (float64, bool) {
m := floatRe.FindString(raw)
if m == "" {
return 0, false
}
v, err := strconv.ParseFloat(m, 64)
if err != nil {
return 0, false
}
return v, true
}
func dbmValue(raw string) (float64, bool) {
parts := strings.Split(strings.ToLower(raw), "dbm")
if len(parts) == 0 {
return 0, false
}
for i := len(parts) - 1; i >= 0; i-- {
candidate := parts[i]
matches := floatRe.FindAllString(candidate, -1)
if len(matches) == 0 {
continue
}
v, err := strconv.ParseFloat(matches[len(matches)-1], 64)
if err == nil {
return v, true
}
}
return 0, false
}

View File

@@ -0,0 +1,51 @@
package collector
import "testing"
func TestParseSFPDOM(t *testing.T) {
raw := `
Module temperature : 41.23 C
Module voltage : 3.30 V
Laser bias current : 6.12 mA
Laser output power : 0.4712 mW / -3.27 dBm
Receiver signal average optical power : 0.4123 mW / -3.85 dBm
`
got := parseSFPDOM(raw)
if v, ok := got["sfp_temperature_c"].(float64); !ok || v != 41.23 {
t.Fatalf("sfp_temperature_c mismatch: %#v", got["sfp_temperature_c"])
}
if v, ok := got["sfp_voltage_v"].(float64); !ok || v != 3.30 {
t.Fatalf("sfp_voltage_v mismatch: %#v", got["sfp_voltage_v"])
}
if v, ok := got["sfp_bias_ma"].(float64); !ok || v != 6.12 {
t.Fatalf("sfp_bias_ma mismatch: %#v", got["sfp_bias_ma"])
}
if v, ok := got["sfp_tx_power_dbm"].(float64); !ok || v != -3.27 {
t.Fatalf("sfp_tx_power_dbm mismatch: %#v", got["sfp_tx_power_dbm"])
}
if v, ok := got["sfp_rx_power_dbm"].(float64); !ok || v != -3.85 {
t.Fatalf("sfp_rx_power_dbm mismatch: %#v", got["sfp_rx_power_dbm"])
}
}
func TestDBMValue(t *testing.T) {
tests := []struct {
in string
want float64
ok bool
}{
{"0.4123 mW / -3.85 dBm", -3.85, true},
{"-1.23 dBm", -1.23, true},
{"not supported", 0, false},
}
for _, tt := range tests {
got, ok := dbmValue(tt.in)
if ok != tt.ok {
t.Fatalf("dbmValue(%q) ok=%v want %v", tt.in, ok, tt.ok)
}
if ok && got != tt.want {
t.Fatalf("dbmValue(%q)=%v want %v", tt.in, got, tt.want)
}
}
}

View File

@@ -0,0 +1,245 @@
package collector
import (
"bee/audit/internal/schema"
"encoding/csv"
"fmt"
"log/slog"
"os/exec"
"strconv"
"strings"
)
const nvidiaVendorID = 0x10de
type nvidiaGPUInfo struct {
BDF string
Serial string
VBIOS string
TemperatureC *float64
PowerW *float64
ECCUncorrected *int64
ECCCorrected *int64
HWSlowdown *bool
}
// enrichPCIeWithNVIDIA enriches NVIDIA PCIe devices with data from nvidia-smi.
// If the driver/tool is unavailable, NVIDIA devices get UNKNOWN status and
// a stable serial fallback based on board serial + slot.
func enrichPCIeWithNVIDIA(devs []schema.HardwarePCIeDevice, boardSerial string) []schema.HardwarePCIeDevice {
gpuByBDF, err := queryNVIDIAGPUs()
if err != nil {
slog.Info("nvidia: enrichment skipped", "err", err)
return enrichPCIeWithNVIDIAData(devs, nil, boardSerial, false)
}
return enrichPCIeWithNVIDIAData(devs, gpuByBDF, boardSerial, true)
}
func enrichPCIeWithNVIDIAData(devs []schema.HardwarePCIeDevice, gpuByBDF map[string]nvidiaGPUInfo, boardSerial string, driverLoaded bool) []schema.HardwarePCIeDevice {
enriched := 0
for i := range devs {
if !isNVIDIADevice(devs[i]) {
continue
}
if !driverLoaded {
setPCIeFallback(&devs[i], boardSerial)
continue
}
bdf := ""
if devs[i].BDF != nil {
bdf = normalizePCIeBDF(*devs[i].BDF)
}
info, ok := gpuByBDF[bdf]
if !ok {
setPCIeFallback(&devs[i], boardSerial)
continue
}
if v := strings.TrimSpace(info.Serial); v != "" {
devs[i].SerialNumber = &v
} else {
setPCIeFallbackSerial(&devs[i], boardSerial)
}
if v := strings.TrimSpace(info.VBIOS); v != "" {
devs[i].Firmware = &v
}
status := "OK"
if info.ECCUncorrected != nil && *info.ECCUncorrected > 0 {
status = "WARNING"
}
devs[i].Status = &status
injectNVIDIATelemetry(&devs[i], info)
enriched++
}
if driverLoaded {
slog.Info("nvidia: enriched", "count", enriched)
}
return devs
}
func queryNVIDIAGPUs() (map[string]nvidiaGPUInfo, error) {
out, err := exec.Command(
"nvidia-smi",
"--query-gpu=index,pci.bus_id,serial,vbios_version,temperature.gpu,power.draw,ecc.errors.uncorrected.aggregate.total,ecc.errors.corrected.aggregate.total,clocks_throttle_reasons.hw_slowdown",
"--format=csv,noheader,nounits",
).Output()
if err != nil {
return nil, err
}
return parseNVIDIASMIQuery(string(out))
}
func parseNVIDIASMIQuery(raw string) (map[string]nvidiaGPUInfo, error) {
r := csv.NewReader(strings.NewReader(raw))
r.TrimLeadingSpace = true
r.FieldsPerRecord = -1
records, err := r.ReadAll()
if err != nil {
return nil, err
}
result := make(map[string]nvidiaGPUInfo)
for _, rec := range records {
if len(rec) == 0 {
continue
}
if len(rec) < 9 {
return nil, fmt.Errorf("unexpected nvidia-smi columns: got %d, want 9", len(rec))
}
bdf := normalizePCIeBDF(rec[1])
if bdf == "" {
continue
}
info := nvidiaGPUInfo{
BDF: bdf,
Serial: strings.TrimSpace(rec[2]),
VBIOS: strings.TrimSpace(rec[3]),
TemperatureC: parseMaybeFloat(rec[4]),
PowerW: parseMaybeFloat(rec[5]),
ECCUncorrected: parseMaybeInt64(rec[6]),
ECCCorrected: parseMaybeInt64(rec[7]),
HWSlowdown: parseMaybeBool(rec[8]),
}
result[bdf] = info
}
return result, nil
}
func parseMaybeFloat(v string) *float64 {
v = strings.TrimSpace(v)
if v == "" || strings.EqualFold(v, "n/a") || strings.EqualFold(v, "not supported") || strings.EqualFold(v, "[not supported]") {
return nil
}
n, err := strconv.ParseFloat(v, 64)
if err != nil {
return nil
}
return &n
}
func parseMaybeInt64(v string) *int64 {
v = strings.TrimSpace(v)
if v == "" || strings.EqualFold(v, "n/a") || strings.EqualFold(v, "not supported") || strings.EqualFold(v, "[not supported]") {
return nil
}
n, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return nil
}
return &n
}
func parseMaybeBool(v string) *bool {
v = strings.TrimSpace(strings.ToLower(v))
switch v {
case "active", "enabled", "true", "1":
b := true
return &b
case "not active", "disabled", "false", "0":
b := false
return &b
default:
return nil
}
}
func normalizePCIeBDF(bdf string) string {
bdf = strings.TrimSpace(strings.ToLower(bdf))
if bdf == "" {
return ""
}
parts := strings.Split(bdf, ":")
if len(parts) == 3 {
domain := parts[0]
if len(domain) > 4 {
domain = domain[len(domain)-4:]
}
return domain + ":" + parts[1] + ":" + parts[2]
}
if len(parts) == 2 {
return "0000:" + parts[0] + ":" + parts[1]
}
return bdf
}
func isNVIDIADevice(dev schema.HardwarePCIeDevice) bool {
if dev.VendorID != nil && *dev.VendorID == nvidiaVendorID {
return true
}
if dev.Manufacturer != nil && strings.Contains(strings.ToLower(*dev.Manufacturer), "nvidia") {
return true
}
return false
}
func setPCIeFallback(dev *schema.HardwarePCIeDevice, boardSerial string) {
setPCIeFallbackSerial(dev, boardSerial)
status := "UNKNOWN"
dev.Status = &status
}
func setPCIeFallbackSerial(dev *schema.HardwarePCIeDevice, boardSerial string) {
if strings.TrimSpace(boardSerial) == "" || dev.SerialNumber != nil {
return
}
slot := "unknown"
if dev.BDF != nil && strings.TrimSpace(*dev.BDF) != "" {
slot = strings.TrimSpace(*dev.BDF)
} else if dev.Slot != nil && strings.TrimSpace(*dev.Slot) != "" {
slot = strings.TrimSpace(*dev.Slot)
}
fb := fmt.Sprintf("%s-PCIE-%s", boardSerial, slot)
dev.SerialNumber = &fb
}
func injectNVIDIATelemetry(dev *schema.HardwarePCIeDevice, info nvidiaGPUInfo) {
if dev.Telemetry == nil {
dev.Telemetry = map[string]any{}
}
if info.TemperatureC != nil {
dev.Telemetry["temperature_c"] = *info.TemperatureC
}
if info.PowerW != nil {
dev.Telemetry["power_w"] = *info.PowerW
}
if info.ECCUncorrected != nil {
dev.Telemetry["ecc_uncorrected_total"] = *info.ECCUncorrected
}
if info.ECCCorrected != nil {
dev.Telemetry["ecc_corrected_total"] = *info.ECCCorrected
}
if info.HWSlowdown != nil {
dev.Telemetry["hw_slowdown_active"] = *info.HWSlowdown
}
if len(dev.Telemetry) == 0 {
dev.Telemetry = nil
}
}

View File

@@ -0,0 +1,116 @@
package collector
import (
"bee/audit/internal/schema"
"testing"
)
func TestParseNVIDIASMIQuery(t *testing.T) {
raw := "0, 00000000:65:00.0, GPU-SERIAL-1, 96.00.1F.00.02, 54, 210.33, 0, 5, Not Active\n"
byBDF, err := parseNVIDIASMIQuery(raw)
if err != nil {
t.Fatalf("parse failed: %v", err)
}
gpu, ok := byBDF["0000:65:00.0"]
if !ok {
t.Fatalf("gpu by normalized bdf not found")
}
if gpu.Serial != "GPU-SERIAL-1" {
t.Fatalf("serial: got %q", gpu.Serial)
}
if gpu.VBIOS != "96.00.1F.00.02" {
t.Fatalf("vbios: got %q", gpu.VBIOS)
}
if gpu.ECCUncorrected == nil || *gpu.ECCUncorrected != 0 {
t.Fatalf("ecc uncorrected: got %v", gpu.ECCUncorrected)
}
if gpu.HWSlowdown == nil || *gpu.HWSlowdown {
t.Fatalf("hw slowdown: got %v, want false", gpu.HWSlowdown)
}
}
func TestNormalizePCIeBDF(t *testing.T) {
tests := []struct {
in string
want string
}{
{"00000000:17:00.0", "0000:17:00.0"},
{"0000:17:00.0", "0000:17:00.0"},
{"17:00.0", "0000:17:00.0"},
}
for _, tt := range tests {
got := normalizePCIeBDF(tt.in)
if got != tt.want {
t.Fatalf("normalizePCIeBDF(%q)=%q want %q", tt.in, got, tt.want)
}
}
}
func TestEnrichPCIeWithNVIDIAData_driverLoaded(t *testing.T) {
vendorID := nvidiaVendorID
bdf := "0000:65:00.0"
manufacturer := "NVIDIA Corporation"
status := "OK"
devices := []schema.HardwarePCIeDevice{
{
VendorID: &vendorID,
BDF: &bdf,
Manufacturer: &manufacturer,
Status: &status,
},
}
byBDF := map[string]nvidiaGPUInfo{
"0000:65:00.0": {
BDF: "0000:65:00.0",
Serial: "GPU-ABC",
VBIOS: "96.00.1F.00.02",
ECCUncorrected: ptrInt64(2),
ECCCorrected: ptrInt64(10),
TemperatureC: ptrFloat(55.5),
PowerW: ptrFloat(230.2),
},
}
out := enrichPCIeWithNVIDIAData(devices, byBDF, "BOARD-001", true)
if out[0].SerialNumber == nil || *out[0].SerialNumber != "GPU-ABC" {
t.Fatalf("serial: got %v", out[0].SerialNumber)
}
if out[0].Firmware == nil || *out[0].Firmware != "96.00.1F.00.02" {
t.Fatalf("firmware: got %v", out[0].Firmware)
}
if out[0].Status == nil || *out[0].Status != "WARNING" {
t.Fatalf("status: got %v", out[0].Status)
}
if out[0].Telemetry == nil {
t.Fatal("expected telemetry")
}
if got, ok := out[0].Telemetry["ecc_uncorrected_total"].(int64); !ok || got != 2 {
t.Fatalf("ecc_uncorrected_total: got %#v", out[0].Telemetry["ecc_uncorrected_total"])
}
}
func TestEnrichPCIeWithNVIDIAData_driverMissingFallback(t *testing.T) {
vendorID := nvidiaVendorID
bdf := "0000:17:00.0"
manufacturer := "NVIDIA Corporation"
devices := []schema.HardwarePCIeDevice{
{
VendorID: &vendorID,
BDF: &bdf,
Manufacturer: &manufacturer,
},
}
out := enrichPCIeWithNVIDIAData(devices, nil, "BOARD-123", false)
if out[0].SerialNumber == nil || *out[0].SerialNumber != "BOARD-123-PCIE-0000:17:00.0" {
t.Fatalf("fallback serial: got %v", out[0].SerialNumber)
}
if out[0].Status == nil || *out[0].Status != "UNKNOWN" {
t.Fatalf("fallback status: got %v", out[0].Status)
}
}
func ptrInt64(v int64) *int64 { return &v }
func ptrFloat(v float64) *float64 { return &v }

View File

@@ -37,12 +37,44 @@ func parseLspci(output string) []schema.HardwarePCIeDevice {
val := strings.TrimSpace(line[idx+2:])
fields[key] = val
}
if !shouldIncludePCIeDevice(fields["Class"]) {
continue
}
dev := parseLspciDevice(fields)
devs = append(devs, dev)
}
return devs
}
func shouldIncludePCIeDevice(class string) bool {
c := strings.ToLower(strings.TrimSpace(class))
if c == "" {
return true
}
// Keep inventory focused on useful replaceable components, not chipset/virtual noise.
excluded := []string{
"host bridge",
"isa bridge",
"pci bridge",
"ram memory",
"system peripheral",
"communication controller",
"signal processing controller",
"usb controller",
"smbus",
"audio device",
"serial bus controller",
"unassigned class",
}
for _, bad := range excluded {
if strings.Contains(c, bad) {
return false
}
}
return true
}
func parseLspciDevice(fields map[string]string) schema.HardwarePCIeDevice {
dev := schema.HardwarePCIeDevice{}
present := true

View File

@@ -0,0 +1,41 @@
package collector
import "testing"
func TestShouldIncludePCIeDevice(t *testing.T) {
tests := []struct {
class string
want bool
}{
{"USB controller", false},
{"System peripheral", false},
{"Audio device", false},
{"Host bridge", false},
{"PCI bridge", false},
{"SMBus", false},
{"Ethernet controller", true},
{"RAID bus controller", true},
{"Non-Volatile memory controller", true},
{"VGA compatible controller", true},
}
for _, tt := range tests {
got := shouldIncludePCIeDevice(tt.class)
if got != tt.want {
t.Fatalf("class %q include=%v want %v", tt.class, got, tt.want)
}
}
}
func TestParseLspci_filtersExcludedClasses(t *testing.T) {
input := "Slot:\t0000:00:14.0\nClass:\tUSB controller\nVendor:\tIntel Corporation\nDevice:\tUSB 3.0\n\n" +
"Slot:\t0000:65:00.0\nClass:\tVGA compatible controller\nVendor:\tNVIDIA Corporation\nDevice:\tH100\n\n"
devs := parseLspci(input)
if len(devs) != 1 {
t.Fatalf("expected 1 filtered device, got %d", len(devs))
}
if devs[0].DeviceClass == nil || *devs[0].DeviceClass != "VGA compatible controller" {
t.Fatalf("unexpected remaining class: %v", devs[0].DeviceClass)
}
}

View File

@@ -0,0 +1,748 @@
package collector
import (
"bee/audit/internal/schema"
"encoding/json"
"log/slog"
"os"
"os/exec"
"regexp"
"sort"
"strconv"
"strings"
)
const (
vendorBroadcomLSI = 0x1000
vendorAdaptec = 0x9005
vendorHPE = 0x103c
vendorIntel = 0x8086
)
var raidToolQuery = func(name string, args ...string) ([]byte, error) {
return exec.Command(name, args...).Output()
}
var readMDStat = func() ([]byte, error) {
return os.ReadFile("/proc/mdstat")
}
// collectRAIDStorage collects physical disks behind RAID controllers that may
// not be exposed as regular block devices.
func collectRAIDStorage(pcie []schema.HardwarePCIeDevice) []schema.HardwareStorage {
vendors := detectRAIDVendors(pcie)
if len(vendors) == 0 {
return nil
}
var out []schema.HardwareStorage
if vendors[vendorBroadcomLSI] {
if drives := collectStorcliDrives(); len(drives) > 0 {
out = append(out, drives...)
}
if drives := collectSASIrcuDrives("sas3ircu"); len(drives) > 0 {
out = append(out, drives...)
}
if drives := collectSASIrcuDrives("sas2ircu"); len(drives) > 0 {
out = append(out, drives...)
}
}
if vendors[vendorAdaptec] {
if drives := collectArcconfDrives(); len(drives) > 0 {
out = append(out, drives...)
}
}
if vendors[vendorHPE] {
if drives := collectSSACLIDrives(); len(drives) > 0 {
out = append(out, drives...)
}
}
if len(out) > 0 {
slog.Info("raid: collected physical drives", "count", len(out))
}
return out
}
func detectRAIDVendors(pcie []schema.HardwarePCIeDevice) map[int]bool {
out := map[int]bool{}
for _, dev := range pcie {
if dev.VendorID == nil {
continue
}
if isLikelyRAIDController(dev) {
out[*dev.VendorID] = true
}
}
return out
}
func isLikelyRAIDController(dev schema.HardwarePCIeDevice) bool {
if dev.DeviceClass == nil {
return false
}
c := strings.ToLower(*dev.DeviceClass)
return strings.Contains(c, "raid") ||
strings.Contains(c, "sas") ||
strings.Contains(c, "mass storage") ||
strings.Contains(c, "serial attached scsi")
}
func collectStorcliDrives() []schema.HardwareStorage {
out, err := raidToolQuery("storcli64", "/call/eall/sall", "show", "all", "J")
if err != nil {
slog.Info("raid: storcli unavailable", "err", err)
return nil
}
drives := parseStorcliDrivesJSON(out)
if len(drives) == 0 {
slog.Info("raid: storcli returned no drives")
}
return drives
}
func collectSASIrcuDrives(tool string) []schema.HardwareStorage {
out, err := raidToolQuery(tool, "list")
if err != nil {
slog.Info("raid: "+tool+" unavailable", "err", err)
return nil
}
var drives []schema.HardwareStorage
for _, ctlID := range parseSASIrcuControllerIDs(string(out)) {
raw, err := raidToolQuery(tool, strconv.Itoa(ctlID), "display")
if err != nil {
continue
}
drives = append(drives, parseSASIrcuDisplay(string(raw))...)
}
return drives
}
func parseSASIrcuControllerIDs(raw string) []int {
lines := strings.Split(raw, "\n")
idsMap := map[int]bool{}
for _, line := range lines {
fields := strings.Fields(strings.TrimSpace(line))
if len(fields) == 0 {
continue
}
id, err := strconv.Atoi(fields[0])
if err != nil {
continue
}
idsMap[id] = true
}
var ids []int
for id := range idsMap {
ids = append(ids, id)
}
sort.Ints(ids)
return ids
}
func parseSASIrcuDisplay(raw string) []schema.HardwareStorage {
var blocks []map[string]string
var cur map[string]string
var currentType string
for _, line := range strings.Split(raw, "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "Device is a ") {
if cur != nil {
cur["__device_type"] = currentType
blocks = append(blocks, cur)
}
cur = map[string]string{}
currentType = strings.TrimSpace(strings.TrimPrefix(trimmed, "Device is a "))
continue
}
if cur == nil {
continue
}
if idx := strings.Index(trimmed, ":"); idx > 0 {
key := strings.TrimSpace(trimmed[:idx])
val := strings.TrimSpace(trimmed[idx+1:])
cur[key] = val
}
}
if cur != nil {
cur["__device_type"] = currentType
blocks = append(blocks, cur)
}
var out []schema.HardwareStorage
for _, b := range blocks {
dt := strings.ToLower(b["__device_type"])
if !strings.Contains(dt, "hard disk") && !strings.Contains(dt, "ssd") && !strings.Contains(dt, "nvme") {
continue
}
present := true
status := mapRAIDDriveStatus(b["State"])
s := schema.HardwareStorage{Present: &present, Status: &status}
enclosure := strings.TrimSpace(b["Enclosure #"])
slot := strings.TrimSpace(b["Slot #"])
if enclosure != "" || slot != "" {
v := enclosure + ":" + slot
v = strings.Trim(v, ":")
s.Slot = &v
}
if v := strings.TrimSpace(b["Model Number"]); v != "" {
s.Model = &v
}
if v := strings.TrimSpace(b["Serial No"]); v != "" {
s.SerialNumber = &v
}
if v := strings.ToUpper(strings.TrimSpace(b["Protocol"])); v != "" {
s.Interface = &v
}
media := strings.ToUpper(strings.TrimSpace(b["Drive Type"]))
if media == "" {
media = strings.ToUpper(dt)
}
intf := ""
if s.Interface != nil {
intf = *s.Interface
}
devType := inferDriveType(media, intf)
s.Type = &devType
if mb := parseSASIrcuMB(b["Size (in MB)/(in sectors)"]); mb > 0 {
gb := mb / 1000
if gb == 0 {
gb = 1
}
s.SizeGB = &gb
}
if s.Slot != nil || s.SerialNumber != nil || s.Model != nil {
out = append(out, s)
}
}
return out
}
func parseSASIrcuMB(raw string) int {
raw = strings.TrimSpace(raw)
if raw == "" {
return 0
}
head := strings.SplitN(raw, "/", 2)[0]
n, err := strconv.Atoi(strings.TrimSpace(head))
if err != nil {
return 0
}
return n
}
func collectArcconfDrives() []schema.HardwareStorage {
raw, err := raidToolQuery("arcconf", "getconfig", "1", "pd")
if err != nil {
slog.Info("raid: arcconf unavailable", "err", err)
return nil
}
return parseArcconfPhysicalDrives(string(raw))
}
func parseArcconfPhysicalDrives(raw string) []schema.HardwareStorage {
lines := strings.Split(raw, "\n")
var blocks []map[string]string
var cur map[string]string
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(strings.ToLower(trimmed), "device #") {
if cur != nil {
blocks = append(blocks, cur)
}
cur = map[string]string{}
continue
}
if cur == nil {
continue
}
if idx := strings.Index(trimmed, ":"); idx > 0 {
key := strings.TrimSpace(trimmed[:idx])
val := strings.TrimSpace(trimmed[idx+1:])
cur[key] = val
}
}
if cur != nil {
blocks = append(blocks, cur)
}
var out []schema.HardwareStorage
for _, b := range blocks {
present := true
status := mapRAIDDriveStatus(b["State"])
s := schema.HardwareStorage{Present: &present, Status: &status}
if v := strings.TrimSpace(b["Reported Location"]); v != "" {
s.Slot = &v
}
if v := strings.TrimSpace(b["Model"]); v != "" {
s.Model = &v
}
if v := strings.TrimSpace(b["Serial number"]); v != "" {
s.SerialNumber = &v
}
if gb := parseHumanSizeToGB(b["Total Size"]); gb > 0 {
s.SizeGB = &gb
}
intf := parseArcconfInterface(b["Transfer Speed"])
if intf != "" {
s.Interface = &intf
}
media := strings.ToUpper(strings.TrimSpace(b["SSD"]))
if media == "YES" || media == "TRUE" {
media = "SSD"
}
devType := inferDriveType(media, intf)
s.Type = &devType
if s.Slot != nil || s.SerialNumber != nil || s.Model != nil {
out = append(out, s)
}
}
return out
}
func parseArcconfInterface(raw string) string {
u := strings.ToUpper(raw)
switch {
case strings.Contains(u, "SAS"):
return "SAS"
case strings.Contains(u, "SATA"):
return "SATA"
case strings.Contains(u, "NVME"):
return "NVME"
default:
return ""
}
}
var ssacliPhysicalDriveLine = regexp.MustCompile(`(?i)^physicaldrive\s+(\S+)\s+\(([^)]*)\)$`)
func collectSSACLIDrives() []schema.HardwareStorage {
raw, err := raidToolQuery("ssacli", "ctrl", "all", "show", "config", "detail")
if err != nil {
slog.Info("raid: ssacli unavailable", "err", err)
return nil
}
return parseSSACLIPhysicalDrives(string(raw))
}
func parseSSACLIPhysicalDrives(raw string) []schema.HardwareStorage {
lines := strings.Split(raw, "\n")
var out []schema.HardwareStorage
var cur *schema.HardwareStorage
flush := func() {
if cur == nil {
return
}
if cur.Slot != nil || cur.SerialNumber != nil || cur.Model != nil {
out = append(out, *cur)
}
cur = nil
}
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
if m := ssacliPhysicalDriveLine.FindStringSubmatch(trimmed); len(m) == 3 {
flush()
present := true
status := "UNKNOWN"
s := schema.HardwareStorage{Present: &present, Status: &status}
slot := m[1]
s.Slot = &slot
meta := strings.Split(m[2], ",")
if len(meta) > 0 {
if gb := parseHumanSizeToGB(strings.TrimSpace(meta[0])); gb > 0 {
s.SizeGB = &gb
}
}
if len(meta) > 1 {
intf := parseSSACLIInterface(meta[1])
if intf != "" {
s.Interface = &intf
}
devType := inferDriveType(strings.ToUpper(meta[1]), intf)
s.Type = &devType
}
if len(meta) > 2 {
st := mapRAIDDriveStatus(meta[len(meta)-1])
s.Status = &st
}
cur = &s
continue
}
if cur == nil {
continue
}
if idx := strings.Index(trimmed, ":"); idx > 0 {
key := strings.ToLower(strings.TrimSpace(trimmed[:idx]))
val := strings.TrimSpace(trimmed[idx+1:])
switch key {
case "serial number":
if val != "" {
cur.SerialNumber = &val
}
case "model":
if val != "" {
cur.Model = &val
}
case "status":
st := mapRAIDDriveStatus(val)
cur.Status = &st
}
}
}
flush()
return out
}
func parseSSACLIInterface(raw string) string {
u := strings.ToUpper(raw)
switch {
case strings.Contains(u, "SAS"):
return "SAS"
case strings.Contains(u, "SATA"):
return "SATA"
case strings.Contains(u, "NVME"):
return "NVME"
default:
return ""
}
}
func parseStorcliDrivesJSON(raw []byte) []schema.HardwareStorage {
var doc struct {
Controllers []struct {
ResponseData struct {
DriveInformation []struct {
EIDSlt string `json:"EID:Slt"`
State string `json:"State"`
Size string `json:"Size"`
Intf string `json:"Intf"`
Med string `json:"Med"`
Model string `json:"Model"`
SN string `json:"SN"`
Sp string `json:"Sp"`
Type string `json:"Type"`
} `json:"Drive Information"`
} `json:"Response Data"`
} `json:"Controllers"`
}
if err := json.Unmarshal(raw, &doc); err != nil {
slog.Warn("raid: parse storcli json failed", "err", err)
return nil
}
var drives []schema.HardwareStorage
for _, ctl := range doc.Controllers {
for _, d := range ctl.ResponseData.DriveInformation {
if s := storcliDriveToStorage(d); s != nil {
drives = append(drives, *s)
}
}
}
return drives
}
func storcliDriveToStorage(d struct {
EIDSlt string `json:"EID:Slt"`
State string `json:"State"`
Size string `json:"Size"`
Intf string `json:"Intf"`
Med string `json:"Med"`
Model string `json:"Model"`
SN string `json:"SN"`
Sp string `json:"Sp"`
Type string `json:"Type"`
}) *schema.HardwareStorage {
present := true
status := mapRAIDDriveStatus(d.State)
s := schema.HardwareStorage{
Present: &present,
Status: &status,
}
if v := strings.TrimSpace(d.EIDSlt); v != "" {
s.Slot = &v
}
if v := strings.TrimSpace(d.Model); v != "" {
s.Model = &v
}
if v := strings.TrimSpace(d.SN); v != "" {
s.SerialNumber = &v
}
if v := strings.TrimSpace(strings.ToUpper(d.Intf)); v != "" {
s.Interface = &v
}
devType := inferDriveType(strings.TrimSpace(strings.ToUpper(d.Med)), strings.TrimSpace(strings.ToUpper(d.Intf)))
if devType != "" {
s.Type = &devType
}
if gb := parseHumanSizeToGB(d.Size); gb > 0 {
s.SizeGB = &gb
}
// return only meaningful records
if s.Model == nil && s.SerialNumber == nil && s.Slot == nil {
return nil
}
return &s
}
func inferDriveType(med, intf string) string {
switch {
case strings.Contains(med, "SSD"):
return "SSD"
case strings.Contains(intf, "NVME"):
return "NVMe"
case strings.Contains(med, "HDD"):
return "HDD"
case strings.Contains(intf, "SAS") || strings.Contains(intf, "SATA"):
return "HDD"
default:
return "Unknown"
}
}
func mapRAIDDriveStatus(raw string) string {
u := strings.ToUpper(strings.TrimSpace(raw))
switch {
case strings.Contains(u, "OK"), strings.Contains(u, "OPTIMAL"), strings.Contains(u, "READY"):
return "OK"
case strings.Contains(u, "ONLN"), strings.Contains(u, "ONLINE"):
return "OK"
case strings.Contains(u, "RBLD"), strings.Contains(u, "REBUILD"):
return "WARNING"
case strings.Contains(u, "FAIL"), strings.Contains(u, "OFFLINE"):
return "CRITICAL"
default:
return "UNKNOWN"
}
}
func parseHumanSizeToGB(raw string) int {
parts := strings.Fields(strings.TrimSpace(raw))
if len(parts) < 2 {
return 0
}
value, err := strconv.ParseFloat(strings.TrimSpace(parts[0]), 64)
if err != nil {
return 0
}
unit := strings.ToUpper(parts[1])
switch {
case strings.HasPrefix(unit, "TB"):
return int(value * 1000)
case strings.HasPrefix(unit, "GB"):
return int(value)
case strings.HasPrefix(unit, "MB"):
return int(value / 1000)
default:
return 0
}
}
func appendUniqueStorage(base, extra []schema.HardwareStorage) []schema.HardwareStorage {
if len(extra) == 0 {
return base
}
seen := map[string]bool{}
for _, d := range base {
seen[storageIdentityKey(d)] = true
}
for _, d := range extra {
key := storageIdentityKey(d)
if key == "" || seen[key] {
continue
}
base = append(base, d)
seen[key] = true
}
return base
}
func storageIdentityKey(d schema.HardwareStorage) string {
if d.SerialNumber != nil && strings.TrimSpace(*d.SerialNumber) != "" {
return "sn:" + strings.ToLower(strings.TrimSpace(*d.SerialNumber))
}
if d.Model != nil && d.Slot != nil {
return "modelslot:" + strings.ToLower(strings.TrimSpace(*d.Model)) + ":" + strings.ToLower(strings.TrimSpace(*d.Slot))
}
return ""
}
type mdArray struct {
Name string
Degraded bool
Members []string
}
func enrichStorageWithVROC(storage []schema.HardwareStorage, pcie []schema.HardwarePCIeDevice) []schema.HardwareStorage {
if !hasVROCController(pcie) {
return storage
}
raw, err := readMDStat()
if err != nil {
slog.Info("vroc: cannot read /proc/mdstat", "err", err)
return storage
}
arrays := parseMDStatArrays(string(raw))
if len(arrays) == 0 {
slog.Info("vroc: no md arrays found")
return storage
}
serialToArray := map[string]mdArray{}
for _, arr := range arrays {
for _, member := range arr.Members {
serial := queryDeviceSerial("/dev/" + member)
if serial == "" {
continue
}
serialToArray[strings.ToLower(serial)] = arr
}
}
if len(serialToArray) == 0 {
return storage
}
updated := 0
for i := range storage {
if storage[i].SerialNumber == nil || strings.TrimSpace(*storage[i].SerialNumber) == "" {
continue
}
arr, ok := serialToArray[strings.ToLower(strings.TrimSpace(*storage[i].SerialNumber))]
if !ok {
continue
}
if storage[i].Telemetry == nil {
storage[i].Telemetry = map[string]any{}
}
storage[i].Telemetry["vroc_array"] = arr.Name
storage[i].Telemetry["vroc_degraded"] = arr.Degraded
if arr.Degraded {
status := "WARNING"
storage[i].Status = &status
}
updated++
}
slog.Info("vroc: enriched storage members", "count", updated)
return storage
}
func hasVROCController(pcie []schema.HardwarePCIeDevice) bool {
for _, dev := range pcie {
if dev.VendorID == nil || *dev.VendorID != vendorIntel {
continue
}
class := ""
if dev.DeviceClass != nil {
class = strings.ToLower(*dev.DeviceClass)
}
model := ""
if dev.Model != nil {
model = strings.ToLower(*dev.Model)
}
if strings.Contains(class, "raid") ||
strings.Contains(model, "vroc") ||
strings.Contains(model, "volume management device") ||
strings.Contains(model, "vmd") {
return true
}
}
return false
}
var mdHealthPattern = regexp.MustCompile(`\[[U_]+\]`)
func parseMDStatArrays(raw string) []mdArray {
lines := strings.Split(raw, "\n")
var arrays []mdArray
var current *mdArray
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
if strings.Contains(line, " : ") && !strings.HasPrefix(strings.TrimLeft(line, " \t"), "[") {
left := strings.TrimSpace(strings.SplitN(line, " : ", 2)[0])
if strings.EqualFold(left, "Personalities") || strings.EqualFold(left, "unused devices") {
continue
}
if current != nil {
arrays = append(arrays, *current)
}
name := left
fields := strings.Fields(strings.SplitN(line, " : ", 2)[1])
arr := mdArray{Name: name}
for _, f := range fields {
if i := strings.IndexByte(f, '['); i > 0 {
member := strings.TrimSpace(f[:i])
if member != "" {
arr.Members = append(arr.Members, member)
}
}
}
current = &arr
continue
}
if current == nil {
continue
}
if m := mdHealthPattern.FindString(trimmed); m != "" && strings.Contains(m, "_") {
current.Degraded = true
}
}
if current != nil {
arrays = append(arrays, *current)
}
return arrays
}
func queryDeviceSerial(devPath string) string {
if out, err := exec.Command("nvme", "id-ctrl", devPath, "-o", "json").Output(); err == nil {
var ctrl nvmeIDCtrl
if json.Unmarshal(out, &ctrl) == nil {
if v := cleanDMIValue(strings.TrimSpace(ctrl.SerialNumber)); v != "" {
return v
}
}
}
if out, err := exec.Command("smartctl", "-j", "-i", devPath).Output(); err == nil {
var info smartctlInfo
if json.Unmarshal(out, &info) == nil {
if v := cleanDMIValue(strings.TrimSpace(info.SerialNumber)); v != "" {
return v
}
}
}
return ""
}

View File

@@ -0,0 +1,96 @@
package collector
import "testing"
func TestParseSASIrcuControllerIDs(t *testing.T) {
raw := `LSI Corporation SAS2 IR Configuration Utility.
Adapter List
==============
0 SAS2008(B2)
1 SAS2308_2(D1)
`
ids := parseSASIrcuControllerIDs(raw)
if len(ids) != 2 || ids[0] != 0 || ids[1] != 1 {
t.Fatalf("unexpected ids: %#v", ids)
}
}
func TestParseSASIrcuDisplay(t *testing.T) {
raw := `Device is a Hard disk
Enclosure # : 32
Slot # : 7
State : Onln
Size (in MB)/(in sectors) : 953869/1953525168
Model Number : ST1000NM0033
Serial No : Z1D12345
Protocol : SAS
Drive Type : HDD
Device is a Enclosure services device
Enclosure # : 32
`
drives := parseSASIrcuDisplay(raw)
if len(drives) != 1 {
t.Fatalf("expected 1 drive, got %d", len(drives))
}
d := drives[0]
if d.Slot == nil || *d.Slot != "32:7" {
t.Fatalf("slot: %v", d.Slot)
}
if d.SerialNumber == nil || *d.SerialNumber != "Z1D12345" {
t.Fatalf("serial: %v", d.SerialNumber)
}
if d.Interface == nil || *d.Interface != "SAS" {
t.Fatalf("interface: %v", d.Interface)
}
if d.Status == nil || *d.Status != "OK" {
t.Fatalf("status: %v", d.Status)
}
}
func TestParseArcconfPhysicalDrives(t *testing.T) {
raw := `Device #0
Reported Location : Channel 0, Device 3
Model : Micron_5300
Serial number : ARC12345
State : Online
Total Size : 894 GB
Transfer Speed : SATA 6.0Gb/s
SSD : Yes
`
drives := parseArcconfPhysicalDrives(raw)
if len(drives) != 1 {
t.Fatalf("expected 1 drive, got %d", len(drives))
}
d := drives[0]
if d.Type == nil || *d.Type != "SSD" {
t.Fatalf("type: %v", d.Type)
}
if d.Interface == nil || *d.Interface != "SATA" {
t.Fatalf("interface: %v", d.Interface)
}
if d.Status == nil || *d.Status != "OK" {
t.Fatalf("status: %v", d.Status)
}
}
func TestParseSSACLIPhysicalDrives(t *testing.T) {
raw := `physicaldrive 1I:1:1 (894 GB, SAS HDD, OK)
Serial Number: SSACLI001
Model: MB8000JVYZQ
physicaldrive 1I:1:2 (894 GB, SAS HDD, Failed)
Serial Number: SSACLI002
Model: MB8000JVYZQ
`
drives := parseSSACLIPhysicalDrives(raw)
if len(drives) != 2 {
t.Fatalf("expected 2 drives, got %d", len(drives))
}
if drives[0].Status == nil || *drives[0].Status != "OK" {
t.Fatalf("drive0 status: %v", drives[0].Status)
}
if drives[1].Status == nil || *drives[1].Status != "CRITICAL" {
t.Fatalf("drive1 status: %v", drives[1].Status)
}
}

View File

@@ -0,0 +1,57 @@
package collector
import (
"bee/audit/internal/schema"
"testing"
)
func TestParseMDStatArrays(t *testing.T) {
raw := `Personalities : [raid1]
md126 : active raid1 nvme0n1[0] nvme1n1[1]
976630464 blocks super external:/md127/0 [2/2] [UU]
md125 : active raid1 nvme2n1[0] nvme3n1[1]
976630464 blocks super external:/md127/1 [2/1] [U_]
`
arrays := parseMDStatArrays(raw)
if len(arrays) != 2 {
t.Fatalf("expected 2 arrays, got %d", len(arrays))
}
if arrays[0].Name != "md126" || arrays[0].Degraded {
t.Fatalf("unexpected array0: %+v", arrays[0])
}
if len(arrays[0].Members) != 2 || arrays[0].Members[0] != "nvme0n1" {
t.Fatalf("unexpected members array0: %+v", arrays[0].Members)
}
if arrays[1].Name != "md125" || !arrays[1].Degraded {
t.Fatalf("unexpected array1: %+v", arrays[1])
}
}
func TestHasVROCController(t *testing.T) {
intel := vendorIntel
model := "Volume Management Device NVMe RAID Controller"
class := "RAID bus controller"
tests := []struct {
name string
pcie []schema.HardwarePCIeDevice
want bool
}{
{
name: "intel vroc",
pcie: []schema.HardwarePCIeDevice{{VendorID: &intel, Model: &model, DeviceClass: &class}},
want: true,
},
{
name: "non-intel raid",
pcie: []schema.HardwarePCIeDevice{{}},
want: false,
},
}
for _, tt := range tests {
got := hasVROCController(tt.pcie)
if got != tt.want {
t.Fatalf("%s: got %v want %v", tt.name, got, tt.want)
}
}
}