Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/PLUGIN_DOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
| StoragePlugin | sh -c 'df -lH -B1 &#124; grep -v 'boot''<br>wmic LogicalDisk Where DriveType="3" Get DeviceId,Size,FreeSpace | **Analyzer Args:**<br>- `min_required_free_space_abs`: Optional[str] — Minimum required free space per mount (e.g. '10G', '1T').<br>- `min_required_free_space_prct`: Optional[int] — Minimum required free space as percentage of total (0–100).<br>- `ignore_devices`: Optional[list[str]] — Mount points or devices to exclude from free-space checks.<br>- `check_devices`: Optional[list[str]] — If non-empty, only these mount points or devices are checked.<br>- `regex_match`: bool — If True, match device/mount names with regex; otherwise exact match. | **Collection Args:**<br>- `html_view`: bool — When true, include logged command artifacts in command_artifacts.html using human-readable output.<br>- `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 &#124; json &#124; no-more<br>show interfaces counters queue &#124; no-more<br>show interfaces counters queue drop-precedence &#124; no-more<br>show qos interfaces ecn counters queue &#124; json &#124; no-more<br>show interfaces counters errors &#124; json &#124; no-more<br>show interfaces phy &#124; no-more<br>show interfaces phy detail &#124; no-more<br>show interfaces counters ip &#124; json &#124; no-more<br>show ip interface &#124; no-more<br>show lldp &#124; no-more<br>show lldp neighbors &#124; json &#124; no-more<br>show interfaces counters &#124; json &#124; no-more<br>show interfaces flow-control &#124; json &#124; no-more<br>show interfaces counters queue detail &#124; no-more<br>show priority-flow-control counters &#124; json &#124; no-more<br>show priority-flow-control status &#124; no-more<br>show interfaces status &#124; json &#124; no-more<br>show qos interfaces &#124; no-more<br>show qos interfaces ecn &#124; no-more<br>show qos interfaces trust &#124; no-more<br>show qos maps &#124; no-more<br>show qos profile &#124; no-more<br>show qos profile summary &#124; no-more<br>show interfaces counters rates &#124; json &#124; no-more<br>show running-config &#124; no-more<br>show startup-config &#124; no-more<br>show system environment cooling &#124; json &#124; no-more<br>show platform trident mmu queue status &#124; no-more<br>show version &#124; json &#124; no-more | **Analyzer Args:**<br>- `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/...<br>- `expected_port_bandwidth`: int — Expected interface bandwidth (bps) from show interfaces status (AristaPortStatus.bandwidth). Ports with a different b... | **Collection Args:**<br>- `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 &#124; no-more<br>show buffer pool &#124; no-more<br>show buffer profile &#124; no-more<br>show clock &#124; no-more<br>show interface counters {port} &#124; no-more<br>show event details &#124; no-more<br>show interface fec status &#124; no-more<br>show interface counters &#124; no-more<br>show interface counters rate &#124; no-more<br>show interface Eth &#124; no-more<br>show interface phy counters &#124; no-more<br>show interface status &#124; no-more<br>show interface transceiver &#124; no-more<br>show interface transceiver dom &#124; no-more<br>show interface transceiver summary &#124; no-more<br>show ip arp &#124; no-more<br>show ip interfaces &#124; no-more<br>show ip route &#124; no-more<br>show lldp neighbor &#124; no-more<br>show lldp table &#124; no-more<br>show qos interface Ethall priority-flow-control statistics &#124; no-more<br>show priority-flow-control watchdog &#124; no-more<br>show qos interface Ethall queue all priority-flow-control watchdog-statistics &#124; no-more<br>show platform environment &#124; no-more<br>show platform firmware detail &#124; no-more<br>show platform syseeprom &#124; no-more<br>show qos interface Eth all &#124; no-more<br>show qos interface Eth all queue all &#124; no-more<br>show qos map dot1p-tc &#124; no-more<br>show qos map dscp-tc &#124; no-more<br>show qos map pfc-priority-pg &#124; no-more<br>show qos map pfc-priority-queue &#124; no-more<br>show qos map tc-dot1p &#124; no-more<br>show qos map tc-dscp &#124; no-more<br>show qos map tc-pg &#124; no-more<br>show qos map tc-queue &#124; no-more<br>show qos scheduler-policy &#124; no-more<br>show qos wred-policy &#124; no-more<br>show queue counters &#124; no-more<br>show queue persistent-watermark multicast &#124; no-more<br>show queue persistent-watermark unicast &#124; no-more<br>show queue watermark multicast &#124; no-more<br>show queue watermark unicast &#124; no-more<br>show running-configuration &#124; no-more<br>show version &#124; no-more | **Analyzer Args:**<br>- `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...<br>- `expected_port_speed`: int — Expected interface speed (Mbps) from show interface status (DellInterfaceStatus.speed). Ports with a different speed... | **Collection Args:**<br>- `html_view`: bool — When true, include logged command artifacts in command_artifacts.html using human-readable output.<br>- `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/{}<br>ls -1 /sys/{}<br>ls -l /sys/{} | **Analyzer Args:**<br>- `checks`: Optional[list[nodescraper.plugins.inband.sys_settings.analyzer_args.SysfsCheck]] — List of sysfs checks (path, expected values or pattern, display name). | **Collection Args:**<br>- `html_view`: bool — When true, include logged command artifacts in command_artifacts.html using human-readable output.<br>- `paths`: list[str] — Sysfs paths to read (cat). Paths with '\*' are collected with ls -l (e.g. class/net/\*/device).<br>- `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/{}<br>ls -1 /sys/{}<br>ls -l /sys/{} | **Analyzer Args:**<br>- `checks`: Optional[list[nodescraper.plugins.inband.sys_settings.analyzer_args.SysfsCheck]] — List of sysfs checks (path, expected values or pattern, display name). | **Collection Args:**<br>- `html_view`: bool — When true, include logged command artifacts in command_artifacts.html using human-readable output.<br>- `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.<br>- `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:**<br>- `exp_vm_swappiness`: Optional[int] — Expected vm.swappiness value.<br>- `exp_vm_numa_balancing`: Optional[int] — Expected vm.numa_balancing value.<br>- `exp_vm_oom_kill_allocating_task`: Optional[int] — Expected vm.oom_kill_allocating_task value.<br>- `exp_vm_compaction_proactiveness`: Optional[int] — Expected vm.compaction_proactiveness value.<br>- `exp_vm_compact_unevictable_allowed`: Optional[int] — Expected vm.compact_unevictable_allowed value.<br>- `exp_vm_extfrag_threshold`: Optional[int] — Expected vm.extfrag_threshold value.<br>- `exp_vm_zone_reclaim_mode`: Optional[int] — Expected vm.zone_reclaim_mode value.<br>- `exp_vm_dirty_background_ratio`: Optional[int] — Expected vm.dirty_background_ratio value.<br>- `exp_vm_dirty_ratio`: Optional[int] — Expected vm.dirty_ratio value.<br>- `exp_vm_dirty_writeback_centisecs`: Optional[int] — Expected vm.dirty_writeback_centisecs value.<br>- `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 &#124; grep -E '^/var/log/syslog(\.[0-9]+(\.gz)?)?$' &#124;&#124; true<br>ls -1 /var/log/messages\* 2>/dev/null &#124; grep -E '^/var/log/messages(\.[0-9]+(\.gz)?)?$' &#124;&#124; true | - | - | [SyslogData](#SyslogData-Model) | [SyslogCollector](#Collector-Class-SyslogCollector) | - |
| UptimePlugin | uptime | - | - | [UptimeDataModel](#UptimeDataModel-Model) | [UptimeCollector](#Collector-Class-UptimeCollector) | - |
Expand Down
9 changes: 7 additions & 2 deletions nodescraper/plugins/inband/sys_settings/collector_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand All @@ -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.

Expand Down Expand Up @@ -153,14 +167,22 @@ 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:
readings[full_path] = res.stdout.strip() if res.stdout else ""
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,
Expand Down
20 changes: 20 additions & 0 deletions test/unit/plugin/test_sys_settings_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
44 changes: 39 additions & 5 deletions test/unit/plugin/test_sys_settings_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -174,17 +174,16 @@ 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"]}
result, data = linux_sys_settings_collector.collect_data(args)

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"
)
Expand All @@ -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")
Expand All @@ -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"
Loading