Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
8b5708b
xgmi check update
alexandraBara Mar 13, 2026
18dc410
Merge branch 'development' into alex_amdsmi_fix
alexandraBara Mar 16, 2026
7e914cb
README updates
alexandraBara Mar 16, 2026
b0fd81f
undid change
alexandraBara Mar 16, 2026
8c6769e
automated updating the README <node-scraper -h> output through doc ge…
alexandraBara Mar 16, 2026
b0c8361
Merge pull request #160 from amd/alex_amdsmi_fix
alexandraBara Mar 16, 2026
9668134
Merge branch 'development' into alex_readme
alexandraBara Mar 17, 2026
8ff63df
concurrent endpoint fetches + redfish tree get
alexandraBara Mar 17, 2026
1e07aeb
more detailed vars
alexandraBara Mar 17, 2026
83a127b
Merge pull request #161 from amd/alex_readme
alexandraBara Mar 17, 2026
1501f06
Merge branch 'development' into alex_var_updates
alexandraBara Mar 17, 2026
6f364e4
docs: Update plugin documentation [automated]
github-actions[bot] Mar 17, 2026
a7f8f1b
fix for cell shift
alexandraBara Mar 17, 2026
5dc0c55
conuccrency for endpoints + redfish tree check
alexandraBara Mar 17, 2026
28f392d
conuccrency for endpoints + redfish tree check
alexandraBara Mar 17, 2026
f46f478
Added ethtool analyzer on matching fields
sunnyhe2 Mar 17, 2026
4f0e7aa
Merge pull request #162 from amd/automated-plugin-docs-update
alexandraBara Mar 18, 2026
5ff55b4
Merge branch 'development' into alex_var_updates
alexandraBara Mar 18, 2026
5697b1a
Merge pull request #163 from amd/alex_var_updates
alexandraBara Mar 18, 2026
afad8da
renamed vars and moved inside the class so they get picked up by doc …
alexandraBara Mar 19, 2026
27350b0
fixes + utest update
alexandraBara Mar 19, 2026
2456858
Merge pull request #165 from amd/alex_doc_fix
alexandraBara Mar 20, 2026
c0e080d
docs: Update plugin documentation [automated]
github-actions[bot] Mar 20, 2026
d8493d0
Merge pull request #166 from amd/automated-plugin-docs-update
alexandraBara Mar 20, 2026
b46423e
functional test fix
alexandraBara Mar 20, 2026
ffda560
Merge branch 'development' into alex_redfish_endpoings_update
alexandraBara Mar 20, 2026
df6e9d6
network ethtool unit tests
sunnyhe2 Mar 20, 2026
1c2cf11
docs: Update plugin documentation [automated]
github-actions[bot] Mar 22, 2026
2a9fe4b
Merge pull request #168 from amd/automated-plugin-docs-update
alexandraBara Mar 23, 2026
9a02310
Merge branch 'development' into alex_redfish_endpoings_update
alexandraBara Mar 23, 2026
83ae777
Change to error_regex and moved to AnalyzerArgs
sunnyhe2 Mar 23, 2026
32b0777
docs: Update plugin documentation [automated]
github-actions[bot] Mar 24, 2026
00e30b7
Merge pull request #164 from amd/alex_redfish_endpoings_update
alexandraBara Mar 24, 2026
6cd5501
Merge branch 'development' into automated-plugin-docs-update
alexandraBara Mar 24, 2026
681d493
Merge pull request #169 from amd/automated-plugin-docs-update
alexandraBara Mar 24, 2026
3270389
added determistic ordering for frozenset
alexandraBara Mar 25, 2026
4546bb8
Merge pull request #172 from amd/alex_doc_forzenset
alexandraBara Mar 26, 2026
379d2b1
Add non-blocking plugin convention checks pre-commit hook
Mar 30, 2026
644e7c8
README update + templates added
alexandraBara Mar 31, 2026
09be837
SECURITY.md
alexandraBara Mar 31, 2026
9b76d9a
fix
alexandraBara Mar 31, 2026
bc61e69
pydantic for args
sunnyhe2 Mar 31, 2026
a073402
run-plugins -h test
Apr 2, 2026
9a419de
Merge pull request #175 from amd/alex_fix
alexandraBara Apr 2, 2026
ede2474
Merge branch 'development' into alex_doc_updates
alexandraBara Apr 2, 2026
5d6ed23
Merge pull request #174 from amd/alex_doc_updates
alexandraBara Apr 2, 2026
7bf440c
docs: Update plugin documentation [automated]
github-actions[bot] Apr 3, 2026
29d758f
Merge pull request #177 from amd/automated-plugin-docs-update
alexandraBara Apr 6, 2026
ef5a6af
moved plugin convention script
Apr 6, 2026
f74981b
moved plugin convention script
Apr 6, 2026
b0cf67b
removed comment
Apr 6, 2026
e282e05
amdsmi update
Apr 6, 2026
a12aafc
Merge branch 'development' into sunny_ethtool_analyzer
alexandraBara Apr 7, 2026
bb90eb2
Merge pull request #167 from amd/sunny_ethtool_analyzer
alexandraBara Apr 7, 2026
8b04125
added doc string
Apr 7, 2026
9de52f0
docs: Update plugin documentation [automated]
github-actions[bot] Apr 8, 2026
ef8abd7
--no-console-log
alexandraBara Apr 8, 2026
0c60fbb
fix
alexandraBara Apr 8, 2026
20b6dc8
extra utest to test every subcommand
alexandraBara Apr 8, 2026
20bd813
Merge branch 'development' into alex_no_console_log
alexandraBara Apr 8, 2026
5377210
Merge pull request #180 from amd/automated-plugin-docs-update
alexandraBara Apr 8, 2026
3ebb076
Merge branch 'development' into jaspals_precommitchecks
alexandraBara Apr 8, 2026
e561b83
Merge pull request #178 from amd/jaspals_precommitchecks
alexandraBara Apr 8, 2026
b3e8052
Merge branch 'development' into alex_no_console_log
alexandraBara Apr 9, 2026
8cbed61
expected-firmware-versions arg for matching any id with any version
jaspals3123 Apr 9, 2026
e7b8205
collector fix
Apr 9, 2026
3883cb7
build_amd_smi_analysis_ref method fix
Apr 9, 2026
72b5a27
model fix
Apr 9, 2026
43373b5
RegexSearchPlugin
alexandraBara Apr 9, 2026
12a396e
improved wording
alexandraBara Apr 9, 2026
8b8bc8f
Merge pull request #181 from amd/alex_no_console_log
alexandraBara Apr 9, 2026
1590468
Merge branch 'development' into alex_regex_plugin
alexandraBara Apr 9, 2026
0f09d0d
Merge branch 'development' into jaspal_amdmiupdate
alexandraBara Apr 9, 2026
cc5422a
utest upadte
alexandraBara Apr 9, 2026
51026bc
fix to show description for --data
alexandraBara Apr 9, 2026
f42ea32
docs: Update plugin documentation [automated]
github-actions[bot] Apr 10, 2026
d51fe73
Merge pull request #183 from amd/automated-plugin-docs-update
alexandraBara Apr 10, 2026
9c8d6f9
Merge branch 'development' into alex_regex_plugin
alexandraBara Apr 13, 2026
f5fb68f
Merge branch 'development' into jaspal_amdmiupdate
alexandraBara Apr 13, 2026
5fe4fb5
NodeStatus + plugin update
alexandraBara Apr 13, 2026
1bcb30b
Merge pull request #179 from amd/jaspal_amdmiupdate
alexandraBara Apr 13, 2026
567a307
Merge branch 'development' into alex_regex_plugin
alexandraBara Apr 13, 2026
3d77f29
fix for flay test
alexandraBara Apr 13, 2026
4dc5981
Merge pull request #185 from amd/alex_import_fix
alexandraBara Apr 13, 2026
9a96af6
Merge branch 'development' into alex_readme_nodestatus
alexandraBara Apr 13, 2026
3d29a0b
Merge pull request #184 from amd/alex_readme_nodestatus
alexandraBara Apr 13, 2026
023d2b7
Merge branch 'development' into alex_regex_plugin
alexandraBara Apr 13, 2026
00b62f7
Merge pull request #182 from amd/alex_regex_plugin
alexandraBara Apr 13, 2026
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
72 changes: 72 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Bug report
description: Report a reproducible problem with Node Scraper
title: "[Bug]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report a bug. Please include enough detail to reproduce the issue.
- type: textarea
id: summary
attributes:
label: Summary
description: What happened and what did you expect to happen?
placeholder: Clear and concise description of the bug.
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
description: Minimal, concrete steps to reproduce the issue.
placeholder: |
1. ...
2. ...
3. ...
validations:
required: true
- type: textarea
id: logs
attributes:
label: CLI output / logs
description: Paste relevant output (redact secrets/tokens/hosts as needed).
render: shell
validations:
required: false
- type: input
id: version
attributes:
label: Version
description: Output of `node-scraper --version` (or the git commit hash).
placeholder: "e.g. 0.6.1"
validations:
required: false
- type: dropdown
id: install_method
attributes:
label: Install method
options:
- PyPI (pip install amd-node-scraper)
- Editable install from source (pip install -e .)
- Other
validations:
required: false
- type: input
id: python
attributes:
label: Python version
placeholder: "e.g. 3.11.8"
validations:
required: false
- type: textarea
id: environment
attributes:
label: Environment
description: OS, target system type (LOCAL/REMOTE), and anything else relevant.
placeholder: |
OS:
Target:
Notes:
validations:
required: false
8 changes: 8 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Questions
url: https://github.com/amd/node-scraper/issues
about: If you're not sure it's a bug/feature request yet, start with an issue and add details.
- name: Security reports
url: https://github.com/amd/node-scraper/security/policy
about: Please report security issues privately (see SECURITY.md).
38 changes: 38 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Feature request
description: Suggest an enhancement or new capability
title: "[Feature]: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thanks for the suggestion. Please describe the problem you're trying to solve and what success looks like.
- type: textarea
id: problem
attributes:
label: Problem / motivation
description: What problem are you trying to solve?
placeholder: "I want to..."
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed solution
description: What would you like Node Scraper to do?
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: What else have you tried or considered?
validations:
required: false
- type: textarea
id: scope
attributes:
label: Scope / impact
description: What plugins/OSes/environments would this affect?
validations:
required: false
12 changes: 12 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## Summary
-

