github.com/jaypipes/ghw@v0.21.1/pkg/net/net_linux.go (about)

     1  // Use and distribution licensed under the Apache license version 2.
     2  //
     3  // See the COPYING file in the root project directory for full text.
     4  //
     5  
     6  package net
     7  
     8  import (
     9  	"bufio"
    10  	"bytes"
    11  	"fmt"
    12  	"os"
    13  	"os/exec"
    14  	"path/filepath"
    15  	"strings"
    16  
    17  	"github.com/jaypipes/ghw/pkg/context"
    18  	"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`
    24  )
    25  
    26  func (i *Info) load() error {
    27  	i.NICs = nics(i.ctx)
    28  	return nil
    29  }
    30  
    31  func nics(ctx *context.Context) []*NIC {
    32  	nics := make([]*NIC, 0)
    33  
    34  	paths := linuxpath.New(ctx)
    35  	files, err := os.ReadDir(paths.SysClassNet)
    36  	if err != nil {
    37  		return nics
    38  	}
    39  
    40  	etAvailable := ctx.EnableTools
    41  	if etAvailable {
    42  		if etInstalled := ethtoolInstalled(); !etInstalled {
    43  			ctx.Warn(warnEthtoolNotInstalled)
    44  			etAvailable = false
    45  		}
    46  	}
    47  
    48  	for _, file := range files {
    49  		filename := file.Name()
    50  		// Ignore loopback and bonding_masters
    51  		if filename == "lo" || filename == "bonding_masters" {
    52  			continue
    53  		}
    54  
    55  		netPath := filepath.Join(paths.SysClassNet, filename)
    56  		dest, _ := os.Readlink(netPath)
    57  		isVirtual := false
    58  		if strings.Contains(dest, "devices/virtual/net") {
    59  			isVirtual = true
    60  		}
    61  
    62  		nic := &NIC{
    63  			Name:      filename,
    64  			IsVirtual: isVirtual,
    65  		}
    66  
    67  		mac := netDeviceMacAddress(paths, filename)
    68  		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  
    78  		nic.PCIAddress = netDevicePCIAddress(paths.SysClassNet, filename)
    79  
    80  		nics = append(nics, nic)
    81  	}
    82  	return nics
    83  }
    84  
    85  func netDeviceMacAddress(paths *linuxpath.Paths, dev string) string {
    86  	// Instead of use udevadm, we can get the device's MAC address by examing
    87  	// the /sys/class/net/$DEVICE/address file in sysfs. However, for devices
    88  	// that have addr_assign_type != 0, return None since the MAC address is
    89  	// random.
    90  	aatPath := filepath.Join(paths.SysClassNet, dev, "addr_assign_type")
    91  	contents, err := os.ReadFile(aatPath)
    92  	if err != nil {
    93  		return ""
    94  	}
    95  	if strings.TrimSpace(string(contents)) != "0" {
    96  		return ""
    97  	}
    98  	addrPath := filepath.Join(paths.SysClassNet, dev, "address")
    99  	contents, err = os.ReadFile(addrPath)
   100  	if err != nil {
   101  		return ""
   102  	}
   103  	return strings.TrimSpace(string(contents))
   104  }
   105  
   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))
   125  
   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)
   137  	}
   138  
   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)
   175  	}
   176  
   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,
   198  	}
   199  }
   200  
   201  func netDevicePCIAddress(netDevDir, netDevName string) *string {
   202  	// what we do here is not that hard in the end: we need to navigate the sysfs
   203  	// up to the directory belonging to the device backing the network interface.
   204  	// we can make few relatively safe assumptions, but the safest way is follow
   205  	// the right links. And so we go.
   206  	// First of all, knowing the network device name we need to resolve the backing
   207  	// device path to its full sysfs path.
   208  	// say we start with netDevDir="/sys/class/net" and netDevName="enp0s31f6"
   209  	netPath := filepath.Join(netDevDir, netDevName)
   210  	dest, err := os.Readlink(netPath)
   211  	if err != nil {
   212  		// bail out with empty value
   213  		return nil
   214  	}
   215  	// now we have something like dest="../../devices/pci0000:00/0000:00:1f.6/net/enp0s31f6"
   216  	// remember the path is relative to netDevDir="/sys/class/net"
   217  
   218  	netDev := filepath.Clean(filepath.Join(netDevDir, dest))
   219  	// so we clean "/sys/class/net/../../devices/pci0000:00/0000:00:1f.6/net/enp0s31f6"
   220  	// leading to "/sys/devices/pci0000:00/0000:00:1f.6/net/enp0s31f6"
   221  	// still not there. We need to access the data of the pci device. So we jump into the path
   222  	// linked to the "device" pseudofile
   223  	dest, err = os.Readlink(filepath.Join(netDev, "device"))
   224  	if err != nil {
   225  		// bail out with empty value
   226  		return nil
   227  	}
   228  	// we expect something like="../../../0000:00:1f.6"
   229  
   230  	devPath := filepath.Clean(filepath.Join(netDev, dest))
   231  	// so we clean "/sys/devices/pci0000:00/0000:00:1f.6/net/enp0s31f6/../../../0000:00:1f.6"
   232  	// leading to "/sys/devices/pci0000:00/0000:00:1f.6/"
   233  	// finally here!
   234  
   235  	// to which bus is this device connected to?
   236  	dest, err = os.Readlink(filepath.Join(devPath, "subsystem"))
   237  	if err != nil {
   238  		// bail out with empty value
   239  		return nil
   240  	}
   241  	// ok, this is hacky, but since we need the last *two* path components and we know we
   242  	// are running on linux...
   243  	if !strings.HasSuffix(dest, "/bus/pci") {
   244  		// unsupported and unexpected bus!
   245  		return nil
   246  	}
   247  
   248  	pciAddr := filepath.Base(devPath)
   249  	return &pciAddr
   250  }
   251  
   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  
   258  func readFile(path string) string {
   259  	contents, err := os.ReadFile(path)
   260  	if err != nil {
   261  		return ""
   262  	}
   263  	return strings.TrimSpace(string(contents))
   264  }
   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  }