3232from nodescraper .models .systeminfo import OSFamily
3333from nodescraper .plugins .inband .network .network_collector import NetworkCollector
3434from nodescraper .plugins .inband .network .networkdata import (
35+ EthtoolInfo ,
3536 IpAddress ,
3637 Neighbor ,
3738 NetworkDataModel ,
@@ -75,6 +76,41 @@ def collector(system_info, conn_mock):
7576IP_NEIGHBOR_OUTPUT = """50.50.1.50 dev eth0 lladdr 11:22:33:44:55:66 STALE
767750.50.1.1 dev eth0 lladdr 99:88:77:66:55:44 REACHABLE"""
7778
79+ ETHTOOL_OUTPUT = """Settings for ethmock123:
80+ Supported ports: [ TP ]
81+ Supported link modes: 10mockbaseT/Half
82+ 123mockbaseT/Half
83+ 1234mockbaseT/Full
84+ Supported pause frame use: Symmetric
85+ Supports auto-negotiation: Yes
86+ Supported FEC modes: Not reported
87+ Advertised link modes: 10mockbaseT/Half 10mockbaseT/Full
88+ 167mockbaseT/Half 167mockbaseT/Full
89+ 1345mockbaseT/Full
90+ Advertised pause frame use: Symmetric
91+ Advertised auto-negotiation: Yes
92+ Advertised FEC modes: Xyz ABCfec
93+ Speed: 1000mockMb/s
94+ Duplex: Full
95+ Port: MockedTwisted Pair
96+ PHYAD: 1
97+ Transceiver: internal
98+ Auto-negotiation: on
99+ MDI-X: on (auto)
100+ Supports Wake-on: qwerty
101+ Wake-on: g
102+ Current message level: 0x123123
103+ Link detected: yes"""
104+
105+ ETHTOOL_NO_LINK_OUTPUT = """Settings for ethmock1:
106+ Supported ports: [ FIBRE ]
107+ Supported link modes: 11122mockbaseT/Full
108+ Speed: Unknown!
109+ Duplex: Unknown!
110+ Port: FIBRE
111+ Auto-negotiation: off
112+ Link detected: no"""
113+
78114
79115def test_parse_ip_addr_loopback (collector ):
80116 """Test parsing loopback interface from ip addr output"""
@@ -266,6 +302,9 @@ def run_sut_cmd_side_effect(cmd):
266302 return MagicMock (exit_code = 0 , stdout = IP_RULE_OUTPUT , command = cmd )
267303 elif "neighbor show" in cmd :
268304 return MagicMock (exit_code = 0 , stdout = IP_NEIGHBOR_OUTPUT , command = cmd )
305+ elif "ethtool" in cmd :
306+ # Fail ethtool commands (simulating no sudo or not supported)
307+ return MagicMock (exit_code = 1 , stdout = "" , command = cmd )
269308 return MagicMock (exit_code = 1 , stdout = "" , command = cmd )
270309
271310 collector ._run_sut_cmd = MagicMock (side_effect = run_sut_cmd_side_effect )
@@ -283,6 +322,7 @@ def run_sut_cmd_side_effect(cmd):
283322 assert "3 routes" in result .message
284323 assert "3 rules" in result .message
285324 assert "2 neighbors" in result .message
325+ assert "ethtool" in result .message
286326
287327
288328def test_collect_data_addr_failure (collector , conn_mock ):
@@ -299,6 +339,8 @@ def run_sut_cmd_side_effect(cmd):
299339 return MagicMock (exit_code = 0 , stdout = IP_RULE_OUTPUT , command = cmd )
300340 elif "neighbor show" in cmd :
301341 return MagicMock (exit_code = 0 , stdout = IP_NEIGHBOR_OUTPUT , command = cmd )
342+ elif "ethtool" in cmd :
343+ return MagicMock (exit_code = 1 , stdout = "" , command = cmd )
302344 return MagicMock (exit_code = 1 , stdout = "" , command = cmd )
303345
304346 collector ._run_sut_cmd = MagicMock (side_effect = run_sut_cmd_side_effect )
@@ -312,15 +354,19 @@ def run_sut_cmd_side_effect(cmd):
312354 assert len (data .routes ) == 3 # Success
313355 assert len (data .rules ) == 3 # Success
314356 assert len (data .neighbors ) == 2 # Success
357+ assert len (data .ethtool_info ) == 0 # No interfaces, so no ethtool data
315358 assert len (result .events ) > 0
316359
317360
318361def test_collect_data_all_failures (collector , conn_mock ):
319362 """Test collection when all commands fail"""
320363 collector .system_info .os_family = OSFamily .LINUX
321364
322- # Mock all commands failing
323- collector ._run_sut_cmd = MagicMock (return_value = MagicMock (exit_code = 1 , stdout = "" , command = "ip" ))
365+ # Mock all commands failing (including ethtool)
366+ def run_sut_cmd_side_effect (cmd ):
367+ return MagicMock (exit_code = 1 , stdout = "" , command = cmd )
368+
369+ collector ._run_sut_cmd = MagicMock (side_effect = run_sut_cmd_side_effect )
324370
325371 result , data = collector .collect_data ()
326372
@@ -389,30 +435,110 @@ def test_parse_ip_rule_with_action(collector):
389435 assert rule .table is None
390436
391437
438+ def test_parse_ethtool_basic (collector ):
439+ """Test parsing basic ethtool output"""
440+ ethtool_info = collector ._parse_ethtool ("ethmock123" , ETHTOOL_OUTPUT )
441+
442+ assert ethtool_info .interface == "ethmock123"
443+ assert ethtool_info .speed == "1000mockMb/s"
444+ assert ethtool_info .duplex == "Full"
445+ assert ethtool_info .port == "MockedTwisted Pair"
446+ assert ethtool_info .auto_negotiation == "on"
447+ assert ethtool_info .link_detected == "yes"
448+ assert "Speed" in ethtool_info .settings
449+ assert ethtool_info .settings ["Speed" ] == "1000mockMb/s"
450+ assert ethtool_info .settings ["PHYAD" ] == "1"
451+ assert ethtool_info .raw_output == ETHTOOL_OUTPUT
452+
453+
454+ def test_parse_ethtool_supported_link_modes (collector ):
455+ """Test parsing supported link modes from ethtool output"""
456+ ethtool_info = collector ._parse_ethtool ("ethmock123" , ETHTOOL_OUTPUT )
457+
458+ # Check supported link modes are stored in settings dict
459+ # Note: The current implementation stores link modes in settings dict,
460+ # not in the supported_link_modes list
461+ assert "Supported link modes" in ethtool_info .settings
462+ assert "10mockbaseT/Half" in ethtool_info .settings ["Supported link modes" ]
463+
464+
465+ def test_parse_ethtool_advertised_link_modes (collector ):
466+ """Test parsing advertised link modes from ethtool output"""
467+ ethtool_info = collector ._parse_ethtool ("ethmock123" , ETHTOOL_OUTPUT )
468+
469+ # Check advertised link modes are stored in settings dict
470+ # Note: The current implementation stores link modes in settings dict,
471+ # not in the advertised_link_modes list
472+ assert "Advertised link modes" in ethtool_info .settings
473+ assert "10mockbaseT/Half" in ethtool_info .settings ["Advertised link modes" ]
474+ assert "10mockbaseT/Full" in ethtool_info .settings ["Advertised link modes" ]
475+
476+
477+ def test_parse_ethtool_no_link (collector ):
478+ """Test parsing ethtool output when link is down"""
479+ ethtool_info = collector ._parse_ethtool ("ethmock1" , ETHTOOL_NO_LINK_OUTPUT )
480+
481+ assert ethtool_info .interface == "ethmock1"
482+ assert ethtool_info .speed == "Unknown!"
483+ assert ethtool_info .duplex == "Unknown!"
484+ assert ethtool_info .port == "FIBRE"
485+ assert ethtool_info .auto_negotiation == "off"
486+ assert ethtool_info .link_detected == "no"
487+ # Check supported link modes are stored in settings dict
488+ assert "Supported link modes" in ethtool_info .settings
489+ assert "11122mockbaseT/Full" in ethtool_info .settings ["Supported link modes" ]
490+
491+
492+ def test_parse_ethtool_empty_output (collector ):
493+ """Test parsing empty ethtool output"""
494+ ethtool_info = collector ._parse_ethtool ("eth0" , "" )
495+
496+ assert ethtool_info .interface == "eth0"
497+ assert ethtool_info .speed is None
498+ assert ethtool_info .duplex is None
499+ assert ethtool_info .link_detected is None
500+ assert len (ethtool_info .settings ) == 0
501+ assert len (ethtool_info .supported_link_modes ) == 0
502+ assert len (ethtool_info .advertised_link_modes ) == 0
503+
504+
392505def test_network_data_model_creation (collector ):
393506 """Test creating NetworkDataModel with all components"""
394507 interface = NetworkInterface (
395- name = "eth0 " ,
508+ name = "ethmock123 " ,
396509 index = 1 ,
397510 state = "UP" ,
398511 mtu = 5678 ,
399512 addresses = [IpAddress (address = "1.123.123.100" , prefix_len = 24 , family = "inet" )],
400513 )
401514
402- route = Route (destination = "default" , gateway = "2.123.123.1" , device = "eth0 " )
515+ route = Route (destination = "default" , gateway = "2.123.123.1" , device = "ethmock123 " )
403516
404517 rule = RoutingRule (priority = 100 , source = "1.123.123.0/24" , table = "main" )
405518
406519 neighbor = Neighbor (
407- ip_address = "50.50.1.1" , device = "eth0" , mac_address = "11:22:33:44:55:66" , state = "REACHABLE"
520+ ip_address = "50.50.1.1" ,
521+ device = "ethmock123" ,
522+ mac_address = "11:22:33:44:55:66" ,
523+ state = "REACHABLE" ,
524+ )
525+
526+ ethtool_info = EthtoolInfo (
527+ interface = "ethmock123" , raw_output = ETHTOOL_OUTPUT , speed = "1000mockMb/s" , duplex = "Full"
408528 )
409529
410530 data = NetworkDataModel (
411- interfaces = [interface ], routes = [route ], rules = [rule ], neighbors = [neighbor ]
531+ interfaces = [interface ],
532+ routes = [route ],
533+ rules = [rule ],
534+ neighbors = [neighbor ],
535+ ethtool_info = {"ethmock123" : ethtool_info },
412536 )
413537
414538 assert len (data .interfaces ) == 1
415539 assert len (data .routes ) == 1
416540 assert len (data .rules ) == 1
417541 assert len (data .neighbors ) == 1
418- assert data .interfaces [0 ].name == "eth0"
542+ assert len (data .ethtool_info ) == 1
543+ assert data .interfaces [0 ].name == "ethmock123"
544+ assert data .ethtool_info ["ethmock123" ].speed == "1000mockMb/s"
0 commit comments