## Test plan
- [ ] `pytest test/unit`
- [ ] `pytest test/functional` (if applicable)
- [ ] `pre-commit run --all-files`

## Checklist
- [ ] Added/updated tests (or explained why not)
- [ ] Updated docs/README if behavior changed
- [ ] No secrets or credentials committed
261 changes: 261 additions & 0 deletions .github/scripts/plugin_convention_warnings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
#!/usr/bin/env python3
"""Checks conventions under ``nodescraper/plugins`` (stderr warnings only; non-blocking).

1. **Command strings in collectors/analyzers** , for ``Collector``
or ``Analyzer`` classes: a *class-level* assignment to a string (or f-string) that
looks like a shell/CLI invocation must use the name ``CMD`` or
``CMD_<suffix>`` (e.g. ``CMD_LIST``). Names starting with ``_`` and names
listed in ``_CMD_CHECK_SKIP_NAMES`` are ignored; see
``_looks_like_shell_command_literal`` for what counts as command-like.

2. **Args models** — In ``collector_args.py`` and ``analyzer_args.py``,
for classes named ``*Args`` that subclass ``BaseModel``, ``CollectorArgs``,
``AnalyzerArgs``, or another ``*Args``: each public field should assign
``pydantic.Field(...)`` with a non-empty ``description=`` (for help/CLI
text). ``ClassVar`` fields, ``_``-prefixed names, and ``model_config`` are
skipped.
"""

