Skip to content

Commit a498e7c

Browse files
Merge pull request #17 from amd/alex_diff_logs
Generate reference config from previous logs
2 parents 6a48259 + 0b50105 commit a498e7c

File tree

9 files changed

+385
-126
lines changed

9 files changed

+385
-126
lines changed

README.md

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ The Node Scraper CLI can be used to run Node Scraper plugins on a target system.
1717
options are available:
1818

1919
```sh
20-
usage: node-scraper [-h] [--sys-name STRING] [--sys-location {LOCAL,REMOTE}]
21-
[--sys-interaction-level {PASSIVE,INTERACTIVE,DISRUPTIVE}] [--sys-sku STRING] [--sys-platform STRING]
22-
[--plugin-configs [STRING ...]] [--system-config STRING] [--connection-config STRING] [--log-path STRING]
23-
[--log-level {CRITICAL,FATAL,ERROR,WARN,WARNING,INFO,DEBUG,NOTSET}] [--gen-reference-config]
20+
usage: node-scraper [-h] [--sys-name STRING] [--sys-location {LOCAL,REMOTE}] [--sys-interaction-level {PASSIVE,INTERACTIVE,DISRUPTIVE}]
21+
[--sys-sku STRING] [--sys-platform STRING] [--plugin-configs [STRING ...]] [--system-config STRING]
22+
[--connection-config STRING] [--log-path STRING] [--log-level {CRITICAL,FATAL,ERROR,WARN,WARNING,INFO,DEBUG,NOTSET}]
23+
[--gen-reference-config]
2424
{run-plugins,describe,gen-plugin-config} ...
2525

2626
node scraper CLI
@@ -38,14 +38,12 @@ options:
3838
--sys-location {LOCAL,REMOTE}
3939
Location of target system (default: LOCAL)
4040
--sys-interaction-level {PASSIVE,INTERACTIVE,DISRUPTIVE}
41-
Specify system interaction level, used to determine the type of actions that plugins can perform (default:
42-
INTERACTIVE)
41+
Specify system interaction level, used to determine the type of actions that plugins can perform (default: INTERACTIVE)
4342
--sys-sku STRING Manually specify SKU of system (default: None)
4443
--sys-platform STRING
4544
Specify system platform (default: None)
4645
--plugin-configs [STRING ...]
47-
built-in config names or paths to plugin config JSONs. Available built-in configs: NodeStatus (default:
48-
None)
46+
built-in config names or paths to plugin config JSONs. Available built-in configs: NodeStatus (default: None)
4947
--system-config STRING
5048
Path to system config json (default: None)
5149
--connection-config STRING
@@ -54,7 +52,8 @@ options:
5452
--log-level {CRITICAL,FATAL,ERROR,WARN,WARNING,INFO,DEBUG,NOTSET}
5553
Change python log level (default: INFO)
5654
--gen-reference-config
57-
Generate reference config. File will be written to ./reference_config.json. (default: False)
55+
Generate reference config from system. Writes to ./reference_config.json. (default: False)
56+
5857

5958
```
6059
@@ -254,8 +253,8 @@ Here is an example of a comprehensive plugin config that specifies analyzer args
254253
```
255254
256255
2. **'gen-reference-config' command**
257-
This command can be used generate a reference config that is populated with current system
258-
configurations. The plugins that use analyzer args, where applied, will be populated with system
256+
This command can be used to generate a reference config that is populated with current system
257+
configurations. Plugins that use analyzer args (where applicable) will be populated with system
259258
data.
260259
Sample command:
261260
```sh
@@ -286,8 +285,16 @@ This will generate the following config:
286285
},
287286
"result_collators": {}
288287
```
289-
This can be later used on a different platform for comparison, using the steps at #2:
288+
This config can later be used on a different platform for comparison, using the steps at #2:
290289
```sh
291290
node-scraper --plugin-configs reference_config.json
292291

