Skip to content

Commit

Permalink
net: replace "ethtool" with a package
Browse files Browse the repository at this point in the history
Replace calls to the "ethtool" external binary to
the go package https://github.com/safchain/ethtool

No feature reduction is expected, internal change only.

Signed-off-by: Francesco Romani <[email protected]>
  • Loading branch information
ffromani committed Jun 11, 2024
1 parent 7e3f410 commit f1141d3
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 338 deletions.
1 change: 0 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ COPY . .
RUN CGO_ENABLED=0 go build -o ghwc ./cmd/ghwc/

FROM alpine:3.7@sha256:8421d9a84432575381bfabd248f1eb56f3aa21d9d7cd2511583c68c9b7511d10
RUN apk add --no-cache ethtool

WORKDIR /bin

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1379,10 +1379,10 @@ if err := snapshot.PackFrom("my-snapshot.tgz", scratchDir); err != nil {

## Calling external programs

By default `ghw` may call external programs, for example `ethtool`, to learn
about hardware capabilities. In some rare circumstances it may be useful to
opt out from this behaviour and rely only on the data provided by
pseudo-filesystems, like sysfs.
By default ghw may call external programs, to learn about hardware capabilities.
The general direction is to gather the information using go packages and/or
system libraries, but ghw still relies on tools especially on less maintained
platforms or architectures (e.g. Darwin).

The most common use case is when we want to read a snapshot from `ghw`. In
these cases the information provided by tools will be inconsistent with the
Expand Down
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ require (
github.com/StackExchange/wmi v1.2.1
github.com/jaypipes/pcidb v1.0.1
github.com/pkg/errors v0.9.1
github.com/safchain/ethtool v0.3.1-0.20240611095507-191590141ec6
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v3 v3.0.1
howett.net/plist v1.0.0
)
Expand All @@ -16,7 +19,5 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.1.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
golang.org/x/sys v0.21.0 // indirect
)
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safchain/ethtool v0.3.1-0.20240611095507-191590141ec6 h1:EDGd3d1JQDq5BFMZOp4ePK1M6Om9ZGhfh/LJfrjiyEQ=
github.com/safchain/ethtool v0.3.1-0.20240611095507-191590141ec6/go.mod h1:XLLnZmy4OCRTkksP/UiMjij96YmIsBfmBQcs7H6tA48=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
248 changes: 35 additions & 213 deletions pkg/net/net_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,14 @@
package net

import (
"bufio"
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/safchain/ethtool"

"github.com/jaypipes/ghw/pkg/context"
"github.com/jaypipes/ghw/pkg/linuxpath"
"github.com/jaypipes/ghw/pkg/util"
)

const (
warnEthtoolNotInstalled = `ethtool not installed. Cannot grab NIC capabilities`
)

