Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b202a5e
DCERR-699: add bcmcli support and expand niccli read-only commands
amd-jolibran Aug 4, 2026
a37ee5c
DCERR-699: add bcmcli support and expand niccli read-only commands
amd-jolibran Aug 6, 2026
b2229ec
DCERR-699: restore _truncate and results_for_model in datamodel seria…
amd-jolibran Aug 6, 2026
6095b04
DCERR-699: remove unnecessary comments
amd-jolibran Aug 6, 2026
5d66c26
DCERR-699: comment formatting
amd-jolibran Aug 6, 2026
be5fb81
DCERR-699: comment formatting
amd-jolibran Aug 6, 2026
4d74fe8
Use copy to give system info to Tasks
graepaul Aug 7, 2026
40d9f34
add unit tests for bcmcli/niccli NicCollector changes
amd-jolibran Aug 7, 2026
386442a
add unit tests for bcmcli/niccli NicCollector changes
amd-jolibran Aug 7, 2026
c149175
format
graepaul Aug 7, 2026
a21a91b
fix for container
alexandraBara Aug 10, 2026
ca355a0
fix mypy issue
amd-jolibran Aug 10, 2026
4c8fdb8
Merge pull request #266 from amd/dcerr-699
alexandraBara Aug 10, 2026
aba3dd2
Merge branch 'development' into feature/use_copy
alexandraBara Aug 10, 2026
b70a022
Merge pull request #267 from amd/feature/use_copy
alexandraBara Aug 14, 2026
c7a5a01
docs: Update plugin documentation [automated]
github-actions[bot] Aug 15, 2026
0e23e4f
Merge pull request #268 from amd/automated-plugin-docs-update
alexandraBara Aug 17, 2026
da25fa3
MI4xxServiceabilityPlugin
alexandraBara Aug 18, 2026
38fe787
pretty fail
alexandraBara Aug 20, 2026
24fe1ab
merged develop
alexandraBara Aug 21, 2026
6c28811
Merge pull request #269 from amd/alex_mi4xxServiceability
alexandraBara Aug 21, 2026
c593f69
docs: Update plugin documentation [automated]
github-actions[bot] Aug 21, 2026
c8f134e
Merge pull request #270 from amd/automated-plugin-docs-update
alexandraBara Aug 21, 2026
3b81151
text updates
alexandraBara Aug 21, 2026
09294d7
docs: Update plugin documentation [automated]
github-actions[bot] Aug 21, 2026
1a94bbf
Merge pull request #271 from amd/automated-plugin-docs-update
alexandraBara Aug 21, 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
2 changes: 1 addition & 1 deletion .github/workflows/code_quality_checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
jobs:
pre-commit:
runs-on: [ self-hosted ]
container: python:3.9
container: public.ecr.aws/docker/library/python:3.9

steps:
- uses: actions/checkout@v5
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/functional-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ permissions:
jobs:
run_tests:
runs-on: [ self-hosted ]
container: python:3.9
container: public.ecr.aws/docker/library/python:3.9

steps:
- uses: actions/checkout@v5
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ permissions:
jobs:
run_tests:
runs-on: [ self-hosted ]
container: python:3.9
container: public.ecr.aws/docker/library/python:3.9

steps:
- uses: actions/checkout@v5
Expand Down
329 changes: 307 additions & 22 deletions docs/PLUGIN_DOC.md

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions nodescraper/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

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

