From a044afc6b4dae5af868efc577ba9ae31839c4f79 Mon Sep 17 00:00:00 2001 From: Jaspal Singh Date: Fri, 11 Sep 2026 19:57:32 +0000 Subject: [PATCH] gpu vbios enhancement --- docs/PLUGIN_DOC.md | 2 +- .../inband/sys_settings/collector_args.py | 9 +++- .../sys_settings/sys_settings_collector.py | 24 +++++++++- .../unit/plugin/test_sys_settings_analyzer.py | 20 +++++++++ .../plugin/test_sys_settings_collector.py | 44 ++++++++++++++++--- 5 files changed, 90 insertions(+), 9 deletions(-) diff --git a/docs/PLUGIN_DOC.md b/docs/PLUGIN_DOC.md index eb7a3601..5fcad7d0 100644 --- a/docs/PLUGIN_DOC.md +++ b/docs/PLUGIN_DOC.md @@ -30,7 +30,7 @@ | StoragePlugin | sh -c 'df -lH -B1 | grep -v 'boot''
wmic LogicalDisk Where DriveType="3" Get DeviceId,Size,FreeSpace | **Analyzer Args:**
- `min_required_free_space_abs`: Optional[str] — Minimum required free space per mount (e.g. '10G', '1T').
- `min_required_free_space_prct`: Optional[int] — Minimum required free space as percentage of total (0–100).
- `ignore_devices`: Optional[list[str]] — Mount points or devices to exclude from free-space checks.
- `check_devices`: Optional[list[str]] — If non-empty, only these mount points or devices are checked.
- `regex_match`: bool — If True, match device/mount names with regex; otherwise exact match. | **Collection Args:**
- `html_view`: bool — When true, include logged command artifacts in command_artifacts.html using human-readable output.
- `skip_sudo`: bool — If True, do not use sudo when running df and related storage commands. | [StorageDataModel](#StorageDataModel-Model) | [StorageCollector](#Collector-Class-StorageCollector) | [StorageAnalyzer](#Data-Analyzer-Class-StorageAnalyzer) | | ScaleOutAristaPlugin | show interfaces counters bins | json | no-more
show interfaces counters queue | no-more
show interfaces counters queue drop-precedence | no-more
show qos interfaces ecn counters queue | json | no-more
show interfaces counters errors | json | no-more
show interfaces phy | no-more
show interfaces phy detail | no-more
show interfaces counters ip | json | no-more
show ip interface | no-more
show lldp | no-more
show lldp neighbors | json | no-more
show interfaces counters | json | no-more
show interfaces flow-control | json | no-more
show interfaces counters queue detail | no-more
show priority-flow-control counters | json | no-more
show priority-flow-control status | no-more
show interfaces status | json | no-more
show qos interfaces | no-more
show qos interfaces ecn | no-more
show qos interfaces trust | no-more
show qos maps | no-more
show qos profile | no-more
show qos profile summary | no-more
show interfaces counters rates | json | no-more
show running-config | no-more
show startup-config | no-more
show system environment cooling | json | no-more
show platform trident mmu queue status | no-more
show version | json | no-more | **Analyzer Args:**
- `analysis_ports`: Optional[List[str]] — Restrict per-port analysis to the given ports. Ports are S/P/[SP] where subport is optional (e.g. ['1/1', '1/31', '1/...
- `expected_port_bandwidth`: int — Expected interface bandwidth (bps) from show interfaces status (AristaPortStatus.bandwidth). Ports with a different b... | **Collection Args:**
- `html_view`: bool — When true, include logged command artifacts in command_artifacts.html using human-readable output. Re-runs successful... | [ScaleOutAristaDataModel](#ScaleOutAristaDataModel-Model) | [ScaleOutAristaCollector](#Collector-Class-ScaleOutAristaCollector) | [ScaleOutAristaAnalyzer](#Data-Analyzer-Class-ScaleOutAristaAnalyzer) | | ScaleOutDellPlugin | show alarm | no-more
show buffer pool | no-more
show buffer profile | no-more
show clock | no-more
show interface counters {port} | no-more
show event details | no-more
show interface fec status | no-more
show interface counters | no-more
show interface counters rate | no-more
show interface Eth | no-more
show interface phy counters | no-more
show interface status | no-more
show interface transceiver | no-more
show interface transceiver dom | no-more
show interface transceiver summary | no-more
show ip arp | no-more
show ip interfaces | no-more
show ip route | no-more
show lldp neighbor | no-more
show lldp table | no-more
show qos interface Ethall priority-flow-control statistics | no-more
show priority-flow-control watchdog | no-more
show qos interface Ethall queue all priority-flow-control watchdog-statistics | no-more
show platform environment | no-more
show platform firmware detail | no-more
show platform syseeprom | no-more
show qos interface Eth all | no-more
show qos interface Eth all queue all | no-more
show qos map dot1p-tc | no-more
show qos map dscp-tc | no-more
show qos map pfc-priority-pg | no-more
show qos map pfc-priority-queue | no-more
show qos map tc-dot1p | no-more
show qos map tc-dscp | no-more
show qos map tc-pg | no-more
show qos map tc-queue | no-more
show qos scheduler-policy | no-more
show qos wred-policy | no-more
show queue counters | no-more
show queue persistent-watermark multicast | no-more
show queue persistent-watermark unicast | no-more
show queue watermark multicast | no-more
show queue watermark unicast | no-more
show running-configuration | no-more
show version | no-more | **Analyzer Args:**
- `analysis_ports`: Optional[List[str]] — Restrict per-port analysis to the given ports. Accepts optional Eth prefix (e.g. ['1/1', '1/31', '1/1/1'] or ['Eth1/1...
- `expected_port_speed`: int — Expected interface speed (Mbps) from show interface status (DellInterfaceStatus.speed). Ports with a different speed... | **Collection Args:**
- `html_view`: bool — When true, include logged command artifacts in command_artifacts.html using human-readable output.
- `collection_ports`: Optional[List[str]] — Restrict detail counter collection to these ports. Accepts the same tokens as analysis_ports (e.g. ['1/1', '1/1/2'] o... | [ScaleOutDellDataModel](#ScaleOutDellDataModel-Model) | [ScaleOutDellCollector](#Collector-Class-ScaleOutDellCollector) | [ScaleOutDellAnalyzer](#Data-Analyzer-Class-ScaleOutDellAnalyzer) | -| SysSettingsPlugin | cat /sys/{}
ls -1 /sys/{}
ls -l /sys/{} | **Analyzer Args:**
- `checks`: Optional[list[nodescraper.plugins.inband.sys_settings.analyzer_args.SysfsCheck]] — List of sysfs checks (path, expected values or pattern, display name). | **Collection Args:**
- `html_view`: bool — When true, include logged command artifacts in command_artifacts.html using human-readable output.
- `paths`: list[str] — Sysfs paths to read (cat). Paths with '\*' are collected with ls -l (e.g. class/net/\*/device).
- `directory_paths`: list[str] — Sysfs paths to list (ls -1); used for checks that match entry names by regex. | [SysSettingsDataModel](#SysSettingsDataModel-Model) | [SysSettingsCollector](#Collector-Class-SysSettingsCollector) | [SysSettingsAnalyzer](#Data-Analyzer-Class-SysSettingsAnalyzer) | +| SysSettingsPlugin | cat /sys/{}
ls -1 /sys/{}
ls -l /sys/{} | **Analyzer Args:**
- `checks`: Optional[list[nodescraper.plugins.inband.sys_settings.analyzer_args.SysfsCheck]] — List of sysfs checks (path, expected values or pattern, display name). | **Collection Args:**
- `html_view`: bool — When true, include logged command artifacts in command_artifacts.html using human-readable output.
- `paths`: list[str] — Sysfs paths to read (cat). Paths with '\*' are cat'd via bash (e.g. class/drm/card\*/device/vbios_version); if cat fails, ls -l is used.
- `directory_paths`: list[str] — Sysfs paths to list (ls -1); used for checks that match entry names by regex. | [SysSettingsDataModel](#SysSettingsDataModel-Model) | [SysSettingsCollector](#Collector-Class-SysSettingsCollector) | [SysSettingsAnalyzer](#Data-Analyzer-Class-SysSettingsAnalyzer) | | SysctlPlugin | sysctl -n | **Analyzer Args:**
- `exp_vm_swappiness`: Optional[int] — Expected vm.swappiness value.
- `exp_vm_numa_balancing`: Optional[int] — Expected vm.numa_balancing value.
- `exp_vm_oom_kill_allocating_task`: Optional[int] — Expected vm.oom_kill_allocating_task value.
- `exp_vm_compaction_proactiveness`: Optional[int] — Expected vm.compaction_proactiveness value.
- `exp_vm_compact_unevictable_allowed`: Optional[int] — Expected vm.compact_unevictable_allowed value.
- `exp_vm_extfrag_threshold`: Optional[int] — Expected vm.extfrag_threshold value.
- `exp_vm_zone_reclaim_mode`: Optional[int] — Expected vm.zone_reclaim_mode value.
- `exp_vm_dirty_background_ratio`: Optional[int] — Expected vm.dirty_background_ratio value.
- `exp_vm_dirty_ratio`: Optional[int] — Expected vm.dirty_ratio value.
- `exp_vm_dirty_writeback_centisecs`: Optional[int] — Expected vm.dirty_writeback_centisecs value.
- `exp_kernel_numa_balancing`: Optional[int] — Expected kernel.numa_balancing value. | - | [SysctlDataModel](#SysctlDataModel-Model) | [SysctlCollector](#Collector-Class-SysctlCollector) | [SysctlAnalyzer](#Data-Analyzer-Class-SysctlAnalyzer) | | SyslogPlugin | ls -1 /var/log/syslog\* 2>/dev/null | grep -E '^/var/log/syslog(\.[0-9]+(\.gz)?)?$' || true
ls -1 /var/log/messages\* 2>/dev/null | grep -E '^/var/log/messages(\.[0-9]+(\.gz)?)?$' || true | - | - | [SyslogData](#SyslogData-Model) | [SyslogCollector](#Collector-Class-SyslogCollector) | - | | UptimePlugin | uptime | - | - | [UptimeDataModel](#UptimeDataModel-Model) | [UptimeCollector](#Collector-Class-UptimeCollector) | - | diff --git a/nodescraper/plugins/inband/sys_settings/collector_args.py b/nodescraper/plugins/inband/sys_settings/collector_args.py index 7e49f228..7fe0bdad 100644 --- a/nodescraper/plugins/inband/sys_settings/collector_args.py +++ b/nodescraper/plugins/inband/sys_settings/collector_args.py @@ -31,13 +31,18 @@ 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). + paths: sysfs paths to read (cat). If a path contains '*', collect with + ``cat`` first (e.g. class/drm/card*/device/vbios_version). If cat fails, + fall back to ls -l (e.g. class/net/*/device). directory_paths: sysfs paths to list (ls -1); use for checks that match entry names by regex. """ paths: list[str] = Field( default_factory=list, - description="Sysfs paths to read (cat). Paths with '*' are collected with ls -l (e.g. class/net/*/device).", + description=( + "Sysfs paths to read (cat). Paths with '*' are cat'd via bash " + "(e.g. class/drm/card*/device/vbios_version); if cat fails, ls -l is used." + ), ) directory_paths: list[str] = Field( default_factory=list, diff --git a/nodescraper/plugins/inband/sys_settings/sys_settings_collector.py b/nodescraper/plugins/inband/sys_settings/sys_settings_collector.py index af51a021..612082a0 100644 --- a/nodescraper/plugins/inband/sys_settings/sys_settings_collector.py +++ b/nodescraper/plugins/inband/sys_settings/sys_settings_collector.py @@ -94,6 +94,17 @@ def _sysfs_full_path(suffix: str) -> str: return f"/sys/{suffix}" +def _collapse_glob_cat(stdout: str) -> str: + """Keep unique non-empty lines; store a single value when every match is identical.""" + lines = [line.strip() for line in (stdout or "").splitlines() if line.strip()] + unique = list(dict.fromkeys(lines)) + if not unique: + return "" + if len(unique) == 1: + return unique[0] + return "\n".join(unique) + + class SysSettingsCollector(InBandDataCollector[SysSettingsDataModel, SysSettingsCollectorArgs]): """Collect sysfs settings from user-specified paths.""" @@ -109,6 +120,9 @@ def collect_data( ) -> tuple[TaskResult, Optional[SysSettingsDataModel]]: """Collect sysfs values for each path in args.paths. + Glob paths (containing '*') are read with ``bash -c 'cat /sys/...'`` first. + Identical cat lines are collapsed to one value. If cat fails, ``ls -l`` is used. + Args: args: Collector args with paths to read; if None or empty paths, returns NOT_RAN. @@ -153,6 +167,14 @@ def collect_data( continue full_path = _sysfs_full_path(suffix) if "*" in suffix: + cat_cmd = self.CMD.format(suffix) + res = self._run_sut_cmd(f"bash -c {cat_cmd!r}", sudo=False) + if res.exit_code == 0 and res.stdout and res.stdout.strip(): + value = _collapse_glob_cat(res.stdout) + if "\n" not in value: + value = _parse_bracketed_setting(value) or value + readings[full_path] = value + continue cmd = self.CMD_LS_LONG.format(suffix) res = self._run_sut_cmd(f"bash -c {cmd!r}", sudo=False) if res.exit_code == 0: @@ -160,7 +182,7 @@ def collect_data( else: self._log_event( category=EventCategory.OS, - description=f"Failed to run ls -l for sysfs path: {full_path}", + description=f"Failed to read sysfs path: {full_path}", data={"exit_code": res.exit_code}, priority=EventPriority.WARNING, console_log=True, diff --git a/test/unit/plugin/test_sys_settings_analyzer.py b/test/unit/plugin/test_sys_settings_analyzer.py index 318093c4..17e4b256 100644 --- a/test/unit/plugin/test_sys_settings_analyzer.py +++ b/test/unit/plugin/test_sys_settings_analyzer.py @@ -105,3 +105,23 @@ def test_analyzer_unknown_path(analyzer, sample_data): assert result.status == ExecutionStatus.ERROR assert "mismatch" in result.message.lower() assert "unknown" in result.message + + +def test_analyzer_gpu_vbios_match(analyzer): + path = "/sys/class/drm/card*/device/vbios_version" + data = SysSettingsDataModel(readings={path: "113-M355-01-1K1-040C"}) + args = SysSettingsAnalyzerArgs( + checks=[SysfsCheck(path=path, expected=["113-M355-01-1K1-040C"], name="gpu_vbios")] + ) + result = analyzer.analyze_data(data, args) + assert result.status == ExecutionStatus.OK + + +def test_analyzer_gpu_vbios_mismatch(analyzer): + path = "/sys/class/drm/card*/device/vbios_version" + data = SysSettingsDataModel(readings={path: "wrong"}) + args = SysSettingsAnalyzerArgs( + checks=[SysfsCheck(path=path, expected=["113-M355-01-1K1-040C"], name="gpu_vbios")] + ) + result = analyzer.analyze_data(data, args) + assert result.status == ExecutionStatus.ERROR diff --git a/test/unit/plugin/test_sys_settings_collector.py b/test/unit/plugin/test_sys_settings_collector.py index b2da8b2e..555b79ee 100644 --- a/test/unit/plugin/test_sys_settings_collector.py +++ b/test/unit/plugin/test_sys_settings_collector.py @@ -165,7 +165,7 @@ def run_cmd(cmd, **kwargs): assert "/etc" not in str(seen_commands) -def test_collect_data_glob_path_uses_ls_long(linux_sys_settings_collector): +def test_collect_data_glob_path_falls_back_to_ls_long(linux_sys_settings_collector): seen_commands = [] def run_cmd(cmd, **kwargs): @@ -174,7 +174,7 @@ def run_cmd(cmd, **kwargs): return make_artifact( 0, "lrwxrwxrwx 1 root root 0 Jan 1 00:00 device -> ../../pci0000:00/0000:00:01.0" ) - return make_artifact(0, "[always] madvise never") + return make_artifact(1, "") linux_sys_settings_collector._run_sut_cmd = run_cmd args = {"paths": ["class/net/*/device"]} @@ -182,9 +182,8 @@ def run_cmd(cmd, **kwargs): assert result.status == ExecutionStatus.OK assert data is not None - assert len(seen_commands) == 1 - assert "ls -l /sys/class/net/*/device" in seen_commands[0] - assert "bash -c" in seen_commands[0] + assert any("cat /sys/class/net/*/device" in cmd for cmd in seen_commands) + assert any("ls -l /sys/class/net/*/device" in cmd for cmd in seen_commands) assert data.readings.get("/sys/class/net/*/device") == ( "lrwxrwxrwx 1 root root 0 Jan 1 00:00 device -> ../../pci0000:00/0000:00:01.0" ) @@ -195,6 +194,8 @@ def test_collect_data_mixed_paths_cat_and_glob(linux_sys_settings_collector): def run_cmd(cmd, **kwargs): if "ls -l" in cmd: return make_artifact(0, "lrwx 1 root root 0 device -> ../../device") + if "net/*" in cmd: + return make_artifact(1, "") if "enabled" in cmd: return make_artifact(0, "[always] madvise never") return make_artifact(0, "[madvise] always never defer") @@ -213,3 +214,36 @@ def run_cmd(cmd, **kwargs): data.readings.get("/sys/class/net/*/device") == "lrwx 1 root root 0 device -> ../../device" ) assert "Sysfs collected 3 path(s)" in result.message + + +VBIOS_PATH = "/sys/class/drm/card*/device/vbios_version" +VBIOS_VALUE = "113-M355-01-1K1-040C" + + +def test_collect_data_glob_cat_vbios_collapses_identical_lines(linux_sys_settings_collector): + seen_commands = [] + + def run_cmd(cmd, **kwargs): + seen_commands.append(cmd) + if "cat /sys/class/drm/card*/device/vbios_version" in cmd: + return make_artifact(0, f"{VBIOS_VALUE}\n{VBIOS_VALUE}\n{VBIOS_VALUE}\n") + return make_artifact(1, "") + + linux_sys_settings_collector._run_sut_cmd = run_cmd + result, data = linux_sys_settings_collector.collect_data({"paths": [VBIOS_PATH]}) + + assert result.status == ExecutionStatus.OK + assert data.readings.get(VBIOS_PATH) == VBIOS_VALUE + assert any("cat /sys/class/drm/card*/device/vbios_version" in cmd for cmd in seen_commands) + assert not any("ls -l" in cmd for cmd in seen_commands) + + +def test_collect_data_glob_cat_vbios_keeps_mixed_versions(linux_sys_settings_collector): + def run_cmd(cmd, **kwargs): + return make_artifact(0, f"{VBIOS_VALUE}\n113-OTHER-0000\n") + + linux_sys_settings_collector._run_sut_cmd = run_cmd + result, data = linux_sys_settings_collector.collect_data({"paths": [VBIOS_PATH]}) + + assert result.status == ExecutionStatus.OK + assert data.readings.get(VBIOS_PATH) == f"{VBIOS_VALUE}\n113-OTHER-0000"