func (i *Info) load() error {
Expand All @@ -37,14 +30,6 @@ func nics(ctx *context.Context) []*NIC {
return nics
}

etAvailable := ctx.EnableTools
if etAvailable {
if etInstalled := ethtoolInstalled(); !etInstalled {
ctx.Warn(warnEthtoolNotInstalled)
etAvailable = false
}
}

for _, file := range files {
filename := file.Name()
// Ignore loopback...
Expand All @@ -66,15 +51,10 @@ func nics(ctx *context.Context) []*NIC {

mac := netDeviceMacAddress(paths, filename)
nic.MacAddress = mac
nic.MACAddress = mac
if etAvailable {
nic.netDeviceParseEthtool(ctx, filename)
} else {
nic.Capabilities = []*NICCapability{}
// Sets NIC struct fields from data in SysFs
nic.setNicAttrSysFs(paths, filename)
}

// Get speed and duplex from /sys/class/net/$DEVICE/ directory
nic.Speed = readFile(filepath.Join(paths.SysClassNet, filename, "speed"))
nic.Duplex = readFile(filepath.Join(paths.SysClassNet, filename, "duplex"))
nic.Capabilities = netDeviceCapabilities(ctx, filename)
nic.PCIAddress = netDevicePCIAddress(paths.SysClassNet, filename)

nics = append(nics, nic)
Expand Down Expand Up @@ -103,99 +83,41 @@ func netDeviceMacAddress(paths *linuxpath.Paths, dev string) string {
return strings.TrimSpace(string(contents))
}

func ethtoolInstalled() bool {
_, err := exec.LookPath("ethtool")
return err == nil
}

func (n *NIC) netDeviceParseEthtool(ctx *context.Context, dev string) {
var out bytes.Buffer
path, _ := exec.LookPath("ethtool")

// Get auto-negotiation and pause-frame-use capabilities from "ethtool" (with no options)
// Populate Speed, Duplex, SupportedLinkModes, SupportedPorts, SupportedFECModes,
// AdvertisedLinkModes, and AdvertisedFECModes attributes from "ethtool" output.
cmd := exec.Command(path, dev)
cmd.Stdout = &out
err := cmd.Run()
if err == nil {
m := parseNicAttrEthtool(&out)
n.Capabilities = append(n.Capabilities, autoNegCap(m))
n.Capabilities = append(n.Capabilities, pauseFrameUseCap(m))

// Update NIC Attributes with ethtool output
n.Speed = strings.Join(m["Speed"], "")
n.Duplex = strings.Join(m["Duplex"], "")
n.SupportedLinkModes = m["Supported link modes"]
n.SupportedPorts = m["Supported ports"]
n.SupportedFECModes = m["Supported FEC modes"]
n.AdvertisedLinkModes = m["Advertised link modes"]
n.AdvertisedFECModes = m["Advertised FEC modes"]
} else {
msg := fmt.Sprintf("could not grab NIC link info for %s: %s", dev, err)
ctx.Warn(msg)
func netDeviceCapabilities(ctx *context.Context, dev string) []*NICCapability {
ethHandle, err := ethtool.NewEthtool()
if err != nil {
ctx.Warn("failed to create ethtool instance: %v", err)
return []*NICCapability{}
}

// Get all other capabilities from "ethtool -k"
cmd = exec.Command(path, "-k", dev)
cmd.Stdout = &out
err = cmd.Run()
if err == nil {
// The out variable will now contain something that looks like the
// following.
//
// Features for enp58s0f1:
// rx-checksumming: on
// tx-checksumming: off
// tx-checksum-ipv4: off
// tx-checksum-ip-generic: off [fixed]
// tx-checksum-ipv6: off
// tx-checksum-fcoe-crc: off [fixed]
// tx-checksum-sctp: off [fixed]
// scatter-gather: off
// tx-scatter-gather: off
// tx-scatter-gather-fraglist: off [fixed]
// tcp-segmentation-offload: off
// tx-tcp-segmentation: off
// tx-tcp-ecn-segmentation: off [fixed]
// tx-tcp-mangleid-segmentation: off
// tx-tcp6-segmentation: off
// < snipped >
scanner := bufio.NewScanner(&out)
// Skip the first line...
scanner.Scan()
for scanner.Scan() {
line := strings.TrimPrefix(scanner.Text(), "\t")
n.Capabilities = append(n.Capabilities, netParseEthtoolFeature(line))
}

} else {
msg := fmt.Sprintf("could not grab NIC capabilities for %s: %s", dev, err)
ctx.Warn(msg)
defer ethHandle.Close()
feats, err := netDeviceCapabilitiesFromEthHandle(ethHandle, dev)
if err != nil {
ctx.Warn(err.Error())
return []*NICCapability{}
}
return feats
}

type ethtoolCollector interface {
FeaturesWithState(intf string) (map[string]ethtool.FeatureState, error)
}

// netParseEthtoolFeature parses a line from the ethtool -k output and returns
// a NICCapability.
//
// The supplied line will look like the following:
//
// tx-checksum-ip-generic: off [fixed]
//
// [fixed] indicates that the feature may not be turned on/off. Note: it makes
// no difference whether a privileged user runs `ethtool -k` when determining
// whether [fixed] appears for a feature.
func netParseEthtoolFeature(line string) *NICCapability {
parts := strings.Fields(line)
cap := strings.TrimSuffix(parts[0], ":")
enabled := parts[1] == "on"
fixed := len(parts) == 3 && parts[2] == "[fixed]"
return &NICCapability{
Name: cap,
IsEnabled: enabled,
CanEnable: !fixed,
// make it mockable for test purposes
func netDeviceCapabilitiesFromEthHandle(collector ethtoolCollector, dev string) ([]*NICCapability, error) {
feats, err := collector.FeaturesWithState(dev)
if err != nil {
return nil, err
}

caps := []*NICCapability{}
for key, state := range feats {
caps = append(caps, &NICCapability{
Name: key,
IsEnabled: state.Active,
CanEnable: state.Available,
})
}
return caps, err
}

func netDevicePCIAddress(netDevDir, netDevName string) *string {
Expand Down Expand Up @@ -249,110 +171,10 @@ func netDevicePCIAddress(netDevDir, netDevName string) *string {
return &pciAddr
}

func (nic *NIC) setNicAttrSysFs(paths *linuxpath.Paths, dev string) {
// Get speed and duplex from /sys/class/net/$DEVICE/ directory
nic.Speed = readFile(filepath.Join(paths.SysClassNet, dev, "speed"))
nic.Duplex = readFile(filepath.Join(paths.SysClassNet, dev, "duplex"))
}

func readFile(path string) string {
contents, err := os.ReadFile(path)
if err != nil {
return ""
}
return strings.TrimSpace(string(contents))
}

func autoNegCap(m map[string][]string) *NICCapability {
autoNegotiation := NICCapability{Name: "auto-negotiation", IsEnabled: false, CanEnable: false}

an, anErr := util.ParseBool(strings.Join(m["Auto-negotiation"], ""))
aan, aanErr := util.ParseBool(strings.Join(m["Advertised auto-negotiation"], ""))
if an && aan && aanErr == nil && anErr == nil {
autoNegotiation.IsEnabled = true
}

san, err := util.ParseBool(strings.Join(m["Supports auto-negotiation"], ""))
if san && err == nil {
autoNegotiation.CanEnable = true
}

return &autoNegotiation
}

func pauseFrameUseCap(m map[string][]string) *NICCapability {
pauseFrameUse := NICCapability{Name: "pause-frame-use", IsEnabled: false, CanEnable: false}

apfu, err := util.ParseBool(strings.Join(m["Advertised pause frame use"], ""))
if apfu && err == nil {
pauseFrameUse.IsEnabled = true
}

spfu, err := util.ParseBool(strings.Join(m["Supports pause frame use"], ""))
if spfu && err == nil {
pauseFrameUse.CanEnable = true
}

return &pauseFrameUse
}

func parseNicAttrEthtool(out *bytes.Buffer) map[string][]string {
// The out variable will now contain something that looks like the
// following.
//
//Settings for eth0:
// Supported ports: [ TP ]
// Supported link modes: 10baseT/Half 10baseT/Full
// 100baseT/Half 100baseT/Full
// 1000baseT/Full
// Supported pause frame use: No
// Supports auto-negotiation: Yes
// Supported FEC modes: Not reported
// Advertised link modes: 10baseT/Half 10baseT/Full
// 100baseT/Half 100baseT/Full
// 1000baseT/Full
// Advertised pause frame use: No
// Advertised auto-negotiation: Yes
// Advertised FEC modes: Not reported
// Speed: 1000Mb/s
// Duplex: Full
// Auto-negotiation: on
// Port: Twisted Pair
// PHYAD: 1
// Transceiver: internal
// MDI-X: off (auto)
// Supports Wake-on: pumbg
// Wake-on: d
// Current message level: 0x00000007 (7)
// drv probe link
// Link detected: yes

scanner := bufio.NewScanner(out)
// Skip the first line
scanner.Scan()
m := make(map[string][]string)
var name string
for scanner.Scan() {
var fields []string
if strings.Contains(scanner.Text(), ":") {
line := strings.Split(scanner.Text(), ":")
name = strings.TrimSpace(line[0])
str := strings.Trim(strings.TrimSpace(line[1]), "[]")
switch str {
case
"Not reported",
"Unknown":
continue
}
fields = strings.Fields(str)
} else {
fields = strings.Fields(strings.Trim(strings.TrimSpace(scanner.Text()), "[]"))
}

for _, f := range fields {
m[name] = append(m[name], strings.TrimSpace(f))
}
}

return m
}
Loading

0 comments on commit f1141d3

Please sign in to comment.