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