Skip to content

Commit

Permalink
Merge pull request #338 from jak3kaj/NicLinkInfo1
Browse files Browse the repository at this point in the history
Add AutoNegotiation and PauseFrameUse NICCapabilities
  • Loading branch information
jaypipes committed May 4, 2023
2 parents 6aad076 + db2b255 commit d89a781
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 31 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,7 @@ net (3 NICs)
- rx-vlan-offload
- tx-vlan-offload
- highdma
- auto-negotiation
wlp59s0
enabled capabilities:
- scatter-gather
Expand Down
9 changes: 9 additions & 0 deletions pkg/net/net.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
174 changes: 143 additions & 31 deletions pkg/net/net_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

"github.com/jaypipes/ghw/pkg/context"
"github.com/jaypipes/ghw/pkg/linuxpath"
"github.com/jaypipes/ghw/pkg/util"
)

const (
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
64 changes: 64 additions & 0 deletions pkg/net/net_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
package net

import (
"bytes"
"os"
"reflect"
"testing"
Expand Down Expand Up @@ -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)
}
}
}
23 changes: 23 additions & 0 deletions pkg/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
50 changes: 50 additions & 0 deletions pkg/util/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}

0 comments on commit d89a781

Please sign in to comment.