293292
```
293+
294+
An alternate way to generate a reference config is by using log files from a previous run. The
295+
example below uses log files from 'scraper_logs_<path>/':
296+
```sh
297+
node-scraper gen-plugin-config --gen-reference-config-from-logs scraper_logs_<path>/ --output-path custom_output_dir
298+
```
299+
This will generate a reference config that includes plugins with logged results in
300+
'scraper_log_<path>' and save the new config to 'custom_output_dir/reference_config.json'.

nodescraper/cli/cli.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from nodescraper.cli.dynamicparserbuilder import DynamicParserBuilder
3737
from nodescraper.cli.helper import (
3838
generate_reference_config,
39+
generate_reference_config_from_logs,
3940
get_plugin_configs,
4041
get_system_info,
4142
log_system_info,
@@ -148,7 +149,7 @@ def build_parser(
148149
"--gen-reference-config",
149150
dest="reference_config",
150151
action="store_true",
151-
help="Generate reference config. File will be written to ./reference_config.json.",
152+
help="Generate reference config from system. Writes to ./reference_config.json.",
152153
)
153154

154155
subparsers = parser.add_subparsers(dest="subcmd", help="Subcommands")
@@ -180,6 +181,13 @@ def build_parser(
180181
help="Generate a config for a plugin or list of plugins",
181182
)
182183

184+
config_builder_parser.add_argument(
185+
"--gen-reference-config-from-logs",
186+
dest="reference_config_from_logs",
187+
type=log_path_arg,
188+
help="Generate reference config from previous run logfiles. Writes to --output-path/reference_config.json if provided, otherwise ./reference_config.json.",
189+
)
190+
183191
config_builder_parser.add_argument(
184192
"--plugins",
185193
nargs="*",
@@ -338,6 +346,27 @@ def main(arg_input: Optional[list[str]] = None):
338346
parse_describe(parsed_args, plugin_reg, config_reg, logger)
339347

340348
if parsed_args.subcmd == "gen-plugin-config":
349+
350+
if parsed_args.reference_config_from_logs:
351+
ref_config = generate_reference_config_from_logs(
352+
parsed_args.reference_config_from_logs, plugin_reg, logger
353+
)
354+
output_path = os.getcwd()
355+
if parsed_args.output_path:
356+
output_path = parsed_args.output_path
357+
path = os.path.join(output_path, "reference_config.json")
358+
try:
359+
with open(path, "w") as f:
360+
json.dump(
361+
ref_config.model_dump(mode="json", exclude_none=True),
362+
f,
363+
indent=2,
364+
)
365+
logger.info("Reference config written to: %s", path)
366+
except Exception as exp:
367+
logger.error(exp)
368+
sys.exit(0)
369+
341370
parse_gen_plugin_config(parsed_args, plugin_reg, config_reg, logger)
342371

343372
parsed_plugin_args = {}

nodescraper/cli/helper.py

Lines changed: 104 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,16 @@
2828
import logging
2929
import os
3030
import sys
31-
from typing import Optional
31+
from pathlib import Path
32+
from typing import Optional, Tuple
33+
34+
from pydantic import BaseModel
3235

3336
from nodescraper.cli.inputargtypes import ModelArgHandler
3437
from nodescraper.configbuilder import ConfigBuilder
3538
from nodescraper.configregistry import ConfigRegistry
3639
from nodescraper.enums import ExecutionStatus, SystemInteractionLevel, SystemLocation
37-
from nodescraper.models import PluginConfig, PluginResult, SystemInfo
40+
from nodescraper.models import PluginConfig, PluginResult, SystemInfo, TaskResult
3841
from nodescraper.pluginexecutor import PluginExecutor
3942
from nodescraper.pluginregistry import PluginRegistry
4043
from nodescraper.resultcollators.tablesummary import TableSummary
@@ -283,6 +286,33 @@ def log_system_info(log_path: str | None, system_info: SystemInfo, logger: loggi
283286
logger.error(exp)
284287

285288

289+
def extract_analyzer_args_from_model(
290+
plugin_cls: type, data_model: BaseModel, logger: logging.Logger
291+
) -> Optional[BaseModel]:
292+
"""Extract analyzer args from a plugin and a data model.
293+
294+
Args:
295+
plugin_cls (type): The plugin class from registry.
296+
data_model (BaseModel): System data model.
297+
logger (logging.Logger): logger.
298+
299+
Returns:
300+
Optional[BaseModel]: Instance of analyzer args model or None if unavailable.
301+
"""
302+
if not hasattr(plugin_cls, "ANALYZER_ARGS") or not plugin_cls.ANALYZER_ARGS:
303+
logger.warning(
304+
"Plugin: %s does not support reference config creation. No analyzer args defined.",
305+
getattr(plugin_cls, "__name__", str(plugin_cls)),
306+
)
307+
return None
308+
309+
try:
310+
return plugin_cls.ANALYZER_ARGS.build_from_model(data_model)
311+
except NotImplementedError as e:
312+
logger.info("%s: %s", plugin_cls.__name__, str(e))
313+
return None
314+
315+
286316
def generate_reference_config(
287317
results: list[PluginResult], plugin_reg: PluginRegistry, logger: logging.Logger
288318
) -> PluginConfig:
@@ -313,21 +343,82 @@ def generate_reference_config(
313343
continue
314344

315345
plugin = plugin_reg.plugins.get(obj.source)
316-
if not plugin.ANALYZER_ARGS:
317-
logger.warning(
318-
"Plugin: %s does not support reference config creation. No analyzer args defined, skipping.",
319-
obj.source,
320-
)
321-
continue
322346

323-
args = None
324-
try:
325-
args = plugin.ANALYZER_ARGS.build_from_model(data_model)
326-
except NotImplementedError as nperr:
327-
logger.info(nperr)
347+
args = extract_analyzer_args_from_model(plugin, data_model, logger)
348+
if not args:
328349
continue
329350
plugins[obj.source] = {"analysis_args": {}}
330351
plugins[obj.source]["analysis_args"] = args.model_dump(exclude_none=True)
331352
plugin_config.plugins = plugins
332353

333354
return plugin_config
355+
356+
357+
def generate_reference_config_from_logs(
358+
path: str, plugin_reg: PluginRegistry, logger: logging.Logger
359+
) -> PluginConfig:
360+
"""Parse previous log files and generate plugin config with populated analyzer args
361+
362+
Args:
363+
path (str): path to log files
364+
plugin_reg (PluginRegistry): plugin registry instance
365+
logger (logging.Logger): logger instance
366+
367+
Returns:
368+
PluginConfig: instance of plugin config
369+
"""
370+
found = find_datamodel_and_result(path)
371+
plugin_config = PluginConfig()
372+
plugins = {}
373+
for dm, res in found:
374+
result_path = Path(res)
375+
res_payload = json.loads(result_path.read_text(encoding="utf-8"))
376+
task_res = TaskResult(**res_payload)
377+
dm_path = Path(dm)
378+
dm_payload = json.loads(dm_path.read_text(encoding="utf-8"))
379+
plugin = plugin_reg.plugins.get(task_res.parent)
380+
if not plugin:
381+
logger.warning(
382+
"Plugin %s not found in the plugin registry: %s.",
383+
task_res.parent,
384+
)
385+
continue
386+
387+
data_model = plugin.DATA_MODEL.model_validate(dm_payload)
388+
389+
args = extract_analyzer_args_from_model(plugin, data_model, logger)
390+
if not args:
391+
continue
392+
393+
plugins[task_res.parent] = {"analysis_args": args.model_dump(exclude_none=True)}
394+
395+
plugin_config.plugins = plugins
396+
return plugin_config
397+
398+
399+
def find_datamodel_and_result(base_path: str) -> list[Tuple[str, str]]:
400+
"""Get datamodel and result files
401+
402+
Args:
403+
base_path (str): location of previous run logs
404+
405+
Returns:
406+
list[Tuple[str, str]]: tuple of datamodel and result json files
407+
"""
408+
tuple_list: list[Tuple[str, str, str]] = []
409+
for root, _, files in os.walk(base_path):
410+
if "collector" in os.path.basename(root).lower():
411+
datamodel_path = None
412+
result_path = None
413+
414+
for fname in files:
415+
low = fname.lower()
416+
if low.endswith("datamodel.json"):
417+
datamodel_path = os.path.join(root, fname)
418+
elif low == "result.json":
419+
result_path = os.path.join(root, fname)
420+
421+
if datamodel_path and result_path:
422+
tuple_list.append((datamodel_path, result_path))
423+
424+
return tuple_list

nodescraper/models/taskresult.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
###############################################################################
2626
import datetime
2727
import logging
28-
from typing import Optional
28+
from typing import Any, Optional
2929

30-
from pydantic import BaseModel, Field, field_serializer
30+
from pydantic import BaseModel, Field, field_serializer, field_validator
3131

3232
from nodescraper.enums import EventPriority, ExecutionStatus
3333

@@ -65,6 +65,29 @@ def serialize_status(self, status: ExecutionStatus, _info) -> str:
6565
"""
6666
return status.name
6767

68+
@field_validator("status", mode="before")
69+
@classmethod
70+
def validate_status(cls, v: Any):
71+
"""Validator to ensure `status` is a valid ExecutionStatus enum.
72+
73+
Args:
74+
v (Any): The input value to validate (can be str or ExecutionStatus).
75+
76+
Returns:
77+
ExecutionStatus: The validated enum value.
78+
79+
Raises:
80+
ValueError: If the string is not a valid enum name.
81+
"""
82+
if isinstance(v, ExecutionStatus):
83+
return v
84+
if isinstance(v, str):
85+
try:
86+
return ExecutionStatus[v]
87+
except KeyError as err:
88+
raise ValueError(f"Unknown status name: {v!r}") from err
89+
return v
90+
6891
@property
6992
def duration(self) -> str | None:
7093
"""return duration of time as a string

nodescraper/plugins/inband/storage/storagedata.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@
2323
# SOFTWARE.
2424
#
2525
###############################################################################
26-
from pydantic import BaseModel, field_serializer
26+
from pydantic import BaseModel, field_serializer, field_validator
2727

2828
from nodescraper.models import DataModel
29-
from nodescraper.utils import bytes_to_human_readable
29+
from nodescraper.utils import bytes_to_human_readable, convert_to_bytes
3030

3131

3232
class DeviceStorageData(BaseModel):
@@ -51,6 +51,20 @@ def serialize_used(self, used: int, _info) -> str:
5151
def serialize_percent(self, percent: float, _info) -> str:
5252
return f"{percent}%"
5353

54+
@field_validator("total", "free", "used", mode="before")
55+
@classmethod
56+
def parse_bytes_fields(cls, v):
57+
if isinstance(v, str):
58+
return convert_to_bytes(v)
59+
return v
60+
61+
@field_validator("percent", mode="before")
62+
@classmethod
63+
def parse_percent_field(cls, v):
64+
if isinstance(v, str) and v.endswith("%"):
65+
return float(v.rstrip("%"))
66+
return v
67+
5468

5569
class StorageDataModel(DataModel):
5670
storage_data: dict[str, DeviceStorageData]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"bios_version": "M17"
3+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"status": "OK",
3+
"message": "BIOS: M17",
4+
"task": "BiosCollector",
5+
"parent": "BiosPlugin",
6+
"start_time": "2025-07-07T11:11:08.186472",
7+
"end_time": "2025-07-07T11:11:08.329110"
8+
}

0 commit comments

Comments
 (0)