From 129d1ef6e1b41de8574131d8198f81ec222d9cd0 Mon Sep 17 00:00:00 2001 From: Anthony Harivel Date: Mon, 9 Feb 2026 09:49:37 +0100 Subject: [PATCH] Add collector for SR-IOV network Virtual Function statistics Add a new netvf collector that exposes SR-IOV network VF statistics and configuration via rtnetlink. The collector queries netlink for interfaces with Virtual Functions and exposes per-VF metrics: - node_net_vf_info: VF configuration (MAC, VLAN, link state, spoof check, trust, PCI address, NUMA node) - node_net_vf_{receive,transmit}_{packets,bytes}_total: traffic counters - node_net_vf_{broadcast,multicast}_packets_total: packet type counters - node_net_vf_{receive,transmit}_dropped_total: drop counters All metrics include a pci_address label resolved from the sysfs virtfn symlink, enabling direct correlation with workloads that reference VFs by PCI BDF address (e.g. OpenStack Nova, libvirt, DPDK). All metrics also include a numa_node label resolved from the PF's PCI device sysfs entry, enabling NUMA alignment verification and cross-NUMA traffic ratio queries in PromQL. The collector is disabled by default and can be enabled with --collector.netvf. PF device filtering is supported via --collector.netvf.device-include/exclude flags. Signed-off-by: Anthony Harivel --- README.md | 2 + collector/netvf_linux.go | 227 ++++++++++++++++++ collector/netvf_linux_test.go | 418 ++++++++++++++++++++++++++++++++++ go.mod | 2 + go.sum | 2 - 5 files changed, 649 insertions(+), 2 deletions(-) create mode 100644 collector/netvf_linux.go create mode 100644 collector/netvf_linux_test.go diff --git a/README.md b/README.md index ec5adbf7ac..3ff6c56563 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ hwmon | chip | --collector.hwmon.chip-include | --collector.hwmon.chip-exclude hwmon | sensor | --collector.hwmon.sensor-include | --collector.hwmon.sensor-exclude interrupts | name | --collector.interrupts.name-include | --collector.interrupts.name-exclude netdev | device | --collector.netdev.device-include | --collector.netdev.device-exclude +netvf | device | --collector.netvf.device-include | --collector.netvf.device-exclude qdisk | device | --collector.qdisk.device-include | --collector.qdisk.device-exclude slabinfo | slab-names | --collector.slabinfo.slabs-include | --collector.slabinfo.slabs-exclude sysctl | all | --collector.sysctl.include | N/A @@ -202,6 +203,7 @@ logind | Exposes session counts from [logind](http://www.freedesktop.org/wiki/So meminfo\_numa | Exposes memory statistics from `/sys/devices/system/node/node[0-9]*/meminfo`, `/sys/devices/system/node/node[0-9]*/numastat`. | Linux mountstats | Exposes filesystem statistics from `/proc/self/mountstats`. Exposes detailed NFS client statistics. | Linux network_route | Exposes the routing table as metrics | Linux +netvf | Exposes SR-IOV Virtual Function statistics and configuration from netlink. | Linux pcidevice | Exposes pci devices' information including their link status and parent devices. | Linux perf | Exposes perf based metrics (Warning: Metrics are dependent on kernel configuration and settings). | Linux processes | Exposes aggregate process statistics from `/proc`. | Linux diff --git a/collector/netvf_linux.go b/collector/netvf_linux.go new file mode 100644 index 0000000000..745010e17e --- /dev/null +++ b/collector/netvf_linux.go @@ -0,0 +1,227 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !nonetvf + +package collector + +import ( + "errors" + "fmt" + "log/slog" + + "github.com/alecthomas/kingpin/v2" + "github.com/jsimonetti/rtnetlink/v2" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/procfs/sysfs" +) + +const netvfSubsystem = "net_vf" + +var ( + netvfDeviceInclude = kingpin.Flag("collector.netvf.device-include", "Regexp of PF devices to include (mutually exclusive to device-exclude).").String() + netvfDeviceExclude = kingpin.Flag("collector.netvf.device-exclude", "Regexp of PF devices to exclude (mutually exclusive to device-include).").String() +) + +func init() { + registerCollector("netvf", defaultDisabled, NewNetVFCollector) +} + +type netvfCollector struct { + logger *slog.Logger + deviceFilter deviceFilter + + info *prometheus.Desc + receivePackets *prometheus.Desc + transmitPackets *prometheus.Desc + receiveBytes *prometheus.Desc + transmitBytes *prometheus.Desc + broadcast *prometheus.Desc + multicast *prometheus.Desc + receiveDropped *prometheus.Desc + transmitDropped *prometheus.Desc +} + +func NewNetVFCollector(logger *slog.Logger) (Collector, error) { + if *netvfDeviceExclude != "" && *netvfDeviceInclude != "" { + return nil, errors.New("device-exclude & device-include are mutually exclusive") + } + + if *netvfDeviceExclude != "" { + logger.Info("Parsed flag --collector.netvf.device-exclude", "flag", *netvfDeviceExclude) + } + + if *netvfDeviceInclude != "" { + logger.Info("Parsed flag --collector.netvf.device-include", "flag", *netvfDeviceInclude) + } + + return &netvfCollector{ + logger: logger, + deviceFilter: newDeviceFilter(*netvfDeviceExclude, *netvfDeviceInclude), + info: prometheus.NewDesc( + prometheus.BuildFQName(namespace, netvfSubsystem, "info"), + "Virtual Function configuration information.", + []string{"device", "vf", "mac", "vlan", "link_state", "spoof_check", "trust", "pci_address", "numa_node"}, nil, + ), + receivePackets: prometheus.NewDesc( + prometheus.BuildFQName(namespace, netvfSubsystem, "receive_packets_total"), + "Number of received packets by the VF.", + []string{"device", "vf", "pci_address", "numa_node"}, nil, + ), + transmitPackets: prometheus.NewDesc( + prometheus.BuildFQName(namespace, netvfSubsystem, "transmit_packets_total"), + "Number of transmitted packets by the VF.", + []string{"device", "vf", "pci_address", "numa_node"}, nil, + ), + receiveBytes: prometheus.NewDesc( + prometheus.BuildFQName(namespace, netvfSubsystem, "receive_bytes_total"), + "Number of received bytes by the VF.", + []string{"device", "vf", "pci_address", "numa_node"}, nil, + ), + transmitBytes: prometheus.NewDesc( + prometheus.BuildFQName(namespace, netvfSubsystem, "transmit_bytes_total"), + "Number of transmitted bytes by the VF.", + []string{"device", "vf", "pci_address", "numa_node"}, nil, + ), + broadcast: prometheus.NewDesc( + prometheus.BuildFQName(namespace, netvfSubsystem, "broadcast_packets_total"), + "Number of broadcast packets received by the VF.", + []string{"device", "vf", "pci_address", "numa_node"}, nil, + ), + multicast: prometheus.NewDesc( + prometheus.BuildFQName(namespace, netvfSubsystem, "multicast_packets_total"), + "Number of multicast packets received by the VF.", + []string{"device", "vf", "pci_address", "numa_node"}, nil, + ), + receiveDropped: prometheus.NewDesc( + prometheus.BuildFQName(namespace, netvfSubsystem, "receive_dropped_total"), + "Number of dropped received packets by the VF.", + []string{"device", "vf", "pci_address", "numa_node"}, nil, + ), + transmitDropped: prometheus.NewDesc( + prometheus.BuildFQName(namespace, netvfSubsystem, "transmit_dropped_total"), + "Number of dropped transmitted packets by the VF.", + []string{"device", "vf", "pci_address", "numa_node"}, nil, + ), + }, nil +} + +func (c *netvfCollector) Update(ch chan<- prometheus.Metric) error { + conn, err := rtnetlink.Dial(nil) + if err != nil { + return fmt.Errorf("failed to connect to rtnetlink: %w", err) + } + defer conn.Close() + + links, err := conn.Link.ListWithVFInfo() + if err != nil { + return fmt.Errorf("failed to list interfaces with VF info: %w", err) + } + + sysFS, sysErr := sysfs.NewFS(sysFilePath("")) + + vfCount := 0 + for _, link := range links { + if link.Attributes == nil { + continue + } + + // Skip interfaces without VFs + if link.Attributes.NumVF == nil || *link.Attributes.NumVF == 0 { + continue + } + + device := link.Attributes.Name + + // Apply device filter + if c.deviceFilter.ignored(device) { + c.logger.Debug("Ignoring device", "device", device) + continue + } + + // Resolve PCI device once per PF to get NUMA node and VF addresses. + numaNode := "-1" + var pciDev *sysfs.PciDevice + if sysErr == nil { + if dev, err := sysFS.NetClassPCIDevice(device); err == nil { + pciDev = dev + if dev.NumaNode != nil { + numaNode = fmt.Sprintf("%d", *dev.NumaNode) + } + } + } + + for _, vf := range link.Attributes.VFInfoList { + vfID := fmt.Sprintf("%d", vf.ID) + + // Emit info metric with VF configuration + mac := "" + if vf.MAC != nil { + mac = vf.MAC.String() + } + vlan := fmt.Sprintf("%d", vf.Vlan) + linkState := vfLinkStateString(vf.LinkState) + spoofCheck := fmt.Sprintf("%t", vf.SpoofCheck) + trust := fmt.Sprintf("%t", vf.Trust) + + pciAddress := "" + if pciDev != nil { + if addr, err := sysFS.PciDeviceVFAddress(pciDev, vf.ID); err == nil { + pciAddress = addr + } + } + + ch <- prometheus.MustNewConstMetric(c.info, prometheus.GaugeValue, 1, device, vfID, mac, vlan, linkState, spoofCheck, trust, pciAddress, numaNode) + + // Emit stats metrics if available + if vf.Stats == nil { + c.logger.Debug("VF has no stats", "device", device, "vf", vf.ID) + vfCount++ + continue + } + + stats := vf.Stats + + ch <- prometheus.MustNewConstMetric(c.receivePackets, prometheus.CounterValue, float64(stats.RxPackets), device, vfID, pciAddress, numaNode) + ch <- prometheus.MustNewConstMetric(c.transmitPackets, prometheus.CounterValue, float64(stats.TxPackets), device, vfID, pciAddress, numaNode) + ch <- prometheus.MustNewConstMetric(c.receiveBytes, prometheus.CounterValue, float64(stats.RxBytes), device, vfID, pciAddress, numaNode) + ch <- prometheus.MustNewConstMetric(c.transmitBytes, prometheus.CounterValue, float64(stats.TxBytes), device, vfID, pciAddress, numaNode) + ch <- prometheus.MustNewConstMetric(c.broadcast, prometheus.CounterValue, float64(stats.Broadcast), device, vfID, pciAddress, numaNode) + ch <- prometheus.MustNewConstMetric(c.multicast, prometheus.CounterValue, float64(stats.Multicast), device, vfID, pciAddress, numaNode) + ch <- prometheus.MustNewConstMetric(c.receiveDropped, prometheus.CounterValue, float64(stats.RxDropped), device, vfID, pciAddress, numaNode) + ch <- prometheus.MustNewConstMetric(c.transmitDropped, prometheus.CounterValue, float64(stats.TxDropped), device, vfID, pciAddress, numaNode) + + vfCount++ + } + } + + if vfCount == 0 { + return ErrNoData + } + + return nil +} + +func vfLinkStateString(state rtnetlink.VFLinkState) string { + switch state { + case rtnetlink.VFLinkStateAuto: + return "auto" + case rtnetlink.VFLinkStateEnable: + return "enable" + case rtnetlink.VFLinkStateDisable: + return "disable" + default: + return "unknown" + } +} + diff --git a/collector/netvf_linux_test.go b/collector/netvf_linux_test.go new file mode 100644 index 0000000000..52cf40e019 --- /dev/null +++ b/collector/netvf_linux_test.go @@ -0,0 +1,418 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !nonetvf + +package collector + +import ( + "io" + "log/slog" + "net" + "os" + "path/filepath" + "testing" + + "github.com/jsimonetti/rtnetlink/v2" + "github.com/prometheus/procfs/sysfs" +) + +func uint32Ptr(v uint32) *uint32 { + return &v +} + +// vfMetrics holds parsed VF metrics for a single VF +type vfMetrics struct { + Device string + VFID uint32 + MAC string + Vlan uint32 + LinkState string + SpoofCheck bool + Trust bool + PCIAddress string + Stats *rtnetlink.VFStats +} + +// parseVFInfo extracts VF information from link messages for testing. +// sysClassPath is the path to the sysfs class directory used to resolve VF PCI addresses. +func parseVFInfo(links []rtnetlink.LinkMessage, filter *deviceFilter, logger *slog.Logger, sysClassPath string) []vfMetrics { + var result []vfMetrics + + for _, link := range links { + if link.Attributes == nil { + continue + } + + if link.Attributes.NumVF == nil || *link.Attributes.NumVF == 0 { + continue + } + + device := link.Attributes.Name + + if filter.ignored(device) { + logger.Debug("Ignoring device", "device", device) + continue + } + + for _, vf := range link.Attributes.VFInfoList { + mac := "" + if vf.MAC != nil { + mac = vf.MAC.String() + } + + pciAddress := "" + if sysClassPath != "" { + if fs, err := sysfs.NewFS(sysClassPath); err == nil { + if dev, err := fs.NetClassPCIDevice(device); err == nil { + pciAddress, _ = fs.PciDeviceVFAddress(dev, vf.ID) + } + } + } + result = append(result, vfMetrics{ + Device: device, + VFID: vf.ID, + MAC: mac, + Vlan: vf.Vlan, + LinkState: vfLinkStateString(vf.LinkState), + SpoofCheck: vf.SpoofCheck, + Trust: vf.Trust, + PCIAddress: pciAddress, + Stats: vf.Stats, + }) + } + } + + return result +} + +var vfLinks = []rtnetlink.LinkMessage{ + { + // Interface without VFs + Attributes: &rtnetlink.LinkAttributes{ + Name: "eth0", + Stats64: &rtnetlink.LinkStats64{ + RXPackets: 1000, + TXPackets: 2000, + }, + }, + }, + { + // Interface with NumVF = 0 + Attributes: &rtnetlink.LinkAttributes{ + Name: "eth1", + NumVF: uint32Ptr(0), + }, + }, + { + // PF with 2 VFs + Attributes: &rtnetlink.LinkAttributes{ + Name: "enp3s0f0", + NumVF: uint32Ptr(2), + VFInfoList: []rtnetlink.VFInfo{ + { + ID: 0, + MAC: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x00}, + Vlan: 100, + LinkState: rtnetlink.VFLinkStateAuto, + SpoofCheck: true, + Trust: false, + Stats: &rtnetlink.VFStats{ + RxPackets: 1000, + TxPackets: 2000, + RxBytes: 100000, + TxBytes: 200000, + Broadcast: 10, + Multicast: 20, + RxDropped: 5, + TxDropped: 3, + }, + }, + { + ID: 1, + MAC: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x01}, + Vlan: 200, + LinkState: rtnetlink.VFLinkStateEnable, + SpoofCheck: false, + Trust: true, + Stats: &rtnetlink.VFStats{ + RxPackets: 3000, + TxPackets: 4000, + RxBytes: 300000, + TxBytes: 400000, + Broadcast: 30, + Multicast: 40, + RxDropped: 7, + TxDropped: 9, + }, + }, + }, + }, + }, + { + // Another PF with 1 VF (no stats) + Attributes: &rtnetlink.LinkAttributes{ + Name: "enp3s0f1", + NumVF: uint32Ptr(1), + VFInfoList: []rtnetlink.VFInfo{ + { + ID: 0, + MAC: net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}, + Vlan: 0, + LinkState: rtnetlink.VFLinkStateDisable, + SpoofCheck: true, + Trust: false, + Stats: nil, // No stats available + }, + }, + }, + }, + { + // Nil attributes (should be skipped) + Attributes: nil, + }, +} + +func TestParseVFInfo(t *testing.T) { + filter := newDeviceFilter("", "") + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + + vfs := parseVFInfo(vfLinks, &filter, logger, "") + + // Should have 3 VFs total (2 from enp3s0f0, 1 from enp3s0f1) + if want, got := 3, len(vfs); want != got { + t.Errorf("want %d VFs, got %d", want, got) + } +} + +func TestParseVFInfoDeviceFilter(t *testing.T) { + // Exclude enp3s0f1 + filter := newDeviceFilter("enp3s0f1", "") + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + + vfs := parseVFInfo(vfLinks, &filter, logger, "") + + // Should have 2 VFs (only from enp3s0f0) + if want, got := 2, len(vfs); want != got { + t.Errorf("want %d VFs, got %d", want, got) + } + + for _, vf := range vfs { + if vf.Device == "enp3s0f1" { + t.Error("enp3s0f1 should be filtered out") + } + } +} + +func TestParseVFInfoDeviceInclude(t *testing.T) { + // Only include enp3s0f1 + filter := newDeviceFilter("", "^enp3s0f1$") + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + + vfs := parseVFInfo(vfLinks, &filter, logger, "") + + // Should have 1 VF (only from enp3s0f1) + if want, got := 1, len(vfs); want != got { + t.Errorf("want %d VFs, got %d", want, got) + } + + if len(vfs) > 0 && vfs[0].Device != "enp3s0f1" { + t.Errorf("want device enp3s0f1, got %s", vfs[0].Device) + } +} + +func TestParseVFInfoStats(t *testing.T) { + filter := newDeviceFilter("", "^enp3s0f0$") + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + + vfs := parseVFInfo(vfLinks, &filter, logger, "") + + if len(vfs) != 2 { + t.Fatalf("expected 2 VFs, got %d", len(vfs)) + } + + // Check VF 0 stats + vf0 := vfs[0] + if vf0.VFID != 0 { + t.Errorf("expected VF ID 0, got %d", vf0.VFID) + } + if vf0.Stats == nil { + t.Fatal("expected stats for VF 0") + } + if want, got := uint64(1000), vf0.Stats.RxPackets; want != got { + t.Errorf("want RxPackets %d, got %d", want, got) + } + if want, got := uint64(200000), vf0.Stats.TxBytes; want != got { + t.Errorf("want TxBytes %d, got %d", want, got) + } + + // Check VF 1 stats + vf1 := vfs[1] + if vf1.VFID != 1 { + t.Errorf("expected VF ID 1, got %d", vf1.VFID) + } + if want, got := uint64(4000), vf1.Stats.TxPackets; want != got { + t.Errorf("want TxPackets %d, got %d", want, got) + } +} + +func TestParseVFInfoMetadata(t *testing.T) { + filter := newDeviceFilter("", "^enp3s0f0$") + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + + vfs := parseVFInfo(vfLinks, &filter, logger, "") + + if len(vfs) != 2 { + t.Fatalf("expected 2 VFs, got %d", len(vfs)) + } + + // Check VF 0 metadata + vf0 := vfs[0] + if want, got := "aa:bb:cc:dd:ee:00", vf0.MAC; want != got { + t.Errorf("want MAC %s, got %s", want, got) + } + if want, got := uint32(100), vf0.Vlan; want != got { + t.Errorf("want VLAN %d, got %d", want, got) + } + if want, got := "auto", vf0.LinkState; want != got { + t.Errorf("want LinkState %s, got %s", want, got) + } + if !vf0.SpoofCheck { + t.Error("expected SpoofCheck to be true") + } + if vf0.Trust { + t.Error("expected Trust to be false") + } + + // Check VF 1 metadata + vf1 := vfs[1] + if want, got := "enable", vf1.LinkState; want != got { + t.Errorf("want LinkState %s, got %s", want, got) + } + if vf1.SpoofCheck { + t.Error("expected SpoofCheck to be false") + } + if !vf1.Trust { + t.Error("expected Trust to be true") + } +} + +func TestParseVFInfoNoStats(t *testing.T) { + filter := newDeviceFilter("", "^enp3s0f1$") + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + + vfs := parseVFInfo(vfLinks, &filter, logger, "") + + if len(vfs) != 1 { + t.Fatalf("expected 1 VF, got %d", len(vfs)) + } + + vf := vfs[0] + if vf.Stats != nil { + t.Error("expected Stats to be nil for this VF") + } + if want, got := "disable", vf.LinkState; want != got { + t.Errorf("want LinkState %s, got %s", want, got) + } +} + +func TestVFLinkStateString(t *testing.T) { + tests := []struct { + state rtnetlink.VFLinkState + expected string + }{ + {rtnetlink.VFLinkStateAuto, "auto"}, + {rtnetlink.VFLinkStateEnable, "enable"}, + {rtnetlink.VFLinkStateDisable, "disable"}, + {rtnetlink.VFLinkState(99), "unknown"}, + } + + for _, tt := range tests { + got := vfLinkStateString(tt.state) + if got != tt.expected { + t.Errorf("vfLinkStateString(%d) = %s, want %s", tt.state, got, tt.expected) + } + } +} + +func TestResolveVFPCIAddress(t *testing.T) { + // Create a fake sysfs tree with: + // - class/net/enp3s0f0/device -> symlink to the PCI device + // - bus/pci/devices/0000:00:01.0 -> symlink to the real device path + // - devices/pci0000:00/0000:00:01.0/ with required PCI files and virtfn0 + tmp := t.TempDir() + + // Real device directory with required PCI files and virtfn0 symlink + pciDevDir := filepath.Join(tmp, "devices", "pci0000:00", "0000:00:01.0") + if err := os.MkdirAll(pciDevDir, 0o755); err != nil { + t.Fatal(err) + } + for _, f := range []struct{ name, val string }{ + {"class", "0x020000"}, {"vendor", "0x8086"}, {"device", "0x1572"}, + {"subsystem_vendor", "0x8086"}, {"subsystem_device", "0x0000"}, {"revision", "0x00"}, + } { + if err := os.WriteFile(filepath.Join(pciDevDir, f.name), []byte(f.val), 0o444); err != nil { + t.Fatal(err) + } + } + if err := os.Symlink("../0000:00:02.0", filepath.Join(pciDevDir, "virtfn0")); err != nil { + t.Fatal(err) + } + + // bus/pci/devices/0000:00:01.0 -> symlink to real device + busDir := filepath.Join(tmp, "bus", "pci", "devices") + if err := os.MkdirAll(busDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.Symlink("../../../devices/pci0000:00/0000:00:01.0", filepath.Join(busDir, "0000:00:01.0")); err != nil { + t.Fatal(err) + } + + // class/net/enp3s0f0/device -> symlink to the PCI device + netDir := filepath.Join(tmp, "class", "net", "enp3s0f0") + if err := os.MkdirAll(netDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.Symlink("../../../devices/pci0000:00/0000:00:01.0", filepath.Join(netDir, "device")); err != nil { + t.Fatal(err) + } + + fs, err := sysfs.NewFS(tmp) + if err != nil { + t.Fatal(err) + } + dev, err := fs.NetClassPCIDevice("enp3s0f0") + if err != nil { + t.Fatal(err) + } + got, err := fs.PciDeviceVFAddress(dev, 0) + if err != nil { + t.Fatal(err) + } + if want := "0000:00:02.0"; got != want { + t.Errorf("PciDeviceVFAddress() = %q, want %q", got, want) + } +} + +func TestResolveVFPCIAddressMissing(t *testing.T) { + tmp := t.TempDir() + + fs, err := sysfs.NewFS(tmp) + if err != nil { + t.Fatal(err) + } + _, err = fs.NetClassPCIDevice("enp3s0f0") + if err == nil { + t.Error("expected error for missing interface, got nil") + } +} diff --git a/go.mod b/go.mod index 055a15abff..51629a95f1 100644 --- a/go.mod +++ b/go.mod @@ -61,3 +61,5 @@ require ( golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect ) + +replace github.com/prometheus/procfs => ../procfs diff --git a/go.sum b/go.sum index 480f8d17f0..bc54a5ff3b 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,6 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/exporter-toolkit v0.15.1 h1:XrGGr/qWl8Gd+pqJqTkNLww9eG8vR/CoRk0FubOKfLE= github.com/prometheus/exporter-toolkit v0.15.1/go.mod h1:P/NR9qFRGbCFgpklyhix9F6v6fFr/VQB/CVsrMDGKo4= -github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= -github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/safchain/ethtool v0.7.0 h1:rlJzfDetsVvT61uz8x1YIcFn12akMfuPulHtZjtb7Is=