Skip to content
Merged
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
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ usage: cli.py [-h] [--version] [--sys-name STRING]
[--sys-location {LOCAL,REMOTE}]
[--sys-interaction-level {PASSIVE,INTERACTIVE,DISRUPTIVE}]
[--sys-sku STRING] [--sys-platform STRING]
[--plugin-configs [STRING ...]] [--system-config STRING]
[--plugin-configs LIST] [--system-config STRING]
[--connection-config STRING] [--log-path STRING]
[--log-level {CRITICAL,FATAL,ERROR,WARN,WARNING,INFO,DEBUG,NOTSET}]
[--no-console-log] [--gen-reference-config] [--skip-sudo]
Expand Down Expand Up @@ -112,10 +112,9 @@ options:
--sys-sku STRING Manually specify SKU of system (default: None)
--sys-platform STRING
Specify system platform (default: None)
--plugin-configs [STRING ...]
built-in config names or paths to plugin config JSONs.
Available built-in configs: NodeStatus, AllPlugins
(default: None)
--plugin-configs LIST
Comma-separated built-in names and/or plugin config JSON
paths. Built-in: NodeStatus, AllPlugins (default: None)
--system-config STRING
Path to system config json (default: None)
--connection-config STRING
Expand Down Expand Up @@ -358,7 +357,7 @@ You can extend the built-in error detection with custom regex patterns. Create a
Save this to `dmesg_custom_config.json` and run:

```sh
node-scraper --plugin-configs dmesg_custom_config.json run-plugins DmesgPlugin
node-scraper --plugin-configs=dmesg_custom_config.json run-plugins DmesgPlugin
```

#### **'compare-runs' subcommand**
Expand Down Expand Up @@ -549,8 +548,9 @@ Built-in configs include **NodeStatus** (a subset of plugins) and **AllPlugins**
registered plugin with default arguments—useful for generating a reference config from the full system).

**NodeStatus plus additional plugins** — built-in configs merge with plugins named after `run-plugins`.
Use **`--plugin-configs=<name>`** (equals form): with a space
after `--plugin-configs`. See below for examples:
Values are comma-separated; pass as **`--plugin-configs=…`** or **`--plugin-configs` …** (same as other
optional flags), e.g. `--plugin-configs=NodeStatus,/path/extra.json`.
Examples:
```sh
node-scraper --plugin-configs=NodeStatus run-plugins PciePlugin
```
Expand All @@ -561,7 +561,7 @@ node-scraper --log-path ./logs --plugin-configs=NodeStatus run-plugins PciePlugi

Using a JSON file:
```sh
node-scraper --plugin-configs plugin_config.json
node-scraper --plugin-configs=plugin_config.json
```
Here is an example of a comprehensive plugin config that specifies analyzer args for each plugin:
```json
Expand Down Expand Up @@ -623,7 +623,7 @@ data.

**Run all registered plugins (AllPlugins config):**
```sh
node-scraper --plugin-config AllPlugins
node-scraper --plugin-configs=AllPlugins

```

Expand Down Expand Up @@ -657,7 +657,7 @@ This will generate the following config:
```
This config can later be used on a different platform for comparison, using the steps at #2:
```sh
node-scraper --plugin-configs reference_config.json
node-scraper --plugin-configs=reference_config.json

```

Expand Down
6 changes: 5 additions & 1 deletion nodescraper/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@
#
###############################################################################

from .cli import get_cli_top_level_subcommands
from .cli import main as cli_entry
from .embed import run_main_return_code
from .embed import CLI_TOP_LEVEL_SUBCOMMANDS, run_cli_return_code, run_main_return_code
from .invocation import (
PluginRunInvocation,
get_plugin_run_invocation,
Expand All @@ -34,7 +35,10 @@
)

