From 2356efabc2b29a9b67652af744a594dcde0e038c Mon Sep 17 00:00:00 2001 From: Alexandra Bara Date: Thu, 20 Nov 2025 09:39:31 -0600 Subject: [PATCH 1/3] some fixes to accomodate for user input + printout fixes to package analyzer --- .../plugins/inband/bios/bios_analyzer.py | 2 +- .../plugins/inband/dkms/dkms_analyzer.py | 2 +- .../plugins/inband/kernel/kernel_analyzer.py | 2 +- nodescraper/plugins/inband/os/os_analyzer.py | 4 +- .../inband/package/package_analyzer.py | 54 ++++++++++--------- .../plugins/inband/rocm/rocm_analyzer.py | 4 +- .../inband/storage/storage_analyzer.py | 2 +- 7 files changed, 37 insertions(+), 33 deletions(-) diff --git a/nodescraper/plugins/inband/bios/bios_analyzer.py b/nodescraper/plugins/inband/bios/bios_analyzer.py index ac03066f..87eca275 100644 --- a/nodescraper/plugins/inband/bios/bios_analyzer.py +++ b/nodescraper/plugins/inband/bios/bios_analyzer.py @@ -75,7 +75,7 @@ def analyze_data( self.result.message = "Bios data matches expected" self.result.status = ExecutionStatus.OK return self.result - elif data.bios_version == bios_version: + elif data.bios_version == str(bios_version).strip(): self.result.message = "Bios data matches expected" self.result.status = ExecutionStatus.OK return self.result diff --git a/nodescraper/plugins/inband/dkms/dkms_analyzer.py b/nodescraper/plugins/inband/dkms/dkms_analyzer.py index fae7b87d..f6b37447 100644 --- a/nodescraper/plugins/inband/dkms/dkms_analyzer.py +++ b/nodescraper/plugins/inband/dkms/dkms_analyzer.py @@ -83,7 +83,7 @@ def analyze_data( ) if regex_data.match(actual_value): break - elif actual_value == accepted_value: + elif actual_value == str(accepted_value).strip(): break else: expected_values.append(accepted_values) diff --git a/nodescraper/plugins/inband/kernel/kernel_analyzer.py b/nodescraper/plugins/inband/kernel/kernel_analyzer.py index 1ff4e45a..cb372b8a 100644 --- a/nodescraper/plugins/inband/kernel/kernel_analyzer.py +++ b/nodescraper/plugins/inband/kernel/kernel_analyzer.py @@ -72,7 +72,7 @@ def analyze_data( self.result.message = "Kernel matches expected" self.result.status = ExecutionStatus.OK return self.result - elif data.kernel_version == kernel: + elif data.kernel_version == str(kernel).strip(): self.result.message = "Kernel matches expected" self.result.status = ExecutionStatus.OK return self.result diff --git a/nodescraper/plugins/inband/os/os_analyzer.py b/nodescraper/plugins/inband/os/os_analyzer.py index 0d952468..a9982c9a 100644 --- a/nodescraper/plugins/inband/os/os_analyzer.py +++ b/nodescraper/plugins/inband/os/os_analyzer.py @@ -54,8 +54,8 @@ def analyze_data(self, data: OsDataModel, args: Optional[OsAnalyzerArgs] = None) return self.result for os_name in args.exp_os: - if (os_name == data.os_name and args.exact_match) or ( - os_name in data.os_name and not args.exact_match + if (str(os_name).strip() == data.os_name and args.exact_match) or ( + str(os_name).strip() in data.os_name and not args.exact_match ): self.result.message = "OS name matches expected" self.result.status = ExecutionStatus.OK diff --git a/nodescraper/plugins/inband/package/package_analyzer.py b/nodescraper/plugins/inband/package/package_analyzer.py index 16215086..adf24aaf 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, - }, - ) - elif version != exp_value: + continue + elif version != str(exp_value).strip(): 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/rocm_analyzer.py b/nodescraper/plugins/inband/rocm/rocm_analyzer.py index 17762af7..f14c9665 100644 --- a/nodescraper/plugins/inband/rocm/rocm_analyzer.py +++ b/nodescraper/plugins/inband/rocm/rocm_analyzer.py @@ -58,13 +58,13 @@ def analyze_data( return self.result for rocm_version in args.exp_rocm: - if data.rocm_version == rocm_version: + if data.rocm_version == str(rocm_version).strip(): self.result.message = "ROCm version matches expected" self.result.status = ExecutionStatus.OK 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/storage_analyzer.py b/nodescraper/plugins/inband/storage/storage_analyzer.py index 6c2f8334..6c80bc4e 100644 --- a/nodescraper/plugins/inband/storage/storage_analyzer.py +++ b/nodescraper/plugins/inband/storage/storage_analyzer.py @@ -66,7 +66,7 @@ def _matches_device_filter( continue if device_regex.match(device_name): return True - elif device_name == exp_device: + elif device_name == str(exp_device).strip(): return True return False From 350728fce914a0608a3c13117280f5a195e2c1e2 Mon Sep 17 00:00:00 2001 From: Alexandra Bara Date: Thu, 20 Nov 2025 12:48:37 -0600 Subject: [PATCH 2/3] added validator for str values in AnalyzerArgs base model --- nodescraper/models/analyzerargs.py | 58 ++++++++++++++++++- .../plugins/inband/bios/bios_analyzer.py | 2 +- .../plugins/inband/dkms/dkms_analyzer.py | 2 +- .../plugins/inband/kernel/kernel_analyzer.py | 2 +- .../plugins/inband/memory/analyzer_args.py | 4 +- nodescraper/plugins/inband/os/os_analyzer.py | 4 +- .../inband/package/package_analyzer.py | 2 +- .../plugins/inband/rocm/analyzer_args.py | 5 +- .../plugins/inband/rocm/rocm_analyzer.py | 2 +- .../plugins/inband/storage/analyzer_args.py | 6 +- .../inband/storage/storage_analyzer.py | 2 +- 11 files changed, 74 insertions(+), 15 deletions(-) diff --git a/nodescraper/models/analyzerargs.py b/nodescraper/models/analyzerargs.py index 209f6a0e..147f9f02 100644 --- a/nodescraper/models/analyzerargs.py +++ b/nodescraper/models/analyzerargs.py @@ -23,12 +23,68 @@ # 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 + instead of BaseModel directly. + + The strip_string_values validator runs in mode="before", which means + it executes before any field validators in child classes. This ensures + all string inputs are cleaned of leading/trailing whitespace before + any further processing. + """ + 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 + + Runs in mode="before" to ensure stripping happens before any + field validators execute in child classes. + + 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/bios/bios_analyzer.py b/nodescraper/plugins/inband/bios/bios_analyzer.py index 87eca275..ac03066f 100644 --- a/nodescraper/plugins/inband/bios/bios_analyzer.py +++ b/nodescraper/plugins/inband/bios/bios_analyzer.py @@ -75,7 +75,7 @@ def analyze_data( self.result.message = "Bios data matches expected" self.result.status = ExecutionStatus.OK return self.result - elif data.bios_version == str(bios_version).strip(): + elif data.bios_version == bios_version: self.result.message = "Bios data matches expected" self.result.status = ExecutionStatus.OK return self.result diff --git a/nodescraper/plugins/inband/dkms/dkms_analyzer.py b/nodescraper/plugins/inband/dkms/dkms_analyzer.py index f6b37447..fae7b87d 100644 --- a/nodescraper/plugins/inband/dkms/dkms_analyzer.py +++ b/nodescraper/plugins/inband/dkms/dkms_analyzer.py @@ -83,7 +83,7 @@ def analyze_data( ) if regex_data.match(actual_value): break - elif actual_value == str(accepted_value).strip(): + elif actual_value == accepted_value: break else: expected_values.append(accepted_values) diff --git a/nodescraper/plugins/inband/kernel/kernel_analyzer.py b/nodescraper/plugins/inband/kernel/kernel_analyzer.py index cb372b8a..1ff4e45a 100644 --- a/nodescraper/plugins/inband/kernel/kernel_analyzer.py +++ b/nodescraper/plugins/inband/kernel/kernel_analyzer.py @@ -72,7 +72,7 @@ def analyze_data( self.result.message = "Kernel matches expected" self.result.status = ExecutionStatus.OK return self.result - elif data.kernel_version == str(kernel).strip(): + elif data.kernel_version == kernel: self.result.message = "Kernel matches expected" self.result.status = ExecutionStatus.OK return self.result 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/os/os_analyzer.py b/nodescraper/plugins/inband/os/os_analyzer.py index a9982c9a..0d952468 100644 --- a/nodescraper/plugins/inband/os/os_analyzer.py +++ b/nodescraper/plugins/inband/os/os_analyzer.py @@ -54,8 +54,8 @@ def analyze_data(self, data: OsDataModel, args: Optional[OsAnalyzerArgs] = None) return self.result for os_name in args.exp_os: - if (str(os_name).strip() == data.os_name and args.exact_match) or ( - str(os_name).strip() in data.os_name and not args.exact_match + if (os_name == data.os_name and args.exact_match) or ( + os_name in data.os_name and not args.exact_match ): self.result.message = "OS name matches expected" self.result.status = ExecutionStatus.OK diff --git a/nodescraper/plugins/inband/package/package_analyzer.py b/nodescraper/plugins/inband/package/package_analyzer.py index adf24aaf..d0a51a54 100644 --- a/nodescraper/plugins/inband/package/package_analyzer.py +++ b/nodescraper/plugins/inband/package/package_analyzer.py @@ -157,7 +157,7 @@ def package_exact_match( elif exp_value is None: # allow any version when expected version is None continue - elif version != str(exp_value).strip(): + elif version != exp_value: not_found_match.append((exp_key, version)) self._log_event( EventCategory.APPLICATION, 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 f14c9665..75aad008 100644 --- a/nodescraper/plugins/inband/rocm/rocm_analyzer.py +++ b/nodescraper/plugins/inband/rocm/rocm_analyzer.py @@ -58,7 +58,7 @@ def analyze_data( return self.result for rocm_version in args.exp_rocm: - if data.rocm_version == str(rocm_version).strip(): + if data.rocm_version == rocm_version: self.result.message = "ROCm version matches expected" self.result.status = ExecutionStatus.OK return self.result 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) diff --git a/nodescraper/plugins/inband/storage/storage_analyzer.py b/nodescraper/plugins/inband/storage/storage_analyzer.py index 6c80bc4e..6c2f8334 100644 --- a/nodescraper/plugins/inband/storage/storage_analyzer.py +++ b/nodescraper/plugins/inband/storage/storage_analyzer.py @@ -66,7 +66,7 @@ def _matches_device_filter( continue if device_regex.match(device_name): return True - elif device_name == str(exp_device).strip(): + elif device_name == exp_device: return True return False From 991ffda513e580d9b544a14bca596810bfca760d Mon Sep 17 00:00:00 2001 From: Alexandra Bara Date: Thu, 20 Nov 2025 12:50:11 -0600 Subject: [PATCH 3/3] dicts --- nodescraper/models/analyzerargs.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/nodescraper/models/analyzerargs.py b/nodescraper/models/analyzerargs.py index 147f9f02..f1782801 100644 --- a/nodescraper/models/analyzerargs.py +++ b/nodescraper/models/analyzerargs.py @@ -33,12 +33,8 @@ class AnalyzerArgs(BaseModel): This class provides automatic string stripping for all string values in analyzer args. All analyzer args classes should inherit from this - instead of BaseModel directly. + directly. - The strip_string_values validator runs in mode="before", which means - it executes before any field validators in child classes. This ensures - all string inputs are cleaned of leading/trailing whitespace before - any further processing. """ model_config = {"extra": "forbid", "exclude_none": True} @@ -54,9 +50,6 @@ def strip_string_values(cls, data: Any) -> Any: - Dicts: strips string values in dicts - Other types: left unchanged - Runs in mode="before" to ensure stripping happens before any - field validators execute in child classes. - Args: data: The input data to validate