Skip to content

Commit 516fcc5

Browse files
authored
Merge branch 'development' into feature/mypy-precommit
2 parents b4c6aae + d44d952 commit 516fcc5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+2442
-115
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

dev-setup.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Create venv if not already present
22
if [ ! -d "venv" ]; then
3-
python3 -m pip install virtualenv
4-
python3 -m virtualenv venv
3+
python3 -m pip install venv
4+
python3 -m venv venv
55
fi
66

77
# Activate the desired venv

nodescraper/base/inbandcollectortask.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from typing import Generic, Optional
2828

2929
from nodescraper.connection.inband import InBandConnection
30-
from nodescraper.connection.inband.inband import CommandArtifact, FileArtifact
30+
from nodescraper.connection.inband.inband import BaseFileArtifact, CommandArtifact
3131
from nodescraper.enums import EventPriority, OSFamily, SystemInteractionLevel
3232
from nodescraper.generictypes import TCollectArg, TDataModel
3333
from nodescraper.interfaces import DataCollector, TaskResultHook
@@ -99,7 +99,7 @@ def _run_sut_cmd(
9999

100100
def _read_sut_file(
101101
self, filename: str, encoding="utf-8", strip: bool = True, log_artifact=True
102-
) -> FileArtifact:
102+
) -> BaseFileArtifact:
103103
"""
104104
Read a file from the SUT and return its content.
105105
@@ -110,7 +110,7 @@ def _read_sut_file(
110110
log_artifact (bool, optional): whether we should log the contents of the file. Defaults to True.
111111
112112
Returns:
113-
FileArtifact: The content of the file read from the SUT, which includes the file name and content
113+
BaseFileArtifact: The content of the file read from the SUT, which includes the file name and content
114114
"""
115115
file_res = self.connection.read_file(filename=filename, encoding=encoding, strip=strip)
116116
if log_artifact:

nodescraper/cli/cli.py

Lines changed: 47 additions & 4 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,
@@ -152,8 +154,34 @@ def build_parser(
152154
help="Generate reference config from system. Writes to ./reference_config.json.",
153155
)
154156

157+
parser.add_argument(
158+
"--skip-sudo",
159+
dest="skip_sudo",
160+
action="store_true",
161+
help="Skip plugins that require sudo permissions",
162+
)
163+
155164
subparsers = parser.add_subparsers(dest="subcmd", help="Subcommands")
156165

166+
summary_parser = subparsers.add_parser(
167+
"summary",
168+
help="Generates summary csv file",
169+
)
170+
171+
summary_parser.add_argument(
172+
"--search-path",
173+
dest="search_path",
174+
type=log_path_arg,
175+
help="Path to node-scraper previously generated results.",
176+
)
177+
178+
summary_parser.add_argument(
179+
"--output-path",
180+
dest="output_path",
181+
type=log_path_arg,
182+
help="Specifies path for summary.csv.",
183+
)
184+
157185
run_plugin_parser = subparsers.add_parser(
158186
"run-plugins",
159187
help="Run a series of plugins",
@@ -249,7 +277,7 @@ def setup_logger(log_level: str = "INFO", log_path: str | None = None) -> loggin
249277
handlers = [logging.StreamHandler(stream=sys.stdout)]
250278

251279
if log_path:
252-
log_file_name = os.path.join(log_path, "errorscraper.log")
280+
log_file_name = os.path.join(log_path, "nodescraper.log")
253281
handlers.append(
254282
logging.FileHandler(filename=log_file_name, mode="wt", encoding="utf-8"),
255283
)
@@ -327,12 +355,13 @@ def main(arg_input: Optional[list[str]] = None):
327355

328356
parsed_args = parser.parse_args(top_level_args)
329357
system_info = get_system_info(parsed_args)
358+
sname = system_info.name.lower().replace("-", "_").replace(".", "_")
359+
timestamp = datetime.datetime.now().strftime("%Y_%m_%d-%I_%M_%S_%p")
330360

331361
if parsed_args.log_path and parsed_args.subcmd not in ["gen-plugin-config", "describe"]:
332-
sname = system_info.name.lower().replace("-", "_").replace(".", "_")
333362
log_path = os.path.join(
334363
parsed_args.log_path,
335-
f"scraper_logs_{sname}_{datetime.datetime.now().strftime('%Y_%m_%d-%I_%M_%S_%p')}",
364+
f"scraper_logs_{sname}_{timestamp}",
336365
)
337366
os.makedirs(log_path)
338367
else:
@@ -342,6 +371,10 @@ def main(arg_input: Optional[list[str]] = None):
342371
if log_path:
343372
logger.info("Log path: %s", log_path)
344373

374+
if parsed_args.subcmd == "summary":
375+
generate_summary(parsed_args.search_path, parsed_args.output_path, logger)
376+
sys.exit(0)
377+
345378
if parsed_args.subcmd == "describe":
346379
parse_describe(parsed_args, plugin_reg, config_reg, logger)
347380

@@ -392,6 +425,11 @@ def main(arg_input: Optional[list[str]] = None):
392425
plugin_subparser_map=plugin_subparser_map,
393426
)
394427

