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 }