325 lines
8.3 KiB
Go
325 lines
8.3 KiB
Go
package collector
|
|
|
|
import (
|
|
"bee/audit/internal/schema"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
ethtoolModuleQuery = func(iface string) (string, error) {
|
|
out, err := commandOutputWithTimeout(nicProbeTimeout, "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
|
|
}
|
|
sort.Strings(ifaces)
|
|
iface := ifaces[0]
|
|
devs[i].MacAddresses = collectInterfaceMACs(ifaces)
|
|
if devs[i].SerialNumber == nil {
|
|
if serial := queryPCIDeviceSerial(bdf); serial != "" {
|
|
devs[i].SerialNumber = &serial
|
|
}
|
|
}
|
|
|
|
if devs[i].Firmware == nil {
|
|
if out, err := ethtoolInfoQuery(iface); err == nil {
|
|
if fw := parseEthtoolFirmwareInfo(out); fw != "" {
|
|
devs[i].Firmware = &fw
|
|
}
|
|
}
|
|
}
|
|
|
|
for port, portIface := range ifaces {
|
|
out, err := ethtoolModuleQuery(portIface)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
legacy := schema.HardwarePCIeDevice{}
|
|
if !injectSFPDOMTelemetry(&legacy, out) || legacy.SFPIdentifier == nil {
|
|
continue
|
|
}
|
|
devs[i].SFPModules = append(devs[i].SFPModules, sfpModuleFromLegacy(port, legacy))
|
|
// Keep the v2.10 scalar representation for old Reanimator installs.
|
|
if len(devs[i].SFPModules) == 1 {
|
|
copyLegacySFPFields(&devs[i], legacy)
|
|
}
|
|
}
|
|
if len(devs[i].SFPModules) > 0 {
|
|
enriched++
|
|
continue
|
|
}
|
|
if len(devs[i].MacAddresses) > 0 || devs[i].Firmware != nil {
|
|
enriched++
|
|
}
|
|
}
|
|
slog.Info("nic: telemetry enriched", "count", enriched)
|
|
return devs
|
|
}
|
|
|
|
func sfpModuleFromLegacy(port int, dev schema.HardwarePCIeDevice) schema.HardwareSFPModule {
|
|
m := schema.HardwareSFPModule{Port: port, Identifier: dev.SFPIdentifier, Connector: dev.SFPConnector,
|
|
Vendor: dev.SFPVendor, PartNumber: dev.SFPPartNumber, SerialNumber: dev.SFPSerialNumber,
|
|
Revision: dev.SFPRevision, TransceiverType: dev.SFPTransceiverType,
|
|
TemperatureC: dev.SFPTemperatureC, TXPowerDBM: dev.SFPTXPowerDBM, RXPowerDBM: dev.SFPRXPowerDBM,
|
|
VoltageV: dev.SFPVoltageV, BiasMA: dev.SFPBiasMA}
|
|
if dev.SFPWavelengthNM != nil {
|
|
v := int(*dev.SFPWavelengthNM + 0.5)
|
|
m.WavelengthNM = &v
|
|
}
|
|
return m
|
|
}
|
|
|
|
func copyLegacySFPFields(dst *schema.HardwarePCIeDevice, src schema.HardwarePCIeDevice) {
|
|
dst.SFPPresent, dst.SFPIdentifier, dst.SFPConnector = src.SFPPresent, src.SFPIdentifier, src.SFPConnector
|
|
dst.SFPVendor, dst.SFPPartNumber, dst.SFPSerialNumber = src.SFPVendor, src.SFPPartNumber, src.SFPSerialNumber
|
|
dst.SFPRevision, dst.SFPTransceiverType = src.SFPRevision, src.SFPTransceiverType
|
|
dst.SFPWavelengthNM, dst.SFPTemperatureC, dst.SFPTXPowerDBM, dst.SFPRXPowerDBM = src.SFPWavelengthNM, src.SFPTemperatureC, src.SFPTXPowerDBM, src.SFPRXPowerDBM
|
|
dst.SFPVoltageV, dst.SFPBiasMA = src.SFPVoltageV, src.SFPBiasMA
|
|
}
|
|
|
|
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:])
|
|
if val == "" || strings.EqualFold(val, "not supported") || strings.EqualFold(val, "unknown") {
|
|
continue
|
|
}
|
|
|
|
switch {
|
|
case key == "identifier":
|
|
s := parseSFPIdentifier(val)
|
|
dev.SFPIdentifier = &s
|
|
t := true
|
|
dev.SFPPresent = &t
|
|
changed = true
|
|
case key == "connector":
|
|
s := parseSFPConnector(val)
|
|
dev.SFPConnector = &s
|
|
changed = true
|
|
case key == "vendor name":
|
|
s := strings.TrimSpace(val)
|
|
dev.SFPVendor = &s
|
|
changed = true
|
|
case key == "vendor pn":
|
|
s := strings.TrimSpace(val)
|
|
dev.SFPPartNumber = &s
|
|
changed = true
|
|
case key == "vendor sn":
|
|
s := strings.TrimSpace(val)
|
|
dev.SFPSerialNumber = &s
|
|
changed = true
|
|
case key == "vendor rev":
|
|
s := strings.TrimSpace(val)
|
|
dev.SFPRevision = &s
|
|
changed = true
|
|
case key == "transceiver type":
|
|
s := strings.TrimSpace(val)
|
|
dev.SFPTransceiverType = &s
|
|
changed = true
|
|
case strings.Contains(key, "laser wavelength"):
|
|
if f, ok := firstFloat(val); ok {
|
|
dev.SFPWavelengthNM = &f
|
|
changed = true
|
|
}
|
|
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
|
|
}
|
|
|
|
// parseSFPIdentifier extracts the human-readable transceiver type from the
|
|
// raw ethtool identifier line, e.g. "0x03 (SFP)" → "SFP".
|
|
func parseSFPIdentifier(val string) string {
|
|
if s := extractParens(val); s != "" {
|
|
return s
|
|
}
|
|
return val
|
|
}
|
|
|
|
// parseSFPConnector extracts the connector type from the raw ethtool line,
|
|
// e.g. "0x07 (LC)" → "LC".
|
|
func parseSFPConnector(val string) string {
|
|
if s := extractParens(val); s != "" {
|
|
return s
|
|
}
|
|
return val
|
|
}
|
|
|
|
var parenRe = regexp.MustCompile(`\(([^)]+)\)`)
|
|
|
|
func extractParens(s string) string {
|
|
m := parenRe.FindStringSubmatch(s)
|
|
if len(m) < 2 {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(m[1])
|
|
}
|
|
|
|
func parseSFPDOM(raw string) map[string]any {
|
|
dev := schema.HardwarePCIeDevice{}
|
|
if !injectSFPDOMTelemetry(&dev, raw) {
|
|
return map[string]any{}
|
|
}
|
|
out := map[string]any{}
|
|
if dev.SFPPresent != nil {
|
|
out["sfp_present"] = *dev.SFPPresent
|
|
}
|
|
if dev.SFPIdentifier != nil {
|
|
out["sfp_identifier"] = *dev.SFPIdentifier
|
|
}
|
|
if dev.SFPConnector != nil {
|
|
out["sfp_connector"] = *dev.SFPConnector
|
|
}
|
|
if dev.SFPVendor != nil {
|
|
out["sfp_vendor"] = *dev.SFPVendor
|
|
}
|
|
if dev.SFPPartNumber != nil {
|
|
out["sfp_part_number"] = *dev.SFPPartNumber
|
|
}
|
|
if dev.SFPSerialNumber != nil {
|
|
out["sfp_serial_number"] = *dev.SFPSerialNumber
|
|
}
|
|
if dev.SFPWavelengthNM != nil {
|
|
out["sfp_wavelength_nm"] = *dev.SFPWavelengthNM
|
|
}
|
|
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
|
|
}
|