Skip to content

Commit 27eb13a

Browse files
Merge pull request #146 from amd/alex_redfish
Redfish OOB check + new OOB plugin: RedfishEndpointPlugin
2 parents 54ebaec + a4798a4 commit 27eb13a

28 files changed

Lines changed: 1884 additions & 154 deletions

README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ system debug.
1414
- ['run-plugins' sub command](#run-plugins-sub-command)
1515
- ['gen-plugin-config' sub command](#gen-plugin-config-sub-command)
1616
- ['compare-runs' subcommand](#compare-runs-subcommand)
17+
- ['show-redfish-oem-allowable' subcommand](#show-redfish-oem-allowable-subcommand)
1718
- ['summary' sub command](#summary-sub-command)
1819
- [Configs](#configs)
1920
- [Global args](#global-args)
@@ -116,6 +117,8 @@ node-scraper --sys-name <remote_host> --sys-location REMOTE --connection-config
116117
117118
##### Example: connection_config.json
118119
120+
In-band (SSH) connection:
121+
119122
```json
120123
{
121124
"InBandConnectionManager": {
@@ -128,6 +131,24 @@ node-scraper --sys-name <remote_host> --sys-location REMOTE --connection-config
128131
}
129132
```
130133
134+
Redfish (BMC) connection for Redfish-only plugins:
135+
136+
```json
137+
{
138+
"RedfishConnectionManager": {
139+
"host": "bmc.example.com",
140+
"port": 443,
141+
"username": "admin",
142+
"password": "secret",
143+
"use_https": true,
144+
"verify_ssl": true,
145+
"api_root": "redfish/v1"
146+
}
147+
}
148+
```
149+
150+
- `api_root` (optional): Redfish API path (e.g. `redfish/v1`). If omitted, the default `redfish/v1` is used. Override this when your BMC uses a different API version path.
151+
131152
**Notes:**
132153
- If using SSH keys, specify `key_filename` instead of `password`.
133154
- 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.
@@ -319,6 +340,63 @@ node-scraper compare-runs path1 path2 --include-plugins DmesgPlugin --dont-trunc
319340
320341
You can pass multiple plugin names to `--skip-plugins` or `--include-plugins`.
321342
343+
#### **'show-redfish-oem-allowable' subcommand**
344+
The `show-redfish-oem-allowable` subcommand fetches the list of OEM diagnostic types supported by your BMC (from the Redfish LogService `OEMDiagnosticDataType@Redfish.AllowableValues`). Use it to discover which types you can put in `oem_diagnostic_types_allowable` and `oem_diagnostic_types` in the Redfish OEM diag plugin config.
345+
346+
**Requirements:** A Redfish connection config (same as for RedfishOemDiagPlugin).
347+
348+
**Command:**
349+
```sh
350+
node-scraper --connection-config connection-config.json show-redfish-oem-allowable --log-service-path "redfish/v1/Systems/UBB/LogServices/DiagLogs"
351+
```
352+
353+
Output is a JSON array of allowable type names (e.g. `["Dmesg", "JournalControl", "AllLogs", ...]`). Copy that list into your plugin config’s `oem_diagnostic_types_allowable` if you want to match your BMC.
354+
355+
**Redfish OEM diag plugin config example**
356+
357+
Use a plugin config that points at your LogService and lists the types to collect. Logs are written under the run log path (see `--log-path`).
358+
359+
```json
360+
{
361+
"name": "Redfish OEM diagnostic logs",
362+
"desc": "Collect OEM diagnostic logs from Redfish LogService. Requires Redfish connection config.",
363+
"global_args": {},
364+
"plugins": {
365+
"RedfishOemDiagPlugin": {
366+
"collection_args": {
367+
"log_service_path": "redfish/v1/Systems/UBB/LogServices/DiagLogs",
368+
"oem_diagnostic_types_allowable": [
369+
"JournalControl",
370+
"AllLogs",
371+
...
372+
],
373+
"oem_diagnostic_types": ["JournalControl", "AllLogs"],
374+
"task_timeout_s": 600
375+
},
376+
"analysis_args": {
377+
"require_all_success": false
378+
}
379+
}
380+
},
381+
"result_collators": {}
382+
}
383+
```
384+
385+
- **`log_service_path`**: Redfish path to the LogService (e.g. DiagLogs). Must match your system (e.g. `UBB` vs. another system id).
386+
- **`oem_diagnostic_types_allowable`**: Full list of types the BMC supports (from `show-redfish-oem-allowable` or vendor docs).
387+
- **`oem_diagnostic_types`**: Subset of types to collect on each run (e.g. `["JournalControl", "AllLogs"]`).
388+
- **`task_timeout_s`**: Max seconds to wait per collection task.
389+
390+
**How to use**
391+
392+
1. **Discover allowable types** (optional): run `show-redfish-oem-allowable` and paste the output into `oem_diagnostic_types_allowable` in your plugin config.
393+
2. **Set `oem_diagnostic_types`** to the list you want to collect (e.g. `["JournalControl", "AllLogs"]`).
394+
3. **Run the plugin** with a Redfish connection config and your plugin config:
395+
```sh
396+
node-scraper --connection-config connection-config.json --plugin-config plugin_config_redfish_oem_diag.json run-plugins RedfishOemDiagPlugin
397+
```
398+
4. Use **`--log-path`** to choose where run logs (and OEM diag archives) are written.
399+
322400
#### **'summary' sub command**
323401
The 'summary' subcommand can be used to combine results from multiple runs of node-scraper to a
324402
single summary.csv file. Sample run:

nodescraper/base/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,14 @@
2525
###############################################################################
2626
from .inbandcollectortask import InBandDataCollector
2727
from .inbanddataplugin import InBandDataPlugin
28+
from .oobanddataplugin import OOBandDataPlugin
29+
from .redfishcollectortask import RedfishDataCollector
2830
from .regexanalyzer import RegexAnalyzer
2931

3032
__all__ = [
3133
"InBandDataCollector",
3234
"InBandDataPlugin",
35+
"OOBandDataPlugin",
36+
"RedfishDataCollector",
3337
"RegexAnalyzer",
3438
]

nodescraper/base/inbanddataplugin.py

Lines changed: 1 addition & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,11 @@
2323
# SOFTWARE.
2424
#
2525
###############################################################################
26-
import json
27-
import os
28-
from pathlib import Path
29-
from typing import Any, Generic, Optional
26+
from typing import Generic
3027

3128
from nodescraper.connection.inband import InBandConnectionManager, SSHConnectionParams
3229
from nodescraper.generictypes import TAnalyzeArg, TCollectArg, TDataModel
3330
from nodescraper.interfaces import DataPlugin
34-
from nodescraper.models import DataModel
35-
from nodescraper.utils import pascal_to_snake
3631

3732

3833
class InBandDataPlugin(
@@ -42,133 +37,3 @@ class InBandDataPlugin(
4237
"""Base class for in band plugins."""
4338

4439
CONNECTION_TYPE = InBandConnectionManager
45-
46-
@classmethod
47-
def find_datamodel_path_in_run(cls, run_path: str) -> Optional[str]:
48-
"""Find this plugin's collector datamodel file under a scraper run directory.
49-
50-
Args:
51-
run_path: Path to a scraper log run directory (e.g. scraper_logs_*).
52-
53-
Returns:
54-
Absolute path to the datamodel file, or None if not found.
55-
"""
56-
run_path = os.path.abspath(run_path)
57-
if not os.path.isdir(run_path):
58-
return None
59-
collector_cls = getattr(cls, "COLLECTOR", None)
60-
data_model_cls = getattr(cls, "DATA_MODEL", None)
61-
if not collector_cls or not data_model_cls:
62-
return None
63-
collector_dir = os.path.join(
64-
run_path,
65-
pascal_to_snake(cls.__name__),
66-
pascal_to_snake(collector_cls.__name__),
67-
)
68-
if not os.path.isdir(collector_dir):
69-
return None
70-
result_path = os.path.join(collector_dir, "result.json")
71-
if not os.path.isfile(result_path):
72-
return None
73-
try:
74-
res_payload = json.loads(Path(result_path).read_text(encoding="utf-8"))
75-
if res_payload.get("parent") != cls.__name__:
76-
return None
77-
except (json.JSONDecodeError, OSError):
78-
return None
79-
want_json = data_model_cls.__name__.lower() + ".json"
80-
for fname in os.listdir(collector_dir):
81-
low = fname.lower()
82-
if low.endswith("datamodel.json") or low == want_json:
83-
return os.path.join(collector_dir, fname)
84-
if low.endswith(".log"):
85-
return os.path.join(collector_dir, fname)
86-
return None
87-
88-
@classmethod
89-
def load_datamodel_from_path(cls, dm_path: str) -> Optional[TDataModel]:
90-
"""Load this plugin's DATA_MODEL from a file path (JSON or .log).
91-
92-
Args:
93-
dm_path: Path to datamodel JSON or to a .log file (if DATA_MODEL
94-
implements import_model for that format).
95-
96-
Returns:
97-
Instance of DATA_MODEL or None if load fails.
98-
"""
99-
dm_path = os.path.abspath(dm_path)
100-
if not os.path.isfile(dm_path):
101-
return None
102-
data_model_cls = getattr(cls, "DATA_MODEL", None)
103-
if not data_model_cls:
104-
return None
105-
try:
106-
if dm_path.lower().endswith(".log"):
107-
import_model = getattr(data_model_cls, "import_model", None)
108-
if not callable(import_model):
109-
return None
110-
base_import = getattr(DataModel.import_model, "__func__", DataModel.import_model)
111-
if getattr(import_model, "__func__", import_model) is base_import:
112-
return None
113-
return import_model(dm_path)
114-
with open(dm_path, encoding="utf-8") as f:
115-
data = json.load(f)
116-
return data_model_cls.model_validate(data)
117-
except (json.JSONDecodeError, OSError, Exception):
118-
return None
119-
120-
@classmethod
121-
def get_extracted_errors(cls, data_model: DataModel) -> Optional[list[str]]:
122-
"""Compute extracted errors from datamodel for compare-runs (in memory only).
123-
124-
Args:
125-
data_model: Loaded DATA_MODEL instance.
126-
127-
Returns:
128-
Sorted list of error match strings, or None if not applicable.
129-
"""
130-
get_content = getattr(data_model, "get_compare_content", None)
131-
if not callable(get_content):
132-
return None
133-
try:
134-
content = get_content()
135-
except Exception:
136-
return None
137-
if not isinstance(content, str):
138-
return None
139-
analyzer_cls = getattr(cls, "ANALYZER", None)
140-
if not analyzer_cls:
141-
return None
142-
get_matches = getattr(analyzer_cls, "get_error_matches", None)
143-
if not callable(get_matches):
144-
return None
145-
try:
146-
matches = get_matches(content)
147-
return sorted(matches) if matches is not None else None
148-
except Exception:
149-
return None
150-
151-
@classmethod
152-
def load_run_data(cls, run_path: str) -> Optional[dict[str, Any]]:
153-
"""Load this plugin's run data from a scraper run directory for comparison.
154-
155-
Args:
156-
run_path: Path to a scraper log run directory or to a datamodel file.
157-
158-
Returns:
159-
Dict suitable for diffing with another run, or None if not found.
160-
"""
161-
run_path = os.path.abspath(run_path)
162-
if not os.path.exists(run_path):
163-
return None
164-
dm_path = run_path if os.path.isfile(run_path) else cls.find_datamodel_path_in_run(run_path)
165-
if not dm_path:
166-
return None
167-
data_model = cls.load_datamodel_from_path(dm_path)
168-
if data_model is None:
169-
return None
170-
out = data_model.model_dump(mode="json")
171-
extracted = cls.get_extracted_errors(data_model)
172-
if extracted is not None:
173-
out["extracted_errors"] = extracted
174-
return out
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
###############################################################################
2+
#
3+
# MIT License
4+
#
5+
# Copyright (c) 2026 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+
from typing import Generic
27+
28+
from nodescraper.connection.redfish import (
29+
RedfishConnectionManager,
30+
RedfishConnectionParams,
31+
)
32+
from nodescraper.generictypes import TAnalyzeArg, TCollectArg, TDataModel
33+
from nodescraper.interfaces import DataPlugin
34+
35+
36+
class OOBandDataPlugin(
37+
DataPlugin[
38+
RedfishConnectionManager,
39+
RedfishConnectionParams,
40+
TDataModel,
41+
TCollectArg,
42+
TAnalyzeArg,
43+
],
44+
Generic[TDataModel, TCollectArg, TAnalyzeArg],
45+
):
46+
"""Base class for out-of-band (OOB) plugins that use Redfish connection."""
47+
48+
CONNECTION_TYPE = RedfishConnectionManager

0 commit comments

Comments
 (0)