From 968d5a83e9e5dc7c63a702e7c7a624d9a4ea8abc Mon Sep 17 00:00:00 2001 From: Jaspal Singh Date: Fri, 25 Sep 2026 16:59:37 +0000 Subject: [PATCH 1/2] memory and load checks --- .../plugins/inband/memory/analyzer_args.py | 8 ++ .../plugins/inband/memory/memory_analyzer.py | 40 +++++++++ .../plugins/inband/nvme/analyzer_args.py | 20 +++++ .../plugins/inband/nvme/nvme_analyzer.py | 83 +++++++++++++++++ .../plugins/inband/nvme/nvme_plugin.py | 8 +- .../plugins/inband/os/analyzer_args.py | 7 +- nodescraper/plugins/inband/os/os_analyzer.py | 89 +++++++++++++++---- nodescraper/plugins/inband/os/os_collector.py | 29 ++++++ nodescraper/plugins/inband/os/osdata.py | 4 + test/unit/plugin/test_memory_analyzer.py | 33 +++++++ test/unit/plugin/test_nvme_analyzer.py | 69 ++++++++++++++ test/unit/plugin/test_os_analyzer.py | 81 +++++++++++++++++ test/unit/plugin/test_os_collector.py | 31 ++++++- 13 files changed, 481 insertions(+), 21 deletions(-) create mode 100644 nodescraper/plugins/inband/nvme/analyzer_args.py create mode 100644 nodescraper/plugins/inband/nvme/nvme_analyzer.py create mode 100644 test/unit/plugin/test_nvme_analyzer.py diff --git a/nodescraper/plugins/inband/memory/analyzer_args.py b/nodescraper/plugins/inband/memory/analyzer_args.py index 88dcd5d2..9a4000a2 100644 --- a/nodescraper/plugins/inband/memory/analyzer_args.py +++ b/nodescraper/plugins/inband/memory/analyzer_args.py @@ -23,6 +23,8 @@ # SOFTWARE. # ############################################################################### +from typing import Optional + from pydantic import Field from nodescraper.models.analyzerargs import AnalyzerArgs @@ -31,6 +33,12 @@ class MemoryAnalyzerArgs(AnalyzerArgs): + minimum_free_memory_percent: Optional[float] = Field( + default=None, + ge=0, + le=100, + description="Minimum available system memory percentage required.", + ) ratio: float = Field( default=0.66, description="Required free-memory ratio (0-1). Analysis fails if free/total < ratio.", diff --git a/nodescraper/plugins/inband/memory/memory_analyzer.py b/nodescraper/plugins/inband/memory/memory_analyzer.py index f0de6013..e48334bc 100644 --- a/nodescraper/plugins/inband/memory/memory_analyzer.py +++ b/nodescraper/plugins/inband/memory/memory_analyzer.py @@ -61,6 +61,46 @@ def _bytes_to_gb(n: float) -> float: total_memory = convert_to_bytes(data.mem_total) used_memory = total_memory - available_memory + if args.minimum_free_memory_percent is not None: + if total_memory <= 0: + self.result.status = ExecutionStatus.WARNING + self.result.message = "Total memory is unavailable" + self._log_event( + category=EventCategory.OS, + description="Cannot validate minimum free memory percentage", + priority=EventPriority.WARNING, + data={"total_memory": total_memory, "available_memory": available_memory}, + console_log=True, + ) + return self.result + + available_percent = available_memory / total_memory * 100 + if available_percent < args.minimum_free_memory_percent: + self.result.status = ExecutionStatus.ERROR + self.result.message = "Minimum free memory percentage not met" + self._log_event( + category=EventCategory.OS, + description=( + f"Available memory is {available_percent:.2f}% " + f"(minimum {args.minimum_free_memory_percent:.2f}%)" + ), + priority=EventPriority.CRITICAL, + data={ + "available_memory": available_memory, + "total_memory": total_memory, + "available_percent": available_percent, + "minimum_free_memory_percent": args.minimum_free_memory_percent, + }, + console_log=True, + ) + else: + self.result.status = ExecutionStatus.OK + self.result.message = ( + f"Available memory is {available_percent:.2f}% " + f"(minimum {args.minimum_free_memory_percent:.2f}%)" + ) + return self.result + threshold_bytes = convert_to_bytes(args.memory_threshold) if total_memory > threshold_bytes: diff --git a/nodescraper/plugins/inband/nvme/analyzer_args.py b/nodescraper/plugins/inband/nvme/analyzer_args.py new file mode 100644 index 00000000..1850d0d2 --- /dev/null +++ b/nodescraper/plugins/inband/nvme/analyzer_args.py @@ -0,0 +1,20 @@ +############################################################################### +# +# MIT License +# +# Copyright (c) 2025 Advanced Micro Devices, Inc. +# +############################################################################### +from typing import Optional + +from pydantic import Field + +from nodescraper.models import AnalyzerArgs + + +class NvmeAnalyzerArgs(AnalyzerArgs): + maximum_smart_error_count: Optional[int] = Field( + default=None, + ge=0, + description="Maximum allowed NVMe SMART media error count per device.", + ) diff --git a/nodescraper/plugins/inband/nvme/nvme_analyzer.py b/nodescraper/plugins/inband/nvme/nvme_analyzer.py new file mode 100644 index 00000000..cfb22f1e --- /dev/null +++ b/nodescraper/plugins/inband/nvme/nvme_analyzer.py @@ -0,0 +1,83 @@ +############################################################################### +# +# MIT License +# +# Copyright (c) 2025 Advanced Micro Devices, Inc. +# +############################################################################### +import re +from typing import Optional + +from nodescraper.enums import EventCategory, EventPriority, ExecutionStatus +from nodescraper.interfaces import DataAnalyzer +from nodescraper.models import TaskResult + +from .analyzer_args import NvmeAnalyzerArgs +from .nvmedata import NvmeDataModel + + +class NvmeAnalyzer(DataAnalyzer[NvmeDataModel, NvmeAnalyzerArgs]): + """Check NVMe SMART health data.""" + + DATA_MODEL = NvmeDataModel + + @staticmethod + def _parse_media_errors(smart_log: Optional[str]) -> Optional[int]: + if not smart_log: + return None + match = re.search(r"(?im)^\s*media_errors\s*:\s*(\d+)\s*$", smart_log) + return int(match.group(1)) if match else None + + def analyze_data( + self, data: NvmeDataModel, args: Optional[NvmeAnalyzerArgs] = None + ) -> TaskResult: + """Check each NVMe device's SMART media error count.""" + if args is None or args.maximum_smart_error_count is None: + self.result.status = ExecutionStatus.NOT_RAN + self.result.message = "Maximum NVMe SMART error count not provided" + return self.result + + if not data.devices: + self.result.status = ExecutionStatus.NOT_RAN + self.result.message = "No NVMe data available" + return self.result + + failures = [] + for device, device_data in data.devices.items(): + media_errors = self._parse_media_errors(device_data.smart_log) + if media_errors is None: + self._log_event( + category=EventCategory.STORAGE, + description=f"NVMe SMART media error count unavailable for {device}", + priority=EventPriority.WARNING, + data={"device": device}, + console_log=True, + ) + continue + + if media_errors > args.maximum_smart_error_count: + failures.append((device, media_errors)) + + if failures: + for device, media_errors in failures: + self._log_event( + category=EventCategory.STORAGE, + description=( + f"NVMe SMART media errors exceeded threshold for {device}: " + f"{media_errors} > {args.maximum_smart_error_count}" + ), + priority=EventPriority.ERROR, + data={ + "device": device, + "media_errors": media_errors, + "maximum_smart_error_count": args.maximum_smart_error_count, + }, + console_log=True, + ) + self.result.status = ExecutionStatus.ERROR + self.result.message = "NVMe SMART media error threshold exceeded" + else: + self.result.status = ExecutionStatus.OK + self.result.message = "NVMe SMART media error counts are within limits" + + return self.result diff --git a/nodescraper/plugins/inband/nvme/nvme_plugin.py b/nodescraper/plugins/inband/nvme/nvme_plugin.py index 29557290..35ecb1bc 100644 --- a/nodescraper/plugins/inband/nvme/nvme_plugin.py +++ b/nodescraper/plugins/inband/nvme/nvme_plugin.py @@ -25,13 +25,19 @@ ############################################################################### from nodescraper.base import InBandDataPlugin +from .analyzer_args import NvmeAnalyzerArgs +from .nvme_analyzer import NvmeAnalyzer from .nvme_collector import NvmeCollector from .nvmedata import NvmeDataModel -class NvmePlugin(InBandDataPlugin[NvmeDataModel, None, None]): +class NvmePlugin(InBandDataPlugin[NvmeDataModel, None, NvmeAnalyzerArgs]): """Plugin for collection and analysis of nvme data""" DATA_MODEL = NvmeDataModel COLLECTOR = NvmeCollector + + ANALYZER = NvmeAnalyzer + + ANALYZER_ARGS = NvmeAnalyzerArgs diff --git a/nodescraper/plugins/inband/os/analyzer_args.py b/nodescraper/plugins/inband/os/analyzer_args.py index 58565123..4fd4ed95 100644 --- a/nodescraper/plugins/inband/os/analyzer_args.py +++ b/nodescraper/plugins/inband/os/analyzer_args.py @@ -23,7 +23,7 @@ # SOFTWARE. # ############################################################################### -from typing import Union +from typing import Optional, Union from pydantic import Field, field_validator @@ -32,6 +32,11 @@ class OsAnalyzerArgs(AnalyzerArgs): + maximum_load_per_cpu_core: Optional[float] = Field( + default=None, + ge=0, + description="Maximum allowed 1-minute load average per CPU core.", + ) exp_os: Union[str, list] = Field( default_factory=list, description="Expected OS name/version string(s) to match (e.g. from lsb_release or /etc/os-release).", diff --git a/nodescraper/plugins/inband/os/os_analyzer.py b/nodescraper/plugins/inband/os/os_analyzer.py index 235b4aab..f168d016 100644 --- a/nodescraper/plugins/inband/os/os_analyzer.py +++ b/nodescraper/plugins/inband/os/os_analyzer.py @@ -48,26 +48,81 @@ def analyze_data(self, data: OsDataModel, args: Optional[OsAnalyzerArgs] = None) Returns: TaskResult: Result of the analysis containing status and message. """ - if not args or not args.exp_os: + if args is None: + args = OsAnalyzerArgs() + + if not args.exp_os and args.maximum_load_per_cpu_core is None: self.result.message = "Expected OS name not provided" self.result.status = ExecutionStatus.NOT_RAN 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 - ): - self.result.message = "OS name matches expected" - self.result.status = ExecutionStatus.OK - return self.result + os_name_matches = True + load_check_warning = False + load_check_error = False + + if args.exp_os: + os_name_matches = False + 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 + ): + os_name_matches = True + break + + if not os_name_matches: + self._log_event( + category=EventCategory.OS, + description=f"OS name mismatch! Expected: {args.exp_os}, actual: {data.os_name}", + data={"expected": args.exp_os, "actual": data.os_name}, + priority=EventPriority.CRITICAL, + console_log=True, + ) + + if args.maximum_load_per_cpu_core is not None: + if data.load_average_1m is None or data.cpu_count is None or data.cpu_count <= 0: + load_check_warning = True + self._log_event( + category=EventCategory.OS, + description="Cannot validate maximum load per CPU core", + data={ + "load_average_1m": data.load_average_1m, + "cpu_count": data.cpu_count, + }, + priority=EventPriority.WARNING, + console_log=True, + ) + else: + load_per_cpu_core = data.load_average_1m / data.cpu_count + if load_per_cpu_core > args.maximum_load_per_cpu_core: + load_check_error = True + self._log_event( + category=EventCategory.OS, + description=( + f"Load per CPU core is {load_per_cpu_core:.2f} " + f"(maximum {args.maximum_load_per_cpu_core:.2f})" + ), + data={ + "load_average_1m": data.load_average_1m, + "cpu_count": data.cpu_count, + "load_per_cpu_core": load_per_cpu_core, + "maximum_load_per_cpu_core": args.maximum_load_per_cpu_core, + }, + priority=EventPriority.CRITICAL, + console_log=True, + ) - self.result.message = "OS name mismatch!" - self.result.status = ExecutionStatus.ERROR - self._log_event( - category=EventCategory.OS, - description=f"OS name mismatch! Expected: {args.exp_os}, actual: {data.os_name}", - data={"expected": args.exp_os, "actual": data.os_name}, - priority=EventPriority.CRITICAL, - console_log=True, - ) + if not os_name_matches: + self.result.message = "OS name mismatch!" + self.result.status = ExecutionStatus.ERROR + elif load_check_error: + self.result.message = "Maximum load per CPU core exceeded" + self.result.status = ExecutionStatus.ERROR + elif load_check_warning: + self.result.message = "CPU load data is not available" + self.result.status = ExecutionStatus.WARNING + else: + self.result.message = ( + "OS name matches expected" if args.exp_os else "Load per CPU core is within limit" + ) + self.result.status = ExecutionStatus.OK return self.result diff --git a/nodescraper/plugins/inband/os/os_collector.py b/nodescraper/plugins/inband/os/os_collector.py index 2fc46ab5..a556e058 100644 --- a/nodescraper/plugins/inband/os/os_collector.py +++ b/nodescraper/plugins/inband/os/os_collector.py @@ -45,6 +45,8 @@ class OsCollector(InBandDataCollector[OsDataModel, None]): CMD_ESXI = "vmware -v" PRETTY_STR = "PRETTY_NAME" # noqa: N806 CMD = f"sh -c '( lsb_release -ds || (cat /etc/*release | grep {PRETTY_STR}) || uname -om ) 2>/dev/null | head -n1'" + CMD_LOAD_AVERAGE = "cat /proc/loadavg" + CMD_CPU_COUNT = "nproc" def collect_version(self) -> str: """Collect OS version. @@ -135,9 +137,36 @@ def collect_data(self, args=None) -> tuple[TaskResult, Optional[OsDataModel]]: if os_name: os_version = self.collect_version() + load_average_1m = None + cpu_count = None + if self.system_info.os_family == OSFamily.LINUX: + load_res = self._run_sut_cmd(self.CMD_LOAD_AVERAGE) + if load_res.exit_code == 0 and load_res.stdout.strip(): + try: + load_average_1m = float(load_res.stdout.split()[0]) + except (ValueError, IndexError): + self._log_event( + category=EventCategory.OS, + description="Invalid 1-minute load average", + priority=EventPriority.WARNING, + ) + + cpu_res = self._run_sut_cmd(self.CMD_CPU_COUNT) + if cpu_res.exit_code == 0 and cpu_res.stdout.strip(): + try: + cpu_count = int(cpu_res.stdout.strip()) + except ValueError: + self._log_event( + category=EventCategory.OS, + description="Invalid CPU count", + priority=EventPriority.WARNING, + ) + os_data = OsDataModel( os_name=os_name, os_version=os_version, + load_average_1m=load_average_1m, + cpu_count=cpu_count, ) self._log_event( category="OS_NAME_READ", diff --git a/nodescraper/plugins/inband/os/osdata.py b/nodescraper/plugins/inband/os/osdata.py index fd861349..7bd6e56d 100644 --- a/nodescraper/plugins/inband/os/osdata.py +++ b/nodescraper/plugins/inband/os/osdata.py @@ -23,9 +23,13 @@ # SOFTWARE. # ############################################################################### +from typing import Optional + from nodescraper.models import DataModel class OsDataModel(DataModel): os_name: str os_version: str = "" + load_average_1m: Optional[float] = None + cpu_count: Optional[int] = None diff --git a/test/unit/plugin/test_memory_analyzer.py b/test/unit/plugin/test_memory_analyzer.py index 0c7c3f03..71ec3448 100644 --- a/test/unit/plugin/test_memory_analyzer.py +++ b/test/unit/plugin/test_memory_analyzer.py @@ -60,6 +60,39 @@ def test_config_provided(analyzer, model_obj): assert result.status == ExecutionStatus.OK +def test_minimum_free_memory_percent_passes(analyzer): + model = MemoryDataModel( + mem_total="100Gi", + mem_free="20Gi", + mem_available="20Gi", + ) + + result = analyzer.analyze_data( + model, + MemoryAnalyzerArgs(minimum_free_memory_percent=10), + ) + + assert result.status == ExecutionStatus.OK + assert "20.00%" in result.message + + +def test_minimum_free_memory_percent_fails(analyzer): + model = MemoryDataModel( + mem_total="100Gi", + mem_free="5Gi", + mem_available="5Gi", + ) + + result = analyzer.analyze_data( + model, + MemoryAnalyzerArgs(minimum_free_memory_percent=10), + ) + + assert result.status == ExecutionStatus.ERROR + assert len(result.events) == 1 + assert result.events[0].data["available_percent"] == pytest.approx(5.0) + + def test_linux_low_free_high_available_passes(analyzer): """MemFree alone looks tight; MemAvailable reflects reclaimable cache.""" model = MemoryDataModel( diff --git a/test/unit/plugin/test_nvme_analyzer.py b/test/unit/plugin/test_nvme_analyzer.py new file mode 100644 index 00000000..8f09a748 --- /dev/null +++ b/test/unit/plugin/test_nvme_analyzer.py @@ -0,0 +1,69 @@ +############################################################################### +# +# MIT License +# +# Copyright (c) 2025 Advanced Micro Devices, Inc. +# +############################################################################### +from nodescraper.enums import EventPriority, ExecutionStatus +from nodescraper.plugins.inband.nvme.analyzer_args import NvmeAnalyzerArgs +from nodescraper.plugins.inband.nvme.nvme_analyzer import NvmeAnalyzer +from nodescraper.plugins.inband.nvme.nvmedata import DeviceNvmeData, NvmeDataModel + + +def test_nvme_smart_media_errors_within_threshold(system_info): + analyzer = NvmeAnalyzer(system_info) + data = NvmeDataModel( + devices={ + "nvme0": DeviceNvmeData(smart_log="media_errors : 0"), + } + ) + + result = analyzer.analyze_data(data, NvmeAnalyzerArgs(maximum_smart_error_count=0)) + + assert result.status == ExecutionStatus.OK + assert not result.events + + +def test_nvme_smart_media_errors_exceed_threshold(system_info): + analyzer = NvmeAnalyzer(system_info) + data = NvmeDataModel( + devices={ + "nvme0": DeviceNvmeData(smart_log="media_errors : 2"), + } + ) + + result = analyzer.analyze_data(data, NvmeAnalyzerArgs(maximum_smart_error_count=0)) + + assert result.status == ExecutionStatus.ERROR + assert len(result.events) == 1 + assert result.events[0].priority == EventPriority.ERROR + assert result.events[0].data["media_errors"] == 2 + + +def test_nvme_smart_media_errors_missing_logs_warning(system_info): + analyzer = NvmeAnalyzer(system_info) + data = NvmeDataModel( + devices={ + "nvme0": DeviceNvmeData(smart_log="critical_warning : 0"), + } + ) + + result = analyzer.analyze_data(data, NvmeAnalyzerArgs(maximum_smart_error_count=0)) + + assert result.status == ExecutionStatus.OK + assert len(result.events) == 1 + assert result.events[0].priority == EventPriority.WARNING + + +def test_nvme_smart_check_not_run_without_threshold(system_info): + analyzer = NvmeAnalyzer(system_info) + data = NvmeDataModel( + devices={ + "nvme0": DeviceNvmeData(smart_log="media_errors : 2"), + } + ) + + result = analyzer.analyze_data(data, NvmeAnalyzerArgs()) + + assert result.status == ExecutionStatus.NOT_RAN diff --git a/test/unit/plugin/test_os_analyzer.py b/test/unit/plugin/test_os_analyzer.py index c872b121..04c98a69 100644 --- a/test/unit/plugin/test_os_analyzer.py +++ b/test/unit/plugin/test_os_analyzer.py @@ -88,6 +88,87 @@ def test_no_config_data(analyzer, model_obj): assert len(result.events) == 0 +def test_maximum_load_per_cpu_core_passes(analyzer): + model = OsDataModel( + os_name="Ubuntu 22.04.2 LTS", + load_average_1m=8.0, + cpu_count=4, + ) + + result = analyzer.analyze_data( + model, + OsAnalyzerArgs(maximum_load_per_cpu_core=2.0), + ) + + assert result.status == ExecutionStatus.OK + + +def test_os_name_and_maximum_load_per_cpu_core_pass(analyzer): + model = OsDataModel( + os_name="Ubuntu 22.04.2 LTS", + load_average_1m=8.0, + cpu_count=4, + ) + + result = analyzer.analyze_data( + model, + OsAnalyzerArgs( + exp_os="Ubuntu 22.04.2 LTS", + maximum_load_per_cpu_core=2.0, + ), + ) + + assert result.status == ExecutionStatus.OK + + +def test_os_name_mismatch_still_checks_load(analyzer): + model = OsDataModel( + os_name="Ubuntu 22.04.2 LTS", + load_average_1m=9.0, + cpu_count=4, + ) + + result = analyzer.analyze_data( + model, + OsAnalyzerArgs( + exp_os="RHEL 9", + maximum_load_per_cpu_core=2.0, + ), + ) + + assert result.status == ExecutionStatus.ERROR + assert len(result.events) == 2 + + +def test_maximum_load_per_cpu_core_fails(analyzer): + model = OsDataModel( + os_name="Ubuntu 22.04.2 LTS", + load_average_1m=9.0, + cpu_count=4, + ) + + result = analyzer.analyze_data( + model, + OsAnalyzerArgs(maximum_load_per_cpu_core=2.0), + ) + + assert result.status == ExecutionStatus.ERROR + assert len(result.events) == 1 + assert result.events[0].data["load_per_cpu_core"] == pytest.approx(2.25) + + +def test_maximum_load_per_cpu_core_missing_data(analyzer): + model = OsDataModel(os_name="Ubuntu 22.04.2 LTS") + + result = analyzer.analyze_data( + model, + OsAnalyzerArgs(maximum_load_per_cpu_core=2.0), + ) + + assert result.status == ExecutionStatus.WARNING + assert len(result.events) == 1 + + def test_invalid_os(analyzer, config): model = OsDataModel(os_name="some invalid os") args = OsAnalyzerArgs(exp_os=config["os_name"]) diff --git a/test/unit/plugin/test_os_collector.py b/test/unit/plugin/test_os_collector.py index 8ac921dc..af09bda9 100644 --- a/test/unit/plugin/test_os_collector.py +++ b/test/unit/plugin/test_os_collector.py @@ -26,6 +26,7 @@ import pytest from nodescraper.connection.inband.inband import CommandArtifact +from nodescraper.enums.eventpriority import EventPriority from nodescraper.enums.executionstatus import ExecutionStatus from nodescraper.enums.systeminteraction import SystemInteractionLevel from nodescraper.models.systeminfo import OSFamily @@ -82,16 +83,24 @@ def collector(system_info, conn_mock): ], ) def test_os_collector_linux( - collector, conn_mock, stdout, version_stdout, expected_os, expected_version + collector, conn_mock, system_info, stdout, version_stdout, expected_os, expected_version ): + system_info.os_family = OSFamily.LINUX conn_mock.run_command.side_effect = [ CommandArtifact(exit_code=0, stdout=stdout, stderr="", command="cmd1"), CommandArtifact(exit_code=0, stdout=version_stdout, stderr="", command="cmd2"), + CommandArtifact(exit_code=0, stdout="0.50 0.40 0.30 1/100 1234", stderr="", command="cmd3"), + CommandArtifact(exit_code=0, stdout="4", stderr="", command="cmd4"), ] result, data = collector.collect_data() assert result.status == ExecutionStatus.OK - assert data == OsDataModel(os_name=expected_os, os_version=expected_version) + assert data == OsDataModel( + os_name=expected_os, + os_version=expected_version, + load_average_1m=0.5, + cpu_count=4, + ) def test_os_collector_windows(system_info, conn_mock): @@ -130,6 +139,24 @@ def test_os_collector_error(collector, conn_mock, system_info): assert data is None +def test_os_collector_invalid_load_data(collector, conn_mock, system_info): + system_info.os_family = OSFamily.LINUX + conn_mock.run_command.side_effect = [ + CommandArtifact(exit_code=0, stdout="Ubuntu 22.04.4 LTS", stderr="", command="cmd1"), + CommandArtifact(exit_code=0, stdout='VERSION_ID="22.04"', stderr="", command="cmd2"), + CommandArtifact(exit_code=0, stdout="not-a-load", stderr="", command="cmd3"), + CommandArtifact(exit_code=0, stdout="not-a-cpu-count", stderr="", command="cmd4"), + ] + + _, data = collector.collect_data() + + assert data == OsDataModel( + os_name="Ubuntu 22.04.4 LTS", + os_version="22.04", + ) + assert sum(event.priority == EventPriority.WARNING for event in collector.result.events) == 2 + + def test_os_collector_esxi(collector, conn_mock, system_info): """ESXi: os_name from `vmware -v`, os_version from the esxcli 'Version:' field.""" system_info.os_family = OSFamily.ESXI From 8c2090cba8b2278574b51c7cacae80d7f21693f3 Mon Sep 17 00:00:00 2001 From: Jaspal Singh Date: Fri, 25 Sep 2026 17:07:04 +0000 Subject: [PATCH 2/2] fixed license banner --- .../plugins/inband/nvme/analyzer_args.py | 20 ++++++++++++++++++- .../plugins/inband/nvme/nvme_analyzer.py | 20 ++++++++++++++++++- test/unit/plugin/test_nvme_analyzer.py | 20 ++++++++++++++++++- 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/nodescraper/plugins/inband/nvme/analyzer_args.py b/nodescraper/plugins/inband/nvme/analyzer_args.py index 1850d0d2..24281034 100644 --- a/nodescraper/plugins/inband/nvme/analyzer_args.py +++ b/nodescraper/plugins/inband/nvme/analyzer_args.py @@ -2,7 +2,25 @@ # # MIT License # -# Copyright (c) 2025 Advanced Micro Devices, Inc. +# 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 diff --git a/nodescraper/plugins/inband/nvme/nvme_analyzer.py b/nodescraper/plugins/inband/nvme/nvme_analyzer.py index cfb22f1e..63d3df0e 100644 --- a/nodescraper/plugins/inband/nvme/nvme_analyzer.py +++ b/nodescraper/plugins/inband/nvme/nvme_analyzer.py @@ -2,7 +2,25 @@ # # MIT License # -# Copyright (c) 2025 Advanced Micro Devices, Inc. +# 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. # ############################################################################### import re diff --git a/test/unit/plugin/test_nvme_analyzer.py b/test/unit/plugin/test_nvme_analyzer.py index 8f09a748..5d88f409 100644 --- a/test/unit/plugin/test_nvme_analyzer.py +++ b/test/unit/plugin/test_nvme_analyzer.py @@ -2,7 +2,25 @@ # # MIT License # -# Copyright (c) 2025 Advanced Micro Devices, Inc. +# 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.enums import EventPriority, ExecutionStatus