__all__ = [
"CLI_TOP_LEVEL_SUBCOMMANDS",
"cli_entry",
"get_cli_top_level_subcommands",
"run_cli_return_code",
Expand Down
307 changes: 162 additions & 145 deletions nodescraper/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@

import nodescraper
from nodescraper.cli.compare_runs import run_compare_runs
from nodescraper.cli.constants import DEFAULT_CONFIG, META_VAR_MAP
from nodescraper.cli.constants import (
DEFAULT_CONFIG,
KEYBOARD_INTERRUPT_EXIT_CODE,
META_VAR_MAP,
)
from nodescraper.cli.dynamicparserbuilder import DynamicParserBuilder
from nodescraper.cli.helper import (
dump_results_to_csv,
Expand Down Expand Up @@ -459,6 +463,14 @@ def setup_logger(
return logger


def _handle_keyboard_interrupt(logger: Optional[logging.Logger] = None) -> None:
if logger is not None:
logger.info("Received Ctrl+C. Shutting down...")
else:
sys.stderr.write("Interrupted.\n")
sys.exit(KEYBOARD_INTERRUPT_EXIT_CODE)


def main(
arg_input: Optional[list[str]] = None,
*,
Expand All @@ -474,169 +486,175 @@ def main(
plugin_run_result_hooks: Optional callbacks invoked with each plugin's :class:`PluginResult`
after ``run()`` completes (used by embedded hosts such as error-scraper).
"""
if arg_input is None:
arg_input = sys.argv[1:]

plugin_reg = PluginRegistry()
config_reg = _default_config_registry(plugin_reg)
parser, plugin_subparser_map = build_parser(plugin_reg, config_reg)

logger: Optional[logging.Logger] = None
try:
top_level_args, plugin_arg_map, invalid_plugins = process_args(
arg_input, list(plugin_subparser_map.keys())
)
if arg_input is None:
arg_input = sys.argv[1:]

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")

if parsed_args.log_path:
log_path = os.path.join(
parsed_args.log_path,
f"scraper_logs_{sname}_{timestamp}",
)
os.makedirs(log_path)
else:
log_path = None
plugin_reg = PluginRegistry()
config_reg = _default_config_registry(plugin_reg)
parser, plugin_subparser_map = build_parser(plugin_reg, config_reg)

if parsed_args.no_console_log and not log_path:
base_dir = parsed_args.log_path if parsed_args.log_path else "."
log_path = os.path.join(base_dir, f"scraper_logs_{sname}_{timestamp}")
os.makedirs(log_path, exist_ok=True)

logger = setup_logger(
parsed_args.log_level,
log_path,
console=not parsed_args.no_console_log,
)
if log_path:
logger.info("Log path: %s", log_path)

# Log warning if invalid plugin names were provided
if invalid_plugins:
logger.warning(
"Invalid plugin name(s) ignored: %s. Use 'describe plugin' to list available plugins.",
", ".join(invalid_plugins),
try:
top_level_args, plugin_arg_map, invalid_plugins = process_args(
arg_input, list(plugin_subparser_map.keys())
)

if parsed_args.subcmd == "summary":
generate_summary(
parsed_args.search_path,
parsed_args.output_path,
logger,
artifact_dir=log_path,
)
sys.exit(0)
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")

if parsed_args.log_path:
log_path = os.path.join(
parsed_args.log_path,
f"scraper_logs_{sname}_{timestamp}",
)
os.makedirs(log_path)
else:
log_path = None

if parsed_args.subcmd == "describe":
parse_describe(parsed_args, plugin_reg, config_reg, logger)

if parsed_args.subcmd == "compare-runs":
run_compare_runs(
parsed_args.path1,
parsed_args.path2,
plugin_reg,
logger,
skip_plugins=getattr(parsed_args, "skip_plugins", None) or [],
include_plugins=getattr(parsed_args, "include_plugins", None),
truncate_message=not getattr(parsed_args, "dont_truncate", False),
artifact_dir=log_path,
)
sys.exit(0)
if parsed_args.no_console_log and not log_path:
base_dir = parsed_args.log_path if parsed_args.log_path else "."
log_path = os.path.join(base_dir, f"scraper_logs_{sname}_{timestamp}")
os.makedirs(log_path, exist_ok=True)

if parsed_args.subcmd == "show-redfish-oem-allowable":
if not parsed_args.connection_config:
parser.error("show-redfish-oem-allowable requires --connection-config")
raw = parsed_args.connection_config.get("RedfishConnectionManager")
if not raw:
logger.error("Connection config must contain RedfishConnectionManager")
sys.exit(1)
params = RedfishConnectionParams.model_validate(raw)
password = params.password.get_secret_value() if params.password else None
base_url = f"{'https' if params.use_https else 'http'}://{params.host}" + (
f":{params.port}" if params.port else ""
)
conn = RedfishConnection(
base_url=base_url,
username=params.username,
password=password,
timeout=params.timeout_seconds,
use_session_auth=params.use_session_auth,
verify_ssl=params.verify_ssl,
api_root=params.api_root,
logger = setup_logger(
parsed_args.log_level,
log_path,
console=not parsed_args.no_console_log,
)
try:
conn._ensure_session()
allowable = get_oem_diagnostic_allowable_values(conn, parsed_args.log_service_path)
if allowable is None:
logger.warning(
"Could not read OEMDiagnosticDataType@Redfish.AllowableValues from LogService"
)
sys.exit(1)
logger.info("%s", json.dumps(allowable, indent=2))
finally:
conn.close()
sys.exit(0)
if log_path:
logger.info("Log path: %s", log_path)

# Log warning if invalid plugin names were provided
if invalid_plugins:
logger.warning(
"Invalid plugin name(s) ignored: %s. Use 'describe plugin' to list available plugins.",
", ".join(invalid_plugins),
)

if parsed_args.subcmd == "summary":
generate_summary(
parsed_args.search_path,
parsed_args.output_path,
logger,
artifact_dir=log_path,
)
sys.exit(0)

if parsed_args.subcmd == "gen-plugin-config":
if parsed_args.subcmd == "describe":
parse_describe(parsed_args, plugin_reg, config_reg, logger)

if parsed_args.subcmd == "compare-runs":
run_compare_runs(
parsed_args.path1,
parsed_args.path2,
plugin_reg,
logger,
skip_plugins=getattr(parsed_args, "skip_plugins", None) or [],
include_plugins=getattr(parsed_args, "include_plugins", None),
truncate_message=not getattr(parsed_args, "dont_truncate", False),
artifact_dir=log_path,
)
sys.exit(0)

if parsed_args.reference_config_from_logs:
ref_config = generate_reference_config_from_logs(
parsed_args.reference_config_from_logs, plugin_reg, logger
if parsed_args.subcmd == "show-redfish-oem-allowable":
if not parsed_args.connection_config:
parser.error("show-redfish-oem-allowable requires --connection-config")
raw = parsed_args.connection_config.get("RedfishConnectionManager")
if not raw:
logger.error("Connection config must contain RedfishConnectionManager")
sys.exit(1)
params = RedfishConnectionParams.model_validate(raw)
password = params.password.get_secret_value() if params.password else None
base_url = f"{'https' if params.use_https else 'http'}://{params.host}" + (
f":{params.port}" if params.port else ""
)
conn = RedfishConnection(
base_url=base_url,
username=params.username,
password=password,
timeout=params.timeout_seconds,
use_session_auth=params.use_session_auth,
verify_ssl=params.verify_ssl,
api_root=params.api_root,
)
out_dir = log_path if log_path else parsed_args.output_path
path = os.path.join(out_dir, "reference_config.json")
try:
with open(path, "w") as f:
json.dump(
ref_config.model_dump(mode="json", exclude_none=True),
f,
indent=2,
conn._ensure_session()
allowable = get_oem_diagnostic_allowable_values(
conn, parsed_args.log_service_path
)
if allowable is None:
logger.warning(
"Could not read OEMDiagnosticDataType@Redfish.AllowableValues from LogService"
)
logger.info("Reference config written to: %s", path)
except Exception as exp:
logger.error(exp)
sys.exit(1)
logger.info("%s", json.dumps(allowable, indent=2))
finally:
conn.close()
sys.exit(0)

parse_gen_plugin_config(
parsed_args, plugin_reg, config_reg, logger, artifact_dir=log_path
)
if parsed_args.subcmd == "gen-plugin-config":

parsed_plugin_args = {}
for plugin, plugin_args in plugin_arg_map.items():
try:
parsed_plugin_args[plugin] = plugin_subparser_map[plugin][0].parse_args(plugin_args)
except Exception as e:
logger.error("%s exception parsing args for plugin: %s", str(e), plugin)
if parsed_args.reference_config_from_logs:
ref_config = generate_reference_config_from_logs(
parsed_args.reference_config_from_logs, plugin_reg, logger
)
out_dir = log_path if log_path else parsed_args.output_path
path = os.path.join(out_dir, "reference_config.json")
try:
with open(path, "w") as f:
json.dump(
ref_config.model_dump(mode="json", exclude_none=True),
f,
indent=2,
)
logger.info("Reference config written to: %s", path)
except Exception as exp:
logger.error(exp)
sys.exit(0)

parse_gen_plugin_config(
parsed_args, plugin_reg, config_reg, logger, artifact_dir=log_path
)

if not parsed_plugin_args and not parsed_args.plugin_configs:
logger.info(
"No plugins config args specified, running default config: %s", DEFAULT_CONFIG
parsed_plugin_args = {}
for plugin, plugin_args in plugin_arg_map.items():
try:
parsed_plugin_args[plugin] = plugin_subparser_map[plugin][0].parse_args(
plugin_args
)
except Exception as e:
logger.error("%s exception parsing args for plugin: %s", str(e), plugin)

if not parsed_plugin_args and not parsed_args.plugin_configs:
logger.info(
"No plugins config args specified, running default config: %s",
DEFAULT_CONFIG,
)
plugin_configs = [DEFAULT_CONFIG]
else:
plugin_configs = parsed_args.plugin_configs or []

plugin_config_inst_list = get_plugin_configs(
plugin_config_input=plugin_configs,
system_interaction_level=parsed_args.sys_interaction_level,
built_in_configs=config_reg.configs,
parsed_plugin_args=parsed_plugin_args,
plugin_subparser_map=plugin_subparser_map,
)
plugin_configs = [DEFAULT_CONFIG]
else:
plugin_configs = parsed_args.plugin_configs or []

plugin_config_inst_list = get_plugin_configs(
plugin_config_input=plugin_configs,
system_interaction_level=parsed_args.sys_interaction_level,
built_in_configs=config_reg.configs,
parsed_plugin_args=parsed_plugin_args,
plugin_subparser_map=plugin_subparser_map,
)

if parsed_args.skip_sudo:
plugin_config_inst_list[-1].global_args.setdefault("collection_args", {})[
"skip_sudo"
] = True
if parsed_args.skip_sudo:
plugin_config_inst_list[-1].global_args.setdefault("collection_args", {})[
"skip_sudo"
] = True

except Exception as e:
parser.error(str(e))
except Exception as e:
parser.error(str(e))

try:
results = run_plugin_queue_with_invocation(
plugin_reg=plugin_reg,
parsed_args=parsed_args,
Expand Down Expand Up @@ -683,8 +701,7 @@ def main(
else:
sys.exit(0)
except KeyboardInterrupt:
logger.info("Received Ctrl+C. Shutting down...")
sys.exit(130)
_handle_keyboard_interrupt(logger)


if __name__ == "__main__":
Expand Down
Loading
Loading