Skip to content

Commit acc901a

Browse files
committed
Merge branch 'development' into alex_nvme
2 parents 3db081b + 0589a6d commit acc901a

File tree

12 files changed

+974
-8
lines changed

12 files changed

+974
-8
lines changed

README.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@ usage: node-scraper [-h] [--sys-name STRING] [--sys-location {LOCAL,REMOTE}] [--
2121
[--sys-sku STRING] [--sys-platform STRING] [--plugin-configs [STRING ...]] [--system-config STRING]
2222
[--connection-config STRING] [--log-path STRING] [--log-level {CRITICAL,FATAL,ERROR,WARN,WARNING,INFO,DEBUG,NOTSET}]
2323
[--gen-reference-config]
24-
{run-plugins,describe,gen-plugin-config} ...
24+
{summary,run-plugins,describe,gen-plugin-config} ...
2525

2626
node scraper CLI
2727

2828
positional arguments:
29-
{run-plugins,describe,gen-plugin-config}
29+
{summary,run-plugins,describe,gen-plugin-config}
3030
Subcommands
31+
summary Generates summary csv file
3132
run-plugins Run a series of plugins
3233
describe Display details on a built-in config or plugin
3334
gen-plugin-config Generate a config for a plugin or list of plugins
@@ -38,7 +39,8 @@ options:
3839
--sys-location {LOCAL,REMOTE}
3940
Location of target system (default: LOCAL)
4041
--sys-interaction-level {PASSIVE,INTERACTIVE,DISRUPTIVE}
41-
Specify system interaction level, used to determine the type of actions that plugins can perform (default: INTERACTIVE)
42+
Specify system interaction level, used to determine the type of actions that plugins can perform (default:
43+
INTERACTIVE)
4244
--sys-sku STRING Manually specify SKU of system (default: None)
4345
--sys-platform STRING
4446
Specify system platform (default: None)
@@ -54,7 +56,6 @@ options:
5456
--gen-reference-config
5557
Generate reference config from system. Writes to ./reference_config.json. (default: False)
5658

57-
5859
```
5960
6061
### Subcommmands
@@ -167,6 +168,16 @@ This would produce the following config:
167168
}
168169
```
169170
171+
4. **'summary' sub command**
172+
The 'summary' subcommand can be used to combine results from multiple runs of node-scraper to a
173+
single summary.csv file. Sample run:
174+
```sh
175+
node-scraper summary --summary_path /<path_to_node-scraper_logs>
176+
```
177+
This will generate a new file '/<path_to_node-scraper_logs>/summary.csv' file. This file will
178+
contain the results from all 'nodescraper.csv' files from '/<path_to_node-scarper_logs>'.
179+
180+
170181
### Plugin Configs
171182
A plugin JSON config should follow the structure of the plugin config model defined here.
172183
The globals field is a dictionary of global key-value pairs; values in globals will be passed to

nodescraper/cli/cli.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@
3535
from nodescraper.cli.constants import DEFAULT_CONFIG, META_VAR_MAP
3636
from nodescraper.cli.dynamicparserbuilder import DynamicParserBuilder
3737
from nodescraper.cli.helper import (
38+
dump_results_to_csv,
3839
generate_reference_config,
3940
generate_reference_config_from_logs,
41+
generate_summary,
4042
get_plugin_configs,
4143
get_system_info,
4244
log_system_info,
@@ -154,6 +156,25 @@ def build_parser(
154156

155157
subparsers = parser.add_subparsers(dest="subcmd", help="Subcommands")
156158

159+
summary_parser = subparsers.add_parser(
160+
"summary",
161+
help="Generates summary csv file",
162+
)
163+
164+
summary_parser.add_argument(
165+
"--search-path",
166+
dest="search_path",
167+
type=log_path_arg,
168+
help="Path to node-scraper previously generated results.",
169+
)
170+
171+
summary_parser.add_argument(
172+
"--output-path",
173+
dest="output_path",
174+
type=log_path_arg,
175+
help="Specifies path for summary.csv.",
176+
)
177+
157178
run_plugin_parser = subparsers.add_parser(
158179
"run-plugins",
159180
help="Run a series of plugins",
@@ -249,7 +270,7 @@ def setup_logger(log_level: str = "INFO", log_path: str | None = None) -> loggin
249270
handlers = [logging.StreamHandler(stream=sys.stdout)]
250271

251272
if log_path:
252-
log_file_name = os.path.join(log_path, "errorscraper.log")
273+
log_file_name = os.path.join(log_path, "nodescraper.log")
253274
handlers.append(
254275
logging.FileHandler(filename=log_file_name, mode="wt", encoding="utf-8"),
255276
)
@@ -327,12 +348,13 @@ def main(arg_input: Optional[list[str]] = None):
327348

328349
parsed_args = parser.parse_args(top_level_args)
329350
system_info = get_system_info(parsed_args)
351+
sname = system_info.name.lower().replace("-", "_").replace(".", "_")
352+
timestamp = datetime.datetime.now().strftime("%Y_%m_%d-%I_%M_%S_%p")
330353

331354
if parsed_args.log_path and parsed_args.subcmd not in ["gen-plugin-config", "describe"]:
332-
sname = system_info.name.lower().replace("-", "_").replace(".", "_")
333355
log_path = os.path.join(
334356
parsed_args.log_path,
335-
f"scraper_logs_{sname}_{datetime.datetime.now().strftime('%Y_%m_%d-%I_%M_%S_%p')}",
357+
f"scraper_logs_{sname}_{timestamp}",
336358
)
337359
os.makedirs(log_path)
338360
else:
@@ -342,6 +364,10 @@ def main(arg_input: Optional[list[str]] = None):
342364
if log_path:
343365
logger.info("Log path: %s", log_path)
344366

367+
if parsed_args.subcmd == "summary":
368+
generate_summary(parsed_args.search_path, parsed_args.output_path, logger)
369+
sys.exit(0)
370+
345371
if parsed_args.subcmd == "describe":
346372
parse_describe(parsed_args, plugin_reg, config_reg, logger)
347373

@@ -407,6 +433,8 @@ def main(arg_input: Optional[list[str]] = None):
407433
try:
408434
results = plugin_executor.run_queue()
409435

436+
dump_results_to_csv(results, sname, log_path, timestamp, logger)
437+
410438
if parsed_args.reference_config:
411439
ref_config = generate_reference_config(results, plugin_reg, logger)
412440
path = os.path.join(os.getcwd(), "reference_config.json")

nodescraper/cli/helper.py

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
#
2525
###############################################################################
2626
import argparse
27+
import csv
28+
import glob
2729
import json
2830
import logging
2931
import os
@@ -331,7 +333,7 @@ def generate_reference_config(
331333
for obj in results:
332334
if obj.result_data.collection_result.status != ExecutionStatus.OK:
333335
logger.warning(
334-
"Plugin: %s result status is %, skipping",
336+
"Plugin: %s result status is %s, skipping",
335337
obj.source,
336338
obj.result_data.collection_result.status,
337339
)
@@ -424,3 +426,91 @@ def find_datamodel_and_result(base_path: str) -> list[Tuple[str, str]]:
424426
tuple_list.append((datamodel_path, result_path))
425427

426428
return tuple_list
429+
430+
431+
def dump_results_to_csv(
432+
results: list[PluginResult],
433+
nodename: str,
434+
log_path: str,
435+
timestamp: str,
436+
logger: logging.Logger,
437+
):
438+
"""dump node-scraper summary results to csv file
439+
440+
Args:
441+
results (list[PluginResult]): list of PluginResults
442+
nodename (str): node where results come from
443+
log_path (str): path to results
444+
timestamp (str): time when results were taken
445+
logger (logging.Logger): instance of logger
446+
"""
447+
fieldnames = ["nodename", "plugin", "status", "timestamp", "message"]
448+
filename = log_path + "/nodescraper.csv"
449+
all_rows = []
450+
for res in results:
451+
row = {
452+
"nodename": nodename,
453+
"plugin": res.source,
454+
"status": res.status.name,
455+
"timestamp": timestamp,
456+
"message": res.message,
457+
}
458+
all_rows.append(row)
459+
dump_to_csv(all_rows, filename, fieldnames, logger)
460+
461+
462+
def dump_to_csv(all_rows: list, filename: str, fieldnames: list[str], logger: logging.Logger):
463+
"""dump data to csv
464+
465+
Args:
466+
all_rows (list): rows to be written
467+
filename (str): name of file to write to
468+
fieldnames (list[str]): header for csv file
469+
logger (logging.Logger): isntance of logger
470+
"""
471+
try:
472+
with open(filename, "w", newline="") as f:
473+
writer = csv.DictWriter(f, fieldnames=fieldnames)
474+
writer.writeheader()
475+
for row in all_rows:
476+
writer.writerow(row)
477+
except Exception as exp:
478+
logger.error("Could not dump data to csv file: %s", exp)
479+
logger.info("Data written to csv file: %s", filename)
480+
481+
482+
def generate_summary(search_path: str, output_path: str | None, logger: logging.Logger):
483+
"""Concatenate csv files into 1 summary csv file
484+
485+
Args:
486+
search_path (str): Path for previous runs
487+
output_path (str | None): Path for new summary csv file
488+
logger (logging.Logger): instance of logger
489+
"""
490+
491+
fieldnames = ["nodename", "plugin", "status", "timestamp", "message"]
492+
all_rows = []
493+
494+
pattern = os.path.join(search_path, "**", "nodescraper.csv")
495+
matched_files = glob.glob(pattern, recursive=True)
496+
497+
if not matched_files:
498+
logger.error(f"No nodescraper.csv files found under {search_path}")
499+
return
500+
501+
for filepath in matched_files:
502+
logger.info(f"Reading: {filepath}")
503+
with open(filepath, newline="") as f:
504+
reader = csv.DictReader(f)
505+
for row in reader:
506+
all_rows.append(row)
507+
508+
if not all_rows:
509+
logger.error("No data rows found in matched CSV files.")
510+
return
511+
512+
if not output_path:
513+
output_path = os.getcwd()
514+
515+
output_path = os.path.join(output_path, "summary.csv")
516+
dump_to_csv(all_rows, output_path, fieldnames, logger)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
###############################################################################
2+
#
3+
# MIT License
4+
#
5+
# Copyright (c) 2025 Advanced Micro Devices, Inc.
6+
#
7+
# Permission is hereby granted, free of charge, to any person obtaining a copy
8+
# of this software and associated documentation files (the "Software"), to deal
9+
# in the Software without restriction, including without limitation the rights
10+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
# copies of the Software, and to permit persons to whom the Software is
12+
# furnished to do so, subject to the following conditions:
13+
#
14+
# The above copyright notice and this permission notice shall be included in all
15+
# copies or substantial portions of the Software.
16+
#
17+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
# SOFTWARE.
24+
#
25+
###############################################################################
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
###############################################################################
2+
#
3+
# MIT License
4+
#
5+
# Copyright (c) 2025 Advanced Micro Devices, Inc.
6+
#
7+
# Permission is hereby granted, free of charge, to any person obtaining a copy
8+
# of this software and associated documentation files (the "Software"), to deal
9+
# in the Software without restriction, including without limitation the rights
10+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
# copies of the Software, and to permit persons to whom the Software is
12+
# furnished to do so, subject to the following conditions:
13+
#
14+
# The above copyright notice and this permission notice shall be included in all
15+
# copies or substantial portions of the Software.
16+
#
17+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
# SOFTWARE.
24+
#
25+
###############################################################################
26+
import re
27+
28+
from nodescraper.models import AnalyzerArgs
29+
from nodescraper.plugins.inband.kernel_module.kernel_module_data import (
30+
KernelModuleDataModel,
31+
)
32+
33+
34+
class KernelModuleAnalyzerArgs(AnalyzerArgs):
35+
kernel_modules: dict[str, dict] = {}
36+
regex_filter: list[str] = ["amd"]
37+
38+
@classmethod
39+
def build_from_model(cls, datamodel: KernelModuleDataModel) -> "KernelModuleAnalyzerArgs":
40+
"""build analyzer args from data model and filter by regex_filter
41+
42+
Args:
43+
datamodel (KernelModuleDataModel): data model for plugin
44+
45+
Returns:
46+
KernelModuleAnalyzerArgs: instance of analyzer args class
47+
"""
48+
49+
pattern_regex = re.compile("amd", re.IGNORECASE)
50+
filtered_mods = {
51+
name: data
52+
for name, data in datamodel.kernel_modules.items()
53+
if pattern_regex.search(name)
54+
}
55+
56+
return cls(
57+
kernel_modules=filtered_mods,
58+
regex_filter=[],
59+
)

0 commit comments

Comments
 (0)