from __future__ import annotations

import ast
import re
import sys
from pathlib import Path

_REPO_ROOT = Path(__file__).resolve().parent.parent.parent
PLUGIN_ROOT = _REPO_ROOT / "nodescraper" / "plugins"

# Class-level names in collectors/analyzers that are not shell-command strings.
_CMD_CHECK_SKIP_NAMES = frozenset(
{
"AMD_SMI_EXE",
"DATA_MODEL",
"SUPPORTED_OS_FAMILY",
"COLLECTOR",
"ANALYZER",
"COLLECTOR_ARGS",
"ANALYZER_ARGS",
"TYPE_CHECKING",
}
)


def _is_stringish(expr: ast.expr) -> bool:
if isinstance(expr, ast.Constant) and isinstance(expr.value, str):
return True
if isinstance(expr, ast.JoinedStr):
return True
return False


def _stringish_preview(expr: ast.expr) -> str | None:
"""Best-effort static string for command-like heuristics (f-strings may be partial)."""
if isinstance(expr, ast.Constant) and isinstance(expr.value, str):
return expr.value
if isinstance(expr, ast.JoinedStr):
parts: list[str] = []
for elt in expr.values:
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
parts.append(elt.value)
else:
parts.append("\x00") # dynamic segment
return "".join(parts) if parts else ""
return None