__all__ = [
"CLI_TOP_LEVEL_SUBCOMMANDS",
"cli_entry",
"get_cli_top_level_subcommands",
"run_cli_return_code",
"run_main_return_code",
"PluginRunInvocation",
"get_plugin_run_invocation",
Expand Down
125 changes: 97 additions & 28 deletions nodescraper/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
###############################################################################
import argparse
import datetime
import functools
import json
import logging
import os
Expand All @@ -48,6 +49,10 @@
parse_gen_plugin_config,
process_args,
)
from nodescraper.cli.host_cli_embed import (
apply_host_cli_args_to_parsed_args,
merge_plugin_connection_config_from_host_ns,
)
from nodescraper.cli.inputargtypes import ModelArgHandler, json_arg, log_path_arg
from nodescraper.cli.invocation import run_plugin_queue_with_invocation
from nodescraper.configregistry import ConfigRegistry
Expand All @@ -63,24 +68,30 @@
from nodescraper.pluginregistry import PluginRegistry


def build_parser(
plugin_reg: PluginRegistry,
config_reg: ConfigRegistry,
) -> tuple[argparse.ArgumentParser, dict[str, tuple[argparse.ArgumentParser, dict]]]:
"""Build an argument parser
def _parse_plugin_configs_csv(value: str) -> list[str]:
"""Split a comma-separated ``--plugin-configs`` value into names/paths."""
return [p.strip() for p in value.split(",") if p.strip()]

Args:
plugin_reg (PluginRegistry): registry of plugins

Returns:
tuple[argparse.ArgumentParser, dict[str, tuple[argparse.ArgumentParser, dict]]]: tuple containing main
parser and subparsers for each plugin module
"""
parser = argparse.ArgumentParser(
description="node scraper CLI",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
def _config_registry_with_all_plugins(plugin_reg: PluginRegistry) -> ConfigRegistry:
"""Synthetic ``AllPlugins`` config used for CLI help and :func:`build_global_argument_parser`."""
config_reg = ConfigRegistry()
config_reg.configs["AllPlugins"] = PluginConfig(
name="AllPlugins",
desc="Run all registered plugins with default arguments",
global_args={},
plugins={name: {} for name in plugin_reg.plugins},
result_collators={},
)
return config_reg


def _add_cli_root_globals(
parser: argparse.ArgumentParser,
plugin_reg: PluginRegistry,
config_reg: ConfigRegistry,
) -> None:
"""Register top-level flags before ``subcmd`` subparsers (shared with :func:`build_global_argument_parser`)."""
parser.add_argument(
"--version",
action="version",
Expand Down Expand Up @@ -125,10 +136,13 @@ def build_parser(

parser.add_argument(
"--plugin-configs",
type=str,
nargs="*",
help=f"built-in config names or paths to plugin config JSONs.\nAvailable built-in configs: {', '.join(config_reg.configs.keys())}",
metavar=META_VAR_MAP[str],
type=_parse_plugin_configs_csv,
default=None,
help=(
"Comma-separated built-in names and/or plugin config JSON paths "
f"(e.g. --plugin-configs=NodeStatus,/path/c.json). Built-ins: {', '.join(config_reg.configs.keys())}"
),
metavar="LIST",
)

parser.add_argument(
Expand Down Expand Up @@ -184,6 +198,40 @@ def build_parser(
help="Skip plugins that require sudo permissions",
)


def build_global_argument_parser(*, add_help: bool = True) -> argparse.ArgumentParser:
"""Globals only (no subcommands), for host CLIs such as amd-error-scraper ``error-scraper``."""
plugin_reg = PluginRegistry()
config_reg = _config_registry_with_all_plugins(plugin_reg)
parser = argparse.ArgumentParser(
description="node scraper CLI (global options only)",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
add_help=add_help,
)
_add_cli_root_globals(parser, plugin_reg, config_reg)
return parser


def build_parser(
plugin_reg: PluginRegistry,
config_reg: ConfigRegistry,
) -> tuple[argparse.ArgumentParser, dict[str, tuple[argparse.ArgumentParser, dict]]]:
"""Build an argument parser

Args:
plugin_reg (PluginRegistry): registry of plugins

Returns:
tuple[argparse.ArgumentParser, dict[str, tuple[argparse.ArgumentParser, dict]]]: tuple containing main
parser and subparsers for each plugin module
"""
parser = argparse.ArgumentParser(
description="node scraper CLI",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)

_add_cli_root_globals(parser, plugin_reg, config_reg)

subparsers = parser.add_subparsers(dest="subcmd", help="Subcommands")
subparsers.default = "run-plugins"

Expand Down Expand Up @@ -334,6 +382,34 @@ def build_parser(
return parser, plugin_subparser_map


def _top_level_subcommand_names(root: argparse.ArgumentParser) -> tuple[str, ...]:
"""Return ``dest=subcmd`` subparser names from the root CLI parser.

Args:
root: Parser returned by :func:`build_parser`.

Returns:
Tuple of top-level subcommand strings.
"""
for action in root._actions:
if isinstance(action, argparse._SubParsersAction) and action.dest == "subcmd":
return tuple(action.choices.keys())
raise RuntimeError("nodescraper CLI root parser has no subcmd subparsers")


@functools.lru_cache(maxsize=1)
def get_cli_top_level_subcommands() -> tuple[str, ...]:
"""Return top-level subcommand names from a parser built like :func:`main` (cached).

Returns:
Tuple of ``subcmd`` subparser names; call ``cache_clear()`` if registries change in-process.
"""
plugin_reg = PluginRegistry()
config_reg = _config_registry_with_all_plugins(plugin_reg)
parser, _plugin_subparser_map = build_parser(plugin_reg, config_reg)
return _top_level_subcommand_names(parser)


def setup_logger(
log_level: str = "INFO",
log_path: Optional[str] = None,
Expand Down Expand Up @@ -397,16 +473,7 @@ def main(
arg_input = sys.argv[1:]

plugin_reg = PluginRegistry()

config_reg = ConfigRegistry()
# Add synthetic "AllPlugins" config that includes every registered plugin
config_reg.configs["AllPlugins"] = PluginConfig(
name="AllPlugins",
desc="Run all registered plugins with default arguments",
global_args={},
plugins={name: {} for name in plugin_reg.plugins},
result_collators={},
)
config_reg = _config_registry_with_all_plugins(plugin_reg)
parser, plugin_subparser_map = build_parser(plugin_reg, config_reg)

try:
Expand All @@ -415,6 +482,8 @@ def main(
)

parsed_args = parser.parse_args(top_level_args)
apply_host_cli_args_to_parsed_args(parsed_args, host_cli_args)
merge_plugin_connection_config_from_host_ns(parsed_args, host_cli_args)
system_info = get_system_info(parsed_args)
sname = system_info.name.lower().replace("-", "_").replace(".", "_")
timestamp = datetime.datetime.now().strftime("%Y_%m_%d-%I_%M_%S_%p")
Expand Down
39 changes: 36 additions & 3 deletions nodescraper/cli/embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,55 @@
# SOFTWARE.
#
###############################################################################
"""In-process CLI entry without adding new argparse flags."""

from __future__ import annotations

import argparse
from typing import Optional

__all__ = ["run_main_return_code"]
from nodescraper.cli.cli import get_cli_top_level_subcommands

CLI_TOP_LEVEL_SUBCOMMANDS = get_cli_top_level_subcommands()

__all__ = [
"CLI_TOP_LEVEL_SUBCOMMANDS",
"get_cli_top_level_subcommands",
"run_cli_return_code",
"run_main_return_code",
]


def run_cli_return_code(
argv: list[str],
*,
host_cli_args: Optional[argparse.Namespace] = None,
) -> int:
"""Run nodescraper in-process; same behavior as :func:`run_main_return_code`.

Args:
argv: Tokens after the program name.
host_cli_args: Optional host namespace forwarded to :func:`nodescraper.cli.cli.main`.

Returns:
Integer exit code (``SystemExit`` is mapped, not raised).
"""
return run_main_return_code(argv, host_cli_args=host_cli_args)


def run_main_return_code(
arg_input: list[str],
*,
host_cli_args: Optional[argparse.Namespace] = None,
) -> int:
"""Runs the nodescraper main entrypoint and maps SystemExit to an integer return code."""
"""Run :func:`nodescraper.cli.cli.main` and map ``SystemExit`` to an exit code.

Args:
arg_input: Tokens after the program name.
host_cli_args: Optional host namespace for embedded runs.

Returns:
Integer exit code.
"""
from nodescraper.cli.cli import main

try:
Expand Down
Loading
Loading