// Copyright 2021 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 !noethtool // +build !noethtool package collector import ( "bufio" "fmt" "io" "log/slog" "os" "path/filepath" "strconv" "strings" "syscall" "testing" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" "github.com/safchain/ethtool" "golang.org/x/sys/unix" ) type EthtoolFixture struct { fixturePath string } type testEthtoolCollector struct { dsc Collector } func (c testEthtoolCollector) Collect(ch chan<- prometheus.Metric) { c.dsc.Update(ch) } func (c testEthtoolCollector) Describe(ch chan<- *prometheus.Desc) { prometheus.DescribeByCollect(c, ch) } func NewTestEthtoolCollector(logger *slog.Logger) (prometheus.Collector, error) { dsc, err := NewEthtoolTestCollector(logger) if err != nil { return testEthtoolCollector{}, err } return testEthtoolCollector{ dsc: dsc, }, err } func (e *EthtoolFixture) DriverInfo(intf string) (ethtool.DrvInfo, error) { res := ethtool.DrvInfo{} fixtureFile, err := os.Open(filepath.Join(e.fixturePath, intf, "driver")) if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOENT { // The fixture for this interface doesn't exist. Translate that to unix.EOPNOTSUPP // to replicate an interface that doesn't support ethtool driver info return res, unix.EOPNOTSUPP } if err != nil { return res, err } defer fixtureFile.Close() scanner := bufio.NewScanner(fixtureFile) for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "#") { continue } line = strings.Trim(line, " ") items := strings.Split(line, ": ") switch items[0] { case "driver": res.Driver = items[1] case "version": res.Version = items[1] case "firmware-version": res.FwVersion = items[1] case "bus-info": res.BusInfo = items[1] case "expansion-rom-version": res.EromVersion = items[1] } } return res, err } func (e *EthtoolFixture) Stats(intf string) (map[string]uint64, error) { res := make(map[string]uint64) fixtureFile, err := os.Open(filepath.Join(e.fixturePath, intf, "statistics")) if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOENT { // The fixture for this interface doesn't exist. Translate that to unix.EOPNOTSUPP // to replicate an interface that doesn't support ethtool stats return res, unix.EOPNOTSUPP } if err != nil { return res, err } defer fixtureFile.Close() scanner := bufio.NewScanner(fixtureFile) for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "#") { continue } if strings.HasPrefix(line, "NIC statistics:") { continue } line = strings.Trim(line, " ") items := strings.Split(line, ": ") val, err := strconv.ParseUint(items[1], 10, 64) if err != nil { return res, err } if items[0] == "ERROR" { return res, unix.Errno(val) } res[items[0]] = val } return res, err } func readModes(modes string) uint32 { var out uint32 for _, mode := range strings.Split(modes, " ") { switch mode { case "10baseT/Half": out |= (1 << unix.ETHTOOL_LINK_MODE_10baseT_Half_BIT) case "10baseT/Full": out |= (1 << unix.ETHTOOL_LINK_MODE_10baseT_Full_BIT) case "100baseT/Half": out |= (1 << unix.ETHTOOL_LINK_MODE_100baseT_Half_BIT) case "100baseT/Full": out |= (1 << unix.ETHTOOL_LINK_MODE_100baseT_Full_BIT) case "1000baseT/Half": out |= (1 << unix.ETHTOOL_LINK_MODE_1000baseT_Half_BIT) case "1000baseT/Full": out |= (1 << unix.ETHTOOL_LINK_MODE_1000baseT_Full_BIT) case "10000baseT/Full": out |= (1 << unix.ETHTOOL_LINK_MODE_10000baseT_Full_BIT) } } return out } func readPortTypes(portTypes string) uint32 { var out uint32 for _, ptype := range strings.Split(portTypes, " ") { ptype = strings.Trim(ptype, " \t") if ptype == "TP" { out |= (1 << unix.ETHTOOL_LINK_MODE_TP_BIT) } if ptype == "MII" { out |= (1 << unix.ETHTOOL_LINK_MODE_MII_BIT) } } return out } func (e *EthtoolFixture) LinkInfo(intf string) (ethtool.EthtoolCmd, error) { var res ethtool.EthtoolCmd fixtureFile, err := os.Open(filepath.Join(e.fixturePath, intf, "settings")) if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOENT { // The fixture for this interface doesn't exist. Translate that to unix.EOPNOTSUPP // to replicate an interface that doesn't support ethtool stats return res, unix.EOPNOTSUPP } if err != nil { return res, err } defer fixtureFile.Close() scanner := bufio.NewScanner(fixtureFile) readingSupportedLinkModes := false readingAdvertisedLinkModes := false for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "#") || strings.HasPrefix(line, "Settings for") { continue } line = strings.Trim(line, " \t") if (readingAdvertisedLinkModes || readingSupportedLinkModes) && strings.Contains(line, ":") { readingAdvertisedLinkModes = false readingSupportedLinkModes = false } if readingAdvertisedLinkModes { res.Advertising |= readModes(line) continue } else if readingSupportedLinkModes { res.Supported |= readModes(line) continue } items := strings.Split(line, ": ") if items[0] == "Supported pause frame use" { if items[1] == "Symmetric" { res.Supported |= (1 << unix.ETHTOOL_LINK_MODE_Pause_BIT) } else if items[1] == "Receive-only" { res.Supported |= (1 << unix.ETHTOOL_LINK_MODE_Asym_Pause_BIT) } } if items[0] == "Advertised pause frame use" { if items[1] == "Symmetric" { res.Advertising |= (1 << unix.ETHTOOL_LINK_MODE_Pause_BIT) } else if items[1] == "Receive-only" { res.Advertising |= (1 << unix.ETHTOOL_LINK_MODE_Asym_Pause_BIT) } } if items[0] == "Supported ports" { res.Supported |= readPortTypes(items[1]) } if items[0] == "Supported link modes" { res.Supported |= readModes(items[1]) readingSupportedLinkModes = true } if items[0] == "Advertised link modes" { res.Advertising |= readModes(items[1]) readingAdvertisedLinkModes = true } if items[0] == "Supports auto-negotiation" { if items[1] == "Yes" { res.Supported |= (1 << unix.ETHTOOL_LINK_MODE_Autoneg_BIT) } } if items[0] == "Advertised auto-negotiation" { if items[1] == "Yes" { res.Advertising |= (1 << unix.ETHTOOL_LINK_MODE_Autoneg_BIT) } } if items[0] == "Auto-negotiation" { if items[1] == "on" { res.Autoneg = 1 } } } return res, err } func NewEthtoolTestCollector(logger *slog.Logger) (Collector, error) { collector, err := makeEthtoolCollector(logger) if err != nil { return nil, err } collector.ethtool = &EthtoolFixture{ fixturePath: "fixtures/ethtool/", } return collector, nil } func TestBuildEthtoolFQName(t *testing.T) { testcases := map[string]string{ "rx_errors": "node_ethtool_received_errors", "Queue[0] AllocFails": "node_ethtool_queue_0_allocfails", "Tx LPI entry count": "node_ethtool_transmitted_lpi_entry_count", "port.VF_admin_queue_requests": "node_ethtool_port_vf_admin_queue_requests", "[3]: tx_bytes": "node_ethtool_3_transmitted_bytes", " err": "node_ethtool_err", } for metric, expected := range testcases { got := buildEthtoolFQName(metric) if expected != got { t.Errorf("Expected '%s' but got '%s'", expected, got) } } } func TestEthToolCollector(t *testing.T) { testcase := `# HELP node_ethtool_align_errors Network interface align_errors # TYPE node_ethtool_align_errors untyped node_ethtool_align_errors{device="eth0"} 0 # HELP node_ethtool_info A metric with a constant '1' value labeled by bus_info, device, driver, expansion_rom_version, firmware_version, version. # TYPE node_ethtool_info gauge node_ethtool_info{bus_info="0000:00:1f.6",device="eth0",driver="e1000e",expansion_rom_version="",firmware_version="0.5-4",version="5.11.0-22-generic"} 1 # HELP node_ethtool_received_broadcast Network interface rx_broadcast # TYPE node_ethtool_received_broadcast untyped node_ethtool_received_broadcast{device="eth0"} 5792 # HELP node_ethtool_received_errors_total Number of received frames with errors # TYPE node_ethtool_received_errors_total untyped node_ethtool_received_errors_total{device="eth0"} 0 # HELP node_ethtool_received_missed Network interface rx_missed # TYPE node_ethtool_received_missed untyped node_ethtool_received_missed{device="eth0"} 401 # HELP node_ethtool_received_multicast Network interface rx_multicast # TYPE node_ethtool_received_multicast untyped node_ethtool_received_multicast{device="eth0"} 23973 # HELP node_ethtool_received_packets_total Network interface packets received # TYPE node_ethtool_received_packets_total untyped node_ethtool_received_packets_total{device="eth0"} 1.260062e+06 # HELP node_ethtool_received_unicast Network interface rx_unicast # TYPE node_ethtool_received_unicast untyped node_ethtool_received_unicast{device="eth0"} 1.230297e+06 # HELP node_ethtool_transmitted_aborted Network interface tx_aborted # TYPE node_ethtool_transmitted_aborted untyped node_ethtool_transmitted_aborted{device="eth0"} 0 # HELP node_ethtool_transmitted_errors_total Number of sent frames with errors # TYPE node_ethtool_transmitted_errors_total untyped node_ethtool_transmitted_errors_total{device="eth0"} 0 # HELP node_ethtool_transmitted_multi_collisions Network interface tx_multi_collisions # TYPE node_ethtool_transmitted_multi_collisions untyped node_ethtool_transmitted_multi_collisions{device="eth0"} 0 # HELP node_ethtool_transmitted_packets_total Network interface packets sent # TYPE node_ethtool_transmitted_packets_total untyped node_ethtool_transmitted_packets_total{device="eth0"} 961500 # HELP node_ethtool_transmitted_single_collisions Network interface tx_single_collisions # TYPE node_ethtool_transmitted_single_collisions untyped node_ethtool_transmitted_single_collisions{device="eth0"} 0 # HELP node_ethtool_transmitted_underrun Network interface tx_underrun # TYPE node_ethtool_transmitted_underrun untyped node_ethtool_transmitted_underrun{device="eth0"} 0 # HELP node_network_advertised_speed_bytes Combination of speeds and features offered by network device # TYPE node_network_advertised_speed_bytes gauge node_network_advertised_speed_bytes{device="eth0",duplex="full",mode="1000baseT"} 1.25e+08 node_network_advertised_speed_bytes{device="eth0",duplex="full",mode="100baseT"} 1.25e+07 node_network_advertised_speed_bytes{device="eth0",duplex="full",mode="10baseT"} 1.25e+06 node_network_advertised_speed_bytes{device="eth0",duplex="half",mode="100baseT"} 1.25e+07 node_network_advertised_speed_bytes{device="eth0",duplex="half",mode="10baseT"} 1.25e+06 # HELP node_network_asymmetricpause_advertised If this port device offers asymmetric pause capability # TYPE node_network_asymmetricpause_advertised gauge node_network_asymmetricpause_advertised{device="eth0"} 0 # HELP node_network_asymmetricpause_supported If this port device supports asymmetric pause frames # TYPE node_network_asymmetricpause_supported gauge node_network_asymmetricpause_supported{device="eth0"} 0 # HELP node_network_autonegotiate If this port is using autonegotiate # TYPE node_network_autonegotiate gauge node_network_autonegotiate{device="eth0"} 1 # HELP node_network_autonegotiate_advertised If this port device offers autonegotiate # TYPE node_network_autonegotiate_advertised gauge node_network_autonegotiate_advertised{device="eth0"} 1 # HELP node_network_autonegotiate_supported If this port device supports autonegotiate # TYPE node_network_autonegotiate_supported gauge node_network_autonegotiate_supported{device="eth0"} 1 # HELP node_network_pause_advertised If this port device offers pause capability # TYPE node_network_pause_advertised gauge node_network_pause_advertised{device="eth0"} 1 # HELP node_network_pause_supported If this port device supports pause frames # TYPE node_network_pause_supported gauge node_network_pause_supported{device="eth0"} 1 # HELP node_network_supported_port_info Type of ports or PHYs supported by network device # TYPE node_network_supported_port_info gauge node_network_supported_port_info{device="eth0",type="MII"} 1 node_network_supported_port_info{device="eth0",type="TP"} 1 # HELP node_network_supported_speed_bytes Combination of speeds and features supported by network device # TYPE node_network_supported_speed_bytes gauge node_network_supported_speed_bytes{device="eth0",duplex="full",mode="10000baseT"} 1.25e+09 node_network_supported_speed_bytes{device="eth0",duplex="full",mode="1000baseT"} 1.25e+08 node_network_supported_speed_bytes{device="eth0",duplex="full",mode="100baseT"} 1.25e+07 node_network_supported_speed_bytes{device="eth0",duplex="full",mode="10baseT"} 1.25e+06 node_network_supported_speed_bytes{device="eth0",duplex="half",mode="100baseT"} 1.25e+07 node_network_supported_speed_bytes{device="eth0",duplex="half",mode="10baseT"} 1.25e+06 ` *sysPath = "fixtures/sys" logger := slog.New(slog.NewTextHandler(io.Discard, nil)) collector, err := NewEthtoolTestCollector(logger) if err != nil { t.Fatal(err) } c, err := NewTestEthtoolCollector(logger) if err != nil { t.Fatal(err) } reg := prometheus.NewRegistry() reg.MustRegister(c) sink := make(chan prometheus.Metric) go func() { err = collector.Update(sink) if err != nil { panic(fmt.Errorf("failed to update collector: %s", err)) } close(sink) }() err = testutil.GatherAndCompare(reg, strings.NewReader(testcase)) if err != nil { t.Fatal(err) } }