Skip to content

Commit 7133a50

Browse files
committed
WIP: net: replace "ethtool" with a package
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]>
1 parent 7e3f410 commit 7133a50

File tree

6 files changed

+42
-362
lines changed

6 files changed

+42
-362
lines changed

Dockerfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ COPY . .
1515
RUN CGO_ENABLED=0 go build -o ghwc ./cmd/ghwc/
1616

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

2019
WORKDIR /bin
2120

README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1379,10 +1379,15 @@ if err := snapshot.PackFrom("my-snapshot.tgz", scratchDir); err != nil {
13791379

13801380
## Calling external programs
13811381

1382-
By default `ghw` may call external programs, for example `ethtool`, to learn
1383-
about hardware capabilities. In some rare circumstances it may be useful to
1384-
opt out from this behaviour and rely only on the data provided by
1385-
pseudo-filesystems, like sysfs.
1382+
By default ghw may call external programs, to learn about hardware capabilities.
1383+
In some rare circumstances it may be useful to opt out from this behaviour and rely only on the data
1384+
provided by pseudo-filesystems, like sysfs.
1385+
The most common use case is when we want to consume a snapshot from ghw. In these cases the information
1386+
provided by tools will be most likely inconsistent with the data from the snapshot - they will run on
1387+
a different host!
1388+
To prevent ghw from calling external tools, set the environs variable `GHW_DISABLE_TOOLS` to any value,
1389+
or, programmatically, check the `WithDisableTools` function.
1390+
The default behaviour of ghw is to call external tools when available.
13861391

13871392
The most common use case is when we want to read a snapshot from `ghw`. In
13881393
these cases the information provided by tools will be inconsistent with the

go.mod

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ require (
66
github.com/StackExchange/wmi v1.2.1
77
github.com/jaypipes/pcidb v1.0.1
88
github.com/pkg/errors v0.9.1
9+
github.com/safchain/ethtool v0.3.1-0.20240611095507-191590141ec6
910
github.com/spf13/cobra v1.8.0
11+
github.com/spf13/pflag v1.0.5 // indirect
12+
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
1013
gopkg.in/yaml.v3 v3.0.1
1114
howett.net/plist v1.0.0
1215
)
@@ -16,7 +19,5 @@ require (
1619
github.com/inconshreveable/mousetrap v1.1.0 // indirect
1720
github.com/kr/pretty v0.1.0 // indirect
1821
github.com/mitchellh/go-homedir v1.1.0 // indirect
19-
github.com/spf13/pflag v1.0.5 // indirect
20-
golang.org/x/sys v0.1.0 // indirect
21-
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
22+
golang.org/x/sys v0.21.0 // indirect
2223
)

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
1919
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
2020
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
2121
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
22+
github.com/safchain/ethtool v0.3.1-0.20240611095507-191590141ec6 h1:EDGd3d1JQDq5BFMZOp4ePK1M6Om9ZGhfh/LJfrjiyEQ=
23+
github.com/safchain/ethtool v0.3.1-0.20240611095507-191590141ec6/go.mod h1:XLLnZmy4OCRTkksP/UiMjij96YmIsBfmBQcs7H6tA48=
2224
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
2325
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
2426
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
2527
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
2628
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
27-
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
28-
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
29+
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
30+
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
2931
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
3032
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
3133
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

pkg/net/net_linux.go

Lines changed: 24 additions & 213 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,14 @@
66
package net
77

88
import (
9-
"bufio"
10-
"bytes"
11-
"fmt"
129
"os"
13-
"os/exec"
1410
"path/filepath"
1511
"strings"
1612

13+
"github.com/safchain/ethtool"
14+
1715
"github.com/jaypipes/ghw/pkg/context"
1816
"github.com/jaypipes/ghw/pkg/linuxpath"
19-
"github.com/jaypipes/ghw/pkg/util"
20-
)
21-
22-
const (
23-
warnEthtoolNotInstalled = `ethtool not installed. Cannot grab NIC capabilities`
2417
)
2518

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

40-
etAvailable := ctx.EnableTools
41-
if etAvailable {
42-
if etInstalled := ethtoolInstalled(); !etInstalled {
43-
ctx.Warn(warnEthtoolNotInstalled)
44-
etAvailable = false
45-
}
46-
}
47-
4833
for _, file := range files {
4934
filename := file.Name()
5035
// Ignore loopback...
@@ -66,15 +51,10 @@ func nics(ctx *context.Context) []*NIC {
6651

6752
mac := netDeviceMacAddress(paths, filename)
6853
nic.MacAddress = mac
69-
nic.MACAddress = mac
70-
if etAvailable {
71-
nic.netDeviceParseEthtool(ctx, filename)
72-
} else {
73-
nic.Capabilities = []*NICCapability{}
74-
// Sets NIC struct fields from data in SysFs
75-
nic.setNicAttrSysFs(paths, filename)
76-
}
77-
54+
// Get speed and duplex from /sys/class/net/$DEVICE/ directory
55+
nic.Speed = readFile(filepath.Join(paths.SysClassNet, filename, "speed"))
56+
nic.Duplex = readFile(filepath.Join(paths.SysClassNet, filename, "duplex"))
57+
nic.Capabilities = netDeviceCapabilities(ctx, filename)
7858
nic.PCIAddress = netDevicePCIAddress(paths.SysClassNet, filename)
7959

8060
nics = append(nics, nic)
@@ -103,99 +83,30 @@ func netDeviceMacAddress(paths *linuxpath.Paths, dev string) string {
10383
return strings.TrimSpace(string(contents))
10484
}
10585

106-
func ethtoolInstalled() bool {
107-
_, err := exec.LookPath("ethtool")
108-
return err == nil
109-
}
110-
111-
func (n *NIC) netDeviceParseEthtool(ctx *context.Context, dev string) {
112-
var out bytes.Buffer
113-
path, _ := exec.LookPath("ethtool")
114-
115-
// Get auto-negotiation and pause-frame-use capabilities from "ethtool" (with no options)
116-
// Populate Speed, Duplex, SupportedLinkModes, SupportedPorts, SupportedFECModes,
117-
// AdvertisedLinkModes, and AdvertisedFECModes attributes from "ethtool" output.
118-
cmd := exec.Command(path, dev)
119-
cmd.Stdout = &out
120-
err := cmd.Run()
121-
if err == nil {
122-
m := parseNicAttrEthtool(&out)
123-
n.Capabilities = append(n.Capabilities, autoNegCap(m))
124-
n.Capabilities = append(n.Capabilities, pauseFrameUseCap(m))
86+
func netDeviceCapabilities(ctx *context.Context, dev string) []*NICCapability {
87+
caps := []*NICCapability{}
12588

126-
// Update NIC Attributes with ethtool output
127-
n.Speed = strings.Join(m["Speed"], "")
128-
n.Duplex = strings.Join(m["Duplex"], "")
129-
n.SupportedLinkModes = m["Supported link modes"]
130-
n.SupportedPorts = m["Supported ports"]
131-
n.SupportedFECModes = m["Supported FEC modes"]
132-
n.AdvertisedLinkModes = m["Advertised link modes"]
133-
n.AdvertisedFECModes = m["Advertised FEC modes"]
134-
} else {
135-
msg := fmt.Sprintf("could not grab NIC link info for %s: %s", dev, err)
136-
ctx.Warn(msg)
89+
ethHandle, err := ethtool.NewEthtool()
90+
if err != nil {
91+
ctx.Warn("failed to create ethtool instance: %v", err)
92+
return caps
13793
}
94+
defer ethHandle.Close()
13895

139-
// Get all other capabilities from "ethtool -k"
140-
cmd = exec.Command(path, "-k", dev)
141-
cmd.Stdout = &out
142-
err = cmd.Run()
143-
if err == nil {
144-
// The out variable will now contain something that looks like the
145-
// following.
146-
//
147-
// Features for enp58s0f1:
148-
// rx-checksumming: on
149-
// tx-checksumming: off
150-
// tx-checksum-ipv4: off
151-
// tx-checksum-ip-generic: off [fixed]
152-
// tx-checksum-ipv6: off
153-
// tx-checksum-fcoe-crc: off [fixed]
154-
// tx-checksum-sctp: off [fixed]
155-
// scatter-gather: off
156-
// tx-scatter-gather: off
157-
// tx-scatter-gather-fraglist: off [fixed]
158-
// tcp-segmentation-offload: off
159-
// tx-tcp-segmentation: off
160-
// tx-tcp-ecn-segmentation: off [fixed]
161-
// tx-tcp-mangleid-segmentation: off
162-
// tx-tcp6-segmentation: off
163-
// < snipped >
164-
scanner := bufio.NewScanner(&out)
165-
// Skip the first line...
166-
scanner.Scan()
167-
for scanner.Scan() {
168-
line := strings.TrimPrefix(scanner.Text(), "\t")
169-
n.Capabilities = append(n.Capabilities, netParseEthtoolFeature(line))
170-
}
171-
172-
} else {
173-
msg := fmt.Sprintf("could not grab NIC capabilities for %s: %s", dev, err)
174-
ctx.Warn(msg)
96+
feats, err := ethHandle.FeaturesWithState(dev)
97+
if err != nil {
98+
ctx.Warn("failed to get ethtool features state for %s: %v", dev, err)
99+
return caps
175100
}
176101

177-
}
178-
179-
// netParseEthtoolFeature parses a line from the ethtool -k output and returns
180-
// a NICCapability.
181-
//
182-
// The supplied line will look like the following:
183-
//
184-
// tx-checksum-ip-generic: off [fixed]
185-
//
186-
// [fixed] indicates that the feature may not be turned on/off. Note: it makes
187-
// no difference whether a privileged user runs `ethtool -k` when determining
188-
// whether [fixed] appears for a feature.
189-
func netParseEthtoolFeature(line string) *NICCapability {
190-
parts := strings.Fields(line)
191-
cap := strings.TrimSuffix(parts[0], ":")
192-
enabled := parts[1] == "on"
193-
fixed := len(parts) == 3 && parts[2] == "[fixed]"
194-
return &NICCapability{
195-
Name: cap,
196-
IsEnabled: enabled,
197-
CanEnable: !fixed,
102+
for key, state := range feats {
103+
caps = append(caps, &NICCapability{
104+
Name: key,
105+
IsEnabled: state.Active,
106+
CanEnable: state.Available,
107+
})
198108
}
109+
return caps
199110
}
200111

201112
func netDevicePCIAddress(netDevDir, netDevName string) *string {
@@ -249,110 +160,10 @@ func netDevicePCIAddress(netDevDir, netDevName string) *string {
249160
return &pciAddr
250161
}
251162

252-
func (nic *NIC) setNicAttrSysFs(paths *linuxpath.Paths, dev string) {
253-
// Get speed and duplex from /sys/class/net/$DEVICE/ directory
254-
nic.Speed = readFile(filepath.Join(paths.SysClassNet, dev, "speed"))
255-
nic.Duplex = readFile(filepath.Join(paths.SysClassNet, dev, "duplex"))
256-
}
257-
258163
func readFile(path string) string {
259164
contents, err := os.ReadFile(path)
260165
if err != nil {
261166
return ""
262167
}
263168
return strings.TrimSpace(string(contents))
264169
}
265-
266-
func autoNegCap(m map[string][]string) *NICCapability {
267-
autoNegotiation := NICCapability{Name: "auto-negotiation", IsEnabled: false, CanEnable: false}
268-
269-
an, anErr := util.ParseBool(strings.Join(m["Auto-negotiation"], ""))
270-
aan, aanErr := util.ParseBool(strings.Join(m["Advertised auto-negotiation"], ""))
271-
if an && aan && aanErr == nil && anErr == nil {
272-
autoNegotiation.IsEnabled = true
273-
}
274-
275-
san, err := util.ParseBool(strings.Join(m["Supports auto-negotiation"], ""))
276-
if san && err == nil {
277-
autoNegotiation.CanEnable = true
278-
}
279-
280-
return &autoNegotiation
281-
}
282-
283-
func pauseFrameUseCap(m map[string][]string) *NICCapability {
284-
pauseFrameUse := NICCapability{Name: "pause-frame-use", IsEnabled: false, CanEnable: false}
285-
286-
apfu, err := util.ParseBool(strings.Join(m["Advertised pause frame use"], ""))
287-
if apfu && err == nil {
288-
pauseFrameUse.IsEnabled = true
289-
}
290-
291-
spfu, err := util.ParseBool(strings.Join(m["Supports pause frame use"], ""))
292-
if spfu && err == nil {
293-
pauseFrameUse.CanEnable = true
294-
}
295-
296-
return &pauseFrameUse
297-
}
298-
299-
func parseNicAttrEthtool(out *bytes.Buffer) map[string][]string {
300-
// The out variable will now contain something that looks like the
301-
// following.
302-
//
303-
//Settings for eth0:
304-
// Supported ports: [ TP ]
305-
// Supported link modes: 10baseT/Half 10baseT/Full
306-
// 100baseT/Half 100baseT/Full
307-
// 1000baseT/Full
308-
// Supported pause frame use: No
309-
// Supports auto-negotiation: Yes
310-
// Supported FEC modes: Not reported
311-
// Advertised link modes: 10baseT/Half 10baseT/Full
312-
// 100baseT/Half 100baseT/Full
313-
// 1000baseT/Full
314-
// Advertised pause frame use: No
315-
// Advertised auto-negotiation: Yes
316-
// Advertised FEC modes: Not reported
317-
// Speed: 1000Mb/s
318-
// Duplex: Full
319-
// Auto-negotiation: on
320-
// Port: Twisted Pair
321-
// PHYAD: 1
322-
// Transceiver: internal
323-
// MDI-X: off (auto)
324-
// Supports Wake-on: pumbg
325-
// Wake-on: d
326-
// Current message level: 0x00000007 (7)
327-
// drv probe link
328-
// Link detected: yes
329-
330-
scanner := bufio.NewScanner(out)
331-
// Skip the first line
332-
scanner.Scan()
333-
m := make(map[string][]string)
334-
var name string
335-
for scanner.Scan() {
336-
var fields []string
337-
if strings.Contains(scanner.Text(), ":") {
338-
line := strings.Split(scanner.Text(), ":")
339-
name = strings.TrimSpace(line[0])
340-
str := strings.Trim(strings.TrimSpace(line[1]), "[]")
341-
switch str {
342-
case
343-
"Not reported",
344-
"Unknown":
345-
continue
346-
}
347-
fields = strings.Fields(str)
348-
} else {
349-
fields = strings.Fields(strings.Trim(strings.TrimSpace(scanner.Text()), "[]"))
350-
}
351-
352-
for _, f := range fields {
353-
m[name] = append(m[name], strings.TrimSpace(f))
354-
}
355-
}
356-
357-
return m
358-
}

0 commit comments

Comments
 (0)