diff --git a/nodescraper/models/analyzerargs.py b/nodescraper/models/analyzerargs.py index 209f6a0e..f1782801 100644 --- a/nodescraper/models/analyzerargs.py +++ b/nodescraper/models/analyzerargs.py @@ -23,12 +23,61 @@ # SOFTWARE. # ############################################################################### -from pydantic import BaseModel +from typing import Any + +from pydantic import BaseModel, model_validator class AnalyzerArgs(BaseModel): + """Base class for all analyzer arguments. + + This class provides automatic string stripping for all string values + in analyzer args. All analyzer args classes should inherit from this + directly. + + """ + model_config = {"extra": "forbid", "exclude_none": True} + @model_validator(mode="before") + @classmethod + def strip_string_values(cls, data: Any) -> Any: + """Strip whitespace from all string values in analyzer args. + + This validator recursively processes: + - String values: strips whitespace + - Lists: strips strings in lists + - Dicts: strips string values in dicts + - Other types: left unchanged + + Args: + data: The input data to validate + + Returns: + The data with all string values stripped + """ + if isinstance(data, dict): + return {k: cls._strip_value(v) for k, v in data.items()} + return data + + @classmethod + def _strip_value(cls, value: Any) -> Any: + """Recursively strip string values. + + Args: + value: The value to process + + Returns: + The processed value + """ + if isinstance(value, str): + return value.strip() + elif isinstance(value, list): + return [cls._strip_value(item) for item in value] + elif isinstance(value, dict): + return {k: cls._strip_value(v) for k, v in value.items()} + return value + @classmethod def build_from_model(cls, datamodel): """Build analyzer args instance from data model object diff --git a/nodescraper/plugins/inband/memory/analyzer_args.py b/nodescraper/plugins/inband/memory/analyzer_args.py index cc5f0ef4..83563476 100644 --- a/nodescraper/plugins/inband/memory/analyzer_args.py +++ b/nodescraper/plugins/inband/memory/analyzer_args.py @@ -23,9 +23,9 @@ # SOFTWARE. # ############################################################################### -from pydantic import BaseModel +from nodescraper.models.analyzerargs import AnalyzerArgs -class MemoryAnalyzerArgs(BaseModel): +class MemoryAnalyzerArgs(AnalyzerArgs): ratio: float = 0.66 memory_threshold: str = "30Gi" diff --git a/nodescraper/plugins/inband/package/package_analyzer.py b/nodescraper/plugins/inband/package/package_analyzer.py index 16215086..d0a51a54 100644 --- a/nodescraper/plugins/inband/package/package_analyzer.py +++ b/nodescraper/plugins/inband/package/package_analyzer.py @@ -58,6 +58,7 @@ def regex_version_data( value_found = False for name, version in package_data.items(): + self.logger.debug("Package data: %s, %s", name, version) key_search_res = key_search.search(name) if key_search_res: value_found = True @@ -79,16 +80,16 @@ def regex_version_data( return value_found def package_regex_search( - self, package_data: dict[str, str], exp_packge_data: dict[str, Optional[str]] + self, package_data: dict[str, str], exp_package_data: dict[str, Optional[str]] ): """Searches the package data for the expected package and version using regex Args: package_data (dict[str, str]): a dictionary of package names and versions - exp_packge_data (dict[str, Optional[str]]): a dictionary of expected package names and versions + exp_package_data (dict[str, Optional[str]]): a dictionary of expected package names and versions """ not_found_keys = [] - for exp_key, exp_value in exp_packge_data.items(): + for exp_key, exp_value in exp_package_data.items(): try: if exp_value is not None: value_search = re.compile(exp_value) @@ -125,44 +126,46 @@ def package_regex_search( return not_found_keys def package_exact_match( - self, package_data: dict[str, str], exp_packge_data: dict[str, Optional[str]] + self, package_data: dict[str, str], exp_package_data: dict[str, Optional[str]] ): """Checks the package data for the expected package and version using exact match Args: package_data (dict[str, str]): a dictionary of package names and versions - exp_packge_data (dict[str, Optional[str]]): a dictionary of expected package names and versions + exp_package_data (dict[str, Optional[str]]): a dictionary of expected package names and versions """ not_found_match = [] not_found_version = [] - for exp_key, exp_value in exp_packge_data.items(): - self.logger.info(exp_key) + for exp_key, exp_value in exp_package_data.items(): + self.logger.info("Expected value: %s, %s", exp_key, exp_value) version = package_data.get(exp_key) - if exp_value is None: + self.logger.info("Found version: %s", version) + if version is None: + # package not found + not_found_version.append((exp_key, exp_value)) + self._log_event( + EventCategory.APPLICATION, + f"Package {exp_key} not found in the package list", + EventPriority.ERROR, + { + "expected_package": exp_key, + "found_package": None, + "expected_version": exp_value, + "found_version": None, + }, + ) + elif exp_value is None: # allow any version when expected version is None - if version is None: - # package not found - not_found_version.append((exp_key, version)) - self._log_event( - EventCategory.APPLICATION, - f"Package {exp_key} not found in the package list", - EventPriority.ERROR, - { - "expected_package": exp_key, - "found_package": None, - "expected_version": exp_value, - "found_version": None, - }, - ) + continue elif version != exp_value: not_found_match.append((exp_key, version)) self._log_event( EventCategory.APPLICATION, - f"Package {exp_key} Version Mismatch, Expected {exp_key} but found {version}", + f"Package {exp_key} Version Mismatch, Expected {exp_value} but found {version}", EventPriority.ERROR, { "expected_package": exp_key, - "found_package": exp_key if version else None, + "found_package": exp_key, "expected_version": exp_value, "found_version": version, }, @@ -191,6 +194,7 @@ def analyze_data( self.result.message = f"Packages not found: {not_found_keys}" self.result.status = ExecutionStatus.ERROR else: + self.logger.info("Expected packages: %s", list(args.exp_package_ver.keys())) not_found_match, not_found_version = self.package_exact_match( data.version_info, args.exp_package_ver ) diff --git a/nodescraper/plugins/inband/rocm/analyzer_args.py b/nodescraper/plugins/inband/rocm/analyzer_args.py index 40a11ebc..cdc04cce 100644 --- a/nodescraper/plugins/inband/rocm/analyzer_args.py +++ b/nodescraper/plugins/inband/rocm/analyzer_args.py @@ -25,12 +25,13 @@ ############################################################################### from typing import Union -from pydantic import BaseModel, Field, field_validator +from pydantic import Field, field_validator +from nodescraper.models.analyzerargs import AnalyzerArgs from nodescraper.plugins.inband.rocm.rocmdata import RocmDataModel -class RocmAnalyzerArgs(BaseModel): +class RocmAnalyzerArgs(AnalyzerArgs): exp_rocm: Union[str, list] = Field(default_factory=list) @field_validator("exp_rocm", mode="before") diff --git a/nodescraper/plugins/inband/rocm/rocm_analyzer.py b/nodescraper/plugins/inband/rocm/rocm_analyzer.py index 17762af7..75aad008 100644 --- a/nodescraper/plugins/inband/rocm/rocm_analyzer.py +++ b/nodescraper/plugins/inband/rocm/rocm_analyzer.py @@ -64,7 +64,7 @@ def analyze_data( return self.result self.result.message = ( - f"ROCm version mismatch! Expected: {rocm_version}, actual: {args.exp_rocm}" + f"ROCm version mismatch! Expected: {args.exp_rocm}, actual: {data.rocm_version}" ) self.result.status = ExecutionStatus.ERROR self._log_event( diff --git a/nodescraper/plugins/inband/storage/analyzer_args.py b/nodescraper/plugins/inband/storage/analyzer_args.py index 1f44d1d3..413c8ec0 100644 --- a/nodescraper/plugins/inband/storage/analyzer_args.py +++ b/nodescraper/plugins/inband/storage/analyzer_args.py @@ -25,10 +25,12 @@ ############################################################################### from typing import Optional -from pydantic import BaseModel, Field +from pydantic import Field +from nodescraper.models.analyzerargs import AnalyzerArgs -class StorageAnalyzerArgs(BaseModel): + +class StorageAnalyzerArgs(AnalyzerArgs): min_required_free_space_abs: Optional[str] = None min_required_free_space_prct: Optional[int] = None ignore_devices: Optional[list[str]] = Field(default_factory=list)