2424#
2525###############################################################################
2626import re
27- from typing import List , Optional , Tuple
27+ from typing import Dict , List , Optional , Tuple
2828
2929from nodescraper .base import InBandDataCollector
3030from nodescraper .enums import EventCategory , EventPriority , ExecutionStatus
3131from nodescraper .models import TaskResult
3232
3333from .networkdata import (
34+ EthtoolInfo ,
3435 IpAddress ,
3536 Neighbor ,
3637 NetworkDataModel ,
@@ -48,6 +49,7 @@ class NetworkCollector(InBandDataCollector[NetworkDataModel, None]):
4849 CMD_ROUTE = "ip route show"
4950 CMD_RULE = "ip rule show"
5051 CMD_NEIGHBOR = "ip neighbor show"
52+ CMD_ETHTOOL_TEMPLATE = "sudo ethtool {interface}"
5153
5254 def _parse_ip_addr (self , output : str ) -> List [NetworkInterface ]:
5355 """Parse 'ip addr show' output into NetworkInterface objects.
@@ -370,6 +372,98 @@ def _parse_ip_neighbor(self, output: str) -> List[Neighbor]:
370372
371373 return neighbors
372374
375+ def _parse_ethtool (self , interface : str , output : str ) -> EthtoolInfo :
376+ """Parse 'ethtool <interface>' output into EthtoolInfo object.
377+
378+ Args:
379+ interface: Name of the network interface
380+ output: Raw output from 'ethtool <interface>' command
381+
382+ Returns:
383+ EthtoolInfo object with parsed data
384+ """
385+ ethtool_info = EthtoolInfo (interface = interface , raw_output = output )
386+
387+ # Parse line by line
388+ current_section = None
389+ for line in output .splitlines ():
390+ line_stripped = line .strip ()
391+ if not line_stripped :
392+ continue
393+
394+ # Detect sections (lines ending with colon and no tab prefix)
395+ if line_stripped .endswith (":" ) and not line .startswith ("\t " ):
396+ current_section = line_stripped .rstrip (":" )
397+ continue
398+
399+ # Parse key-value pairs (lines with colon in the middle)
400+ if ":" in line_stripped :
401+ # Split on first colon
402+ parts = line_stripped .split (":" , 1 )
403+ if len (parts ) == 2 :
404+ key = parts [0 ].strip ()
405+ value = parts [1 ].strip ()
406+
407+ # Store in settings dict
408+ ethtool_info .settings [key ] = value
409+
410+ # Extract specific important fields
411+ if key == "Speed" :
412+ ethtool_info .speed = value
413+ elif key == "Duplex" :
414+ ethtool_info .duplex = value
415+ elif key == "Port" :
416+ ethtool_info .port = value
417+ elif key == "Auto-negotiation" :
418+ ethtool_info .auto_negotiation = value
419+ elif key == "Link detected" :
420+ ethtool_info .link_detected = value
421+
422+ # Parse supported/advertised link modes (typically indented list items)
423+ elif current_section in ["Supported link modes" , "Advertised link modes" ]:
424+ # These are typically list items, possibly with speeds like "10baseT/Half"
425+ if line .startswith ("\t " ) or line .startswith (" " ):
426+ mode = line_stripped
427+ if current_section == "Supported link modes" :
428+ ethtool_info .supported_link_modes .append (mode )
429+ elif current_section == "Advertised link modes" :
430+ ethtool_info .advertised_link_modes .append (mode )
431+
432+ return ethtool_info
433+
434+ def _collect_ethtool_info (self , interfaces : List [NetworkInterface ]) -> Dict [str , EthtoolInfo ]:
435+ """Collect ethtool information for all network interfaces.
436+
437+ Args:
438+ interfaces: List of NetworkInterface objects to collect ethtool info for
439+
440+ Returns:
441+ Dictionary mapping interface name to EthtoolInfo
442+ """
443+ ethtool_data = {}
444+
445+ for iface in interfaces :
446+ cmd = self .CMD_ETHTOOL_TEMPLATE .format (interface = iface .name )
447+ res_ethtool = self ._run_sut_cmd (cmd )
448+
449+ if res_ethtool .exit_code == 0 :
450+ ethtool_info = self ._parse_ethtool (iface .name , res_ethtool .stdout )
451+ ethtool_data [iface .name ] = ethtool_info
452+ self ._log_event (
453+ category = EventCategory .OS ,
454+ description = f"Collected ethtool info for interface: { iface .name } " ,
455+ priority = EventPriority .INFO ,
456+ )
457+ else :
458+ self ._log_event (
459+ category = EventCategory .OS ,
460+ description = f"Error collecting ethtool info for interface: { iface .name } " ,
461+ data = {"command" : res_ethtool .command , "exit_code" : res_ethtool .exit_code },
462+ priority = EventPriority .WARNING ,
463+ )
464+
465+ return ethtool_data
466+
373467 def collect_data (
374468 self ,
375469 args = None ,
@@ -384,6 +478,7 @@ def collect_data(
384478 routes = []
385479 rules = []
386480 neighbors = []
481+ ethtool_data = {}
387482
388483 # Collect interface/address information
389484 res_addr = self ._run_sut_cmd (self .CMD_ADDR )
@@ -403,6 +498,15 @@ def collect_data(
403498 console_log = True ,
404499 )
405500
501+ # Collect ethtool information for interfaces
502+ if interfaces :
503+ ethtool_data = self ._collect_ethtool_info (interfaces )
504+ self ._log_event (
505+ category = EventCategory .OS ,
506+ description = f"Collected ethtool info for { len (ethtool_data )} interfaces" ,
507+ priority = EventPriority .INFO ,
508+ )
509+
406510 # Collect routing table
407511 res_route = self ._run_sut_cmd (self .CMD_ROUTE )
408512 if res_route .exit_code == 0 :
@@ -456,11 +560,16 @@ def collect_data(
456560
457561 if interfaces or routes or rules or neighbors :
458562 network_data = NetworkDataModel (
459- interfaces = interfaces , routes = routes , rules = rules , neighbors = neighbors
563+ interfaces = interfaces ,
564+ routes = routes ,
565+ rules = rules ,
566+ neighbors = neighbors ,
567+ ethtool_info = ethtool_data ,
460568 )
461569 self .result .message = (
462570 f"Collected network data: { len (interfaces )} interfaces, "
463- f"{ len (routes )} routes, { len (rules )} rules, { len (neighbors )} neighbors"
571+ f"{ len (routes )} routes, { len (rules )} rules, { len (neighbors )} neighbors, "
572+ f"{ len (ethtool_data )} ethtool entries"
464573 )
465574 self .result .status = ExecutionStatus .OK
466575 return self .result , network_data
0 commit comments