Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1379,10 +1379,9 @@ 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.
We gather information using Go packages and system libraries, but in some cases ghw
may call external tools/binaries, particularly on non-Linux platforms.

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
249 changes: 36 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 @@ -65,16 +50,12 @@ func nics(ctx *context.Context) []*NIC {
}

mac := netDeviceMacAddress(paths, filename)
nic.MacAddress = mac
nic.MacAddress = mac // keep this for backward compatibility
nic.MACAddress = mac
Comment thread
ffromani marked this conversation as resolved.
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 +84,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
Comment on lines -147 to -162

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the capability strings exactly the same with the ethtool library as they were with the raw parsing of the ethtool binary?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, pretty bad oversight on my side. Let me add a test which ensure this. On the code side, if this is not already the case, we can add a thin translation layer.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ffromani I was only making sure :) it may be that the ethtool library does use the exact same strings. but if not, we'll need to add a translation table to ensure backwards compat.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so the ethtool program gets the identifiers from the kernel using the ioctl interface: https://kernel.googlesource.com/pub/scm/network/ethtool/ethtool/+/refs/heads/ethtool-3.4.y/ethtool.c#1310 https://docs.kernel.org/networking/ethtool-netlink.html#strset-get

And so it does the ethtool package: https://github.com/safchain/ethtool/blob/master/ethtool.go#L733

Hence the names should be exactly the same, unless it is the ethtool program which does some translation, which I haven't checked yet. On it.

// < 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 +172,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