Skip to content

Commit 158b766

Browse files
author
jaspals
committed
ethtool utests and EventCategory.Network change
1 parent e32142e commit 158b766

File tree

3 files changed

+147
-18
lines changed

3 files changed

+147
-18
lines changed

nodescraper/enums/eventcategory.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ class EventCategory(AutoNameStrEnum):
6363
SBIOS/VBIOS/IFWI Errors
6464
- INFRASTRUCTURE
6565
Network, IT issues, Downtime
66+
- NETWORK
67+
Network configuration, interfaces, routing, neighbors, ethtool data
6668
- RUNTIME
6769
Framework issues, does not include content failures
6870
- UNKNOWN
@@ -82,5 +84,6 @@ class EventCategory(AutoNameStrEnum):
8284
SW_DRIVER = auto()
8385
BIOS = auto()
8486
INFRASTRUCTURE = auto()
87+
NETWORK = auto()
8588
RUNTIME = auto()
8689
UNKNOWN = auto()

nodescraper/plugins/inband/network/network_collector.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -450,13 +450,13 @@ def _collect_ethtool_info(self, interfaces: List[NetworkInterface]) -> Dict[str,
450450
ethtool_info = self._parse_ethtool(iface.name, res_ethtool.stdout)
451451
ethtool_data[iface.name] = ethtool_info
452452
self._log_event(
453-
category=EventCategory.OS,
453+
category=EventCategory.NETWORK,
454454
description=f"Collected ethtool info for interface: {iface.name}",
455455
priority=EventPriority.INFO,
456456
)
457457
else:
458458
self._log_event(
459-
category=EventCategory.OS,
459+
category=EventCategory.NETWORK,
460460
description=f"Error collecting ethtool info for interface: {iface.name}",
461461
data={"command": res_ethtool.command, "exit_code": res_ethtool.exit_code},
462462
priority=EventPriority.WARNING,
@@ -485,13 +485,13 @@ def collect_data(
485485
if res_addr.exit_code == 0:
486486
interfaces = self._parse_ip_addr(res_addr.stdout)
487487
self._log_event(
488-
category=EventCategory.OS,
488+
category=EventCategory.NETWORK,
489489
description=f"Collected {len(interfaces)} network interfaces",
490490
priority=EventPriority.INFO,
491491
)
492492
else:
493493
self._log_event(
494-
category=EventCategory.OS,
494+
category=EventCategory.NETWORK,
495495
description="Error collecting network interfaces",
496496
data={"command": res_addr.command, "exit_code": res_addr.exit_code},
497497
priority=EventPriority.ERROR,
@@ -502,7 +502,7 @@ def collect_data(
502502
if interfaces:
503503
ethtool_data = self._collect_ethtool_info(interfaces)
504504
self._log_event(
505-
category=EventCategory.OS,
505+
category=EventCategory.NETWORK,
506506
description=f"Collected ethtool info for {len(ethtool_data)} interfaces",
507507
priority=EventPriority.INFO,
508508
)
@@ -512,13 +512,13 @@ def collect_data(
512512
if res_route.exit_code == 0:
513513
routes = self._parse_ip_route(res_route.stdout)
514514
self._log_event(
515-
category=EventCategory.OS,
515+
category=EventCategory.NETWORK,
516516
description=f"Collected {len(routes)} routes",
517517
priority=EventPriority.INFO,
518518
)
519519
else:
520520
self._log_event(
521-
category=EventCategory.OS,
521+
category=EventCategory.NETWORK,
522522
description="Error collecting routes",
523523
data={"command": res_route.command, "exit_code": res_route.exit_code},
524524
priority=EventPriority.WARNING,
@@ -529,13 +529,13 @@ def collect_data(
529529
if res_rule.exit_code == 0:
530530
rules = self._parse_ip_rule(res_rule.stdout)
531531
self._log_event(
532-
category=EventCategory.OS,
532+
category=EventCategory.NETWORK,
533533
description=f"Collected {len(rules)} routing rules",
534534
priority=EventPriority.INFO,
535535
)
536536
else:
537537
self._log_event(
538-
category=EventCategory.OS,
538+
category=EventCategory.NETWORK,
539539
description="Error collecting routing rules",
540540
data={"command": res_rule.command, "exit_code": res_rule.exit_code},
541541
priority=EventPriority.WARNING,
@@ -546,13 +546,13 @@ def collect_data(
546546
if res_neighbor.exit_code == 0:
547547
neighbors = self._parse_ip_neighbor(res_neighbor.stdout)
548548
self._log_event(
549-
category=EventCategory.OS,
549+
category=EventCategory.NETWORK,
550550
description=f"Collected {len(neighbors)} neighbor entries",
551551
priority=EventPriority.INFO,
552552
)
553553
else:
554554
self._log_event(
555-
category=EventCategory.OS,
555+
category=EventCategory.NETWORK,
556556
description="Error collecting neighbor table",
557557
data={"command": res_neighbor.command, "exit_code": res_neighbor.exit_code},
558558
priority=EventPriority.WARNING,

test/unit/plugin/test_network_collector.py

Lines changed: 133 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from nodescraper.models.systeminfo import OSFamily
3333
from nodescraper.plugins.inband.network.network_collector import NetworkCollector
3434
from nodescraper.plugins.inband.network.networkdata import (
35+
EthtoolInfo,
3536
IpAddress,
3637
Neighbor,
3738
NetworkDataModel,
@@ -75,6 +76,41 @@ def collector(system_info, conn_mock):
7576
IP_NEIGHBOR_OUTPUT = """50.50.1.50 dev eth0 lladdr 11:22:33:44:55:66 STALE
7677
50.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

79115
def 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

288328
def 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

318361
def 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+
392505
def 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

Comments
 (0)