Skip to content

Commit f1f2f78

Browse files
committed
Merge branch 'development' into alex_journal_log
2 parents 195544b + 1bddedf commit f1f2f78

22 files changed

+346
-67
lines changed

README.md

Lines changed: 167 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22
Node Scraper is a tool which performs automated data collection and analysis for the purposes of
33
system debug.
44

5+
## Table of Contents
6+
- [Installation](#installation)
7+
- [Install From Source](#install-from-source)
8+
- [CLI Usage](#cli-usage)
9+
- [Execution Methods](#execution-methods)
10+
- [Example: Remote Execution](#example-remote-execution)
11+
- [Example: connection_config.json](#example-connection_configjson)
12+
- [Subcommands](#subcommands)
13+
- ['describe' subcommand](#describe-subcommand)
14+
- ['run-plugins' sub command](#run-plugins-sub-command)
15+
- ['gen-plugin-config' sub command](#gen-plugin-config-sub-command)
16+
- ['summary' sub command](#summary-sub-command)
17+
- [Configs](#configs)
18+
- [Global args](#global-args)
19+
- [Plugin config: `--plugin-configs` command](#plugin-config---plugin-configs-command)
20+
- [Reference config: `gen-reference-config` command](#reference-config-gen-reference-config-command)
21+
- [nodescraper integration](#nodescraper-integration)
22+
23+
524
## Installation
625
### Install From Source
726
Node Scraper requires Python 3.10+ for installation. After cloning this repository,
@@ -72,7 +91,7 @@ To use remote execution, specify `--sys-location REMOTE` and provide a connectio
7291
node-scraper --sys-name <remote_host> --sys-location REMOTE --connection-config ./connection_config.json run-plugins DmesgPlugin
7392
```
7493
75-
##### Example connection_config.json
94+
##### Example: connection_config.json
7695
7796
```json
7897
{
@@ -90,13 +109,13 @@ node-scraper --sys-name <remote_host> --sys-location REMOTE --connection-config
90109
- If using SSH keys, specify `key_filename` instead of `password`.
91110
- The remote user must have permissions to run the requested plugins and access required files. If needed, use the `--skip-sudo` argument to skip plugins requiring sudo.
92111
93-
### Subcommmands
112+
### Subcommands
94113
95114
Plugins to run can be specified in two ways, using a plugin JSON config file or using the
96115
'run-plugins' sub command. These two options are not mutually exclusive and can be used together.
97116
98117
99-
1. **'describe' subcommand**
118+
#### **'describe' subcommand**
100119
101120
You can use the `describe` subcommand to display details about built-in configs or plugins.
102121
List all built-in configs:
@@ -119,7 +138,7 @@ Show details for a specific plugin
119138
node-scraper describe plugin <plugin-name>
120139
```
121140
122-
2. **'run-plugins' sub command**
141+
#### **'run-plugins' sub command**
123142
The plugins to run and their associated arguments can also be specified directly on the CLI using
124143
the 'run-plugins' sub-command. Using this sub-command you can specify a plugin name followed by
125144
the arguments for that particular plugin. Multiple plugins can be specified at once.
@@ -165,7 +184,7 @@ Use plugin configs and 'run-plugins'
165184
node-scraper run-plugins BiosPlugin
166185
```
167186
168-
3. **'gen-plugin-config' sub command**
187+
#### **'gen-plugin-config' sub command**
169188
The 'gen-plugin-config' sub command can be used to generate a plugin config JSON file for a plugin
170189
or list of plugins that can then be customized. Plugin arguments which have default values will be
171190
prepopulated in the JSON file, arguments without default values will have a value of 'null'.
@@ -200,7 +219,7 @@ This would produce the following config:
200219
}
201220
```
202221
203-
4. **'summary' sub command**
222+
#### **'summary' sub command**
204223
The 'summary' subcommand can be used to combine results from multiple runs of node-scraper to a
205224
single summary.csv file. Sample run:
206225
```sh
@@ -210,7 +229,7 @@ This will generate a new file '/<path_to_node-scraper_logs>/summary.csv' file. T
210229
contain the results from all 'nodescraper.csv' files from '/<path_to_node-scarper_logs>'.
211230
212231
213-
### Plugin Configs
232+
### Configs
214233
A plugin JSON config should follow the structure of the plugin config model defined here.
215234
The globals field is a dictionary of global key-value pairs; values in globals will be passed to
216235
any plugin that supports the corresponding key. The plugins field should be a dictionary mapping
@@ -237,7 +256,21 @@ tabular format to the console.
237256
}
238257
```
239258
240-
1. **'--plugin-configs' command**
259+
#### Global args
260+
Global args can be used to skip sudo plugins or enable/disble either collection or analysis.
261+
Below is an example that skips sudo requiring plugins and disables analysis.
262+
263+
```json
264+
"global_args": {
265+
"collection_args": {
266+
"skip_sudo" : 1
267+
},
268+
"collection" : 1,
269+
"analysis" : 0
270+
},
271+
```
272+
273+
#### Plugin config: **'--plugin-configs' command**
241274
A plugin config can be used to compare the system data against the config specifications:
242275
```sh
243276
node-scraper --plugin-configs plugin_config.json
@@ -295,7 +328,7 @@ Here is an example of a comprehensive plugin config that specifies analyzer args
295328
}
296329
```
297330
298-
2. **'gen-reference-config' command**
331+
#### Reference config: **'gen-reference-config' command**
299332
This command can be used to generate a reference config that is populated with current system
300333
configurations. Plugins that use analyzer args (where applicable) will be populated with system
301334
data.
@@ -341,3 +374,128 @@ node-scraper gen-plugin-config --gen-reference-config-from-logs scraper_logs_<pa
341374
```
342375
This will generate a reference config that includes plugins with logged results in
343376
'scraper_log_<path>' and save the new config to 'custom_output_dir/reference_config.json'.
377+
378+
379+
## nodescraper integration
380+
Nodescraper can be integrated inside another Python tool by leveraging its classes and functionality.
381+
See below for a comprehensive example on how to create plugins and run the associated data
382+
collection and analysis.
383+
Sample run command:
384+
```sh
385+
python3 sample.py
386+
```
387+
388+
Sample.py file:
389+
```python
390+
import logging
391+
import sys
392+
from nodescraper.plugins.inband.bios.bios_plugin import BiosPlugin
393+
from nodescraper.plugins.inband.bios.analyzer_args import BiosAnalyzerArgs
394+
from nodescraper.plugins.inband.kernel.kernel_plugin import KernelPlugin
395+
from nodescraper.plugins.inband.kernel.analyzer_args import KernelAnalyzerArgs
396+
from nodescraper.plugins.inband.os.os_plugin import OsPlugin
397+
from nodescraper.plugins.inband.os.analyzer_args import OsAnalyzerArgs
398+
from nodescraper.models.systeminfo import SystemInfo, OSFamily
399+
from nodescraper.enums import EventPriority, SystemLocation
400+
from nodescraper.resultcollators.tablesummary import TableSummary
401+
from nodescraper.connection.inband.inbandmanager import InBandConnectionManager
402+
from nodescraper.connection.inband.sshparams import SSHConnectionParams
403+
from nodescraper.pluginregistry import PluginRegistry
404+
from nodescraper.models.pluginconfig import PluginConfig
405+
from nodescraper.pluginexecutor import PluginExecutor
406+
407+
def main():
408+
409+
#setting up my custom logger
410+
log_level = "INFO"
411+
handlers = [logging.StreamHandler(stream=sys.stdout)]
412+
logging.basicConfig(
413+
force=True,
414+
level=log_level,
415+
format="%(asctime)25s %(levelname)10s %(name)25s | %(message)s",
416+
datefmt="%Y-%m-%d %H:%M:%S %Z",
417+
handlers=handlers,
418+
encoding="utf-8",
419+
)
420+
logging.root.setLevel(logging.INFO)
421+
logging.getLogger("paramiko").setLevel(logging.ERROR)
422+
logger = logging.getLogger("nodescraper")
423+
424+
#setting up system info
425+
system_info = SystemInfo(name="test_host",
426+
platform="X",
427+
os_familty=OSFamily.LINUX,
428+
sku="some_sku")
429+
430+
#initiate plugins
431+
bios_plugin = BiosPlugin(system_info=system_info, logger=logger)
432+
kernel_plugin = KernelPlugin(system_info=system_info, logger=logger)
433+
434+
#launch data collection
435+
_ = bios_plugin.collect()
436+
_ = kernel_plugin.collect()
437+
438+
#launch data analysis
439+
bios_plugin.analyze(analysis_args=BiosAnalyzerArgs(exp_bios_version="XYZ"))
440+
kernel_plugin.analyze(analysis_args=KernelAnalyzerArgs(exp_kernel="ABC"))
441+
442+
#log plugin data models
443+
logger.info(kernel_plugin.data.model_dump())
444+
logger.info(bios_plugin.data.model_dump())
445+
446+
#alternate method
447+
all_res = []
448+
449+
#launch plugin collection & analysis
450+
bios_result = bios_plugin.run(analysis_args={"exp_bios_version":"ABC"})
451+
all_res.append(bios_result)
452+
table_summary = TableSummary()
453+
table_summary.collate_results(all_res, None)
454+
455+
#remote connection
456+
system_info.location=SystemLocation.REMOTE
457+
ssh_params = SSHConnectionParams(hostname="my_system",
458+
port=22,
459+
username="my_username",
460+
key_filename="/home/user/.ssh/ssh_key")
461+
conn_manager = InBandConnectionManager(system_info=system_info, connection_args=ssh_params)
462+
os_plugin = OsPlugin(system_info=system_info, logger=logger, connection_manager=conn_manager)
463+
os_plugin.run(analysis_args=OsAnalyzerArgs(exp_os="DEF"))
464+
465+
#run multiple plugins through a queue
466+
system_info.location=SystemLocation.LOCAL
467+
config_dict = {
468+
"global_args": {
469+
"collection" : 1,
470+
"analysis" : 1
471+
},
472+
"plugins": {
473+
"BiosPlugin": {
474+
"analysis_args": {
475+
"exp_bios_version": "123",
476+
}
477+
},
478+
"KernelPlugin": {
479+
"analysis_args": {
480+
"exp_kernel": "ABC",
481+
}
482+
}
483+
},
484+
"result_collators": {},
485+
"name": "plugin_config",
486+
"desc": "Auto generated config"
487+
}
488+
489+
config1 = PluginConfig(**config_dict)
490+
plugin_executor = PluginExecutor(
491+
logger=logger,
492+
plugin_configs=[config1],
493+
system_info=system_info
494+
)
495+
results = plugin_executor.run_queue()
496+
497+
498+
499+
if __name__ == "__main__":
500+
main()
501+
```

nodescraper/cli/helper.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ def dump_results_to_csv(
456456
"message": res.message,
457457
}
458458
all_rows.append(row)
459+
459460
dump_to_csv(all_rows, filename, fieldnames, logger)
460461

461462

nodescraper/connection/inband/inbandmanager.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ def connect(
120120
return self.result
121121

122122
try:
123-
self.logger.info("Initializing SSH connection to system")
123+
self.logger.info(
124+
"Initializing SSH connection to system '%s'", self.connection_args.hostname
125+
)
124126
self.connection = RemoteShell(self.connection_args)
125127
self.connection.connect_ssh()
126128
self._check_os_family()

nodescraper/plugins/inband/bios/bios_analyzer.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ def analyze_data(
8080
self.result.status = ExecutionStatus.OK
8181
return self.result
8282

83-
self.result.message = "Bios data mismatch!"
83+
self.result.message = (
84+
f"Bios data mismatch! Expected {args.exp_bios_version}, actual: {data.bios_version}"
85+
)
8486
self.result.status = ExecutionStatus.ERROR
8587
self._log_event(
8688
category=EventCategory.BIOS,

nodescraper/plugins/inband/cmdline/cmdline_analyzer.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def _compare_cmdline(self, cmdline: str, required_cmdline: list, banned_cmdline:
7272
console_log=True,
7373
)
7474

75-
return not (missing_required or found_banned)
75+
return not (missing_required or found_banned), missing_required, found_banned
7676

7777
def analyze_data(
7878
self, data: CmdlineDataModel, args: Optional[CmdlineAnalyzerArgs] = None
@@ -93,12 +93,16 @@ def analyze_data(
9393
return self.result
9494

9595
# check if any of the cmdline defined in the list match the actual kernel cmdline
96-
if self._compare_cmdline(data.cmdline, args.required_cmdline, args.banned_cmdline):
96+
check, missing_required, found_banned = self._compare_cmdline(
97+
data.cmdline, args.required_cmdline, args.banned_cmdline
98+
)
99+
100+
if check:
97101
self.result.message = "Kernel cmdline matches expected"
98102
self.result.status = ExecutionStatus.OK
99103
return self.result
100104

101-
self.result.message = "Illegal kernel cmdline"
105+
self.result.message = f"Illegal kernel cmdline, found_banned: {found_banned}, missing required: {missing_required}"
102106
self.result.status = ExecutionStatus.ERROR
103107
self._log_event(
104108
category=EventCategory.OS,

nodescraper/plugins/inband/dkms/dkms_analyzer.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ def analyze_data(
6666

6767
error_state = False
6868

69+
actual_values = []
70+
expected_values = []
6971
for check, accepted_values in check_map.items():
7072
actual_value = getattr(data, check)
7173
for accepted_value in accepted_values:
@@ -84,6 +86,8 @@ def analyze_data(
8486
elif actual_value == accepted_value:
8587
break
8688
else:
89+
expected_values.append(accepted_values)
90+
actual_values.append(actual_value)
8791
self._log_event(
8892
category=EventCategory.SW_DRIVER,
8993
description=f"DKMS {check} has an unexpected value",
@@ -95,6 +99,8 @@ def analyze_data(
9599

96100
if error_state:
97101
self.result.status = ExecutionStatus.ERROR
98-
self.result.message = "DKMS data mismatch"
102+
self.result.message = (
103+
f"DKMS data mismatch. \nActual: {actual_values}.\nExpected: {expected_values}"
104+
)
99105

100106
return self.result

nodescraper/plugins/inband/kernel/kernel_analyzer.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ def analyze_data(
7777
self.result.status = ExecutionStatus.OK
7878
return self.result
7979

80-
self.result.message = "Kernel mismatch!"
80+
self.result.message = (
81+
f"Kernel mismatch! Expected: {args.exp_kernel}, actual: {data.kernel_version}"
82+
)
8183
self.result.status = ExecutionStatus.ERROR
8284
self._log_event(
8385
category=EventCategory.OS,

nodescraper/plugins/inband/kernel_module/kernel_module_analyzer.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ def analyze_data(
150150
data={"regex_filters": {args.regex_filter}},
151151
priority=EventPriority.ERROR,
152152
)
153-
self.result.message = "Kernel modules failed to match regex"
153+
self.result.message = (
154+
f"Kernel modules failed to match regex. Regex: {args.regex_filter}"
155+
)
154156
self.result.status = ExecutionStatus.ERROR
155157
return self.result
156158

@@ -161,7 +163,7 @@ def analyze_data(
161163
data={"unmatched_pattern: ": unmatched_pattern},
162164
priority=EventPriority.INFO,
163165
)
164-
self.result.message = "Kernel modules failed to match every pattern"
166+
self.result.message = f"Kernel modules failed to match every pattern. Unmatched patterns: {unmatched_pattern}"
165167
self.result.status = ExecutionStatus.ERROR
166168
return self.result
167169

@@ -186,7 +188,7 @@ def analyze_data(
186188
data=args.kernel_modules,
187189
priority=EventPriority.ERROR,
188190
)
189-
self.result.message = "Kernel modules not matched"
191+
self.result.message = f"Kernel modules not matched: {not_matched}"
190192
self.result.status = ExecutionStatus.ERROR
191193
return self.result
192194
# some modules matched
@@ -198,7 +200,7 @@ def analyze_data(
198200
data=not_matched,
199201
priority=EventPriority.ERROR,
200202
)
201-
self.result.message = "Kernel modules not matched"
203+
self.result.message = f"Kernel modules not matched: {not_matched}"
202204
self.result.status = ExecutionStatus.ERROR
203205
return self.result
204206
else:

nodescraper/plugins/inband/memory/memory_analyzer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def analyze_data(
6767
self.result.message = "Memory usage is within maximum allowed used memory"
6868
self.result.status = ExecutionStatus.OK
6969
else:
70-
self.result.message = "Memory usage is more than the maximum allowed used memory!"
70+
self.result.message = f"Memory usage exceeded max allowed! Used: {used_memory}, max allowed: {max_allowed_used_mem}"
7171
self.result.status = ExecutionStatus.ERROR
7272
self._log_event(
7373
category=EventCategory.OS,

0 commit comments

Comments
 (0)