node_exporter/collector/ethtool_linux_test.go

400 lines
14 KiB
Go
Raw Permalink Normal View History

// 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)
}
}