A (hardware-ingest-json v2.8-2.9): remove sensor location fields from schema and collector; tag HardwareMemory.Location as json:"-"; add PlatformConfig to HardwareSnapshot. B (no-hardcoded-vendors): consolidate PCI vendor IDs into collector/pci_vendors.go; replace all vendor-name string checks in isGPUDevice, isNVIDIADevice, isMellanoxDevice, isAMDGPUDevice, matchesGPUVendor (sat_overlay), and validateIsVendorGPU (page_validate) with numeric vendor_id comparisons. C (module-structure): split app/app.go (1413 lines) into app.go + app_format.go, app_network.go, app_services.go, app_packs.go, app_install.go — no logic changes. D (go-code-style): wrap bare return err in interfaceAdminState and interfaceIPv4Addrs (platform/network.go) with fmt.Errorf context including the interface name. E (go-project-bible): add bible-local/architecture/data-model.md and bible-local/architecture/api-surface.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
172 lines
3.6 KiB
Go
172 lines
3.6 KiB
Go
package collector
|
|
|
|
import (
|
|
"bee/audit/internal/schema"
|
|
"context"
|
|
"log/slog"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const nicProbeTimeout = 2 * time.Second
|
|
|
|
var (
|
|
mstflintQuery = func(bdf string) (string, error) {
|
|
out, err := commandOutputWithTimeout(nicProbeTimeout, "mstflint", "-d", bdf, "q")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(out), nil
|
|
}
|
|
|
|
ethtoolInfoQuery = func(iface string) (string, error) {
|
|
out, err := commandOutputWithTimeout(nicProbeTimeout, "ethtool", "-i", iface)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(out), nil
|
|
}
|
|
|
|
netIfacesByBDF = listNetIfacesByBDF
|
|
readNetCarrierFile = func(iface string) (string, error) {
|
|
path := filepath.Join("/sys/class/net", iface, "carrier")
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.TrimSpace(string(raw)), nil
|
|
}
|
|
)
|
|
|
|
// 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 {
|
|
return dev.VendorID != nil && *dev.VendorID == MellanoxVendorID
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func commandOutputWithTimeout(timeout time.Duration, name string, args ...string) ([]byte, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
return exec.CommandContext(ctx, name, args...).Output()
|
|
}
|