Skip to content

Commit 812e820

Browse files
Merge pull request #44 from amd/alex_ext_plugins
External plugins example
2 parents 4e68db4 + 48042f9 commit 812e820

File tree

11 files changed

+273
-133
lines changed

11 files changed

+273
-133
lines changed

EXTENDING.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Extending Node Scraper
2+
3+
This guide covers how to integrate Nodescraper into another Python tool and how to create and use external plugins.
4+
5+
## Table of Contents
6+
- [nodescraper integration](#nodescraper-integration)
7+
- [external plugins](#external-plugins)
8+
9+
## nodescraper integration
10+
Nodescraper can be integrated inside another Python tool by leveraging its classes and functionality.
11+
See below for a comprehensive example on how to create plugins and run the associated data
12+
collection and analysis.
13+
Sample run command:
14+
```sh
15+
python3 sample.py
16+
```
17+
18+
Sample.py file:
19+
```python
20+
import logging
21+
import sys
22+
from nodescraper.plugins.inband.bios.bios_plugin import BiosPlugin
23+
from nodescraper.plugins.inband.bios.analyzer_args import BiosAnalyzerArgs
24+
from nodescraper.plugins.inband.kernel.kernel_plugin import KernelPlugin
25+
from nodescraper.plugins.inband.kernel.analyzer_args import KernelAnalyzerArgs
26+
from nodescraper.plugins.inband.os.os_plugin import OsPlugin
27+
from nodescraper.plugins.inband.os.analyzer_args import OsAnalyzerArgs
28+
from nodescraper.models.systeminfo import SystemInfo, OSFamily
29+
from nodescraper.enums import EventPriority, SystemLocation
30+
from nodescraper.resultcollators.tablesummary import TableSummary
31+
from nodescraper.connection.inband.inbandmanager import InBandConnectionManager
32+
from nodescraper.connection.inband.sshparams import SSHConnectionParams
33+
from nodescraper.pluginregistry import PluginRegistry
34+
from nodescraper.models.pluginconfig import PluginConfig
35+
from nodescraper.pluginexecutor import PluginExecutor
36+
37+
def main():
38+
39+
#setting up my custom logger
40+
log_level = "INFO"
41+
handlers = [logging.StreamHandler(stream=sys.stdout)]
42+
logging.basicConfig(
43+
force=True,
44+
level=log_level,
45+
format="%(asctime)25s %(levelname)10s %(name)25s | %(message)s",
46+
datefmt="%Y-%m-%d %H:%M:%S %Z",
47+
handlers=handlers,
48+
encoding="utf-8",
49+
)
50+
logging.root.setLevel(logging.INFO)
51+
logging.getLogger("paramiko").setLevel(logging.ERROR)
52+
logger = logging.getLogger("nodescraper")
53+
54+
#setting up system info
55+
system_info = SystemInfo(name="test_host",
56+
platform="X",
57+
os_familty=OSFamily.LINUX,
58+
sku="some_sku")
59+
60+
#initiate plugins
61+
bios_plugin = BiosPlugin(system_info=system_info, logger=logger)
62+
kernel_plugin = KernelPlugin(system_info=system_info, logger=logger)
63+
64+
#launch data collection
65+
_ = bios_plugin.collect()
66+
_ = kernel_plugin.collect()
67+
68+
#launch data analysis
69+
bios_plugin.analyze(analysis_args=BiosAnalyzerArgs(exp_bios_version="XYZ"))
70+
kernel_plugin.analyze(analysis_args=KernelAnalyzerArgs(exp_kernel="ABC"))
71+
72+
#log plugin data models
73+
logger.info(kernel_plugin.data.model_dump())
74+
logger.info(bios_plugin.data.model_dump())
75+
76+
#alternate method
77+
all_res = []
78+
79+
#launch plugin collection & analysis
80+
bios_result = bios_plugin.run(analysis_args={"exp_bios_version":"ABC"})
81+
all_res.append(bios_result)
82+
table_summary = TableSummary()
83+
table_summary.collate_results(all_res, None)
84+
85+
#remote connection
86+
system_info.location=SystemLocation.REMOTE
87+
ssh_params = SSHConnectionParams(hostname="my_system",
88+
port=22,
89+
username="my_username",
90+
key_filename="/home/user/.ssh/ssh_key")
91+
conn_manager = InBandConnectionManager(system_info=system_info, connection_args=ssh_params)
92+
os_plugin = OsPlugin(system_info=system_info, logger=logger, connection_manager=conn_manager)
93+
os_plugin.run(analysis_args=OsAnalyzerArgs(exp_os="DEF"))
94+
95+
#run multiple plugins through a queue
96+
system_info.location=SystemLocation.LOCAL
97+
config_dict = {
98+
"global_args": {
99+
"collection" : 1,
100+
"analysis" : 1
101+
},
102+
"plugins": {
103+
"BiosPlugin": {
104+
"analysis_args": {
105+
"exp_bios_version": "123",
106+
}
107+
},
108+
"KernelPlugin": {
109+
"analysis_args": {
110+
"exp_kernel": "ABC",
111+
}
112+
}
113+
},
114+
"result_collators": {},
115+
"name": "plugin_config",
116+
"desc": "Auto generated config"
117+
}
118+
119+
config1 = PluginConfig(**config_dict)
120+
plugin_executor = PluginExecutor(
121+
logger=logger,
122+
plugin_configs=[config1],
123+
system_info=system_info
124+
)
125+
results = plugin_executor.run_queue()
126+
127+
128+
129+
if __name__ == "__main__":
130+
main()
131+
```
132+
133+
## external plugins
134+
External plugins can be added and installed in the same env as node-scraper plugins. Find an
135+
example of an external plugin in **`/docs/node-scraper-external`**

README.md

Lines changed: 1 addition & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ system debug.
1818
- [Global args](#global-args)
1919
- [Plugin config: `--plugin-configs` command](#plugin-config---plugin-configs-command)
2020
- [Reference config: `gen-reference-config` command](#reference-config-gen-reference-config-command)
21-
- [nodescraper integration](#nodescraper-integration)
22-
21+
- **Extending Node Scraper (integration & external plugins)** → See [EXTENDING.md](EXTENDING.md)
2322

2423
## Installation
2524
### Install From Source
@@ -114,7 +113,6 @@ node-scraper --sys-name <remote_host> --sys-location REMOTE --connection-config
114113
Plugins to run can be specified in two ways, using a plugin JSON config file or using the
115114
'run-plugins' sub command. These two options are not mutually exclusive and can be used together.
116115
117-
118116
#### **'describe' subcommand**
119117
120118
You can use the `describe` subcommand to display details about built-in configs or plugins.
@@ -228,7 +226,6 @@ node-scraper summary --summary_path /<path_to_node-scraper_logs>
228226
This will generate a new file '/<path_to_node-scraper_logs>/summary.csv' file. This file will
229227
contain the results from all 'nodescraper.csv' files from '/<path_to_node-scarper_logs>'.
230228
231-
232229
### Configs
233230
A plugin JSON config should follow the structure of the plugin config model defined here.
234231
The globals field is a dictionary of global key-value pairs; values in globals will be passed to
@@ -374,128 +371,3 @@ node-scraper gen-plugin-config --gen-reference-config-from-logs scraper_logs_<pa
374371
```
375372
This will generate a reference config that includes plugins with logged results in
376373
'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-
```
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# node-scraper external plugins (example)
2+
3+
This directory lives at **`/docs/node-scraper-external`** in the `node-scraper` repo and contains
4+
an example external plugin package you can install in editable mode.
5+
6+
## Installation
7+
8+
Use the same Python environment as `node-scraper`.
9+
10+
```bash
11+
cd ~/node-scraper
12+
source venv/bin/activate
13+
pip install -e ./docs/node-scraper-external
14+
```
15+
You should see `ext-nodescraper-plugins` installed in editable mode.
16+
17+
18+
## Verify the external package is importable
19+
20+
```bash
21+
python - <<'PY'
22+
import ext_nodescraper_plugins
23+
print("ext_nodescraper_plugins loaded from:", ext_nodescraper_plugins.__file__)
24+
PY
25+
```
26+
27+
## Run external plugins
28+
29+
Confirm the CLI sees your external plugin(s):
30+
31+
```bash
32+
node-scraper run-plugins -h
33+
node-scraper run-plugins SamplePlugin
34+
```
35+
36+
## Add your own plugins
37+
38+
Add new modules under the **`ext_nodescraper_plugins/`** package. Example layout:
39+
40+
```
41+
/docs/node-scraper-external
42+
├─ pyproject.toml
43+
└─ ext_nodescraper_plugins/
44+
└─ sample/
45+
├─ __init__.py
46+
└─ sample_plugin.py
47+
```
48+
49+
```
50+
51+
Re-install (editable mode picks up code changes automatically, but if you add new files you may
52+
need to re-run):
53+
```bash
54+
pip install -e .
55+
```

docs/node-scraper-external/ext_nodescraper_plugins/sample/__init__.py

Whitespace-only changes.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from nodescraper.enums import EventCategory, EventPriority, ExecutionStatus
2+
from nodescraper.interfaces import DataAnalyzer
3+
from nodescraper.models import TaskResult
4+
5+
from .sample_data import SampleDataModel
6+
7+
8+
class SampleAnalyzer(DataAnalyzer[SampleDataModel, None]):
9+
10+
DATA_MODEL = SampleDataModel
11+
12+
def analyze_data(self, data: SampleDataModel, args=None) -> TaskResult:
13+
if data.some_str != "expected_str":
14+
self.result.message = "String does not match expected"
15+
self.result.status = ExecutionStatus.ERROR
16+
return self.result
17+
18+
self._log_event(
19+
category=EventCategory.OS,
20+
description=f"{self.result.message}",
21+
data={"expected": "expected_str", "actual": data.some_str},
22+
priority=EventPriority.CRITICAL,
23+
console_log=True,
24+
)
25+
return self.result
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from nodescraper.base import InBandDataCollector
2+
from nodescraper.enums import ExecutionStatus
3+
from nodescraper.models import TaskResult
4+
5+
from .sample_data import SampleDataModel
6+
7+
8+
class SampleCollector(InBandDataCollector[SampleDataModel, None]):
9+
10+
DATA_MODEL = SampleDataModel
11+
12+
def collect_data(self, args=None) -> tuple[TaskResult, SampleDataModel | None]:
13+
sample_data = SampleDataModel(some_str="example123")
14+
self.result.message = "Collector ran successfully"
15+
self.result.status = ExecutionStatus.OK
16+
17+
return self.result, sample_data
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from nodescraper.models import DataModel
2+
3+
4+
class SampleDataModel(DataModel):
5+
some_str: str

0 commit comments

Comments
 (0)