diff --git a/nodescraper/connection/inband/inbandmanager.py b/nodescraper/connection/inband/inbandmanager.py index c1c6bea5..28e65d63 100644 --- a/nodescraper/connection/inband/inbandmanager.py +++ b/nodescraper/connection/inband/inbandmanager.py @@ -32,7 +32,6 @@ EventCategory, EventPriority, ExecutionStatus, - OSFamily, SystemLocation, ) from nodescraper.interfaces.connectionmanager import ConnectionManager @@ -43,7 +42,6 @@ from .inband import InBandConnection from .inbandlocal import LocalShell from .inbandremote import RemoteShell, SSHConnectionError -from .osdetection import NetworkOsDetection, detect_network_os from .sshparams import SSHConnectionParams @@ -69,52 +67,6 @@ def __init__( **kwargs, ) - @staticmethod - def _apply_network_os_detection( - system_info: SystemInfo, - detection: NetworkOsDetection, - ) -> None: - """Apply network OS probe results to system info.""" - system_info.os_family = detection.os_family - system_info.platform = detection.platform - if system_info.metadata is None: - system_info.metadata = {} - system_info.metadata.update(detection.metadata) - - def _check_os_family(self): - """Check the OS family of the system under test (SUT) - - Raises: - RuntimeError: If the connection is not initialized - """ - if not self.connection: - raise RuntimeError("Connection not initialized") - - self.logger.info("Checking OS family") - res = self.connection.run_command("uname -s") - if "not recognized as an internal or external command" in res.stdout + res.stderr: - self.system_info.os_family = OSFamily.WINDOWS - elif res.exit_code == 0: - self.system_info.os_family = OSFamily.LINUX - else: - detection = detect_network_os(self.connection) - if detection is not None: - self._apply_network_os_detection(self.system_info, detection) - else: - self._log_event( - category=EventCategory.UNKNOWN, - description="Unable to determine SUT OS", - priority=EventPriority.WARNING, - ) - if self.system_info.platform: - self.logger.info( - "OS Family: %s (%s)", - self.system_info.os_family.name, - self.system_info.platform, - ) - else: - self.logger.info("OS Family: %s", self.system_info.os_family.name) - def connect( self, ) -> TaskResult: @@ -126,7 +78,6 @@ def connect( if self.system_info.location == SystemLocation.LOCAL: self.logger.info("Using local shell") self.connection = LocalShell() - self._check_os_family() return self.result if not self.connection_args or not isinstance(self.connection_args, SSHConnectionParams): @@ -150,7 +101,6 @@ def connect( ) self.connection = RemoteShell(self.connection_args) self.connection.connect_ssh() - self._check_os_family() except SSHConnectionError as exception: self._log_event( category=EventCategory.SSH, diff --git a/nodescraper/connection/inband/osdetection.py b/nodescraper/connection/inband/osdetection.py index 9353439d..84fbb66c 100644 --- a/nodescraper/connection/inband/osdetection.py +++ b/nodescraper/connection/inband/osdetection.py @@ -24,11 +24,14 @@ # ############################################################################### import json +import logging import re from dataclasses import dataclass from typing import Optional +from nodescraper.connection.inband import InBandConnectionManager from nodescraper.enums import OSFamily +from nodescraper.models import SystemInfo from .inband import InBandConnection @@ -150,3 +153,55 @@ def detect_network_os(connection: InBandConnection) -> Optional[NetworkOsDetecti return detection return None + + +def apply_network_os_detection( + system_info: SystemInfo, + detection: NetworkOsDetection, +) -> None: + """Apply network OS probe results to system info.""" + system_info.os_family = detection.os_family + system_info.platform = detection.platform + if system_info.metadata is None: + system_info.metadata = {} + system_info.metadata.update(detection.metadata) + + +def discover_and_write_os_family( + connection_manager: InBandConnectionManager, + system_info: SystemInfo, + logger: logging.Logger, +) -> None: + """Check + + Args: + connection_manager (InBandConnectionManager): _description_ + system_info (SystemInfo): _description_ + logger (logging.Logger): _description_ + """ + if connection_manager.connection is None: + logger.error("Connection is not initialized, OS family check cannot be performed.") + return + logger.info("Checking OS family") + res = connection_manager.connection.run_command("uname -s") + if "not recognized as an internal or external command" in res.stdout + res.stderr: + system_info.os_family = OSFamily.WINDOWS + elif res.exit_code == 0: + system_info.os_family = OSFamily.LINUX + else: + detection = detect_network_os(connection_manager.connection) + if detection is not None: + apply_network_os_detection(system_info, detection) + else: + logger.warning( + "Unable to determine OS family. uname failed and no supported network OS detected." + ) + + if system_info.platform: + logger.info( + "OS Family: %s (%s)", + system_info.os_family.name, + system_info.platform, + ) + else: + logger.info("OS Family: %s", system_info.os_family.name) diff --git a/nodescraper/pluginexecutor.py b/nodescraper/pluginexecutor.py index 772f3662..d868c62c 100644 --- a/nodescraper/pluginexecutor.py +++ b/nodescraper/pluginexecutor.py @@ -36,8 +36,11 @@ from pydantic import BaseModel from nodescraper.base.oobsshdataplugin import OOBSSHDataPlugin +from nodescraper.connection.inband import InBandConnectionManager +from nodescraper.connection.inband.osdetection import discover_and_write_os_family from nodescraper.connection.oob_ssh import OobSshConnectionManager from nodescraper.constants import DEFAULT_LOGGER +from nodescraper.enums import ExecutionStatus from nodescraper.interfaces import ConnectionManager, DataPlugin, PluginInterface from nodescraper.interfaces.taskresulthook import TaskResultHook from nodescraper.models import PluginConfig, SystemInfo @@ -104,7 +107,8 @@ def __init__( for connection, connection_args in connections.items(): if connection not in self.plugin_registry.connection_managers: self.logger.error( - "Unable to find registered connection manager class for %s", connection + "Unable to find registered connection manager class for %s", + connection, ) continue @@ -161,6 +165,20 @@ def merge_configs(plugin_configs: list[PluginConfig]) -> PluginConfig: return merged_config + def discover_os_info(self) -> None: + """If the connection library has an InBandConnectionManager, use it to discover OS info and update system_info + self.system_info will be updated with the discovered OS info. + """ + inband_connection = self.connection_library.get(InBandConnectionManager) + result = inband_connection.connect() if inband_connection else None + if (not inband_connection) or (not result) or (result.status != ExecutionStatus.OK): + self.logger.info( + "InBandConnectionManager not available or failed to connect for OS discovery. Skipping OS discovery." + ) + return + discover_and_write_os_family(inband_connection, self.system_info, self.logger) + inband_connection.disconnect() + def run_queue(self) -> list[PluginResult]: """Run the plugin queue and return results @@ -168,6 +186,8 @@ def run_queue(self) -> list[PluginResult]: list[PluginResult]: List of results from running the plugins in the queue """ plugin_results = [] + # For Plugins discover OS Family + self.discover_os_info() plugin_queue = deque(self.plugin_config.plugins.items()) try: while len(plugin_queue) > 0: @@ -276,7 +296,9 @@ def run_queue(self) -> list[PluginResult]: hook(plugin_result) except Exception as e: self.logger.exception( - "Unexpected exception when running plugin %s: %s", plugin_name, e + "Unexpected exception when running plugin %s: %s", + plugin_name, + e, ) except Exception as e: self.logger.exception("Unexpected exception running plugin queue: %s", str(e)) @@ -285,11 +307,15 @@ def run_queue(self) -> list[PluginResult]: if self.plugin_config.result_collators: self.logger.info("Running result collators") - for collator, collator_args in self.plugin_config.result_collators.items(): + for ( + collator, + collator_args, + ) in self.plugin_config.result_collators.items(): collator_class = self.plugin_registry.result_collators.get(collator) if collator_class is None: self.logger.warning( - "No result collator found in registry for name: %s", collator + "No result collator found in registry for name: %s", + collator, ) continue diff --git a/nodescraper/pluginregistry.py b/nodescraper/pluginregistry.py index cca5bbf2..537f470c 100644 --- a/nodescraper/pluginregistry.py +++ b/nodescraper/pluginregistry.py @@ -274,10 +274,8 @@ def _load_plugins_uncached() -> dict[str, type]: """Internal: Load plugins without caching logic.""" plugins = {} eps: Iterable = PluginRegistry.load_entry_points(ENTRY_POINT_PLUGINS) - for entry_point in eps: plugin_class = entry_point.load() # type: ignore[attr-defined, union-attr] - if not PluginRegistry._valid_sub_class_check( in_cls=plugin_class, base_class=PluginInterface ): diff --git a/pyproject.toml b/pyproject.toml index d2f1bdef..d9721f5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,3 +91,20 @@ explicit_package_bases = true [tool.setuptools_scm] version_scheme = "post-release" + +[dependency-groups] +dev = [ + "build", + "black", + "pylint", + "coverage", + "twine", + "ruff", + "pre-commit", + "pytest", + "pytest-cov", + "mypy", + "types-paramiko", + "types-requests", + "types-setuptools", +]