-
Notifications
You must be signed in to change notification settings - Fork 219
net: replace the ethtool external program calls with the safchain/ethtool package
#321
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
|
@@ -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... | ||
|
|
@@ -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 | ||
| 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) | ||
|
|
@@ -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
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| // < 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 { | ||
|
|
@@ -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 | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.