From 413c5948e793f78812577a0c280d14fb385fdca0 Mon Sep 17 00:00:00 2001 From: jaspals3123 Date: Fri, 30 Jan 2026 15:19:36 -0500 Subject: [PATCH 1/6] url arg and test enhancements --- .../plugins/inband/network/collector_args.py | 33 ++++++++ .../inband/network/network_collector.py | 52 +++++++++++- .../plugins/inband/network/network_plugin.py | 5 +- .../plugins/inband/network/networkdata.py | 1 + test/unit/plugin/test_network_collector.py | 82 +++++++++++++++++++ 5 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 nodescraper/plugins/inband/network/collector_args.py diff --git a/nodescraper/plugins/inband/network/collector_args.py b/nodescraper/plugins/inband/network/collector_args.py new file mode 100644 index 00000000..1e1bcedf --- /dev/null +++ b/nodescraper/plugins/inband/network/collector_args.py @@ -0,0 +1,33 @@ +############################################################################### +# +# MIT License +# +# Copyright (c) 2025 Advanced Micro Devices, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +############################################################################### + +from typing import Optional + +from nodescraper.models import CollectorArgs + + +class NetworkCollectorArgs(CollectorArgs): + url: Optional[str] = None diff --git a/nodescraper/plugins/inband/network/network_collector.py b/nodescraper/plugins/inband/network/network_collector.py index 1dac9ac4..a123b98a 100644 --- a/nodescraper/plugins/inband/network/network_collector.py +++ b/nodescraper/plugins/inband/network/network_collector.py @@ -27,9 +27,10 @@ from typing import Dict, List, Optional, Tuple from nodescraper.base import InBandDataCollector -from nodescraper.enums import EventCategory, EventPriority, ExecutionStatus +from nodescraper.enums import EventCategory, EventPriority, ExecutionStatus, OSFamily from nodescraper.models import TaskResult +from .collector_args import NetworkCollectorArgs from .networkdata import ( BroadcomNicDevice, BroadcomNicQos, @@ -55,7 +56,7 @@ ) -class NetworkCollector(InBandDataCollector[NetworkDataModel, None]): +class NetworkCollector(InBandDataCollector[NetworkDataModel, NetworkCollectorArgs]): """Collect network configuration details using ip command""" DATA_MODEL = NetworkDataModel @@ -1669,12 +1670,51 @@ def _collect_pensando_nic_info( uncollected_commands, ) + def _check_network_connectivity(self, url: str) -> bool: + """Check network connectivity by pinging a URL. + + Args: + url: URL or hostname to ping + + Returns: + bool: True if network is accessible, False otherwise + """ + cmd = "ping" + + # Determine ping options based on OS + ping_option = "-c 1" if self.system_info.os_family == OSFamily.LINUX else "-n 1" + + # Run ping command + result = self._run_sut_cmd(f"{cmd} {url} {ping_option}") + + network_accessible = result.exit_code == 0 + + if network_accessible: + self._log_event( + category=EventCategory.NETWORK, + description="System networking is up", + data={"url": url, "accessible": network_accessible}, + priority=EventPriority.INFO, + ) + else: + self._log_event( + category=EventCategory.NETWORK, + description=f"{cmd} to {url} failed!", + data={"url": url, "accessible": network_accessible}, + priority=EventPriority.ERROR, + ) + + return network_accessible + def collect_data( self, - args=None, + args: Optional[NetworkCollectorArgs] = None, ) -> Tuple[TaskResult, Optional[NetworkDataModel]]: """Collect network configuration from the system. + Args: + args: Optional NetworkCollectorArgs with URL for network connectivity check + Returns: Tuple[TaskResult, Optional[NetworkDataModel]]: tuple containing the task result and an instance of NetworkDataModel or None if collection failed. @@ -1695,6 +1735,11 @@ def collect_data( pensando_rdma_statistics: List[PensandoNicRdmaStatistics] = [] pensando_version_host_software: Optional[PensandoNicVersionHostSoftware] = None pensando_version_firmware: List[PensandoNicVersionFirmware] = [] + network_accessible: Optional[bool] = None + + # Check network connectivity if URL is provided + if args and args.url: + network_accessible = self._check_network_connectivity(args.url) # Collect interface/address information res_addr = self._run_sut_cmd(self.CMD_ADDR) @@ -1823,6 +1868,7 @@ def collect_data( pensando_nic_rdma_statistics=pensando_rdma_statistics, pensando_nic_version_host_software=pensando_version_host_software, pensando_nic_version_firmware=pensando_version_firmware, + accessible=network_accessible, ) self.result.status = ExecutionStatus.OK return self.result, network_data diff --git a/nodescraper/plugins/inband/network/network_plugin.py b/nodescraper/plugins/inband/network/network_plugin.py index 2735e705..0ba55e79 100644 --- a/nodescraper/plugins/inband/network/network_plugin.py +++ b/nodescraper/plugins/inband/network/network_plugin.py @@ -25,13 +25,16 @@ ############################################################################### from nodescraper.base import InBandDataPlugin +from .collector_args import NetworkCollectorArgs from .network_collector import NetworkCollector from .networkdata import NetworkDataModel -class NetworkPlugin(InBandDataPlugin[NetworkDataModel, None, None]): +class NetworkPlugin(InBandDataPlugin[NetworkDataModel, NetworkCollectorArgs, None]): """Plugin for collection of network configuration data""" DATA_MODEL = NetworkDataModel COLLECTOR = NetworkCollector + + COLLECTOR_ARGS = NetworkCollectorArgs diff --git a/nodescraper/plugins/inband/network/networkdata.py b/nodescraper/plugins/inband/network/networkdata.py index 34d1f63e..e6817514 100644 --- a/nodescraper/plugins/inband/network/networkdata.py +++ b/nodescraper/plugins/inband/network/networkdata.py @@ -317,3 +317,4 @@ class NetworkDataModel(DataModel): pensando_nic_rdma_statistics: List[PensandoNicRdmaStatistics] = Field(default_factory=list) pensando_nic_version_host_software: Optional[PensandoNicVersionHostSoftware] = None pensando_nic_version_firmware: List[PensandoNicVersionFirmware] = Field(default_factory=list) + accessible: Optional[bool] = None # Network accessibility check via ping diff --git a/test/unit/plugin/test_network_collector.py b/test/unit/plugin/test_network_collector.py index 222c1fc0..2de1374d 100644 --- a/test/unit/plugin/test_network_collector.py +++ b/test/unit/plugin/test_network_collector.py @@ -1859,3 +1859,85 @@ def test_network_data_model_with_pensando_nic_version_firmware(): assert len(data.pensando_nic_version_firmware) == 1 assert data.pensando_nic_version_firmware[0].nic_id == "42424650-4c32-3533-3330-323934000000" assert data.pensando_nic_version_firmware[0].cpld == "3.16 (primary)" + + +def test_network_accessibility_linux_success(collector, conn_mock): + """Test network accessibility check on Linux with successful ping""" + collector.system_info.os_family = OSFamily.LINUX + + # Mock successful ping command + def run_sut_cmd_side_effect(cmd, **kwargs): + if "ping" in cmd: + return MagicMock( + exit_code=0, + stdout=( + "PING sample.mock.com (11.22.33.44) 56(84) bytes of data.\n" + "64 bytes from mock-server 55.66.77.88): icmp_seq=1 ttl=63 time=0.408 ms\n" + "--- sample.mock.com ping statistics ---\n" + "1 packets transmitted, 1 received, 0% packet loss, time 0ms\n" + "rtt min/avg/max/mdev = 0.408/0.408/0.408/0.000 ms\n" + ), + command=cmd, + ) + return MagicMock(exit_code=1, stdout="", command=cmd) + + collector._run_sut_cmd = MagicMock(side_effect=run_sut_cmd_side_effect) + + # Test if collector has accessibility check method + if hasattr(collector, "check_network_accessibility"): + result, accessible = collector.check_network_accessibility() + assert result.status == ExecutionStatus.OK + assert accessible is True + + +def test_network_accessibility_windows_success(collector, conn_mock): + """Test network accessibility check on Windows with successful ping""" + collector.system_info.os_family = OSFamily.WINDOWS + + # Mock successful ping command + def run_sut_cmd_side_effect(cmd, **kwargs): + if "ping" in cmd: + return MagicMock( + exit_code=0, + stdout=( + "Pinging sample.mock.com [11.22.33.44] with 32 bytes of data:\n" + "Reply from 10.228.151.8: bytes=32 time=224ms TTL=55\n" + "Ping statistics for 11.22.33.44:\n" + "Packets: Sent = 1, Received = 1, Lost = 0 (0% loss),\n" + "Approximate round trip times in milli-seconds:\n" + "Minimum = 224ms, Maximum = 224ms, Average = 224ms\n" + ), + command=cmd, + ) + return MagicMock(exit_code=1, stdout="", command=cmd) + + collector._run_sut_cmd = MagicMock(side_effect=run_sut_cmd_side_effect) + + # Test if collector has accessibility check method + if hasattr(collector, "check_network_accessibility"): + result, accessible = collector.check_network_accessibility() + assert result.status == ExecutionStatus.OK + assert accessible is True + + +def test_network_accessibility_failure(collector, conn_mock): + """Test network accessibility check with failed ping""" + collector.system_info.os_family = OSFamily.LINUX + + # Mock failed ping command + def run_sut_cmd_side_effect(cmd, **kwargs): + if "ping" in cmd: + return MagicMock( + exit_code=1, + stdout="ping: www.sample.mock.com: Name or service not known", + command=cmd, + ) + return MagicMock(exit_code=1, stdout="", command=cmd) + + collector._run_sut_cmd = MagicMock(side_effect=run_sut_cmd_side_effect) + + # Test if collector has accessibility check method + if hasattr(collector, "check_network_accessibility"): + result, accessible = collector.check_network_accessibility() + assert result.status == ExecutionStatus.ERRORS_DETECTED + assert accessible is False From 99ceb7a7bd5f3e6c2bebcd71a169df2ce5df2692 Mon Sep 17 00:00:00 2001 From: jaspals3123 Date: Mon, 2 Feb 2026 13:25:41 -0500 Subject: [PATCH 2/6] review changes --- .../plugins/inband/network/network_collector.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/nodescraper/plugins/inband/network/network_collector.py b/nodescraper/plugins/inband/network/network_collector.py index a123b98a..16c69dea 100644 --- a/nodescraper/plugins/inband/network/network_collector.py +++ b/nodescraper/plugins/inband/network/network_collector.py @@ -65,6 +65,7 @@ class NetworkCollector(InBandDataCollector[NetworkDataModel, NetworkCollectorArg CMD_RULE = "ip rule show" CMD_NEIGHBOR = "ip neighbor show" CMD_ETHTOOL_TEMPLATE = "ethtool {interface}" + CMD_PING = "ping" # LLDP commands CMD_LLDPCLI_NEIGHBOR = "lldpcli show neighbor" @@ -1679,32 +1680,29 @@ def _check_network_connectivity(self, url: str) -> bool: Returns: bool: True if network is accessible, False otherwise """ - cmd = "ping" # Determine ping options based on OS ping_option = "-c 1" if self.system_info.os_family == OSFamily.LINUX else "-n 1" # Run ping command - result = self._run_sut_cmd(f"{cmd} {url} {ping_option}") + result = self._run_sut_cmd(f"{self.CMD_PING} {url} {ping_option}") - network_accessible = result.exit_code == 0 - - if network_accessible: + if result.exit_code == 0: self._log_event( category=EventCategory.NETWORK, description="System networking is up", - data={"url": url, "accessible": network_accessible}, + data={"url": url, "accessible": result.exit_code == 0}, priority=EventPriority.INFO, ) else: self._log_event( category=EventCategory.NETWORK, - description=f"{cmd} to {url} failed!", - data={"url": url, "accessible": network_accessible}, + description=f"{self.CMD_PING} to {url} failed!", + data={"url": url, "not accessible": result.exit_code == 0}, priority=EventPriority.ERROR, ) - return network_accessible + return result.exit_code == 0 def collect_data( self, From be293123df5072fea2b2baff520707fc4a2834c1 Mon Sep 17 00:00:00 2001 From: jaspals3123 Date: Mon, 2 Feb 2026 17:15:27 -0500 Subject: [PATCH 3/6] functional test with url arg --- .../fixtures/network_plugin_config.json | 3 +++ test/functional/test_network_plugin.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/test/functional/fixtures/network_plugin_config.json b/test/functional/fixtures/network_plugin_config.json index aa4b6bc0..65a7f99e 100644 --- a/test/functional/fixtures/network_plugin_config.json +++ b/test/functional/fixtures/network_plugin_config.json @@ -2,6 +2,9 @@ "global_args": {}, "plugins": { "NetworkPlugin": { + "collector_args": { + "url": "mock.example.com" + }, "analysis_args": {} } }, diff --git a/test/functional/test_network_plugin.py b/test/functional/test_network_plugin.py index 27776c8e..94729385 100644 --- a/test/functional/test_network_plugin.py +++ b/test/functional/test_network_plugin.py @@ -104,3 +104,21 @@ def test_network_plugin_skip_sudo(run_cli_command, network_config_file, tmp_path assert result.returncode in [0, 1, 2] output = result.stdout + result.stderr assert len(output) > 0 + + +def test_network_plugin_with_url(run_cli_command, network_config_file, tmp_path): + """Test NetworkPlugin with url collector argument.""" + log_path = str(tmp_path / "logs_network_url") + result = run_cli_command( + [ + "--log-path", + log_path, + "--plugin-configs", + str(network_config_file), + ], + check=False, + ) + + assert result.returncode in [0, 1, 2] + output = result.stdout + result.stderr + assert len(output) > 0 From a5105f7b6c3f40a7d67940635249db927a7473b5 Mon Sep 17 00:00:00 2001 From: jaspals Date: Tue, 3 Feb 2026 17:09:31 -0600 Subject: [PATCH 4/6] added netprobe arg --- nodescraper/cli/dynamicparserbuilder.py | 48 ++++++- .../plugins/inband/network/collector_args.py | 3 +- .../inband/network/network_collector.py | 39 +++++- .../fixtures/network_plugin_config.json | 3 +- test/functional/test_network_plugin.py | 132 ++++++++++++++++++ 5 files changed, 214 insertions(+), 11 deletions(-) diff --git a/nodescraper/cli/dynamicparserbuilder.py b/nodescraper/cli/dynamicparserbuilder.py index 87d0fe0f..7c759a93 100644 --- a/nodescraper/cli/dynamicparserbuilder.py +++ b/nodescraper/cli/dynamicparserbuilder.py @@ -24,7 +24,7 @@ # ############################################################################### import argparse -from typing import Optional, Type +from typing import Literal, Optional, Type, get_args, get_origin from pydantic import BaseModel @@ -96,11 +96,31 @@ def get_model_arg(cls, type_class_map: dict) -> Optional[Type[BaseModel]]: None, ) + @classmethod + def get_literal_choices(cls, type_class_map: dict) -> Optional[list]: + """Get the choices from a Literal type if present + + Args: + type_class_map (dict): mapping of type classes + + Returns: + Optional[list]: list of valid choices for the Literal type, or None if not a Literal + """ + # Check if Literal is in the type_class_map + literal_type = type_class_map.get(Literal) + if literal_type and literal_type.inner_type is not None: + # The inner_type contains the first literal value, but we need all of them + # We need to get the original annotation to extract all literal values + # For now, return None and we'll handle this differently + return None + return None + def add_argument( self, type_class_map: dict, arg_name: str, required: bool, + annotation: Optional[Type] = None, ) -> None: """Add an argument to a parser with an appropriate type @@ -108,7 +128,18 @@ def add_argument( type_class_map (dict): type classes for the arg arg_name (str): argument name required (bool): whether or not the arg is required + annotation (Optional[Type]): full type annotation for extracting Literal choices """ + # Check for Literal types and extract choices + literal_choices = None + if Literal in type_class_map and annotation: + # Extract all arguments from the annotation + args = get_args(annotation) + for arg in args: + if get_origin(arg) is Literal: + literal_choices = list(get_args(arg)) + break + if list in type_class_map: type_class = type_class_map[list] self.parser.add_argument( @@ -125,6 +156,15 @@ def add_argument( required=required, choices=[True, False], ) + elif Literal in type_class_map and literal_choices: + # Add argument with choices for Literal types + self.parser.add_argument( + f"--{arg_name}", + type=str, + required=required, + choices=literal_choices, + metavar=f"{{{','.join(literal_choices)}}}", + ) elif float in type_class_map: self.parser.add_argument( f"--{arg_name}", type=float, required=required, metavar=META_VAR_MAP[float] @@ -166,6 +206,10 @@ def build_model_arg_parser(self, model: type[BaseModel], required: bool) -> list if type(None) in type_class_map and len(attr_data.type_classes) == 1: continue - self.add_argument(type_class_map, attr.replace("_", "-"), required) + # Get the full annotation from the model field + field = model.model_fields.get(attr) + annotation = field.annotation if field else None + + self.add_argument(type_class_map, attr.replace("_", "-"), required, annotation) return list(type_map.keys()) diff --git a/nodescraper/plugins/inband/network/collector_args.py b/nodescraper/plugins/inband/network/collector_args.py index 1e1bcedf..70e84cf4 100644 --- a/nodescraper/plugins/inband/network/collector_args.py +++ b/nodescraper/plugins/inband/network/collector_args.py @@ -24,10 +24,11 @@ # ############################################################################### -from typing import Optional +from typing import Literal, Optional from nodescraper.models import CollectorArgs class NetworkCollectorArgs(CollectorArgs): url: Optional[str] = None + netprobe: Optional[Literal["ping", "wget", "curl"]] = None diff --git a/nodescraper/plugins/inband/network/network_collector.py b/nodescraper/plugins/inband/network/network_collector.py index 16c69dea..f7de2014 100644 --- a/nodescraper/plugins/inband/network/network_collector.py +++ b/nodescraper/plugins/inband/network/network_collector.py @@ -66,6 +66,8 @@ class NetworkCollector(InBandDataCollector[NetworkDataModel, NetworkCollectorArg CMD_NEIGHBOR = "ip neighbor show" CMD_ETHTOOL_TEMPLATE = "ethtool {interface}" CMD_PING = "ping" + CMD_WGET = "wget" + CMD_CURL = "curl" # LLDP commands CMD_LLDPCLI_NEIGHBOR = "lldpcli show neighbor" @@ -1671,21 +1673,32 @@ def _collect_pensando_nic_info( uncollected_commands, ) - def _check_network_connectivity(self, url: str) -> bool: - """Check network connectivity by pinging a URL. + def _check_network_connectivity(self, cmd: str, url: str) -> bool: + """Check network connectivity using specified command. Args: - url: URL or hostname to ping + cmd: Command to use for connectivity check (ping, wget, or curl) + url: URL or hostname to check Returns: bool: True if network is accessible, False otherwise """ + if cmd not in {"ping", "wget", "curl"}: + raise ValueError( + f"Invalid network probe command: '{cmd}'. " + f"Valid options are: 'ping', 'wget', 'curl'" + ) # Determine ping options based on OS ping_option = "-c 1" if self.system_info.os_family == OSFamily.LINUX else "-n 1" - # Run ping command - result = self._run_sut_cmd(f"{self.CMD_PING} {url} {ping_option}") + # Build command based on cmd parameter using class constants + if cmd == "ping": + result = self._run_sut_cmd(f"{self.CMD_PING} {url} {ping_option}") + elif cmd == "wget": + result = self._run_sut_cmd(f"{self.CMD_WGET} {url}") + else: # curl + result = self._run_sut_cmd(f"{self.CMD_CURL} {url}") if result.exit_code == 0: self._log_event( @@ -1697,7 +1710,7 @@ def _check_network_connectivity(self, url: str) -> bool: else: self._log_event( category=EventCategory.NETWORK, - description=f"{self.CMD_PING} to {url} failed!", + description=f"{cmd} to {url} failed!", data={"url": url, "not accessible": result.exit_code == 0}, priority=EventPriority.ERROR, ) @@ -1737,7 +1750,19 @@ def collect_data( # Check network connectivity if URL is provided if args and args.url: - network_accessible = self._check_network_connectivity(args.url) + cmd = args.netprobe if args.netprobe else "ping" + try: + network_accessible = self._check_network_connectivity(cmd, args.url) + except ValueError as e: + self._log_event( + category=EventCategory.NETWORK, + description=str(e), + data={"netprobe": cmd, "url": args.url}, + priority=EventPriority.ERROR, + console_log=True, + ) + # Set network_accessible to None since we couldn't check + network_accessible = None # Collect interface/address information res_addr = self._run_sut_cmd(self.CMD_ADDR) diff --git a/test/functional/fixtures/network_plugin_config.json b/test/functional/fixtures/network_plugin_config.json index 65a7f99e..b9357762 100644 --- a/test/functional/fixtures/network_plugin_config.json +++ b/test/functional/fixtures/network_plugin_config.json @@ -3,7 +3,8 @@ "plugins": { "NetworkPlugin": { "collector_args": { - "url": "mock.example.com" + "url": "mock.example.com", + "netprobe": "ping" }, "analysis_args": {} } diff --git a/test/functional/test_network_plugin.py b/test/functional/test_network_plugin.py index 94729385..5759ad3b 100644 --- a/test/functional/test_network_plugin.py +++ b/test/functional/test_network_plugin.py @@ -122,3 +122,135 @@ def test_network_plugin_with_url(run_cli_command, network_config_file, tmp_path) assert result.returncode in [0, 1, 2] output = result.stdout + result.stderr assert len(output) > 0 + + +def test_network_plugin_with_netprobe_ping(run_cli_command, tmp_path): + """Test NetworkPlugin with netprobe set to ping.""" + log_path = str(tmp_path / "logs_network_netprobe_ping") + result = run_cli_command( + [ + "--log-path", + log_path, + "run-plugins", + "NetworkPlugin", + "--url", + "google.com", + "--netprobe", + "ping", + ], + check=False, + ) + + assert result.returncode in [0, 1, 2] + output = result.stdout + result.stderr + assert len(output) > 0 + + +def test_network_plugin_with_netprobe_wget(run_cli_command, tmp_path): + """Test NetworkPlugin with netprobe set to wget.""" + log_path = str(tmp_path / "logs_network_netprobe_wget") + result = run_cli_command( + [ + "--log-path", + log_path, + "run-plugins", + "NetworkPlugin", + "--url", + "google.com", + "--netprobe", + "wget", + ], + check=False, + ) + + assert result.returncode in [0, 1, 2] + output = result.stdout + result.stderr + assert len(output) > 0 + + +def test_network_plugin_with_netprobe_curl(run_cli_command, tmp_path): + """Test NetworkPlugin with netprobe set to curl.""" + log_path = str(tmp_path / "logs_network_netprobe_curl") + result = run_cli_command( + [ + "--log-path", + log_path, + "run-plugins", + "NetworkPlugin", + "--url", + "google.com", + "--netprobe", + "curl", + ], + check=False, + ) + + assert result.returncode in [0, 1, 2] + output = result.stdout + result.stderr + assert len(output) > 0 + + +def test_network_plugin_with_invalid_netprobe(run_cli_command, tmp_path): + """Test NetworkPlugin with invalid netprobe value - should fail at CLI validation.""" + log_path = str(tmp_path / "logs_network_invalid_netprobe") + result = run_cli_command( + [ + "--log-path", + log_path, + "run-plugins", + "NetworkPlugin", + "--url", + "google.com", + "--netprobe", + "invalid", + ], + check=False, + ) + + # Should fail with exit code 2 (argparse error) + assert result.returncode == 2 + output = result.stdout + result.stderr + assert len(output) > 0 + assert "invalid choice" in output.lower() + assert "choose from" in output.lower() + + +def test_network_plugin_with_url_no_netprobe(run_cli_command, tmp_path): + """Test NetworkPlugin with URL but no netprobe - should default to ping.""" + log_path = str(tmp_path / "logs_network_url_default") + result = run_cli_command( + [ + "--log-path", + log_path, + "run-plugins", + "NetworkPlugin", + "--url", + "google.com", + ], + check=False, + ) + + assert result.returncode in [0, 1, 2] + output = result.stdout + result.stderr + assert len(output) > 0 + + +def test_network_plugin_with_netprobe_no_url(run_cli_command, tmp_path): + """Test NetworkPlugin with netprobe but no URL - should skip connectivity check.""" + log_path = str(tmp_path / "logs_network_netprobe_no_url") + result = run_cli_command( + [ + "--log-path", + log_path, + "run-plugins", + "NetworkPlugin", + "--netprobe", + "ping", + ], + check=False, + ) + + # Should succeed but skip connectivity check + assert result.returncode in [0, 1, 2] + output = result.stdout + result.stderr + assert len(output) > 0 From 9267683f6ba8660d92e50fcd75f4870c2ec0cc67 Mon Sep 17 00:00:00 2001 From: jaspals3123 Date: Tue, 3 Feb 2026 18:11:59 -0500 Subject: [PATCH 5/6] Update dynamicparserbuilder.py --- nodescraper/cli/dynamicparserbuilder.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/nodescraper/cli/dynamicparserbuilder.py b/nodescraper/cli/dynamicparserbuilder.py index 7c759a93..0616a4ac 100644 --- a/nodescraper/cli/dynamicparserbuilder.py +++ b/nodescraper/cli/dynamicparserbuilder.py @@ -109,9 +109,6 @@ def get_literal_choices(cls, type_class_map: dict) -> Optional[list]: # Check if Literal is in the type_class_map literal_type = type_class_map.get(Literal) if literal_type and literal_type.inner_type is not None: - # The inner_type contains the first literal value, but we need all of them - # We need to get the original annotation to extract all literal values - # For now, return None and we'll handle this differently return None return None From 6bf340e034ce1d19540f95f30ad6f699dd81d914 Mon Sep 17 00:00:00 2001 From: jaspals Date: Mon, 9 Feb 2026 09:56:55 -0600 Subject: [PATCH 6/6] logs fix --- nodescraper/plugins/inband/network/network_collector.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nodescraper/plugins/inband/network/network_collector.py b/nodescraper/plugins/inband/network/network_collector.py index f7de2014..4a87936a 100644 --- a/nodescraper/plugins/inband/network/network_collector.py +++ b/nodescraper/plugins/inband/network/network_collector.py @@ -1703,9 +1703,10 @@ def _check_network_connectivity(self, cmd: str, url: str) -> bool: if result.exit_code == 0: self._log_event( category=EventCategory.NETWORK, - description="System networking is up", - data={"url": url, "accessible": result.exit_code == 0}, + description=f"Network connectivity check successful: {cmd} to {url} succeeded", + data={"url": url, "command": cmd, "accessible": True}, priority=EventPriority.INFO, + console_log=True, ) else: self._log_event( @@ -1713,6 +1714,7 @@ def _check_network_connectivity(self, cmd: str, url: str) -> bool: description=f"{cmd} to {url} failed!", data={"url": url, "not accessible": result.exit_code == 0}, priority=EventPriority.ERROR, + console_log=True, ) return result.exit_code == 0