Skip to content

Commit cfb3618

Browse files
Merge branch 'development' into alex_license
2 parents a2e9f8c + b5c592a commit cfb3618

File tree

4 files changed

+275
-19
lines changed

4 files changed

+275
-19
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: 120 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@
2424
#
2525
###############################################################################
2626
import re
27-
from typing import List, Optional, Tuple
27+
from typing import Dict, List, Optional, Tuple
2828

2929
from nodescraper.base import InBandDataCollector
3030
from nodescraper.enums import EventCategory, EventPriority, ExecutionStatus
3131
from nodescraper.models import TaskResult
3232

3333
from .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

nodescraper/plugins/inband/network/networkdata.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
# SOFTWARE.
2424
#
2525
###############################################################################
26-
from typing import List, Optional
26+
from typing import Dict, List, Optional
2727

2828
from pydantic import BaseModel, Field
2929

@@ -90,10 +90,28 @@ class Neighbor(BaseModel):
9090
flags: List[str] = Field(default_factory=list) # Additional flags like "router", "proxy"
9191

9292

93+
class EthtoolInfo(BaseModel):
94+
"""Ethtool information for a network interface"""
95+
96+
interface: str # Interface name this info belongs to
97+
raw_output: str # Raw ethtool command output
98+
settings: Dict[str, str] = Field(default_factory=dict) # Parsed key-value settings
99+
supported_link_modes: List[str] = Field(default_factory=list) # Supported link modes
100+
advertised_link_modes: List[str] = Field(default_factory=list) # Advertised link modes
101+
speed: Optional[str] = None # Link speed (e.g., "10000Mb/s")
102+
duplex: Optional[str] = None # Duplex mode (e.g., "Full")
103+
port: Optional[str] = None # Port type (e.g., "Twisted Pair")
104+
auto_negotiation: Optional[str] = None # Auto-negotiation status (e.g., "on", "off")
105+
link_detected: Optional[str] = None # Link detection status (e.g., "yes", "no")
106+
107+
93108
class NetworkDataModel(DataModel):
94109
"""Complete network configuration data"""
95110

96111
interfaces: List[NetworkInterface] = Field(default_factory=list)
97112
routes: List[Route] = Field(default_factory=list)
98113
rules: List[RoutingRule] = Field(default_factory=list)
99114
neighbors: List[Neighbor] = Field(default_factory=list)
115+
ethtool_info: Dict[str, EthtoolInfo] = Field(
116+
default_factory=dict
117+
) # Interface name -> EthtoolInfo mapping

0 commit comments

Comments
 (0)