Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 4 additions & 56 deletions nodescraper/connection/inband/inbandmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,12 @@
from __future__ import annotations

from logging import Logger
from typing import Optional, Union
from typing import Any, Optional, Union

from nodescraper.enums import (
EventCategory,
EventPriority,
ExecutionStatus,
OSFamily,
SystemLocation,
)
from nodescraper.interfaces.connectionmanager import ConnectionManager
Expand All @@ -43,20 +42,18 @@
from .inband import InBandConnection
from .inbandlocal import LocalShell
from .inbandremote import RemoteShell, SSHConnectionError
from .osdetection import NetworkOsDetection, detect_network_os
from .sshparams import SSHConnectionParams


class InBandConnectionManager(ConnectionManager[InBandConnection, SSHConnectionParams]):

def __init__(
self,
system_info: SystemInfo,
logger: Optional[Logger] = None,
max_event_priority_level: Union[EventPriority, str] = EventPriority.CRITICAL,
parent: Optional[str] = None,
task_result_hooks: Optional[list[TaskResultHook]] = None,
connection_args: Optional[SSHConnectionParams] = None,
connection_args: Optional[SSHConnectionParams | dict[str, Any]] = None,
**kwargs,
):
super().__init__(
Expand All @@ -69,54 +66,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 and "VMkernel" in res.stdout:
self.system_info.os_family = OSFamily.ESXI
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:
Expand All @@ -128,7 +77,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):
Expand All @@ -148,11 +96,11 @@ def connect(

try:
self.logger.info(
"Initializing SSH connection to system '%s'", self.connection_args.hostname
"Initializing SSH connection to system '%s'",
self.connection_args.hostname,
)
self.connection = RemoteShell(self.connection_args)
self.connection.connect_ssh()
self._check_os_family()
except SSHConnectionError as exception:
self._log_event(
category=EventCategory.SSH,
Expand Down
55 changes: 55 additions & 0 deletions nodescraper/connection/inband/osdetection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
4 changes: 2 additions & 2 deletions nodescraper/interfaces/connectionmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import logging
import types
from functools import wraps
from typing import Callable, Generic, Optional, TypeVar, Union
from typing import Any, Callable, Generic, Optional, TypeVar, Union

from pydantic import BaseModel

Expand Down Expand Up @@ -96,7 +96,7 @@ def __init__(
max_event_priority_level: Union[EventPriority, str] = EventPriority.CRITICAL,
parent: Optional[str] = None,
task_result_hooks: Optional[list[TaskResultHook], None] = None,
connection_args: Optional[Union[TConnectArg, dict]] = None,
connection_args: Optional[Union[TConnectArg, dict[str, Any]]] = None,
event_reporter: str = DEFAULT_EVENT_REPORTER,
session_id: Optional[str] = None,
**kwargs,
Expand Down
2 changes: 1 addition & 1 deletion nodescraper/interfaces/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def __init__(

if system_info is None:
system_info = SystemInfo()
self.system_info = system_info
self.system_info: SystemInfo = system_info

if not task_result_hooks:
task_result_hooks = []
Expand Down
94 changes: 74 additions & 20 deletions nodescraper/pluginexecutor.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,16 @@
import uuid
from collections import deque
from collections.abc import Callable, Sequence
from typing import Optional, Type, Union
from typing import Any, Optional, Type, Union

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, OSFamily
from nodescraper.helpers.plugin_execution_target import (
format_in_band_target_summary,
)
Expand Down Expand Up @@ -103,23 +106,7 @@ def __init__(
if log_path:
self.connection_result_hooks.append(FileSystemLogHook(log_base_path=log_path))

if connections:
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
)
continue

connection_manager = self.plugin_registry.connection_managers[connection]

self.connection_library[connection_manager] = connection_manager(
system_info=self.system_info,
logger=self.logger,
connection_args=connection_args,
task_result_hooks=self.connection_result_hooks,
session_id=self.session_id,
)
self._populate_connection_library(connections)

self.logger.info("System Name: %s", self.system_info.name)
if self.system_info.sku:
Expand All @@ -131,6 +118,35 @@ def __init__(
format_in_band_target_summary(self.system_info, self.connection_configs),
)

def _populate_connection_library(
self, connections: dict[str, dict[str, Any] | BaseModel] | None
) -> None:
"""Init the connection library with the provided connections.

Args:
connections (dict[str, dict[str, Any]]): A dictionary mapping connection names to their arguments.
where the first level of the dict is always a name of the connection class and then its arguments.
"""
if connections is None:
return
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,
)
continue

connection_manager = self.plugin_registry.connection_managers[connection]

self.connection_library[connection_manager] = connection_manager(
system_info=self.system_info,
logger=self.logger,
connection_args=connection_args,
task_result_hooks=self.connection_result_hooks,
session_id=self.session_id,
)

@staticmethod
def _deep_merge_plugin_args(existing: dict, incoming: dict) -> dict:
"""Merge incoming plugin args into existing; do not let empty dicts overwrite
Expand Down Expand Up @@ -168,6 +184,37 @@ 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)
if inband_connection is None:
# Init use and discard after
inband_connection = InBandConnectionManager(
system_info=self.system_info,
logger=self.logger,
connection_args=self.connection_configs.get(InBandConnectionManager.__name__),
task_result_hooks=self.connection_result_hooks,
session_id=self.session_id,
)
try:
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)
except Exception as e:
self.logger.error(
"Error occurred during OS discovery with InBandConnectionManager: %s",
str(e),
)
finally:
if inband_connection is not None:
inband_connection.disconnect()

def _get_connection_manager_for_plugin(
self,
plugin_class: type,
Expand Down Expand Up @@ -313,6 +360,9 @@ 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
if self.system_info.os_family is None or self.system_info.os_family == OSFamily.UNKNOWN:
self.discover_os_info()
plugin_queue = deque(self.plugin_config.plugins.items())
try:
while len(plugin_queue) > 0:
Expand All @@ -334,11 +384,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

Expand Down
2 changes: 0 additions & 2 deletions nodescraper/pluginregistry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
):
Expand Down
17 changes: 17 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Loading
Loading