def _looks_like_shell_command_literal(s: str) -> bool:
"""True if this class-level string is plausibly a shell/CLI invocation (not IDs, tokens, paths)."""
s = s.strip()
if not s:
return False
if re.fullmatch(r"0x[0-9a-fA-F]+", s):
return False
# OS / config tokens such as PRETTY_NAME, VERSION_ID
if re.fullmatch(r"[A-Z][A-Z0-9_]+", s):
return False
# Filenames / simple paths (no shell metacharacters)
if "." in s and not re.search(r"[\s|;&$`]", s):
return False
if re.search(r"[\s|;&$`<>]", s):
return True
# Typical one-word inband commands: uptime, sysctl, dmesg, amd-smi, etc.
if re.fullmatch(r"[a-z][a-z0-9_.-]*", s, flags=re.IGNORECASE):
return True
return False


def _base_name(node: ast.expr) -> str | None:
if isinstance(node, ast.Name):
return node.id
if isinstance(node, ast.Subscript):
return _base_name(node.value)
if isinstance(node, ast.Attribute):
return node.attr
return None


def _is_collector_or_analyzer_class(cls: ast.ClassDef) -> bool:
return cls.name.endswith("Collector") or cls.name.endswith("Analyzer")


def _field_call_name(func: ast.expr) -> bool:
if isinstance(func, ast.Name) and func.id == "Field":
return True
if isinstance(func, ast.Attribute) and func.attr == "Field":
return True
return False


def _field_has_nonempty_description(call: ast.Call) -> bool:
for kw in call.keywords:
if kw.arg != "description" or kw.value is None:
continue
v = kw.value
if isinstance(v, ast.Constant) and isinstance(v.value, str) and v.value.strip():
return True
return False


def _check_cmd_prefixes(path: Path, tree: ast.Module) -> list[str]:
"""Rule #1: warn when a command-like class attr is not ``CMD`` / ``CMD_*``."""
msgs: list[str] = []
for node in tree.body:
# Keeps only classes whose names end with Collector or Analyzer (e.g. ProcessCollector, PcieAnalyzer).
if not isinstance(node, ast.ClassDef) or not _is_collector_or_analyzer_class(node):
continue
for stmt in node.body:
if not isinstance(stmt, ast.Assign) or len(stmt.targets) != 1:
continue
t = stmt.targets[0]
if not isinstance(t, ast.Name):
continue
name = t.id
if name.startswith("_") or name in _CMD_CHECK_SKIP_NAMES:
continue
if not _is_stringish(stmt.value):
continue
preview = _stringish_preview(stmt.value)
if preview is None or not _looks_like_shell_command_literal(preview):
continue
if name == "CMD" or name.startswith("CMD_"):
continue
msgs.append(
f"{path}:{stmt.lineno}: [{node.name}] command-like class attribute {name!r} "
"should be renamed to CMD or to start with CMD_."
)
return msgs