428+
if parsed_args.skip_sudo:
429+
plugin_config_inst_list[-1].global_args.setdefault("collection_args", {})[
430+
"skip_sudo"
431+
] = True
432+
395433
log_system_info(log_path, system_info, logger)
396434
except Exception as e:
397435
parser.error(str(e))
@@ -407,9 +445,14 @@ def main(arg_input: Optional[list[str]] = None):
407445
try:
408446
results = plugin_executor.run_queue()
409447

448+
dump_results_to_csv(results, sname, log_path, timestamp, logger)
449+
410450
if parsed_args.reference_config:
411451
ref_config = generate_reference_config(results, plugin_reg, logger)
412-
path = os.path.join(os.getcwd(), "reference_config.json")
452+
if log_path:
453+
path = os.path.join(log_path, "reference_config.json")
454+
else:
455+
path = os.path.join(os.getcwd(), "reference_config.json")
413456
try:
414457
with open(path, "w") as f:
415458
json.dump(

nodescraper/cli/helper.py

Lines changed: 98 additions & 6 deletions
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
)
@@ -344,11 +346,13 @@ def generate_reference_config(
344346

345347
plugin = plugin_reg.plugins.get(obj.source)
346348

347-
args = extract_analyzer_args_from_model(plugin, data_model, logger)
348-
if not args:
349-
continue
350-
plugins[obj.source] = {"analysis_args": {}}
351-
plugins[obj.source]["analysis_args"] = args.model_dump(exclude_none=True)
349+
if obj.source not in plugins:
350+
plugins[obj.source] = {}
351+
352+
a_args = extract_analyzer_args_from_model(plugin, data_model, logger)
353+
if a_args:
354+
plugins[obj.source]["analysis_args"] = a_args.model_dump(exclude_none=True)
355+
352356
plugin_config.plugins = plugins
353357

354358
return plugin_config
@@ -422,3 +426,91 @@ def find_datamodel_and_result(base_path: str) -> list[Tuple[str, str]]:
422426
tuple_list.append((datamodel_path, result_path))
423427

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

nodescraper/connection/inband/__init__.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@
2323
# SOFTWARE.
2424
#
2525
###############################################################################
26-
from .inband import CommandArtifact, FileArtifact, InBandConnection
26+
from .inband import (
27+
BaseFileArtifact,
28+
BinaryFileArtifact,
29+
CommandArtifact,
30+
InBandConnection,
31+
TextFileArtifact,
32+
)
2733
from .inbandlocal import LocalShell
2834
from .inbandmanager import InBandConnectionManager
2935
from .sshparams import SSHConnectionParams
@@ -33,6 +39,8 @@
3339
"LocalShell",
3440
"InBandConnectionManager",
3541
"InBandConnection",
36-
"FileArtifact",
42+
"BaseFileArtifact",
43+
"TextFileArtifact",
44+
"BinaryFileArtifact",
3745
"CommandArtifact",
3846
]

0 commit comments

Comments
 (0)