From 09449a20a0dce1ed87fa284034dc0679ef730229 Mon Sep 17 00:00:00 2001 From: Alexandra Bara Date: Mon, 8 Jun 2026 13:49:10 -0500 Subject: [PATCH 1/4] adding ability to specify multiple collectors --- nodescraper/interfaces/dataplugin.py | 252 +++++++++++++++++++------ nodescraper/pluginexecutor.py | 56 ++++-- nodescraper/pluginrecipe/discovery.py | 12 +- test/unit/framework/test_dataplugin.py | 107 ++++++++++- 4 files changed, 352 insertions(+), 75 deletions(-) diff --git a/nodescraper/interfaces/dataplugin.py b/nodescraper/interfaces/dataplugin.py index 8448dff3..f065f93b 100644 --- a/nodescraper/interfaces/dataplugin.py +++ b/nodescraper/interfaces/dataplugin.py @@ -39,6 +39,7 @@ from nodescraper.interfaces.plugin import PluginInterface from nodescraper.models import ( AnalyzerArgs, + CollectorArgs, DataModel, DataPluginResult, PluginResult, @@ -51,6 +52,17 @@ from .task import SystemCompatibilityError from .taskresulthook import TaskResultHook +CollectorClasses = Union[ + Type[DataCollector], + tuple[Type[DataCollector], ...], + list[Type[DataCollector]], +] + +CollectorArgsClasses = Union[ + Type[CollectorArgs], + dict[str, Type[CollectorArgs]], +] + class DataPlugin( PluginInterface, Generic[TConnectionManager, TConnectArg, TDataModel, TCollectArg, TAnalyzeArg] @@ -61,7 +73,9 @@ class DataPlugin( CONNECTION_TYPE: Optional[Type[TConnectionManager]] - COLLECTOR: Optional[Type[DataCollector]] = None + COLLECTOR: Optional[CollectorClasses] = None + + COLLECTOR_ARGS: Optional[CollectorArgsClasses] = None ANALYZER: Optional[Type[DataAnalyzer]] = None @@ -101,6 +115,43 @@ def __init__( ) self._data: Optional[TDataModel] = None + @classmethod + def get_collector_classes(cls) -> tuple[Type[DataCollector], ...]: + """Return all collector classes configured on this plugin.""" + collector = cls.COLLECTOR + if collector is None: + return () + if isinstance(collector, (tuple, list)): + return tuple(collector) + return (collector,) + + @classmethod + def _collector_args_class( + cls, collector_cls: Type[DataCollector] + ) -> Optional[Type[CollectorArgs]]: + collector_args = cls.COLLECTOR_ARGS + if isinstance(collector_args, dict): + return collector_args.get(collector_cls.__name__) + return collector_args + + @classmethod + def _validate_collector_args(cls) -> None: + collector_args = cls.COLLECTOR_ARGS + if collector_args is None: + return + if isinstance(collector_args, dict): + for collector_name, args_cls in collector_args.items(): + if not isinstance(args_cls, type) or not issubclass(args_cls, CollectorArgs): + raise TypeError( + f"COLLECTOR_ARGS[{collector_name!r}] must be a CollectorArgs subclass, " + f"got {args_cls!r}" + ) + return + if not isinstance(collector_args, type) or not issubclass(collector_args, CollectorArgs): + raise TypeError( + f"COLLECTOR_ARGS must be a CollectorArgs subclass or dict, got {collector_args!r}" + ) + @classmethod def _validate_class_var(cls): if not hasattr(cls, "DATA_MODEL"): @@ -109,12 +160,96 @@ def _validate_class_var(cls): if cls.DATA_MODEL is None: raise TypeError("DATA_MODEL class variable not defined") - if not cls.COLLECTOR and not cls.ANALYZER: + if not cls.get_collector_classes() and not cls.ANALYZER: raise TypeError("No collector or analyzer task defined") - if cls.COLLECTOR and not cls.CONNECTION_TYPE: + if cls.get_collector_classes() and not cls.CONNECTION_TYPE: raise TypeError("CONNECTION_TYPE must be defined for collector") + for collector_cls in cls.get_collector_classes(): + if not isinstance(collector_cls, type) or not issubclass(collector_cls, DataCollector): + raise TypeError( + f"COLLECTOR entries must be DataCollector subclasses, got {collector_cls!r}" + ) + + cls._validate_collector_args() + + @classmethod + def _merge_collected_data( + cls, + existing: Optional[TDataModel], + new_data: Optional[TDataModel], + ) -> Optional[TDataModel]: + if new_data is None: + return existing + if existing is None: + return new_data + if not isinstance(new_data, existing.__class__): + raise TypeError( + f"Collector returned {new_data.__class__.__name__}, " + f"expected {existing.__class__.__name__}" + ) + merged = { + **existing.model_dump(exclude_unset=True), + **new_data.model_dump(exclude_unset=True), + } + return existing.__class__.model_validate(merged) + + @classmethod + def _aggregate_collection_results( + cls, + plugin_name: str, + results: list[TaskResult], + ) -> TaskResult: + if not results: + return TaskResult( + parent=plugin_name, + status=ExecutionStatus.NOT_RAN, + message=f"Data collection not ran for {plugin_name}", + ) + if len(results) == 1: + return results[0] + + aggregated = TaskResult( + parent=plugin_name, + status=max(result.status for result in results), + task=",".join(result.task for result in results if result.task), + ) + messages = [result.message for result in results if result.message] + if messages: + aggregated.message = "; ".join(messages) + for result in results: + aggregated.artifacts.extend(result.artifacts) + aggregated.events.extend(result.events) + aggregated.details["collector_results"] = [ + result.model_dump(exclude={"artifacts", "events"}) for result in results + ] + return aggregated + + def _resolve_collector_args( + self, + collector_cls: Type[DataCollector], + collection_args: Optional[Union[TCollectArg, dict]], + ) -> Optional[Union[TCollectArg, dict]]: + if collection_args is None: + return None + + collector_name = collector_cls.__name__ + collector_names = {cls.__name__ for cls in self.get_collector_classes()} + raw_args: Optional[Union[TCollectArg, dict]] = collection_args + + if isinstance(collection_args, dict) and collector_names.intersection( + collection_args.keys() + ): + raw_args = collection_args.get(collector_name) + if raw_args is None: + return None + + args_cls = self._collector_args_class(collector_cls) + if args_cls is not None and isinstance(raw_args, dict): + return args_cls.model_validate(raw_args) + return raw_args + @classmethod def is_valid(cls) -> bool: """Check that all required class variables are set @@ -167,7 +302,8 @@ def collect( Returns: TaskResult: task result for data collection """ - if not self.COLLECTOR: + collector_classes = self.get_collector_classes() + if not collector_classes: self.collection_result = TaskResult( parent=self.__class__.__name__, status=ExecutionStatus.NOT_RAN, @@ -175,11 +311,13 @@ def collect( ) return self.collection_result + primary_collector = collector_classes[0] + try: if not self.connection_manager: if not self.CONNECTION_TYPE: self.collection_result = TaskResult( - task=self.COLLECTOR.__name__, + task=primary_collector.__name__, parent=self.__class__.__name__, status=ExecutionStatus.NOT_RAN, message=f"No connection manager type provided for {self.__class__.__name__}", @@ -203,49 +341,53 @@ def collect( if self.connection_manager.result.status != ExecutionStatus.OK: self.collection_result = TaskResult( - task=self.COLLECTOR.__name__, + task=primary_collector.__name__, parent=self.__class__.__name__, status=ExecutionStatus.NOT_RAN, message="Connection not available, data collection skipped", ) else: - if ( - collection_args is not None - and isinstance(collection_args, dict) - and hasattr(self, "COLLECTOR_ARGS") - and self.COLLECTOR_ARGS is not None - ): - collection_args = self.COLLECTOR_ARGS.model_validate(collection_args) - - collection_task = self.COLLECTOR( - system_info=self.system_info, - logger=self.logger, - system_interaction_level=system_interaction_level, - connection=self.connection_manager.connection, - max_event_priority_level=max_event_priority_level, - parent=self.__class__.__name__, - task_result_hooks=self.task_result_hooks, - log_path=self.log_path, - event_reporter=self.event_reporter, - session_id=self.session_id, + collector_results: list[TaskResult] = [] + merged_data: Optional[TDataModel] = None + + for collector_cls in collector_classes: + collector_args = self._resolve_collector_args(collector_cls, collection_args) + collection_task = collector_cls( + system_info=self.system_info, + logger=self.logger, + system_interaction_level=system_interaction_level, + connection=self.connection_manager.connection, + max_event_priority_level=max_event_priority_level, + parent=self.__class__.__name__, + task_result_hooks=self.task_result_hooks, + log_path=self.log_path, + event_reporter=self.event_reporter, + session_id=self.session_id, + ) + result, data = collection_task.collect_data(collector_args) + collector_results.append(result) + merged_data = self._merge_collected_data(merged_data, data) + + self.collection_result = self._aggregate_collection_results( + self.__class__.__name__, + collector_results, ) - self.collection_result, self._data = collection_task.collect_data(collection_args) + self._data = merged_data except SystemCompatibilityError as e: self.collection_result = TaskResult( - task=self.COLLECTOR.__name__, + task=primary_collector.__name__, parent=self.__class__.__name__, status=ExecutionStatus.NOT_RAN, message=str(e), ) except Exception as e: self.logger.exception( - "Unhandled exception running collector %s for plugin %s", - self.COLLECTOR.__name__, + "Unhandled exception running collectors for plugin %s", self.__class__.__name__, ) self.collection_result = TaskResult( - task=self.COLLECTOR.__name__, + task=primary_collector.__name__, parent=self.__class__.__name__, status=ExecutionStatus.EXECUTION_FAILURE, message=f"Unhandled exception running data collector: {str(e)}", @@ -422,33 +564,33 @@ def find_datamodel_path_in_run(cls, run_path: str) -> Optional[str]: run_path = os.path.abspath(run_path) if not os.path.isdir(run_path): return None - collector_cls = getattr(cls, "COLLECTOR", None) data_model_cls = getattr(cls, "DATA_MODEL", None) - if not collector_cls or not data_model_cls: - return None - collector_dir = os.path.join( - run_path, - pascal_to_snake(cls.__name__), - pascal_to_snake(collector_cls.__name__), - ) - if not os.path.isdir(collector_dir): - return None - result_path = os.path.join(collector_dir, "result.json") - if not os.path.isfile(result_path): - return None - try: - res_payload = json.loads(Path(result_path).read_text(encoding="utf-8")) - if res_payload.get("parent") != cls.__name__: - return None - except (json.JSONDecodeError, OSError): + if not data_model_cls: return None - want_json = data_model_cls.__name__.lower() + ".json" - for fname in os.listdir(collector_dir): - low = fname.lower() - if low.endswith("datamodel.json") or low == want_json: - return os.path.join(collector_dir, fname) - if low.endswith(".log"): - return os.path.join(collector_dir, fname) + for collector_cls in cls.get_collector_classes(): + collector_dir = os.path.join( + run_path, + pascal_to_snake(cls.__name__), + pascal_to_snake(collector_cls.__name__), + ) + if not os.path.isdir(collector_dir): + continue + result_path = os.path.join(collector_dir, "result.json") + if not os.path.isfile(result_path): + continue + try: + res_payload = json.loads(Path(result_path).read_text(encoding="utf-8")) + if res_payload.get("parent") != cls.__name__: + continue + except (json.JSONDecodeError, OSError): + continue + want_json = data_model_cls.__name__.lower() + ".json" + for fname in os.listdir(collector_dir): + low = fname.lower() + if low.endswith("datamodel.json") or low == want_json: + return os.path.join(collector_dir, fname) + if low.endswith(".log"): + return os.path.join(collector_dir, fname) return None @classmethod diff --git a/nodescraper/pluginexecutor.py b/nodescraper/pluginexecutor.py index 0821ff20..92a8d770 100644 --- a/nodescraper/pluginexecutor.py +++ b/nodescraper/pluginexecutor.py @@ -295,31 +295,51 @@ def apply_global_args_to_plugin( run_args = {} for key in global_args: - if key in ["collection_args", "analysis_args"] and isinstance(plugin_inst, DataPlugin): + if key in ("collection_args", "analysis_args"): continue - else: - run_args[key] = global_args[key] - - if ( - "collection_args" in global_args - and hasattr(plugin_class, "COLLECTOR_ARGS") - and plugin_class.COLLECTOR_ARGS is not None - ): - - plugin_fields = set(plugin_class.COLLECTOR_ARGS.model_fields.keys()) - filtered = { - k: v for k, v in global_args["collection_args"].items() if k in plugin_fields - } - if filtered: - run_args["collection_args"] = filtered + run_args[key] = global_args[key] + + if "collection_args" in global_args and hasattr(plugin_class, "COLLECTOR_ARGS"): + collector_args = plugin_class.COLLECTOR_ARGS + if ( + isinstance(plugin_inst, DataPlugin) + and plugin_class.get_collector_classes() + and isinstance(collector_args, dict) + ): + per_collector_args: dict[str, dict] = {} + for collector_cls in plugin_class.get_collector_classes(): + args_cls = plugin_class._collector_args_class(collector_cls) + if args_cls is None: + continue + plugin_fields = set(args_cls.model_fields.keys()) + filtered = { + k: v + for k, v in global_args["collection_args"].items() + if k in plugin_fields + } + if filtered: + per_collector_args[collector_cls.__name__] = filtered + if per_collector_args: + run_args["collection_args"] = per_collector_args + elif collector_args is not None and not isinstance(collector_args, dict): + args_cls = ( + collector_args if isinstance(collector_args, type) else type(collector_args) + ) + plugin_fields = set(args_cls.model_fields.keys()) + filtered = { + k: v for k, v in global_args["collection_args"].items() if k in plugin_fields + } + if filtered: + run_args["collection_args"] = filtered if ( "analysis_args" in global_args and hasattr(plugin_class, "ANALYZER_ARGS") and plugin_class.ANALYZER_ARGS is not None ): - - plugin_fields = set(plugin_class.ANALYZER_ARGS.model_fields.keys()) + analyzer_args = plugin_class.ANALYZER_ARGS + args_cls = analyzer_args if isinstance(analyzer_args, type) else type(analyzer_args) + plugin_fields = set(args_cls.model_fields.keys()) filtered = {k: v for k, v in global_args["analysis_args"].items() if k in plugin_fields} if filtered: run_args["analysis_args"] = filtered diff --git a/nodescraper/pluginrecipe/discovery.py b/nodescraper/pluginrecipe/discovery.py index aebeea3c..be0eaa03 100644 --- a/nodescraper/pluginrecipe/discovery.py +++ b/nodescraper/pluginrecipe/discovery.py @@ -58,7 +58,17 @@ def plugin_has_collector(plugin_name: str) -> bool: bool: ``True`` when the plugin class defines ``COLLECTOR``. """ plugin_class = load_plugin_class(plugin_name) - return plugin_class is not None and getattr(plugin_class, "COLLECTOR", None) is not None + if plugin_class is None: + return False + collectors = getattr(plugin_class, "get_collector_classes", None) + if callable(collectors): + return bool(collectors()) + collector = getattr(plugin_class, "COLLECTOR", None) + if collector is None: + return False + if isinstance(collector, (tuple, list)): + return len(collector) > 0 + return True def plugin_has_analyzer(plugin_name: str) -> bool: diff --git a/test/unit/framework/test_dataplugin.py b/test/unit/framework/test_dataplugin.py index e88f8cc5..eaea6351 100644 --- a/test/unit/framework/test_dataplugin.py +++ b/test/unit/framework/test_dataplugin.py @@ -25,6 +25,7 @@ ############################################################################### import json from pathlib import Path +from typing import Optional from unittest.mock import MagicMock, patch import pytest @@ -34,7 +35,7 @@ from nodescraper.interfaces.dataanalyzertask import DataAnalyzer from nodescraper.interfaces.datacollectortask import DataCollector from nodescraper.interfaces.dataplugin import DataPlugin -from nodescraper.models import DataModel, TaskResult +from nodescraper.models import CollectorArgs, DataModel, TaskResult class StandardDataModel(DataModel): @@ -543,3 +544,107 @@ def test_load_run_data_direct_file(self, tmp_path: Path) -> None: loaded = ExtractPlugin.load_run_data(str(p)) assert loaded is not None assert loaded["value"] == "direct" + + +class MultiPartDataModel(DataModel): + alpha: Optional[str] = None + beta: Optional[str] = None + + +class AlphaCollector(DataCollector): + DATA_MODEL = MultiPartDataModel + + def collect_data(self, args=None): + return TaskResult(status=ExecutionStatus.OK, task="AlphaCollector"), MultiPartDataModel( + alpha="alpha-value" + ) + + +class BetaCollector(DataCollector): + DATA_MODEL = MultiPartDataModel + + def collect_data(self, args=None): + return TaskResult(status=ExecutionStatus.OK, task="BetaCollector"), MultiPartDataModel( + beta="beta-value" + ) + + +class AlphaCollectorArgs(CollectorArgs): + alpha_path: str = "/alpha" + + +class BetaCollectorArgs(CollectorArgs): + beta_path: str = "/beta" + + +class MultiCollectorPlugin(DataPlugin): + DATA_MODEL = MultiPartDataModel + CONNECTION_TYPE = MockConnectionManager + COLLECTOR = (AlphaCollector, BetaCollector) + ANALYZER = StandardAnalyzer + + +class TestMultiCollectorDataPlugin: + def test_get_collector_classes_accepts_collector_tuple(self): + assert MultiCollectorPlugin.get_collector_classes() == (AlphaCollector, BetaCollector) + + def test_get_collector_classes_accepts_single_collector(self): + assert CoreDataPlugin.get_collector_classes() == (BaseDataCollector,) + + def test_collector_args_class_accepts_args_map(self): + class MappedArgsPlugin(DataPlugin): + DATA_MODEL = MultiPartDataModel + CONNECTION_TYPE = MockConnectionManager + COLLECTOR = (AlphaCollector, BetaCollector) + COLLECTOR_ARGS = { + "AlphaCollector": AlphaCollectorArgs, + "BetaCollector": BetaCollectorArgs, + } + ANALYZER = StandardAnalyzer + + assert MappedArgsPlugin._collector_args_class(AlphaCollector) is AlphaCollectorArgs + assert MappedArgsPlugin._collector_args_class(BetaCollector) is BetaCollectorArgs + + def test_collect_runs_all_collectors_and_merges_data(self, plugin_with_conn): + multi_plugin = MultiCollectorPlugin( + system_info=plugin_with_conn.system_info, + logger=plugin_with_conn.logger, + connection_manager=plugin_with_conn.connection_manager, + ) + + with ( + patch.object(AlphaCollector, "collect_data") as alpha_collect, + patch.object(BetaCollector, "collect_data") as beta_collect, + ): + alpha_collect.return_value = ( + TaskResult(status=ExecutionStatus.OK, task="AlphaCollector"), + MultiPartDataModel(alpha="alpha-value"), + ) + beta_collect.return_value = ( + TaskResult(status=ExecutionStatus.OK, task="BetaCollector"), + MultiPartDataModel(beta="beta-value"), + ) + + result = multi_plugin.collect() + + alpha_collect.assert_called_once() + beta_collect.assert_called_once() + assert result.status == ExecutionStatus.OK + assert multi_plugin.data.alpha == "alpha-value" + assert multi_plugin.data.beta == "beta-value" + assert "AlphaCollector" in result.task + assert "BetaCollector" in result.task + + def test_find_datamodel_path_in_run_checks_all_collectors(self, tmp_path: Path) -> None: + beta_dir = tmp_path / "multi_collector_plugin" / "beta_collector" + beta_dir.mkdir(parents=True) + (beta_dir / "result.json").write_text( + json.dumps({"parent": "MultiCollectorPlugin"}), encoding="utf-8" + ) + (beta_dir / "multipartdatamodel.json").write_text( + json.dumps({"alpha": "a", "beta": "b"}), encoding="utf-8" + ) + + found = MultiCollectorPlugin.find_datamodel_path_in_run(str(tmp_path)) + assert found is not None + assert found.endswith("multipartdatamodel.json") From 87acdb2a982ed763b01c20aa1c4c3a8dab4513be Mon Sep 17 00:00:00 2001 From: Alexandra Bara Date: Mon, 8 Jun 2026 14:48:12 -0500 Subject: [PATCH 2/4] new oobdata plugin with ssh --- .../inband/hsio/ifoe_plugin/__init__.py | 28 ++++++ .../inband/hsio/ifoe_plugin/ifoe_data.py | 39 ++++++++ .../hsio/ifoe_plugin/ifoe_driver_collector.py | 85 ++++++++++++++++ .../inband/hsio/ifoe_plugin/ifoe_plugin.py | 37 +++++++ .../unit/plugin/test_ifoe_driver_collector.py | 96 +++++++++++++++++++ 5 files changed, 285 insertions(+) create mode 100644 nodescraper/plugins/inband/hsio/ifoe_plugin/__init__.py create mode 100644 nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_data.py create mode 100644 nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_driver_collector.py create mode 100644 nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_plugin.py create mode 100644 test/unit/plugin/test_ifoe_driver_collector.py diff --git a/nodescraper/plugins/inband/hsio/ifoe_plugin/__init__.py b/nodescraper/plugins/inband/hsio/ifoe_plugin/__init__.py new file mode 100644 index 00000000..8daabf4c --- /dev/null +++ b/nodescraper/plugins/inband/hsio/ifoe_plugin/__init__.py @@ -0,0 +1,28 @@ +############################################################################### +# +# MIT License +# +# Copyright (c) 2026 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 .ifoe_plugin import IfoePlugin + +__all__ = ["IfoePlugin"] diff --git a/nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_data.py b/nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_data.py new file mode 100644 index 00000000..34833afa --- /dev/null +++ b/nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_data.py @@ -0,0 +1,39 @@ +############################################################################### +# +# MIT License +# +# Copyright (c) 2026 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 pydantic import Field + +from nodescraper.models import DataModel + + +class IfoeDataModel(DataModel): + """Collected MP-IFoE data.""" + + driver_version: Optional[str] = Field( + default=None, + description="MP-IFoE driver version for GPUs", + ) diff --git a/nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_driver_collector.py b/nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_driver_collector.py new file mode 100644 index 00000000..e73aeca0 --- /dev/null +++ b/nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_driver_collector.py @@ -0,0 +1,85 @@ +############################################################################### +# +# MIT License +# +# Copyright (c) 2026 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.base import InBandDataCollector +from nodescraper.enums import EventCategory, EventPriority, ExecutionStatus, OSFamily +from nodescraper.models import TaskResult + +from .ifoe_data import IfoeDataModel + + +class IfoeDriverCollector(InBandDataCollector[IfoeDataModel, None]): + """Collect IFoE driver details from modinfo.""" + + DATA_MODEL = IfoeDataModel + + SUPPORTED_OS_FAMILY: set[OSFamily] = {OSFamily.LINUX} + + CMD_MODINFO_VERSION = "modinfo ifoe | grep -i version" + + def collect_data(self, args=None) -> tuple[TaskResult, Optional[IfoeDataModel]]: + """Read IFoE driver version from modinfo.""" + ifoe_data: Optional[IfoeDataModel] = None + res = self._run_sut_cmd(self.CMD_MODINFO_VERSION) + + if res.exit_code == 0 and res.stdout: + for line in res.stdout.splitlines(): + if line.startswith("version:"): + driver_version = line.split("version:", 1)[1].strip() + ifoe_data = IfoeDataModel(driver_version=driver_version) + break + + if ifoe_data is None: + self._log_event( + category=EventCategory.SW_DRIVER, + description="IFoE driver version field not found.", + data={"command": res.command}, + priority=EventPriority.WARNING, + ) + self.result.message = "IFoE Driver version not found." + self.result.status = ExecutionStatus.ERROR + return self.result, ifoe_data + + self._log_event( + category=EventCategory.SW_DRIVER, + description="IFoE Driver version read.", + data=ifoe_data.model_dump(), + priority=EventPriority.INFO, + ) + self.result.message = f"IFoE Driver version: {ifoe_data.driver_version}" + self.result.status = ExecutionStatus.OK + return self.result, ifoe_data + + self._log_event( + category=EventCategory.SW_DRIVER, + description="Error checking IFoE Driver version.", + data={"command": res.command, "exit_code": res.exit_code}, + priority=EventPriority.ERROR, + ) + self.result.message = "Error checking IFoE Driver version." + self.result.status = ExecutionStatus.ERROR + return self.result, ifoe_data diff --git a/nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_plugin.py b/nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_plugin.py new file mode 100644 index 00000000..2cd2763d --- /dev/null +++ b/nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_plugin.py @@ -0,0 +1,37 @@ +############################################################################### +# +# MIT License +# +# Copyright (c) 2026 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 nodescraper.base import InBandDataPlugin + +from .ifoe_data import IfoeDataModel +from .ifoe_driver_collector import IfoeDriverCollector + + +class IfoePlugin(InBandDataPlugin[IfoeDataModel, None, None]): + """Plugin for collection of in-band MP-IFoE (HSIO) data.""" + + DATA_MODEL = IfoeDataModel + + COLLECTOR = IfoeDriverCollector diff --git a/test/unit/plugin/test_ifoe_driver_collector.py b/test/unit/plugin/test_ifoe_driver_collector.py new file mode 100644 index 00000000..603e20db --- /dev/null +++ b/test/unit/plugin/test_ifoe_driver_collector.py @@ -0,0 +1,96 @@ +############################################################################### +# +# MIT License +# +# Copyright (c) 2026 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 unittest.mock import MagicMock + +import pytest + +from nodescraper.enums import ExecutionStatus +from nodescraper.enums.systeminteraction import SystemInteractionLevel +from nodescraper.plugins.inband.hsio.ifoe_plugin.ifoe_data import IfoeDataModel +from nodescraper.plugins.inband.hsio.ifoe_plugin.ifoe_driver_collector import ( + IfoeDriverCollector, +) +from nodescraper.plugins.inband.hsio.ifoe_plugin.ifoe_plugin import IfoePlugin + + +@pytest.fixture +def collector(system_info, conn_mock): + return IfoeDriverCollector( + system_info=system_info, + system_interaction_level=SystemInteractionLevel.PASSIVE, + connection=conn_mock, + ) + + +def test_collect_driver_version_success(collector): + collector._run_sut_cmd = MagicMock( + return_value=MagicMock( + exit_code=0, + stdout="version: 1.2.3\n", + command=IfoeDriverCollector.CMD_MODINFO_VERSION, + ) + ) + + result, data = collector.collect_data() + + assert result.status == ExecutionStatus.OK + assert data == IfoeDataModel(driver_version="1.2.3") + collector._run_sut_cmd.assert_called_once_with(IfoeDriverCollector.CMD_MODINFO_VERSION) + + +def test_collect_driver_version_missing_field(collector): + collector._run_sut_cmd = MagicMock( + return_value=MagicMock( + exit_code=0, + stdout="description: IFoE driver\n", + command=IfoeDriverCollector.CMD_MODINFO_VERSION, + ) + ) + + result, data = collector.collect_data() + + assert result.status == ExecutionStatus.ERROR + assert data is None + + +def test_collect_driver_version_command_failure(collector): + collector._run_sut_cmd = MagicMock( + return_value=MagicMock( + exit_code=1, + stdout="", + command=IfoeDriverCollector.CMD_MODINFO_VERSION, + ) + ) + + result, data = collector.collect_data() + + assert result.status == ExecutionStatus.ERROR + assert data is None + + +def test_ifoe_plugin_wiring(): + assert IfoePlugin.DATA_MODEL is IfoeDataModel + assert IfoePlugin.get_collector_classes() == (IfoeDriverCollector,) From d3cdd5e142b0d61913aff70573c5f4de5adbce84 Mon Sep 17 00:00:00 2001 From: Alexandra Bara Date: Tue, 9 Jun 2026 10:05:44 -0500 Subject: [PATCH 3/4] removed ifoe plugin --- .../inband/hsio/ifoe_plugin/__init__.py | 28 ------ .../inband/hsio/ifoe_plugin/ifoe_data.py | 39 -------- .../hsio/ifoe_plugin/ifoe_driver_collector.py | 85 ---------------- .../inband/hsio/ifoe_plugin/ifoe_plugin.py | 37 ------- .../unit/plugin/test_ifoe_driver_collector.py | 96 ------------------- 5 files changed, 285 deletions(-) delete mode 100644 nodescraper/plugins/inband/hsio/ifoe_plugin/__init__.py delete mode 100644 nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_data.py delete mode 100644 nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_driver_collector.py delete mode 100644 nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_plugin.py delete mode 100644 test/unit/plugin/test_ifoe_driver_collector.py diff --git a/nodescraper/plugins/inband/hsio/ifoe_plugin/__init__.py b/nodescraper/plugins/inband/hsio/ifoe_plugin/__init__.py deleted file mode 100644 index 8daabf4c..00000000 --- a/nodescraper/plugins/inband/hsio/ifoe_plugin/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -############################################################################### -# -# MIT License -# -# Copyright (c) 2026 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 .ifoe_plugin import IfoePlugin - -__all__ = ["IfoePlugin"] diff --git a/nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_data.py b/nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_data.py deleted file mode 100644 index 34833afa..00000000 --- a/nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_data.py +++ /dev/null @@ -1,39 +0,0 @@ -############################################################################### -# -# MIT License -# -# Copyright (c) 2026 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 pydantic import Field - -from nodescraper.models import DataModel - - -class IfoeDataModel(DataModel): - """Collected MP-IFoE data.""" - - driver_version: Optional[str] = Field( - default=None, - description="MP-IFoE driver version for GPUs", - ) diff --git a/nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_driver_collector.py b/nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_driver_collector.py deleted file mode 100644 index e73aeca0..00000000 --- a/nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_driver_collector.py +++ /dev/null @@ -1,85 +0,0 @@ -############################################################################### -# -# MIT License -# -# Copyright (c) 2026 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.base import InBandDataCollector -from nodescraper.enums import EventCategory, EventPriority, ExecutionStatus, OSFamily -from nodescraper.models import TaskResult - -from .ifoe_data import IfoeDataModel - - -class IfoeDriverCollector(InBandDataCollector[IfoeDataModel, None]): - """Collect IFoE driver details from modinfo.""" - - DATA_MODEL = IfoeDataModel - - SUPPORTED_OS_FAMILY: set[OSFamily] = {OSFamily.LINUX} - - CMD_MODINFO_VERSION = "modinfo ifoe | grep -i version" - - def collect_data(self, args=None) -> tuple[TaskResult, Optional[IfoeDataModel]]: - """Read IFoE driver version from modinfo.""" - ifoe_data: Optional[IfoeDataModel] = None - res = self._run_sut_cmd(self.CMD_MODINFO_VERSION) - - if res.exit_code == 0 and res.stdout: - for line in res.stdout.splitlines(): - if line.startswith("version:"): - driver_version = line.split("version:", 1)[1].strip() - ifoe_data = IfoeDataModel(driver_version=driver_version) - break - - if ifoe_data is None: - self._log_event( - category=EventCategory.SW_DRIVER, - description="IFoE driver version field not found.", - data={"command": res.command}, - priority=EventPriority.WARNING, - ) - self.result.message = "IFoE Driver version not found." - self.result.status = ExecutionStatus.ERROR - return self.result, ifoe_data - - self._log_event( - category=EventCategory.SW_DRIVER, - description="IFoE Driver version read.", - data=ifoe_data.model_dump(), - priority=EventPriority.INFO, - ) - self.result.message = f"IFoE Driver version: {ifoe_data.driver_version}" - self.result.status = ExecutionStatus.OK - return self.result, ifoe_data - - self._log_event( - category=EventCategory.SW_DRIVER, - description="Error checking IFoE Driver version.", - data={"command": res.command, "exit_code": res.exit_code}, - priority=EventPriority.ERROR, - ) - self.result.message = "Error checking IFoE Driver version." - self.result.status = ExecutionStatus.ERROR - return self.result, ifoe_data diff --git a/nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_plugin.py b/nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_plugin.py deleted file mode 100644 index 2cd2763d..00000000 --- a/nodescraper/plugins/inband/hsio/ifoe_plugin/ifoe_plugin.py +++ /dev/null @@ -1,37 +0,0 @@ -############################################################################### -# -# MIT License -# -# Copyright (c) 2026 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 nodescraper.base import InBandDataPlugin - -from .ifoe_data import IfoeDataModel -from .ifoe_driver_collector import IfoeDriverCollector - - -class IfoePlugin(InBandDataPlugin[IfoeDataModel, None, None]): - """Plugin for collection of in-band MP-IFoE (HSIO) data.""" - - DATA_MODEL = IfoeDataModel - - COLLECTOR = IfoeDriverCollector diff --git a/test/unit/plugin/test_ifoe_driver_collector.py b/test/unit/plugin/test_ifoe_driver_collector.py deleted file mode 100644 index 603e20db..00000000 --- a/test/unit/plugin/test_ifoe_driver_collector.py +++ /dev/null @@ -1,96 +0,0 @@ -############################################################################### -# -# MIT License -# -# Copyright (c) 2026 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 unittest.mock import MagicMock - -import pytest - -from nodescraper.enums import ExecutionStatus -from nodescraper.enums.systeminteraction import SystemInteractionLevel -from nodescraper.plugins.inband.hsio.ifoe_plugin.ifoe_data import IfoeDataModel -from nodescraper.plugins.inband.hsio.ifoe_plugin.ifoe_driver_collector import ( - IfoeDriverCollector, -) -from nodescraper.plugins.inband.hsio.ifoe_plugin.ifoe_plugin import IfoePlugin - - -@pytest.fixture -def collector(system_info, conn_mock): - return IfoeDriverCollector( - system_info=system_info, - system_interaction_level=SystemInteractionLevel.PASSIVE, - connection=conn_mock, - ) - - -def test_collect_driver_version_success(collector): - collector._run_sut_cmd = MagicMock( - return_value=MagicMock( - exit_code=0, - stdout="version: 1.2.3\n", - command=IfoeDriverCollector.CMD_MODINFO_VERSION, - ) - ) - - result, data = collector.collect_data() - - assert result.status == ExecutionStatus.OK - assert data == IfoeDataModel(driver_version="1.2.3") - collector._run_sut_cmd.assert_called_once_with(IfoeDriverCollector.CMD_MODINFO_VERSION) - - -def test_collect_driver_version_missing_field(collector): - collector._run_sut_cmd = MagicMock( - return_value=MagicMock( - exit_code=0, - stdout="description: IFoE driver\n", - command=IfoeDriverCollector.CMD_MODINFO_VERSION, - ) - ) - - result, data = collector.collect_data() - - assert result.status == ExecutionStatus.ERROR - assert data is None - - -def test_collect_driver_version_command_failure(collector): - collector._run_sut_cmd = MagicMock( - return_value=MagicMock( - exit_code=1, - stdout="", - command=IfoeDriverCollector.CMD_MODINFO_VERSION, - ) - ) - - result, data = collector.collect_data() - - assert result.status == ExecutionStatus.ERROR - assert data is None - - -def test_ifoe_plugin_wiring(): - assert IfoePlugin.DATA_MODEL is IfoeDataModel - assert IfoePlugin.get_collector_classes() == (IfoeDriverCollector,) From f7ab83547b5501853cb8e4196725365408eadd5e Mon Sep 17 00:00:00 2001 From: Alexandra Bara Date: Tue, 9 Jun 2026 15:01:38 -0500 Subject: [PATCH 4/4] inheritance fix --- nodescraper/plugins/inband/sys_settings/collector_args.py | 6 ++++-- .../plugins/ooband/redfish_endpoint/collector_args.py | 6 ++++-- .../plugins/ooband/redfish_oem_diag/collector_args.py | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/nodescraper/plugins/inband/sys_settings/collector_args.py b/nodescraper/plugins/inband/sys_settings/collector_args.py index 207c46b3..7e49f228 100644 --- a/nodescraper/plugins/inband/sys_settings/collector_args.py +++ b/nodescraper/plugins/inband/sys_settings/collector_args.py @@ -23,10 +23,12 @@ # SOFTWARE. # ############################################################################### -from pydantic import BaseModel, Field +from pydantic import Field +from nodescraper.models import CollectorArgs -class SysSettingsCollectorArgs(BaseModel): + +class SysSettingsCollectorArgs(CollectorArgs): """Collection args for SysSettingsCollector. paths: sysfs paths to read (cat). If a path contains '*', collect with ls -l instead (e.g. class/net/*/device). diff --git a/nodescraper/plugins/ooband/redfish_endpoint/collector_args.py b/nodescraper/plugins/ooband/redfish_endpoint/collector_args.py index 55bb4269..189c5edf 100644 --- a/nodescraper/plugins/ooband/redfish_endpoint/collector_args.py +++ b/nodescraper/plugins/ooband/redfish_endpoint/collector_args.py @@ -23,10 +23,12 @@ # SOFTWARE. # ############################################################################### -from pydantic import BaseModel, Field, field_validator +from pydantic import Field, field_validator +from nodescraper.models import CollectorArgs -class RedfishEndpointCollectorArgs(BaseModel): + +class RedfishEndpointCollectorArgs(CollectorArgs): """Collection args: uris to GET (or discover from tree), optional concurrency and tree discovery.""" uris: list[str] = Field( diff --git a/nodescraper/plugins/ooband/redfish_oem_diag/collector_args.py b/nodescraper/plugins/ooband/redfish_oem_diag/collector_args.py index da5bd50c..7eb6bac7 100644 --- a/nodescraper/plugins/ooband/redfish_oem_diag/collector_args.py +++ b/nodescraper/plugins/ooband/redfish_oem_diag/collector_args.py @@ -27,12 +27,14 @@ from typing import Optional -from pydantic import BaseModel, Field, model_validator +from pydantic import Field, model_validator + +from nodescraper.models import CollectorArgs DEFAULT_TASK_TIMEOUT_S = 1800 -class RedfishOemDiagCollectorArgs(BaseModel): +class RedfishOemDiagCollectorArgs(CollectorArgs): """Collector/analyzer args for Redfish OEM diagnostic log collection.""" log_service_path: str = Field(