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 .NETWORK ,
454+ description = f"Collected ethtool info for interface: { iface .name } " ,
455+ priority = EventPriority .INFO ,
456+ )
457+ else :
458+ self ._log_event (
459+ category = EventCategory .NETWORK ,
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,37 +478,47 @@ 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 )
390485 if res_addr .exit_code == 0 :
391486 interfaces = self ._parse_ip_addr (res_addr .stdout )
392487 self ._log_event (
393- category = EventCategory .OS ,
488+ category = EventCategory .NETWORK ,
394489 description = f"Collected { len (interfaces )} network interfaces" ,
395490 priority = EventPriority .INFO ,
396491 )
397492 else :
398493 self ._log_event (
399- category = EventCategory .OS ,
494+ category = EventCategory .NETWORK ,
400495 description = "Error collecting network interfaces" ,
401496 data = {"command" : res_addr .command , "exit_code" : res_addr .exit_code },
402497 priority = EventPriority .ERROR ,
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 .NETWORK ,
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 :
409513 routes = self ._parse_ip_route (res_route .stdout )
410514 self ._log_event (
411- category = EventCategory .OS ,
515+ category = EventCategory .NETWORK ,
412516 description = f"Collected { len (routes )} routes" ,
413517 priority = EventPriority .INFO ,
414518 )
415519 else :
416520 self ._log_event (
417- category = EventCategory .OS ,
521+ category = EventCategory .NETWORK ,
418522 description = "Error collecting routes" ,
419523 data = {"command" : res_route .command , "exit_code" : res_route .exit_code },
420524 priority = EventPriority .WARNING ,
@@ -425,13 +529,13 @@ def collect_data(
425529 if res_rule .exit_code == 0 :
426530 rules = self ._parse_ip_rule (res_rule .stdout )
427531 self ._log_event (
428- category = EventCategory .OS ,
532+ category = EventCategory .NETWORK ,
429533 description = f"Collected { len (rules )} routing rules" ,
430534 priority = EventPriority .INFO ,
431535 )
432536 else :
433537 self ._log_event (
434- category = EventCategory .OS ,
538+ category = EventCategory .NETWORK ,
435539 description = "Error collecting routing rules" ,
436540 data = {"command" : res_rule .command , "exit_code" : res_rule .exit_code },
437541 priority = EventPriority .WARNING ,
@@ -442,25 +546,30 @@ def collect_data(
442546 if res_neighbor .exit_code == 0 :
443547 neighbors = self ._parse_ip_neighbor (res_neighbor .stdout )
444548 self ._log_event (
445- category = EventCategory .OS ,
549+ category = EventCategory .NETWORK ,
446550 description = f"Collected { len (neighbors )} neighbor entries" ,
447551 priority = EventPriority .INFO ,
448552 )
449553 else :
450554 self ._log_event (
451- category = EventCategory .OS ,
555+ category = EventCategory .NETWORK ,
452556 description = "Error collecting neighbor table" ,
453557 data = {"command" : res_neighbor .command , "exit_code" : res_neighbor .exit_code },
454558 priority = EventPriority .WARNING ,
455559 )
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