diff --git a/README.md b/README.md index 498a0f78..410e36e9 100644 --- a/README.md +++ b/README.md @@ -803,6 +803,7 @@ net (3 NICs) - rx-vlan-offload - tx-vlan-offload - highdma + - auto-negotiation wlp59s0 enabled capabilities: - scatter-gather diff --git a/pkg/net/net.go b/pkg/net/net.go index 8994d112..dfc1dc52 100644 --- a/pkg/net/net.go +++ b/pkg/net/net.go @@ -29,6 +29,15 @@ type NIC struct { // TODO(fromani): add other hw addresses (USB) when we support them } +func (nc *NICCapability) String() string { + return fmt.Sprintf( + "{Name:%s IsEnabled:%t CanEnable:%t}", + nc.Name, + nc.IsEnabled, + nc.CanEnable, + ) +} + func (n *NIC) String() string { isVirtualStr := "" if n.IsVirtual { diff --git a/pkg/net/net_linux.go b/pkg/net/net_linux.go index 1b338dfa..6d4300f8 100644 --- a/pkg/net/net_linux.go +++ b/pkg/net/net_linux.go @@ -17,6 +17,7 @@ import ( "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/linuxpath" + "github.com/jaypipes/ghw/pkg/util" ) const ( @@ -107,45 +108,62 @@ func ethtoolInstalled() bool { func netDeviceCapabilities(ctx *context.Context, dev string) []*NICCapability { caps := make([]*NICCapability, 0) - path, _ := exec.LookPath("ethtool") - cmd := exec.Command(path, "-k", dev) var out bytes.Buffer + path, _ := exec.LookPath("ethtool") + + // Get auto-negotiation and pause-frame-use capabilities from "ethtool" (with no options) + cmd := exec.Command(path, dev) cmd.Stdout = &out err := cmd.Run() - if err != nil { - msg := fmt.Sprintf("could not grab NIC capabilities for %s: %s", dev, err) + if err == nil { + m := parseNicAttrEthtool(&out) + caps = append(caps, autoNegCap(m)) + caps = append(caps, pauseFrameUseCap(m)) + } else { + msg := fmt.Sprintf("could not grab NIC link info for %s: %s", dev, err) ctx.Warn(msg) - return caps } - // 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") - caps = append(caps, netParseEthtoolFeature(line)) + // 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") + caps = append(caps, netParseEthtoolFeature(line)) + } + + } else { + msg := fmt.Sprintf("could not grab NIC capabilities for %s: %s", dev, err) + ctx.Warn(msg) } + return caps + } // netParseEthtoolFeature parses a line from the ethtool -k output and returns @@ -220,3 +238,97 @@ func netDevicePCIAddress(netDevDir, netDevName string) *string { pciAddr := filepath.Base(devPath) return &pciAddr } + +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 +} diff --git a/pkg/net/net_linux_test.go b/pkg/net/net_linux_test.go index 88678894..6da777e4 100644 --- a/pkg/net/net_linux_test.go +++ b/pkg/net/net_linux_test.go @@ -10,6 +10,7 @@ package net import ( + "bytes" "os" "reflect" "testing" @@ -57,3 +58,66 @@ func TestParseEthtoolFeature(t *testing.T) { } } } + +func TestParseNicAttrEthtool(t *testing.T) { + if _, ok := os.LookupEnv("GHW_TESTING_SKIP_NET"); ok { + t.Skip("Skipping network tests.") + } + + tests := []struct { + input string + expected []*NICCapability + }{ + { + input: `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 +`, + expected: []*NICCapability{ + { + Name: "auto-negotiation", + IsEnabled: true, + CanEnable: true, + }, + { + Name: "pause-frame-use", + IsEnabled: false, + CanEnable: false, + }, + }, + }, + } + + for x, test := range tests { + m := parseNicAttrEthtool(bytes.NewBufferString(test.input)) + actual := make([]*NICCapability, 0) + actual = append(actual, autoNegCap(m)) + actual = append(actual, pauseFrameUseCap(m)) + if !reflect.DeepEqual(test.expected, actual) { + t.Fatalf("In test %d\nExpected:\n%+v\nActual:\n%+v\n", x, test.expected, actual) + } + } +} diff --git a/pkg/util/util.go b/pkg/util/util.go index b72430e2..5824f2f1 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -57,3 +57,26 @@ func SafeIntFromFile(ctx *context.Context, path string) int { func ConcatStrings(items ...string) string { return strings.Join(items, "") } + +// Convert strings to bool using strconv.ParseBool() when recognized, otherwise +// use map lookup to convert strings like "Yes" "No" "On" "Off" to bool +// `ethtool` uses on, off, yes, no (upper and lower case) rather than true and +// false. +func ParseBool(str string) (bool, error) { + if b, err := strconv.ParseBool(str); err == nil { + return b, err + } else { + ExtraBools := map[string]bool{ + "on": true, + "off": false, + "yes": true, + "no": false, + } + if b, ok := ExtraBools[strings.ToLower(str)]; ok { + return b, nil + } else { + // Return strconv.ParseBool's error here + return b, err + } + } +} diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go index 3f96b1ae..e249a393 100644 --- a/pkg/util/util_test.go +++ b/pkg/util/util_test.go @@ -55,3 +55,53 @@ func TestConcatStrings(t *testing.T) { }) } } + +func TestParseBool(t *testing.T) { + type testCase struct { + item string + expected bool + } + + testCases := []testCase{ + { + item: "False", + expected: false, + }, + { + item: "F", + expected: false, + }, + { + item: "1", + expected: true, + }, + { + item: "on", + expected: true, + }, + { + item: "Off", + expected: false, + }, + { + item: "Yes", + expected: true, + }, + { + item: "no", + expected: false, + }, + } + + for _, tCase := range testCases { + t.Run(tCase.item, func(t *testing.T) { + got, err := util.ParseBool(tCase.item) + if got != tCase.expected { + t.Errorf("expected %t got %t", tCase.expected, got) + } + if err != nil { + t.Errorf("util.ParseBool threw error %s", err) + } + }) + } +}