Skip to content

Commit

Permalink
WIP: net: replace "ethtool" with a package
Browse files Browse the repository at this point in the history
DEMO PR (please not merge!) to illustrate how it could
like to consume https://github.com/safchain/ethtool

Depends on unmerged feature (FeaturesWithState function)
atm only available on my fork (PR pending)

Signed-off-by: Francesco Romani <[email protected]>
  • Loading branch information
ffromani committed Jun 11, 2024
1 parent 7e3f410 commit 7133a50
Show file tree
Hide file tree
Showing 6 changed files with 42 additions and 362 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
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1379,10 +1379,15 @@ 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.
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.
The most common use case is when we want to consume a snapshot from ghw. In these cases the information
provided by tools will be most likely inconsistent with the data from the snapshot - they will run on
a different host!
To prevent ghw from calling external tools, set the environs variable `GHW_DISABLE_TOOLS` to any value,
or, programmatically, check the `WithDisableTools` function.
The default behaviour of ghw is to call external tools when available.

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
237 changes: 24 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,30 @@ 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))
func netDeviceCapabilities(ctx *context.Context, dev string) []*NICCapability {
caps := []*NICCapability{}

// 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)
ethHandle, err := ethtool.NewEthtool()
if err != nil {
ctx.Warn("failed to create ethtool instance: %v", err)
return caps
}
defer ethHandle.Close()

// 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)
feats, err := ethHandle.FeaturesWithState(dev)
if err != nil {
ctx.Warn("failed to get ethtool features state for %s: %v", dev, err)
return caps
}

}

// 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,
for key, state := range feats {
caps = append(caps, &NICCapability{
Name: key,
IsEnabled: state.Active,
CanEnable: state.Available,
})
}
return caps
}

func netDevicePCIAddress(netDevDir, netDevName string) *string {
Expand Down Expand Up @@ -249,110 +160,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 7133a50

Please sign in to comment.