def _is_args_class(cls: ast.ClassDef) -> bool:
if not cls.name.endswith("Args"):
return False
if not cls.bases:
return False
for b in cls.bases:
bn = _base_name(b)
if bn in ("BaseModel", "CollectorArgs", "AnalyzerArgs"):
return True
if bn and bn.endswith("Args"):
return True
return False


def _annotation_mentions_classvar(ann: ast.expr | None) -> bool:
if ann is None:
return False
if isinstance(ann, ast.Name) and ann.id == "ClassVar":
return True
if isinstance(ann, ast.Subscript):
return _annotation_mentions_classvar(ann.value)
if isinstance(ann, ast.Attribute) and ann.attr == "ClassVar":
return True
if isinstance(ann, ast.BinOp) and isinstance(ann.op, ast.BitOr):
return _annotation_mentions_classvar(ann.left) or _annotation_mentions_classvar(ann.right)
return False


def _check_args_fields(path: Path, tree: ast.Module) -> list[str]:
"""Rule #2: warn when Args fields lack ``Field`` with a non-empty ``description``."""
msgs: list[str] = []
for node in tree.body:
if not isinstance(node, ast.ClassDef) or not _is_args_class(node):
continue
for stmt in node.body:
if isinstance(stmt, ast.AnnAssign):
if _annotation_mentions_classvar(stmt.annotation):
continue
if not isinstance(stmt.target, ast.Name):
continue
field_name = stmt.target.id
if field_name.startswith("_") or field_name in ("model_config",):
continue
if stmt.value is None:
msgs.append(
f"{path}:{stmt.lineno}: [{node.name}] {field_name}: "
"use Field(..., description='...') for every Args field."
)
continue
if isinstance(stmt.value, ast.Call) and _field_call_name(stmt.value.func):
if not _field_has_nonempty_description(stmt.value):
msgs.append(
f"{path}:{stmt.lineno}: [{node.name}] {field_name}: "
"Field(...) must include a non-empty description= for help text."
)
else:
msgs.append(
f"{path}:{stmt.lineno}: [{node.name}] {field_name}: "
"must assign pydantic Field(...) with description=."
)
elif isinstance(stmt, ast.Assign) and len(stmt.targets) == 1:
t = stmt.targets[0]
if not isinstance(t, ast.Name):
continue
field_name = t.id
if field_name.startswith("_") or field_name in ("model_config",):
continue
val = stmt.value
if isinstance(val, ast.Call) and _field_call_name(val.func):
if not _field_has_nonempty_description(val):
msgs.append(
f"{path}:{stmt.lineno}: [{node.name}] {field_name}: "
"Field(...) must include a non-empty description= for help text."
)
return msgs


def main() -> None:
if not PLUGIN_ROOT.is_dir():
sys.stderr.write(f"warning: plugins directory not found: {PLUGIN_ROOT}\n")
return

all_msgs: list[str] = []
for path in sorted(PLUGIN_ROOT.rglob("*.py")):
rel = path.relative_to(_REPO_ROOT)
name = path.name
try:
src = path.read_text(encoding="utf-8")
tree = ast.parse(src, filename=str(path))
except (OSError, SyntaxError) as e:
all_msgs.append(f"{rel}: could not parse: {e}")
continue

if "collector" in name and name.endswith(".py"):
all_msgs.extend(_check_cmd_prefixes(rel, tree))
if "analyzer" in name and name.endswith(".py"):
all_msgs.extend(_check_cmd_prefixes(rel, tree))

if name == "collector_args.py" or name == "analyzer_args.py":
all_msgs.extend(_check_args_fields(rel, tree))

if all_msgs:
sys.stderr.write("plugin convention warnings (commit not blocked):\n")
for m in all_msgs:
sys.stderr.write(f" WARNING: {m}\n")
else:
sys.stdout.write("Success: no plugin convention warnings.\n")
sys.exit(0)


if __name__ == "__main__":
main()
Loading
Loading