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 } readNetAddressFile = func(iface string) (string, error) { path := filepath.Join("/sys/class/net", iface, "address") raw, err := os.ReadFile(path) if err != nil { return "", err } return strings.TrimSpace(string(raw)), 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] devs[i].MacAddresses = collectInterfaceMACs(ifaces) if devs[i].Firmware == nil { if out, err := ethtoolInfoQuery(iface); err == nil { if fw := parseEthtoolFirmwareInfo(out); fw != "" { devs[i].Firmware = &fw } } } if out, err := ethtoolModuleQuery(iface); err == nil { if injectSFPDOMTelemetry(&devs[i], out) { enriched++ continue } } if len(devs[i].MacAddresses) > 0 || devs[i].Firmware != nil { enriched++ } } slog.Info("nic: telemetry enriched", "count", enriched) return devs } func isNICDevice(dev schema.HardwarePCIeDevice) bool { if dev.DeviceClass == nil { return false } c := strings.TrimSpace(*dev.DeviceClass) return isNICClass(c) || strings.EqualFold(c, "FibreChannelController") } func collectInterfaceMACs(ifaces []string) []string { seen := map[string]struct{}{} var out []string for _, iface := range ifaces { mac, err := readNetAddressFile(iface) if err != nil || mac == "" { continue } mac = strings.ToLower(strings.TrimSpace(mac)) if _, ok := seen[mac]; ok { continue } seen[mac] = struct{}{} out = append(out, mac) } return out } var floatRe = regexp.MustCompile(`[-+]?[0-9]*\.?[0-9]+`) func injectSFPDOMTelemetry(dev *schema.HardwarePCIeDevice, raw string) bool { var changed bool 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 { dev.SFPTemperatureC = &f changed = true } case strings.Contains(key, "laser output power"): if f, ok := dbmValue(val); ok { dev.SFPTXPowerDBM = &f changed = true } case strings.Contains(key, "receiver signal"): if f, ok := dbmValue(val); ok { dev.SFPRXPowerDBM = &f changed = true } case strings.Contains(key, "module voltage"): if f, ok := firstFloat(val); ok { dev.SFPVoltageV = &f changed = true } case strings.Contains(key, "laser bias current"): if f, ok := firstFloat(val); ok { dev.SFPBiasMA = &f changed = true } } } return changed } func parseSFPDOM(raw string) map[string]any { dev := schema.HardwarePCIeDevice{} if !injectSFPDOMTelemetry(&dev, raw) { return map[string]any{} } out := map[string]any{} if dev.SFPTemperatureC != nil { out["sfp_temperature_c"] = *dev.SFPTemperatureC } if dev.SFPTXPowerDBM != nil { out["sfp_tx_power_dbm"] = *dev.SFPTXPowerDBM } if dev.SFPRXPowerDBM != nil { out["sfp_rx_power_dbm"] = *dev.SFPRXPowerDBM } if dev.SFPVoltageV != nil { out["sfp_voltage_v"] = *dev.SFPVoltageV } if dev.SFPBiasMA != nil { out["sfp_bias_ma"] = *dev.SFPBiasMA } 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 }