redfish-python-sdk 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- redfish_python_sdk-1.0.0.dist-info/METADATA +164 -0
- redfish_python_sdk-1.0.0.dist-info/RECORD +56 -0
- redfish_python_sdk-1.0.0.dist-info/WHEEL +5 -0
- redfish_python_sdk-1.0.0.dist-info/licenses/LICENSE +29 -0
- redfish_python_sdk-1.0.0.dist-info/top_level.txt +1 -0
- redfish_sdk/__init__.py +63 -0
- redfish_sdk/client.py +1894 -0
- redfish_sdk/exceptions.py +56 -0
- redfish_sdk/http_client.py +452 -0
- redfish_sdk/managers/__init__.py +21 -0
- redfish_sdk/managers/_log_helpers.py +144 -0
- redfish_sdk/managers/account.py +96 -0
- redfish_sdk/managers/chassis.py +327 -0
- redfish_sdk/managers/event.py +264 -0
- redfish_sdk/managers/managers.py +120 -0
- redfish_sdk/managers/registries.py +48 -0
- redfish_sdk/managers/session.py +130 -0
- redfish_sdk/managers/systems.py +630 -0
- redfish_sdk/managers/task.py +89 -0
- redfish_sdk/managers/update.py +99 -0
- redfish_sdk/managers/update_strategies/__init__.py +51 -0
- redfish_sdk/managers/update_strategies/base.py +101 -0
- redfish_sdk/managers/update_strategies/h3c.py +140 -0
- redfish_sdk/managers/update_strategies/inspur.py +89 -0
- redfish_sdk/managers/update_strategies/lenovo.py +59 -0
- redfish_sdk/managers/update_strategies/nettrix.py +56 -0
- redfish_sdk/managers/update_strategies/registry.py +72 -0
- redfish_sdk/managers/update_strategies/vendor_detect.py +111 -0
- redfish_sdk/managers/update_strategies/xfusion.py +60 -0
- redfish_sdk/managers/update_strategies/zte.py +68 -0
- redfish_sdk/models/__init__.py +55 -0
- redfish_sdk/models/account.py +60 -0
- redfish_sdk/models/chassis.py +86 -0
- redfish_sdk/models/check.py +231 -0
- redfish_sdk/models/common.py +102 -0
- redfish_sdk/models/drive.py +53 -0
- redfish_sdk/models/event.py +64 -0
- redfish_sdk/models/fru.py +59 -0
- redfish_sdk/models/gpu.py +33 -0
- redfish_sdk/models/logs.py +56 -0
- redfish_sdk/models/managers.py +153 -0
- redfish_sdk/models/memory.py +50 -0
- redfish_sdk/models/network_adapter.py +76 -0
- redfish_sdk/models/oem.py +173 -0
- redfish_sdk/models/pcie_device.py +89 -0
- redfish_sdk/models/power.py +92 -0
- redfish_sdk/models/processor.py +50 -0
- redfish_sdk/models/registry.py +34 -0
- redfish_sdk/models/resource_key.py +70 -0
- redfish_sdk/models/root.py +50 -0
- redfish_sdk/models/session.py +42 -0
- redfish_sdk/models/storage.py +77 -0
- redfish_sdk/models/systems.py +194 -0
- redfish_sdk/models/task.py +54 -0
- redfish_sdk/models/thermal.py +111 -0
- redfish_sdk/models/update.py +55 -0
redfish_sdk/client.py
ADDED
|
@@ -0,0 +1,1894 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RedfishClient — the top-level entry point for the Redfish Python SDK.
|
|
3
|
+
|
|
4
|
+
Aggregates all service managers into a single object, providing a clean
|
|
5
|
+
and intuitive interface for interacting with a BMC via Redfish.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
import os
|
|
9
|
+
from redfish_sdk import RedfishClient
|
|
10
|
+
|
|
11
|
+
# Credentials are read from environment variables:
|
|
12
|
+
# BMC_IP, BMC_USERNAME, BMC_PASSWORD
|
|
13
|
+
client = RedfishClient(
|
|
14
|
+
host=os.environ["BMC_IP"],
|
|
15
|
+
username=os.environ["BMC_USERNAME"],
|
|
16
|
+
password=os.environ["BMC_PASSWORD"],
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Get system info
|
|
20
|
+
system = client.get_system()
|
|
21
|
+
|
|
22
|
+
# List CPUs
|
|
23
|
+
cpus = client.get_processors()
|
|
24
|
+
|
|
25
|
+
# Reset server
|
|
26
|
+
client.reset("GracefulRestart")
|
|
27
|
+
|
|
28
|
+
# Close when done
|
|
29
|
+
client.close()
|
|
30
|
+
|
|
31
|
+
# Or use as context manager
|
|
32
|
+
with RedfishClient(
|
|
33
|
+
host=os.environ["BMC_IP"],
|
|
34
|
+
username=os.environ["BMC_USERNAME"],
|
|
35
|
+
password=os.environ["BMC_PASSWORD"],
|
|
36
|
+
) as client:
|
|
37
|
+
system = client.get_system()
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import logging
|
|
43
|
+
from functools import cached_property
|
|
44
|
+
from typing import Any, Dict, List, Optional, Type, TypeVar
|
|
45
|
+
|
|
46
|
+
from .exceptions import RedfishException
|
|
47
|
+
from .http_client import RedfishHttpClient
|
|
48
|
+
from .models.account import Account, AccountService, Role
|
|
49
|
+
from .models.resource_key import RedfishResource
|
|
50
|
+
from .models.chassis import Chassis
|
|
51
|
+
from .models.drive import Drive
|
|
52
|
+
from .models.common import Collection, Entity, Link, RedfishResponse
|
|
53
|
+
from .models.event import EventService, Subscription
|
|
54
|
+
from .models.fru import Fru
|
|
55
|
+
from .models.logs import Log, LogEntry
|
|
56
|
+
from .models.oem import MainBoard
|
|
57
|
+
from .models.managers import EthernetInterface, HostInterface, Manager, NetworkProtocol
|
|
58
|
+
from .models.memory import Memory
|
|
59
|
+
from .models.power import Power, PowerSupply
|
|
60
|
+
from .models.processor import Processor
|
|
61
|
+
from .models.registry import Registry
|
|
62
|
+
from .models.root import RootService
|
|
63
|
+
from .models.session import Session, SessionService
|
|
64
|
+
from .models.systems import Bios, BootOption, System, SystemPatchSetting
|
|
65
|
+
from .models.task import Task, TaskService
|
|
66
|
+
from .models.thermal import Fan, InletHistoryTemperature, Thermal
|
|
67
|
+
from .models.update import ClientCertificate, FirmwareInventory, UpdateService
|
|
68
|
+
|
|
69
|
+
logger = logging.getLogger(__name__)
|
|
70
|
+
|
|
71
|
+
T = TypeVar("T", bound=Entity)
|
|
72
|
+
|
|
73
|
+
# Names of all cached_property manager attributes, used by close()
|
|
74
|
+
_MANAGER_ATTRS = (
|
|
75
|
+
"_systems", "_chassis", "_managers", "_accounts",
|
|
76
|
+
"_sessions", "_events", "_updates", "_registries", "_tasks",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class RedfishClient:
|
|
81
|
+
"""
|
|
82
|
+
Top-level Redfish Client.
|
|
83
|
+
|
|
84
|
+
Aggregates all service managers and provides the main entry point
|
|
85
|
+
for interacting with a BMC via the Redfish protocol.
|
|
86
|
+
|
|
87
|
+
All operations are accessible directly via ``client.get_xxx()`` /
|
|
88
|
+
``client.xxx()`` style methods. The underlying service managers
|
|
89
|
+
are private implementation details and should not be accessed
|
|
90
|
+
directly.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
host: str,
|
|
96
|
+
username: str,
|
|
97
|
+
password: str,
|
|
98
|
+
verify_ssl: bool = False,
|
|
99
|
+
proxy: Optional[str] = None,
|
|
100
|
+
connect_timeout: int = 10,
|
|
101
|
+
read_timeout: int = 30,
|
|
102
|
+
scheme: str = "https",
|
|
103
|
+
):
|
|
104
|
+
"""
|
|
105
|
+
Initialize the Redfish Client.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
host: BMC IP address or hostname (e.g., ``os.environ["BMC_IP"]``)
|
|
109
|
+
username: BMC username (e.g., ``os.environ["BMC_USERNAME"]``)
|
|
110
|
+
password: BMC password (e.g., ``os.environ["BMC_PASSWORD"]``)
|
|
111
|
+
verify_ssl: Whether to verify SSL certificates.
|
|
112
|
+
Default False — BMCs typically use self-signed certificates.
|
|
113
|
+
proxy: Optional HTTP/HTTPS proxy URL.
|
|
114
|
+
Example: "http://127.0.0.1:8080" (matches Java's proxy config)
|
|
115
|
+
connect_timeout: TCP connection timeout in seconds (default 10)
|
|
116
|
+
read_timeout: HTTP response read timeout in seconds (default 30)
|
|
117
|
+
scheme: URL scheme — "https" (default) or "http"
|
|
118
|
+
"""
|
|
119
|
+
self._http_client = RedfishHttpClient(
|
|
120
|
+
host=host,
|
|
121
|
+
username=username,
|
|
122
|
+
password=password,
|
|
123
|
+
verify_ssl=verify_ssl,
|
|
124
|
+
proxy=proxy,
|
|
125
|
+
connect_timeout=connect_timeout,
|
|
126
|
+
read_timeout=read_timeout,
|
|
127
|
+
scheme=scheme,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Root service cache
|
|
131
|
+
self._root_cache: Optional[RootService] = None
|
|
132
|
+
|
|
133
|
+
logger.info("RedfishClient initialized for host: %s", host)
|
|
134
|
+
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
# Private manager accessors (lazy-loaded via cached_property)
|
|
137
|
+
# ------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
@cached_property
|
|
140
|
+
def _systems(self):
|
|
141
|
+
from .managers.systems import SystemsManager
|
|
142
|
+
return SystemsManager(self)
|
|
143
|
+
|
|
144
|
+
@cached_property
|
|
145
|
+
def _chassis(self):
|
|
146
|
+
from .managers.chassis import ChassisManager
|
|
147
|
+
return ChassisManager(self)
|
|
148
|
+
|
|
149
|
+
@cached_property
|
|
150
|
+
def _managers(self):
|
|
151
|
+
from .managers.managers import ManagersManager
|
|
152
|
+
return ManagersManager(self)
|
|
153
|
+
|
|
154
|
+
@cached_property
|
|
155
|
+
def _accounts(self):
|
|
156
|
+
from .managers.account import AccountServiceManager
|
|
157
|
+
return AccountServiceManager(self)
|
|
158
|
+
|
|
159
|
+
@cached_property
|
|
160
|
+
def _sessions(self):
|
|
161
|
+
from .managers.session import SessionServiceManager
|
|
162
|
+
return SessionServiceManager(self)
|
|
163
|
+
|
|
164
|
+
@cached_property
|
|
165
|
+
def _events(self):
|
|
166
|
+
from .managers.event import EventServiceManager
|
|
167
|
+
return EventServiceManager(self)
|
|
168
|
+
|
|
169
|
+
@cached_property
|
|
170
|
+
def _updates(self):
|
|
171
|
+
from .managers.update import UpdateServiceManager
|
|
172
|
+
return UpdateServiceManager(self)
|
|
173
|
+
|
|
174
|
+
@cached_property
|
|
175
|
+
def _registries(self):
|
|
176
|
+
from .managers.registries import RegistriesManager
|
|
177
|
+
return RegistriesManager(self)
|
|
178
|
+
|
|
179
|
+
@cached_property
|
|
180
|
+
def _tasks(self):
|
|
181
|
+
from .managers.task import TaskServiceManager
|
|
182
|
+
return TaskServiceManager(self)
|
|
183
|
+
|
|
184
|
+
# ------------------------------------------------------------------
|
|
185
|
+
# Lifecycle
|
|
186
|
+
# ------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def host(self) -> str:
|
|
190
|
+
"""Return the configured BMC host."""
|
|
191
|
+
return self._http_client.host
|
|
192
|
+
|
|
193
|
+
def root(self):
|
|
194
|
+
"""
|
|
195
|
+
Get the Redfish root service document.
|
|
196
|
+
Returns the raw RootService resource with all top-level links.
|
|
197
|
+
"""
|
|
198
|
+
return self._get_root()
|
|
199
|
+
|
|
200
|
+
def close(self) -> None:
|
|
201
|
+
"""Close the underlying HTTP session and free resources."""
|
|
202
|
+
for attr in _MANAGER_ATTRS:
|
|
203
|
+
self.__dict__.pop(attr, None)
|
|
204
|
+
self._http_client.close()
|
|
205
|
+
logger.info("RedfishClient closed for host: %s", self._http_client.host)
|
|
206
|
+
|
|
207
|
+
def __enter__(self) -> RedfishClient:
|
|
208
|
+
return self
|
|
209
|
+
|
|
210
|
+
def __exit__(self, *args) -> None:
|
|
211
|
+
self.close()
|
|
212
|
+
|
|
213
|
+
def __repr__(self) -> str:
|
|
214
|
+
return f"RedfishClient(host={self._http_client.host!r})"
|
|
215
|
+
|
|
216
|
+
# ------------------------------------------------------------------
|
|
217
|
+
# Core methods (absorbed from RootServiceManager)
|
|
218
|
+
# ------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
def _get_root(self) -> RootService:
|
|
221
|
+
"""
|
|
222
|
+
Fetch the Redfish root service document.
|
|
223
|
+
Endpoint: GET /redfish/v1/
|
|
224
|
+
"""
|
|
225
|
+
return self._http_client.get("/redfish/v1/", RootService)
|
|
226
|
+
|
|
227
|
+
def _get_collection(self, odata_id: str, model_class: Type[T]) -> List[T]:
|
|
228
|
+
"""
|
|
229
|
+
Generic collection fetcher.
|
|
230
|
+
|
|
231
|
+
1. Fetches the collection index (Members list with @odata.id links)
|
|
232
|
+
2. Individually fetches each member to get full details
|
|
233
|
+
3. Returns a list of fully populated model instances
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
odata_id: The @odata.id of the collection resource
|
|
237
|
+
model_class: The pydantic model class for collection members
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
List of fully populated model instances
|
|
241
|
+
"""
|
|
242
|
+
from pydantic import TypeAdapter
|
|
243
|
+
|
|
244
|
+
# Step 1: fetch collection index
|
|
245
|
+
collection_type = Collection[model_class]
|
|
246
|
+
try:
|
|
247
|
+
adapter = TypeAdapter(collection_type)
|
|
248
|
+
except Exception:
|
|
249
|
+
# Fallback: fetch raw and parse manually
|
|
250
|
+
raw = self._http_client.get_raw(odata_id)
|
|
251
|
+
members_raw = raw.get("Members", [])
|
|
252
|
+
members = []
|
|
253
|
+
for member_raw in members_raw:
|
|
254
|
+
link_id = member_raw.get("@odata.id")
|
|
255
|
+
if link_id:
|
|
256
|
+
try:
|
|
257
|
+
member = self._http_client.get(link_id, model_class)
|
|
258
|
+
members.append(member)
|
|
259
|
+
except RedfishException as exc:
|
|
260
|
+
logger.warning("Failed to fetch member %s: %s, skipping", link_id, exc)
|
|
261
|
+
return members
|
|
262
|
+
|
|
263
|
+
raw = self._http_client.get_raw(odata_id)
|
|
264
|
+
collection = adapter.validate_python(raw)
|
|
265
|
+
|
|
266
|
+
if not collection.members:
|
|
267
|
+
logger.warning("Collection %s has no members", odata_id)
|
|
268
|
+
return []
|
|
269
|
+
|
|
270
|
+
# Step 2: expand each member
|
|
271
|
+
expanded_members = []
|
|
272
|
+
for member in collection.members:
|
|
273
|
+
if not member.odata_id:
|
|
274
|
+
continue
|
|
275
|
+
try:
|
|
276
|
+
full_member = self._http_client.get(member.odata_id, model_class)
|
|
277
|
+
expanded_members.append(full_member)
|
|
278
|
+
except RedfishException as exc:
|
|
279
|
+
logger.warning(
|
|
280
|
+
"Failed to fetch collection member %s: %s, skipping",
|
|
281
|
+
member.odata_id, exc
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
return expanded_members
|
|
285
|
+
|
|
286
|
+
def _get_list(self, links: List[Link], model_class: Type[T]) -> List[T]:
|
|
287
|
+
"""
|
|
288
|
+
Fetch a list of resources from a list of Link objects.
|
|
289
|
+
Skips items that fail to fetch (logs warning and continues).
|
|
290
|
+
"""
|
|
291
|
+
results = []
|
|
292
|
+
for link in links:
|
|
293
|
+
if not link.odata_id:
|
|
294
|
+
continue
|
|
295
|
+
try:
|
|
296
|
+
item = self._http_client.get(link.odata_id, model_class)
|
|
297
|
+
if item is not None:
|
|
298
|
+
results.append(item)
|
|
299
|
+
except RedfishException as exc:
|
|
300
|
+
logger.warning("Failed to fetch %s: %s, skipping", link.odata_id, exc)
|
|
301
|
+
return results
|
|
302
|
+
|
|
303
|
+
# ------------------------------------------------------------------
|
|
304
|
+
# Cached service accessors (absorbed from RootServiceManager)
|
|
305
|
+
# ------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
def _get_session_service(self) -> SessionService:
|
|
308
|
+
"""Get the SessionService resource."""
|
|
309
|
+
root = self._get_root()
|
|
310
|
+
return self._http_client.get(root.session_service.odata_id, SessionService)
|
|
311
|
+
|
|
312
|
+
def _get_account_service(self) -> AccountService:
|
|
313
|
+
"""Get the AccountService resource."""
|
|
314
|
+
root = self._get_root()
|
|
315
|
+
return self._http_client.get(root.account_service.odata_id, AccountService)
|
|
316
|
+
|
|
317
|
+
def _get_chassis_collection(self) -> List[Chassis]:
|
|
318
|
+
"""
|
|
319
|
+
Get the Chassis collection.
|
|
320
|
+
|
|
321
|
+
Special handling for Lenovo servers: if the collection endpoint returns
|
|
322
|
+
an error, falls back to fetching /redfish/v1/Chassis/1 directly.
|
|
323
|
+
"""
|
|
324
|
+
root = self._get_root()
|
|
325
|
+
try:
|
|
326
|
+
return self._get_collection(root.chassis.odata_id, Chassis)
|
|
327
|
+
except RedfishException as exc:
|
|
328
|
+
logger.warning(
|
|
329
|
+
"Chassis collection endpoint failed (%s), falling back to /redfish/v1/Chassis/1",
|
|
330
|
+
exc
|
|
331
|
+
)
|
|
332
|
+
# Lenovo workaround: /redfish/v1/Chassis returns 500
|
|
333
|
+
chassis = self._http_client.get("/redfish/v1/Chassis/1", Chassis)
|
|
334
|
+
return [chassis]
|
|
335
|
+
|
|
336
|
+
def _get_chassis_collection_odata_id(self) -> str:
|
|
337
|
+
"""Get the Chassis collection @odata.id from the root service."""
|
|
338
|
+
root = self._get_root()
|
|
339
|
+
return root.chassis.odata_id or "/redfish/v1/Chassis"
|
|
340
|
+
|
|
341
|
+
def _get_event_service(self) -> EventService:
|
|
342
|
+
"""Get the EventService resource."""
|
|
343
|
+
root = self._get_root()
|
|
344
|
+
return self._http_client.get(root.event_service.odata_id, EventService)
|
|
345
|
+
|
|
346
|
+
def _get_managers_collection(self) -> List[Manager]:
|
|
347
|
+
"""Get the Managers collection."""
|
|
348
|
+
root = self._get_root()
|
|
349
|
+
return self._get_collection(root.managers.odata_id, Manager)
|
|
350
|
+
|
|
351
|
+
def _get_managers_collection_odata_id(self) -> str:
|
|
352
|
+
"""Get the Managers collection @odata.id from the root service."""
|
|
353
|
+
root = self._get_root()
|
|
354
|
+
return root.managers.odata_id
|
|
355
|
+
|
|
356
|
+
def _get_registries_collection(self) -> List[Registry]:
|
|
357
|
+
"""Get the Registries collection."""
|
|
358
|
+
root = self._get_root()
|
|
359
|
+
return self._get_collection(root.registries.odata_id, Registry)
|
|
360
|
+
|
|
361
|
+
def _get_registries_collection_odata_id(self) -> str:
|
|
362
|
+
"""Get the Registries collection @odata.id from the root service."""
|
|
363
|
+
root = self._get_root()
|
|
364
|
+
return root.registries.odata_id
|
|
365
|
+
|
|
366
|
+
def _get_systems_collection(self) -> List[System]:
|
|
367
|
+
"""Get the Systems collection."""
|
|
368
|
+
root = self._get_root()
|
|
369
|
+
return self._get_collection(root.systems.odata_id, System)
|
|
370
|
+
|
|
371
|
+
def _get_systems_collection_odata_id(self) -> str:
|
|
372
|
+
"""Get the Systems collection @odata.id from the root service."""
|
|
373
|
+
root = self._get_root()
|
|
374
|
+
return root.systems.odata_id
|
|
375
|
+
|
|
376
|
+
def _get_task_service(self) -> TaskService:
|
|
377
|
+
"""Get the TaskService resource."""
|
|
378
|
+
root = self._get_root()
|
|
379
|
+
return self._http_client.get(root.tasks.odata_id, TaskService)
|
|
380
|
+
|
|
381
|
+
def _get_update_service(self) -> UpdateService:
|
|
382
|
+
"""Get the UpdateService resource."""
|
|
383
|
+
root = self._get_root()
|
|
384
|
+
return self._http_client.get(root.update_service.odata_id, UpdateService)
|
|
385
|
+
|
|
386
|
+
# ==================================================================
|
|
387
|
+
# Component query methods — Systems side
|
|
388
|
+
# ==================================================================
|
|
389
|
+
|
|
390
|
+
def get_system(self, system_id: Optional[str] = None) -> System:
|
|
391
|
+
"""
|
|
392
|
+
Get a single system (physical server) resource.
|
|
393
|
+
|
|
394
|
+
If system_id is None and there is exactly one system, returns it
|
|
395
|
+
automatically. If there are multiple systems and no ID is specified,
|
|
396
|
+
raises an exception.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
system_id: System ID (e.g., "1"). Auto-selected if only one system exists.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
System object with manufacturer, model, serial_number, power_state, etc.
|
|
403
|
+
|
|
404
|
+
Raises:
|
|
405
|
+
RedfishException: If system not found or multiple systems exist without ID
|
|
406
|
+
"""
|
|
407
|
+
return self._systems.get(system_id)
|
|
408
|
+
|
|
409
|
+
def get_systems(self) -> List[System]:
|
|
410
|
+
"""
|
|
411
|
+
Get all system (physical server) resources.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
List of System objects
|
|
415
|
+
"""
|
|
416
|
+
return self._get_systems_collection()
|
|
417
|
+
|
|
418
|
+
def get_processors(self, system_id: Optional[str] = None) -> List[Processor]:
|
|
419
|
+
"""
|
|
420
|
+
Get the list of processors (CPUs) for a system.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
List of Processor objects
|
|
427
|
+
"""
|
|
428
|
+
return self._systems.processors(system_id)
|
|
429
|
+
|
|
430
|
+
def get_processor(self, processor_id: str, system_id: Optional[str] = None) -> Processor:
|
|
431
|
+
"""
|
|
432
|
+
Get a single processor (CPU) by ID.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
processor_id: Processor ID (e.g., "1")
|
|
436
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
Processor resource
|
|
440
|
+
"""
|
|
441
|
+
system = self._systems.get(system_id)
|
|
442
|
+
return self._http_client.get(
|
|
443
|
+
f"{system.processors.odata_id}/{processor_id}", Processor
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
def get_memory(self, system_id: Optional[str] = None) -> List[Memory]:
|
|
447
|
+
"""
|
|
448
|
+
Get the list of memory modules (DIMMs) for a system.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
List of Memory objects
|
|
455
|
+
"""
|
|
456
|
+
return self._systems.memory(system_id)
|
|
457
|
+
|
|
458
|
+
def get_memory_device(self, memory_id: str, system_id: Optional[str] = None) -> Memory:
|
|
459
|
+
"""
|
|
460
|
+
Get a single memory module (DIMM) by ID.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
memory_id: Memory ID (e.g., "1")
|
|
464
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
Memory resource
|
|
468
|
+
"""
|
|
469
|
+
system = self._systems.get(system_id)
|
|
470
|
+
return self._http_client.get(
|
|
471
|
+
f"{system.odata_id}/Memory/{memory_id}", Memory
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
def get_storages(self, system_id: Optional[str] = None) -> List:
|
|
475
|
+
"""
|
|
476
|
+
Get the list of storage controllers for a system.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
List of Storage objects
|
|
483
|
+
"""
|
|
484
|
+
return self._systems.storages(system_id)
|
|
485
|
+
|
|
486
|
+
def get_volumes(self, storage_id: str, system_id: Optional[str] = None) -> List:
|
|
487
|
+
"""
|
|
488
|
+
Get the list of volumes for a given storage controller.
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
storage_id: Storage controller ID
|
|
492
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
List of Volume objects
|
|
496
|
+
"""
|
|
497
|
+
return self._systems.volumes(storage_id, system_id)
|
|
498
|
+
|
|
499
|
+
def get_gpus(self, system_id: Optional[str] = None) -> List:
|
|
500
|
+
"""
|
|
501
|
+
Get GPU information for a system.
|
|
502
|
+
|
|
503
|
+
Uses multi-vendor fallback strategy:
|
|
504
|
+
1. Try GraphicsControllers (standard path)
|
|
505
|
+
2. Fall back to Chassis PCIeDevices filtered by GPU name
|
|
506
|
+
3. Fall back to System.Links.PCIeDevices
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
List of Gpu objects
|
|
513
|
+
"""
|
|
514
|
+
return self._systems.gpus(system_id)
|
|
515
|
+
|
|
516
|
+
def get_bios(self, system_id: Optional[str] = None) -> Bios:
|
|
517
|
+
"""
|
|
518
|
+
Get BIOS information for a system.
|
|
519
|
+
|
|
520
|
+
Args:
|
|
521
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
Bios resource
|
|
525
|
+
"""
|
|
526
|
+
return self._systems.bios(system_id)
|
|
527
|
+
|
|
528
|
+
def get_system_log_services(self, system_id: Optional[str] = None) -> List[Log]:
|
|
529
|
+
"""
|
|
530
|
+
Get the list of log services for a system.
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
List of Log objects
|
|
537
|
+
"""
|
|
538
|
+
return self._systems.log_services(system_id)
|
|
539
|
+
|
|
540
|
+
def get_system_log_entries(
|
|
541
|
+
self,
|
|
542
|
+
log_id: Optional[str] = None,
|
|
543
|
+
system_id: Optional[str] = None,
|
|
544
|
+
) -> List[LogEntry]:
|
|
545
|
+
"""
|
|
546
|
+
Get log entries for a system log service.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
log_id: Log service ID (e.g., "Log1", "Sel"). Optional
|
|
550
|
+
— when omitted and there is exactly one log service,
|
|
551
|
+
it is auto-selected.
|
|
552
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
List of LogEntry objects (uses ``?$expand=.($levels=1)`` to
|
|
556
|
+
inline members in one HTTP round trip when supported).
|
|
557
|
+
"""
|
|
558
|
+
return self._systems.log_entries(log_id, system_id)
|
|
559
|
+
|
|
560
|
+
# ------------------------------------------------------------------
|
|
561
|
+
# Log service single resource + ClearLog action
|
|
562
|
+
# ------------------------------------------------------------------
|
|
563
|
+
|
|
564
|
+
def get_system_log_service(
|
|
565
|
+
self,
|
|
566
|
+
log_id: Optional[str] = None,
|
|
567
|
+
system_id: Optional[str] = None,
|
|
568
|
+
) -> Log:
|
|
569
|
+
"""
|
|
570
|
+
Get a single LogService resource (includes Actions block such as
|
|
571
|
+
``#LogService.ClearLog``).
|
|
572
|
+
|
|
573
|
+
Differs from :meth:`get_system_log_services` which returns the
|
|
574
|
+
collection members and may omit Actions on some BMCs.
|
|
575
|
+
|
|
576
|
+
``log_id`` is optional (auto-selected when there is exactly one
|
|
577
|
+
log service) and the per-service URL is discovered from the
|
|
578
|
+
LogServices collection rather than built by string concatenation.
|
|
579
|
+
"""
|
|
580
|
+
return self._systems.log_service(log_id, system_id)
|
|
581
|
+
|
|
582
|
+
def clear_system_log(
|
|
583
|
+
self,
|
|
584
|
+
log_id: Optional[str] = None,
|
|
585
|
+
system_id: Optional[str] = None,
|
|
586
|
+
) -> None:
|
|
587
|
+
"""
|
|
588
|
+
Invoke ``#LogService.ClearLog`` on a system log service.
|
|
589
|
+
|
|
590
|
+
Raises :class:`RedfishValidationError` if the BMC does not advertise
|
|
591
|
+
the ClearLog action.
|
|
592
|
+
"""
|
|
593
|
+
return self._systems.clear_system_log(log_id, system_id)
|
|
594
|
+
|
|
595
|
+
# ------------------------------------------------------------------
|
|
596
|
+
# BootOptions collection
|
|
597
|
+
# ------------------------------------------------------------------
|
|
598
|
+
|
|
599
|
+
def get_boot_options(self, system_id: Optional[str] = None) -> List[BootOption]:
|
|
600
|
+
"""
|
|
601
|
+
Get the BootOptions collection for a system (modern boot model).
|
|
602
|
+
|
|
603
|
+
Returns an empty list when the system does not expose a BootOptions
|
|
604
|
+
link (legacy ``BootSourceOverrideTarget`` only).
|
|
605
|
+
"""
|
|
606
|
+
return self._systems.boot_options(system_id)
|
|
607
|
+
|
|
608
|
+
def get_boot_option(self, option_id: str, system_id: Optional[str] = None) -> BootOption:
|
|
609
|
+
"""Get a single BootOption resource by ID."""
|
|
610
|
+
return self._systems.boot_option(option_id, system_id)
|
|
611
|
+
|
|
612
|
+
def set_boot_option_enabled(
|
|
613
|
+
self,
|
|
614
|
+
option_id: str,
|
|
615
|
+
enabled: bool,
|
|
616
|
+
system_id: Optional[str] = None,
|
|
617
|
+
) -> BootOption:
|
|
618
|
+
"""
|
|
619
|
+
Toggle a BootOption's ``BootOptionEnabled`` flag via PATCH and
|
|
620
|
+
return the re-read resource.
|
|
621
|
+
"""
|
|
622
|
+
return self._systems.set_boot_option_enabled(option_id, enabled, system_id)
|
|
623
|
+
|
|
624
|
+
def get_system_fru(self, system_id: Optional[str] = None) -> Optional[Fru]:
|
|
625
|
+
"""
|
|
626
|
+
Get FRU (Field Replaceable Unit) information for a system.
|
|
627
|
+
|
|
628
|
+
This is a vendor-specific extension. Returns None if not available.
|
|
629
|
+
|
|
630
|
+
Args:
|
|
631
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
632
|
+
|
|
633
|
+
Returns:
|
|
634
|
+
Fru object, or None if not available
|
|
635
|
+
"""
|
|
636
|
+
return self._systems.fru_info(system_id)
|
|
637
|
+
|
|
638
|
+
def get_pcie_device(self, odata_id: str):
|
|
639
|
+
"""
|
|
640
|
+
Get a specific PCIe device by its @odata.id.
|
|
641
|
+
|
|
642
|
+
Args:
|
|
643
|
+
odata_id: The @odata.id of the PCIe device
|
|
644
|
+
|
|
645
|
+
Returns:
|
|
646
|
+
PCIeDevice resource
|
|
647
|
+
"""
|
|
648
|
+
return self._systems.pcie_device(odata_id)
|
|
649
|
+
|
|
650
|
+
def change_boot_source(
|
|
651
|
+
self,
|
|
652
|
+
target: str,
|
|
653
|
+
system_id: Optional[str] = None,
|
|
654
|
+
mode: str = "UEFI",
|
|
655
|
+
enabled: str = "Once",
|
|
656
|
+
) -> SystemPatchSetting:
|
|
657
|
+
"""
|
|
658
|
+
Change the boot source override target.
|
|
659
|
+
|
|
660
|
+
Validates the target against the system's allowable values,
|
|
661
|
+
then sends a PATCH request with the new boot settings.
|
|
662
|
+
|
|
663
|
+
Args:
|
|
664
|
+
target: Boot target (e.g., "Pxe", "Hdd", "Cd", "BiosSetup")
|
|
665
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
666
|
+
mode: Boot source override mode ("UEFI" or "Legacy")
|
|
667
|
+
enabled: Override enable mode ("Once", "Continuous", "Disabled")
|
|
668
|
+
|
|
669
|
+
Returns:
|
|
670
|
+
Updated SystemPatchSetting
|
|
671
|
+
|
|
672
|
+
Raises:
|
|
673
|
+
RedfishValidationError: If target is not in allowable values
|
|
674
|
+
"""
|
|
675
|
+
return self._systems.change_boot_source(target, system_id, mode, enabled)
|
|
676
|
+
|
|
677
|
+
def reset(
|
|
678
|
+
self,
|
|
679
|
+
reset_type: str,
|
|
680
|
+
system_id: Optional[str] = None,
|
|
681
|
+
skip_power_state_check: bool = False,
|
|
682
|
+
):
|
|
683
|
+
"""
|
|
684
|
+
Perform a system reset (power on/off/restart).
|
|
685
|
+
|
|
686
|
+
Validates that the requested reset type is compatible with the current
|
|
687
|
+
power state before sending the request.
|
|
688
|
+
|
|
689
|
+
Args:
|
|
690
|
+
reset_type: Reset type string (e.g., "GracefulRestart", "ForceOff", "On")
|
|
691
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
692
|
+
skip_power_state_check: Skip power state compatibility check
|
|
693
|
+
|
|
694
|
+
Returns:
|
|
695
|
+
RedfishResponse
|
|
696
|
+
|
|
697
|
+
Raises:
|
|
698
|
+
RedfishValidationError: If reset_type is incompatible with current power state
|
|
699
|
+
"""
|
|
700
|
+
return self._systems.reset(reset_type, system_id, skip_power_state_check)
|
|
701
|
+
|
|
702
|
+
# ==================================================================
|
|
703
|
+
# Component query methods — Chassis side
|
|
704
|
+
# ==================================================================
|
|
705
|
+
|
|
706
|
+
def get_chassis(self, chassis_id: str = "1") -> Chassis:
|
|
707
|
+
"""
|
|
708
|
+
Get chassis (physical enclosure) information.
|
|
709
|
+
|
|
710
|
+
Args:
|
|
711
|
+
chassis_id: Chassis ID (default "1")
|
|
712
|
+
|
|
713
|
+
Returns:
|
|
714
|
+
Chassis resource with manufacturer, model, serial number, etc.
|
|
715
|
+
"""
|
|
716
|
+
return self._chassis.get(chassis_id)
|
|
717
|
+
|
|
718
|
+
def get_drives(self, chassis_id: str = "1") -> List:
|
|
719
|
+
"""
|
|
720
|
+
Get the list of physical drives (HDD/SSD/NVMe) in a chassis.
|
|
721
|
+
|
|
722
|
+
Args:
|
|
723
|
+
chassis_id: Chassis ID (default "1")
|
|
724
|
+
|
|
725
|
+
Returns:
|
|
726
|
+
List of Drive objects
|
|
727
|
+
"""
|
|
728
|
+
return self._chassis.drives(chassis_id)
|
|
729
|
+
|
|
730
|
+
# ------------------------------------------------------------------
|
|
731
|
+
# Drive single-resource + Drive.Reset action
|
|
732
|
+
# ------------------------------------------------------------------
|
|
733
|
+
|
|
734
|
+
def get_drive(self, odata_id: str) -> Drive:
|
|
735
|
+
"""
|
|
736
|
+
Get a single Drive resource directly by its ``@odata.id``.
|
|
737
|
+
|
|
738
|
+
Useful when a Storage controller only exposes Link references and the
|
|
739
|
+
caller needs the full Drive detail.
|
|
740
|
+
"""
|
|
741
|
+
return self._systems.drive_by_odata_id(odata_id)
|
|
742
|
+
|
|
743
|
+
def drive_reset(self, drive_odata_id: str, reset_type: str) -> None:
|
|
744
|
+
"""
|
|
745
|
+
Invoke ``#Drive.Reset`` on a drive (NVMe power cycle, etc.).
|
|
746
|
+
|
|
747
|
+
See :meth:`SystemsManager.drive_reset` for behaviour.
|
|
748
|
+
"""
|
|
749
|
+
return self._systems.drive_reset(drive_odata_id, reset_type)
|
|
750
|
+
|
|
751
|
+
# ------------------------------------------------------------------
|
|
752
|
+
# IndicatorLED writes
|
|
753
|
+
# ------------------------------------------------------------------
|
|
754
|
+
|
|
755
|
+
def set_indicator_led(self, state: str, chassis_id: str = "1") -> str:
|
|
756
|
+
"""
|
|
757
|
+
Set ``Chassis.IndicatorLED``. ``state`` must be one of
|
|
758
|
+
``Lit`` / ``Blinking`` / ``Off``.
|
|
759
|
+
"""
|
|
760
|
+
return self._chassis.set_indicator_led(state, chassis_id)
|
|
761
|
+
|
|
762
|
+
def set_drive_indicator_led(self, drive_odata_id: str, state: str) -> str:
|
|
763
|
+
"""
|
|
764
|
+
Set ``Drive.IndicatorLED`` on a specific drive. ``state`` must be one
|
|
765
|
+
of ``Lit`` / ``Blinking`` / ``Off``.
|
|
766
|
+
"""
|
|
767
|
+
return self._chassis.set_drive_indicator_led(drive_odata_id, state)
|
|
768
|
+
|
|
769
|
+
def get_network_adapters(self, chassis_id: str = "1") -> List:
|
|
770
|
+
"""
|
|
771
|
+
Get the list of network adapters (NICs) in a chassis.
|
|
772
|
+
|
|
773
|
+
Args:
|
|
774
|
+
chassis_id: Chassis ID (default "1")
|
|
775
|
+
|
|
776
|
+
Returns:
|
|
777
|
+
List of NetworkAdapter objects
|
|
778
|
+
"""
|
|
779
|
+
return self._chassis.network_adapters(chassis_id)
|
|
780
|
+
|
|
781
|
+
def get_pcie_devices(self, chassis_id: str = "1") -> List:
|
|
782
|
+
"""
|
|
783
|
+
Get the list of PCIe devices in a chassis.
|
|
784
|
+
|
|
785
|
+
Args:
|
|
786
|
+
chassis_id: Chassis ID (default "1")
|
|
787
|
+
|
|
788
|
+
Returns:
|
|
789
|
+
List of PCIeDevice objects
|
|
790
|
+
"""
|
|
791
|
+
return self._chassis.pcie_devices(chassis_id)
|
|
792
|
+
|
|
793
|
+
def get_power(self, chassis_id: str = "1") -> Power:
|
|
794
|
+
"""
|
|
795
|
+
Get power information (PSUs, power controls, voltages) for a chassis.
|
|
796
|
+
|
|
797
|
+
Args:
|
|
798
|
+
chassis_id: Chassis ID (default "1")
|
|
799
|
+
|
|
800
|
+
Returns:
|
|
801
|
+
Power resource
|
|
802
|
+
"""
|
|
803
|
+
return self._chassis.power(chassis_id)
|
|
804
|
+
|
|
805
|
+
def get_thermal(self, chassis_id: str = "1") -> Thermal:
|
|
806
|
+
"""
|
|
807
|
+
Get thermal information (fans, temperatures) for a chassis.
|
|
808
|
+
|
|
809
|
+
Args:
|
|
810
|
+
chassis_id: Chassis ID (default "1")
|
|
811
|
+
|
|
812
|
+
Returns:
|
|
813
|
+
Thermal resource
|
|
814
|
+
"""
|
|
815
|
+
return self._chassis.thermal(chassis_id)
|
|
816
|
+
|
|
817
|
+
def get_inlet_history_temperature(
|
|
818
|
+
self, chassis_id: str = "1"
|
|
819
|
+
) -> Optional[InletHistoryTemperature]:
|
|
820
|
+
"""
|
|
821
|
+
Get air inlet historical temperature samples for a chassis.
|
|
822
|
+
|
|
823
|
+
Wraps ``ChassisManager.inlet_history_temperature``. The method first
|
|
824
|
+
resolves the sub-resource URL via ``get_thermal()`` (Redfish link
|
|
825
|
+
discovery), then GETs the InletHistoryTemperature resource.
|
|
826
|
+
|
|
827
|
+
Returns ``None`` when the BMC does not advertise the sub-resource or
|
|
828
|
+
returns 404; other errors (auth/network/parse) propagate.
|
|
829
|
+
|
|
830
|
+
Args:
|
|
831
|
+
chassis_id: Chassis ID (default "1")
|
|
832
|
+
|
|
833
|
+
Returns:
|
|
834
|
+
InletHistoryTemperature model, or None when not supported.
|
|
835
|
+
"""
|
|
836
|
+
return self._chassis.inlet_history_temperature(chassis_id)
|
|
837
|
+
|
|
838
|
+
def get_fru_service(self, chassis_id: str = "1") -> List[dict]:
|
|
839
|
+
"""
|
|
840
|
+
Get FRU service data from the chassis OEM extension.
|
|
841
|
+
|
|
842
|
+
This is a vendor-specific feature (e.g., Huawei/xFusion iBMC).
|
|
843
|
+
|
|
844
|
+
Args:
|
|
845
|
+
chassis_id: Chassis ID (default "1")
|
|
846
|
+
|
|
847
|
+
Returns:
|
|
848
|
+
List of raw FRU service data dicts
|
|
849
|
+
"""
|
|
850
|
+
return self._chassis.fru_service(chassis_id)
|
|
851
|
+
|
|
852
|
+
# ==================================================================
|
|
853
|
+
# Component query methods — Managers (BMC) side
|
|
854
|
+
# ==================================================================
|
|
855
|
+
|
|
856
|
+
def get_manager(self, manager_id: str = "1") -> Manager:
|
|
857
|
+
"""
|
|
858
|
+
Get BMC manager information.
|
|
859
|
+
|
|
860
|
+
Args:
|
|
861
|
+
manager_id: Manager ID (default "1")
|
|
862
|
+
|
|
863
|
+
Returns:
|
|
864
|
+
Manager resource with firmware_version, model, etc.
|
|
865
|
+
"""
|
|
866
|
+
return self._managers.get(manager_id)
|
|
867
|
+
|
|
868
|
+
def get_manager_log_services(self, manager_id: str = "1") -> List[Log]:
|
|
869
|
+
"""
|
|
870
|
+
Get the list of log services for a BMC manager.
|
|
871
|
+
|
|
872
|
+
Args:
|
|
873
|
+
manager_id: Manager ID (default "1")
|
|
874
|
+
|
|
875
|
+
Returns:
|
|
876
|
+
List of Log objects
|
|
877
|
+
"""
|
|
878
|
+
return self._managers.log_services(manager_id)
|
|
879
|
+
|
|
880
|
+
def get_manager_log_entries(
|
|
881
|
+
self,
|
|
882
|
+
log_id: Optional[str] = None,
|
|
883
|
+
manager_id: str = "1",
|
|
884
|
+
) -> List[LogEntry]:
|
|
885
|
+
"""
|
|
886
|
+
Get log entries for a BMC manager log service.
|
|
887
|
+
|
|
888
|
+
Args:
|
|
889
|
+
log_id: Log service ID (e.g., "Sel", "OperateLog"). Optional
|
|
890
|
+
— when omitted and there is exactly one log
|
|
891
|
+
service, it is auto-selected.
|
|
892
|
+
manager_id: Manager ID (default "1")
|
|
893
|
+
|
|
894
|
+
Returns:
|
|
895
|
+
List of LogEntry objects (uses ``?$expand=.($levels=1)`` to
|
|
896
|
+
inline members in one HTTP round trip when supported).
|
|
897
|
+
"""
|
|
898
|
+
return self._managers.log_entries(log_id, manager_id)
|
|
899
|
+
|
|
900
|
+
def get_network_protocol(self, manager_id: str = "1") -> NetworkProtocol:
|
|
901
|
+
"""
|
|
902
|
+
Get network protocol configuration for a BMC manager.
|
|
903
|
+
|
|
904
|
+
Args:
|
|
905
|
+
manager_id: Manager ID (default "1")
|
|
906
|
+
|
|
907
|
+
Returns:
|
|
908
|
+
NetworkProtocol resource
|
|
909
|
+
"""
|
|
910
|
+
return self._managers.network_protocol(manager_id)
|
|
911
|
+
|
|
912
|
+
def get_manager_ethernet_interfaces(self, manager_id: str = "1") -> List[EthernetInterface]:
|
|
913
|
+
"""
|
|
914
|
+
Get the list of Ethernet interfaces for a BMC manager.
|
|
915
|
+
|
|
916
|
+
Args:
|
|
917
|
+
manager_id: Manager ID (default "1")
|
|
918
|
+
|
|
919
|
+
Returns:
|
|
920
|
+
List of EthernetInterface objects
|
|
921
|
+
"""
|
|
922
|
+
return self._managers.ethernet_interfaces(manager_id)
|
|
923
|
+
|
|
924
|
+
def get_host_interfaces(self, manager_id: str = "1") -> List[HostInterface]:
|
|
925
|
+
"""
|
|
926
|
+
Get the list of host interfaces for a BMC manager.
|
|
927
|
+
|
|
928
|
+
Args:
|
|
929
|
+
manager_id: Manager ID (default "1")
|
|
930
|
+
|
|
931
|
+
Returns:
|
|
932
|
+
List of HostInterface objects
|
|
933
|
+
"""
|
|
934
|
+
return self._managers.host_interfaces(manager_id)
|
|
935
|
+
|
|
936
|
+
# ==================================================================
|
|
937
|
+
# Component query methods — Account service
|
|
938
|
+
# ==================================================================
|
|
939
|
+
|
|
940
|
+
def get_accounts(self) -> List[Account]:
|
|
941
|
+
"""
|
|
942
|
+
Get all user accounts.
|
|
943
|
+
|
|
944
|
+
Returns:
|
|
945
|
+
List of Account objects
|
|
946
|
+
"""
|
|
947
|
+
return self._accounts.accounts()
|
|
948
|
+
|
|
949
|
+
def get_roles(self) -> List[Role]:
|
|
950
|
+
"""
|
|
951
|
+
Get all user roles.
|
|
952
|
+
|
|
953
|
+
Returns:
|
|
954
|
+
List of Role objects
|
|
955
|
+
"""
|
|
956
|
+
return self._accounts.roles()
|
|
957
|
+
|
|
958
|
+
def add_account(self, account: Account) -> Account:
|
|
959
|
+
"""
|
|
960
|
+
Create a new user account.
|
|
961
|
+
|
|
962
|
+
Args:
|
|
963
|
+
account: Account model with UserName, Password, RoleId, Enabled fields
|
|
964
|
+
|
|
965
|
+
Returns:
|
|
966
|
+
Created Account resource
|
|
967
|
+
"""
|
|
968
|
+
return self._accounts.add(account)
|
|
969
|
+
|
|
970
|
+
def update_account(self, username: str, account: Account) -> Account:
|
|
971
|
+
"""
|
|
972
|
+
Update an existing user account.
|
|
973
|
+
|
|
974
|
+
Args:
|
|
975
|
+
username: Username of the account to update
|
|
976
|
+
account: Account model with fields to update
|
|
977
|
+
|
|
978
|
+
Returns:
|
|
979
|
+
Updated Account resource
|
|
980
|
+
"""
|
|
981
|
+
return self._accounts.update(username, account)
|
|
982
|
+
|
|
983
|
+
def delete_account(self, username: str) -> str:
|
|
984
|
+
"""
|
|
985
|
+
Delete a user account.
|
|
986
|
+
|
|
987
|
+
Args:
|
|
988
|
+
username: Username of the account to delete
|
|
989
|
+
|
|
990
|
+
Returns:
|
|
991
|
+
Response body (usually empty)
|
|
992
|
+
"""
|
|
993
|
+
return self._accounts.delete(username)
|
|
994
|
+
|
|
995
|
+
# ==================================================================
|
|
996
|
+
# Component query methods — Session service
|
|
997
|
+
# ==================================================================
|
|
998
|
+
|
|
999
|
+
def get_sessions(self) -> List[Session]:
|
|
1000
|
+
"""
|
|
1001
|
+
Get all active sessions.
|
|
1002
|
+
|
|
1003
|
+
Returns:
|
|
1004
|
+
List of Session objects
|
|
1005
|
+
"""
|
|
1006
|
+
return self._sessions.sessions()
|
|
1007
|
+
|
|
1008
|
+
def get_session(self, session_id: str) -> Session:
|
|
1009
|
+
"""
|
|
1010
|
+
Get a specific session by ID.
|
|
1011
|
+
|
|
1012
|
+
Args:
|
|
1013
|
+
session_id: Session ID
|
|
1014
|
+
|
|
1015
|
+
Returns:
|
|
1016
|
+
Session resource
|
|
1017
|
+
"""
|
|
1018
|
+
return self._sessions.get(session_id)
|
|
1019
|
+
|
|
1020
|
+
def create_session(
|
|
1021
|
+
self,
|
|
1022
|
+
username: str,
|
|
1023
|
+
password: str,
|
|
1024
|
+
switch_to_token_auth: bool = False,
|
|
1025
|
+
) -> Session:
|
|
1026
|
+
"""
|
|
1027
|
+
Create a new session (login to the BMC).
|
|
1028
|
+
|
|
1029
|
+
After creation, the X-Auth-Token is returned in the response header.
|
|
1030
|
+
If switch_to_token_auth=True, the SDK client will use this token
|
|
1031
|
+
for subsequent requests instead of Basic Auth.
|
|
1032
|
+
|
|
1033
|
+
Args:
|
|
1034
|
+
username: BMC username
|
|
1035
|
+
password: BMC password
|
|
1036
|
+
switch_to_token_auth: If True, switch client to token-based auth
|
|
1037
|
+
|
|
1038
|
+
Returns:
|
|
1039
|
+
Session resource with x_auth_token populated
|
|
1040
|
+
"""
|
|
1041
|
+
return self._sessions.create(username, password, switch_to_token_auth)
|
|
1042
|
+
|
|
1043
|
+
def delete_session(self, session_id: str) -> str:
|
|
1044
|
+
"""
|
|
1045
|
+
Delete a session (logout).
|
|
1046
|
+
|
|
1047
|
+
Args:
|
|
1048
|
+
session_id: Session ID to delete
|
|
1049
|
+
|
|
1050
|
+
Returns:
|
|
1051
|
+
Response body (usually empty)
|
|
1052
|
+
"""
|
|
1053
|
+
return self._sessions.delete(session_id)
|
|
1054
|
+
|
|
1055
|
+
# ==================================================================
|
|
1056
|
+
# Component query methods — Event service
|
|
1057
|
+
# ==================================================================
|
|
1058
|
+
|
|
1059
|
+
def get_subscriptions(self) -> List[Subscription]:
|
|
1060
|
+
"""
|
|
1061
|
+
Get all event subscriptions (collection-expanded).
|
|
1062
|
+
|
|
1063
|
+
Internally lists the ``Subscriptions`` collection and fetches each
|
|
1064
|
+
member by its ``@odata.id``; members that fail to fetch are skipped
|
|
1065
|
+
with a warning.
|
|
1066
|
+
|
|
1067
|
+
Returns:
|
|
1068
|
+
List of Subscription objects
|
|
1069
|
+
"""
|
|
1070
|
+
return self._events.subscriptions()
|
|
1071
|
+
|
|
1072
|
+
def get_subscription(self, id_or_uri: str) -> Subscription:
|
|
1073
|
+
"""
|
|
1074
|
+
Get a single event subscription by Id or full ``@odata.id``.
|
|
1075
|
+
|
|
1076
|
+
Args:
|
|
1077
|
+
id_or_uri: Either a bare subscription Id (e.g. ``"1"``) or the
|
|
1078
|
+
full ``@odata.id`` path
|
|
1079
|
+
(e.g. ``"/redfish/v1/EventService/Subscriptions/1"``).
|
|
1080
|
+
|
|
1081
|
+
Returns:
|
|
1082
|
+
The Subscription resource.
|
|
1083
|
+
"""
|
|
1084
|
+
return self._events.get_subscription(id_or_uri)
|
|
1085
|
+
|
|
1086
|
+
def subscribe(
|
|
1087
|
+
self,
|
|
1088
|
+
destination: str,
|
|
1089
|
+
event_types: Optional[List[str]] = None,
|
|
1090
|
+
context: Optional[str] = None,
|
|
1091
|
+
*,
|
|
1092
|
+
protocol: str = "Redfish",
|
|
1093
|
+
http_headers: Optional[Any] = None,
|
|
1094
|
+
origin_resources: Optional[List[Dict[str, Any]]] = None,
|
|
1095
|
+
subscription_type: Optional[str] = None,
|
|
1096
|
+
registry_prefixes: Optional[List[str]] = None,
|
|
1097
|
+
resource_types: Optional[List[str]] = None,
|
|
1098
|
+
message_ids: Optional[List[str]] = None,
|
|
1099
|
+
delivery_retry_policy: Optional[str] = None,
|
|
1100
|
+
event_format_type: Optional[str] = None,
|
|
1101
|
+
severities: Optional[List[str]] = None,
|
|
1102
|
+
oem_subscription_type: Optional[str] = None,
|
|
1103
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
1104
|
+
raw_body: Optional[Dict[str, Any]] = None,
|
|
1105
|
+
) -> Subscription:
|
|
1106
|
+
"""
|
|
1107
|
+
Create a new event subscription (webhook).
|
|
1108
|
+
|
|
1109
|
+
Every Redfish ``EventDestination`` field observed in the wild is
|
|
1110
|
+
exposed as a keyword-only argument, and ``extra`` / ``raw_body``
|
|
1111
|
+
provide an escape hatch for OEM-specific payloads. The SDK does
|
|
1112
|
+
**not** apply any vendor-default values — callers wishing to support
|
|
1113
|
+
multiple BMC vendors may try several payload shapes in sequence
|
|
1114
|
+
(catching :class:`RedfishException` between attempts).
|
|
1115
|
+
|
|
1116
|
+
Args:
|
|
1117
|
+
destination: URL to receive events (e.g. ``"https://my-server/events"``).
|
|
1118
|
+
event_types: Optional list of event types (e.g. ``["Alert"]``).
|
|
1119
|
+
context: Optional context string identifying the subscription.
|
|
1120
|
+
protocol: Wire protocol; defaults to ``"Redfish"``.
|
|
1121
|
+
http_headers: Optional headers to send on the callback POST.
|
|
1122
|
+
Pass either a ``dict`` or a ``list[dict]`` — both forms are
|
|
1123
|
+
commonly seen across BMC vendors.
|
|
1124
|
+
origin_resources: Optional list of ``{"@odata.id": "..."}``.
|
|
1125
|
+
subscription_type: Optional ``SubscriptionType`` value.
|
|
1126
|
+
registry_prefixes: Optional message-registry filter list.
|
|
1127
|
+
resource_types: Optional list of resource-type filters.
|
|
1128
|
+
message_ids: Optional list of message Ids to filter on.
|
|
1129
|
+
delivery_retry_policy: Optional retry policy.
|
|
1130
|
+
event_format_type: Optional event format type.
|
|
1131
|
+
severities: Optional severity filter list.
|
|
1132
|
+
oem_subscription_type: Optional vendor-specific subscription type.
|
|
1133
|
+
extra: Optional dict shallow-merged into the request body.
|
|
1134
|
+
raw_body: If provided, replaces the entire auto-generated body.
|
|
1135
|
+
|
|
1136
|
+
Returns:
|
|
1137
|
+
The created Subscription resource (as echoed by the BMC).
|
|
1138
|
+
"""
|
|
1139
|
+
return self._events.subscribe(
|
|
1140
|
+
destination,
|
|
1141
|
+
event_types,
|
|
1142
|
+
context,
|
|
1143
|
+
protocol=protocol,
|
|
1144
|
+
http_headers=http_headers,
|
|
1145
|
+
origin_resources=origin_resources,
|
|
1146
|
+
subscription_type=subscription_type,
|
|
1147
|
+
registry_prefixes=registry_prefixes,
|
|
1148
|
+
resource_types=resource_types,
|
|
1149
|
+
message_ids=message_ids,
|
|
1150
|
+
delivery_retry_policy=delivery_retry_policy,
|
|
1151
|
+
event_format_type=event_format_type,
|
|
1152
|
+
severities=severities,
|
|
1153
|
+
oem_subscription_type=oem_subscription_type,
|
|
1154
|
+
extra=extra,
|
|
1155
|
+
raw_body=raw_body,
|
|
1156
|
+
)
|
|
1157
|
+
|
|
1158
|
+
def delete_subscription(self, id_or_uri: str) -> str:
|
|
1159
|
+
"""
|
|
1160
|
+
Delete an event subscription.
|
|
1161
|
+
|
|
1162
|
+
Args:
|
|
1163
|
+
id_or_uri: Either a bare subscription Id (e.g. ``"1"``) or the
|
|
1164
|
+
full ``@odata.id`` path
|
|
1165
|
+
(e.g. ``"/redfish/v1/EventService/Subscriptions/1"``).
|
|
1166
|
+
|
|
1167
|
+
Returns:
|
|
1168
|
+
Raw response body (typically empty on 204).
|
|
1169
|
+
"""
|
|
1170
|
+
return self._events.delete(id_or_uri)
|
|
1171
|
+
|
|
1172
|
+
# ------------------------------------------------------------------
|
|
1173
|
+
# Event service — extra accessors
|
|
1174
|
+
# ------------------------------------------------------------------
|
|
1175
|
+
|
|
1176
|
+
def get_event_service(self) -> EventService:
|
|
1177
|
+
"""
|
|
1178
|
+
Get the full EventService resource (includes Actions block).
|
|
1179
|
+
|
|
1180
|
+
Use this when you need ``event_service.actions`` (e.g. to discover
|
|
1181
|
+
the SubmitTestEvent target or its AllowableValues).
|
|
1182
|
+
"""
|
|
1183
|
+
return self._events.service()
|
|
1184
|
+
|
|
1185
|
+
def submit_test_event(
|
|
1186
|
+
self,
|
|
1187
|
+
event_type: str,
|
|
1188
|
+
message: Optional[str] = None,
|
|
1189
|
+
message_id: Optional[str] = None,
|
|
1190
|
+
severity: Optional[str] = None,
|
|
1191
|
+
message_args: Optional[List[str]] = None,
|
|
1192
|
+
) -> None:
|
|
1193
|
+
"""
|
|
1194
|
+
Invoke ``#EventService.SubmitTestEvent`` on the BMC.
|
|
1195
|
+
|
|
1196
|
+
See :meth:`EventServiceManager.submit_test_event` for details.
|
|
1197
|
+
"""
|
|
1198
|
+
return self._events.submit_test_event(
|
|
1199
|
+
event_type, message, message_id, severity, message_args
|
|
1200
|
+
)
|
|
1201
|
+
|
|
1202
|
+
# ==================================================================
|
|
1203
|
+
# Component query methods — Update service
|
|
1204
|
+
# ==================================================================
|
|
1205
|
+
|
|
1206
|
+
def get_firmware_inventory(self) -> List[FirmwareInventory]:
|
|
1207
|
+
"""
|
|
1208
|
+
Get the list of firmware inventory entries.
|
|
1209
|
+
|
|
1210
|
+
Returns all firmware/software component versions installed on the system
|
|
1211
|
+
(BIOS, BMC, CPLD, NIC firmware, etc.).
|
|
1212
|
+
|
|
1213
|
+
Returns:
|
|
1214
|
+
List of FirmwareInventory objects
|
|
1215
|
+
"""
|
|
1216
|
+
return self._updates.firmware_inventory()
|
|
1217
|
+
|
|
1218
|
+
def get_client_certificates(self) -> List[ClientCertificate]:
|
|
1219
|
+
"""
|
|
1220
|
+
Get the list of client certificates for firmware update authentication.
|
|
1221
|
+
|
|
1222
|
+
Returns:
|
|
1223
|
+
List of ClientCertificate objects
|
|
1224
|
+
"""
|
|
1225
|
+
return self._updates.client_certificates()
|
|
1226
|
+
|
|
1227
|
+
def simple_update(
|
|
1228
|
+
self,
|
|
1229
|
+
image_uri: str,
|
|
1230
|
+
transfer_protocol: str = "HTTP",
|
|
1231
|
+
targets: Optional[list] = None,
|
|
1232
|
+
vendor: Optional[str] = None,
|
|
1233
|
+
**kwargs,
|
|
1234
|
+
) -> RedfishResponse:
|
|
1235
|
+
"""
|
|
1236
|
+
Trigger a firmware update via a remote image URI (e.g., NFS, HTTP).
|
|
1237
|
+
|
|
1238
|
+
Automatically detects the server vendor and uses the appropriate
|
|
1239
|
+
request body format. The vendor can be manually overridden.
|
|
1240
|
+
|
|
1241
|
+
Args:
|
|
1242
|
+
image_uri: URI of the firmware image (e.g., "http://nas/fw/bmc.bin")
|
|
1243
|
+
transfer_protocol: Transfer protocol (e.g., "HTTP", "NFS", "TFTP")
|
|
1244
|
+
targets: Optional list of firmware target paths
|
|
1245
|
+
vendor: Optional vendor override (e.g., "inspur", "lenovo").
|
|
1246
|
+
If not set, the vendor is auto-detected.
|
|
1247
|
+
**kwargs: Vendor-specific parameters (e.g., preserve_config,
|
|
1248
|
+
username, password, flash_item, etc.)
|
|
1249
|
+
|
|
1250
|
+
Returns:
|
|
1251
|
+
RedfishResponse (may contain a task reference for async update)
|
|
1252
|
+
"""
|
|
1253
|
+
return self._updates.simple_update(
|
|
1254
|
+
image_uri, transfer_protocol, targets, vendor, **kwargs
|
|
1255
|
+
)
|
|
1256
|
+
|
|
1257
|
+
# ==================================================================
|
|
1258
|
+
# Component query methods — Registries
|
|
1259
|
+
# ==================================================================
|
|
1260
|
+
|
|
1261
|
+
def get_registries(self) -> List[Registry]:
|
|
1262
|
+
"""
|
|
1263
|
+
Get the list of message registries.
|
|
1264
|
+
|
|
1265
|
+
Returns:
|
|
1266
|
+
List of Registry objects
|
|
1267
|
+
"""
|
|
1268
|
+
return self._registries.registries()
|
|
1269
|
+
|
|
1270
|
+
def get_registry(self, registry_id: str) -> Registry:
|
|
1271
|
+
"""
|
|
1272
|
+
Get a specific message registry by ID.
|
|
1273
|
+
|
|
1274
|
+
Args:
|
|
1275
|
+
registry_id: Registry ID (e.g., "Base.1.15.0")
|
|
1276
|
+
|
|
1277
|
+
Returns:
|
|
1278
|
+
Registry resource
|
|
1279
|
+
"""
|
|
1280
|
+
return self._registries.get(registry_id)
|
|
1281
|
+
|
|
1282
|
+
# ==================================================================
|
|
1283
|
+
# Component query methods — Tasks
|
|
1284
|
+
# ==================================================================
|
|
1285
|
+
|
|
1286
|
+
def get_tasks(self) -> List[Task]:
|
|
1287
|
+
"""
|
|
1288
|
+
Get the list of all tasks.
|
|
1289
|
+
|
|
1290
|
+
Returns:
|
|
1291
|
+
List of Task objects
|
|
1292
|
+
"""
|
|
1293
|
+
return self._tasks.tasks()
|
|
1294
|
+
|
|
1295
|
+
def get_task(self, task_id: str) -> Task:
|
|
1296
|
+
"""
|
|
1297
|
+
Get a specific task by ID.
|
|
1298
|
+
|
|
1299
|
+
Args:
|
|
1300
|
+
task_id: Task ID
|
|
1301
|
+
|
|
1302
|
+
Returns:
|
|
1303
|
+
Task resource
|
|
1304
|
+
"""
|
|
1305
|
+
return self._tasks.get(task_id)
|
|
1306
|
+
|
|
1307
|
+
def wait_for_task(
|
|
1308
|
+
self,
|
|
1309
|
+
task_id: str,
|
|
1310
|
+
poll_interval: int = 5,
|
|
1311
|
+
timeout: int = 600,
|
|
1312
|
+
) -> Task:
|
|
1313
|
+
"""
|
|
1314
|
+
Poll a task until it completes or times out.
|
|
1315
|
+
|
|
1316
|
+
Useful for monitoring long-running firmware update tasks.
|
|
1317
|
+
|
|
1318
|
+
Args:
|
|
1319
|
+
task_id: Task ID to monitor
|
|
1320
|
+
poll_interval: Seconds between polls (default 5)
|
|
1321
|
+
timeout: Maximum wait time in seconds (default 600)
|
|
1322
|
+
|
|
1323
|
+
Returns:
|
|
1324
|
+
Completed Task resource
|
|
1325
|
+
|
|
1326
|
+
Raises:
|
|
1327
|
+
TimeoutError: If task does not complete within timeout
|
|
1328
|
+
"""
|
|
1329
|
+
return self._tasks.wait_for_task(task_id, poll_interval, timeout)
|
|
1330
|
+
|
|
1331
|
+
# ==================================================================
|
|
1332
|
+
# Component query methods — Firmware / FRU
|
|
1333
|
+
# ==================================================================
|
|
1334
|
+
|
|
1335
|
+
def get_baseboard_fru(self, chassis_id: str = "1") -> Optional[dict]:
|
|
1336
|
+
"""
|
|
1337
|
+
Get baseboard (motherboard) FRU data.
|
|
1338
|
+
|
|
1339
|
+
Uses the Chassis OEM FRU service to retrieve FRU board info.
|
|
1340
|
+
This is a vendor-specific extension (e.g., Huawei/xFusion iBMC).
|
|
1341
|
+
|
|
1342
|
+
Args:
|
|
1343
|
+
chassis_id: Chassis ID (default "1")
|
|
1344
|
+
|
|
1345
|
+
Returns:
|
|
1346
|
+
Raw FRU board data dict, or None if not available
|
|
1347
|
+
"""
|
|
1348
|
+
return self._chassis.fru_service_board(chassis_id)
|
|
1349
|
+
|
|
1350
|
+
def get_mainboard(
|
|
1351
|
+
self,
|
|
1352
|
+
system_id: Optional[str] = None,
|
|
1353
|
+
chassis_id: str = "1",
|
|
1354
|
+
) -> Optional[MainBoard]:
|
|
1355
|
+
"""
|
|
1356
|
+
Get mainboard (motherboard) information with multi-path fallback.
|
|
1357
|
+
|
|
1358
|
+
Fallback order:
|
|
1359
|
+
1. System FRU board info
|
|
1360
|
+
2. Chassis OEM FRU service board info
|
|
1361
|
+
3. Chassis OEM mainboard field
|
|
1362
|
+
|
|
1363
|
+
Args:
|
|
1364
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
1365
|
+
chassis_id: Chassis ID (default "1")
|
|
1366
|
+
|
|
1367
|
+
Returns:
|
|
1368
|
+
MainBoard model, or None if not available from any supported source
|
|
1369
|
+
"""
|
|
1370
|
+
try:
|
|
1371
|
+
system_fru = self.get_system_fru(system_id)
|
|
1372
|
+
if system_fru is not None and system_fru.board is not None:
|
|
1373
|
+
logger.debug("Mainboard found via system FRU")
|
|
1374
|
+
return system_fru.board
|
|
1375
|
+
except RedfishException as exc:
|
|
1376
|
+
logger.debug("Failed to get mainboard via system FRU: %s", exc)
|
|
1377
|
+
|
|
1378
|
+
try:
|
|
1379
|
+
board_raw = self.get_baseboard_fru(chassis_id)
|
|
1380
|
+
if board_raw:
|
|
1381
|
+
if "BoardInfo" in board_raw:
|
|
1382
|
+
mainboard_raw = board_raw["BoardInfo"]
|
|
1383
|
+
mainboard_raw["@odata.type"] = board_raw.get("@odata.type", "#MainBoard.v1_0_0.MainBoard")
|
|
1384
|
+
mainboard_raw["@odata.id"] = board_raw.get("@odata.id")
|
|
1385
|
+
mainboard_raw["@odata.context"] = board_raw.get("@odata.context")
|
|
1386
|
+
logger.debug("Mainboard found via chassis FRU service")
|
|
1387
|
+
return MainBoard.model_validate(mainboard_raw)
|
|
1388
|
+
return MainBoard.model_validate(board_raw)
|
|
1389
|
+
except RedfishException as exc:
|
|
1390
|
+
logger.debug("Failed to get mainboard via chassis FRU service: %s", exc)
|
|
1391
|
+
except Exception as exc:
|
|
1392
|
+
logger.warning("Failed to parse mainboard from chassis FRU service: %s", exc)
|
|
1393
|
+
|
|
1394
|
+
try:
|
|
1395
|
+
chassis = self.get_chassis(chassis_id)
|
|
1396
|
+
if chassis.oem and chassis.oem.bmc and chassis.oem.bmc.mainboard:
|
|
1397
|
+
logger.debug("Mainboard found via chassis OEM mainboard")
|
|
1398
|
+
return chassis.oem.bmc.mainboard
|
|
1399
|
+
except RedfishException as exc:
|
|
1400
|
+
logger.debug("Failed to get mainboard via chassis OEM data: %s", exc)
|
|
1401
|
+
|
|
1402
|
+
logger.debug("Mainboard not available from any supported source")
|
|
1403
|
+
return None
|
|
1404
|
+
|
|
1405
|
+
# ------------------------------------------------------------------
|
|
1406
|
+
# Component query methods — Extracted sub-resources
|
|
1407
|
+
# ------------------------------------------------------------------
|
|
1408
|
+
|
|
1409
|
+
def get_fan(self, chassis_id: str = "1") -> List[Fan]:
|
|
1410
|
+
"""
|
|
1411
|
+
Get fan information with multi-path fallback.
|
|
1412
|
+
|
|
1413
|
+
Fallback order:
|
|
1414
|
+
1. ``/redfish/v1/Chassis/{id}/ThermalSubsystem/Fans`` — newer Redfish schema,
|
|
1415
|
+
fetches the collection then GETs each member individually.
|
|
1416
|
+
2. ``/redfish/v1/Chassis/{id}/Thermal`` — legacy schema,
|
|
1417
|
+
extracts the ``Fans`` array from the Thermal resource.
|
|
1418
|
+
|
|
1419
|
+
Args:
|
|
1420
|
+
chassis_id: Chassis ID (default "1")
|
|
1421
|
+
|
|
1422
|
+
Returns:
|
|
1423
|
+
List of Fan objects (empty list if not available from any supported path)
|
|
1424
|
+
"""
|
|
1425
|
+
# Path 1: ThermalSubsystem/Fans (newer Redfish schema)
|
|
1426
|
+
try:
|
|
1427
|
+
subsystem_path = f"/redfish/v1/Chassis/{chassis_id}/ThermalSubsystem/Fans"
|
|
1428
|
+
collection = self.get_raw(subsystem_path)
|
|
1429
|
+
if collection is not None:
|
|
1430
|
+
members = collection.get("Members", [])
|
|
1431
|
+
fans: List[Fan] = []
|
|
1432
|
+
for member in members:
|
|
1433
|
+
odata_id = member.get("@odata.id")
|
|
1434
|
+
if odata_id:
|
|
1435
|
+
try:
|
|
1436
|
+
fan_raw = self.get_raw(odata_id)
|
|
1437
|
+
fans.append(Fan.model_validate(fan_raw))
|
|
1438
|
+
except Exception as exc:
|
|
1439
|
+
logger.debug("Failed to fetch fan member %s: %s", odata_id, exc)
|
|
1440
|
+
if fans:
|
|
1441
|
+
logger.debug("Fans found via %s (%d fans)", subsystem_path, len(fans))
|
|
1442
|
+
return fans
|
|
1443
|
+
except RedfishException as exc:
|
|
1444
|
+
logger.debug("Failed to get fans via ThermalSubsystem: %s", exc)
|
|
1445
|
+
|
|
1446
|
+
# Path 2: Thermal (legacy schema)
|
|
1447
|
+
try:
|
|
1448
|
+
thermal = self.get_thermal(chassis_id)
|
|
1449
|
+
if thermal.fans:
|
|
1450
|
+
logger.debug("Fans found via Thermal resource (%d fans)", len(thermal.fans))
|
|
1451
|
+
return thermal.fans
|
|
1452
|
+
except RedfishException as exc:
|
|
1453
|
+
logger.debug("Failed to get fans via Thermal: %s", exc)
|
|
1454
|
+
|
|
1455
|
+
logger.debug("Fan info not available from any supported source")
|
|
1456
|
+
return []
|
|
1457
|
+
|
|
1458
|
+
def get_power_supplies(self, chassis_id: str = "1") -> List[PowerSupply]:
|
|
1459
|
+
"""
|
|
1460
|
+
Get the list of power supply units (PSUs) for a chassis.
|
|
1461
|
+
|
|
1462
|
+
Extracts the PowerSupplies array from the Power resource.
|
|
1463
|
+
|
|
1464
|
+
Args:
|
|
1465
|
+
chassis_id: Chassis ID (default "1")
|
|
1466
|
+
|
|
1467
|
+
Returns:
|
|
1468
|
+
List of PowerSupply objects (empty list if no PSUs found)
|
|
1469
|
+
"""
|
|
1470
|
+
power = self.get_power(chassis_id)
|
|
1471
|
+
return power.power_supplies or []
|
|
1472
|
+
|
|
1473
|
+
# ------------------------------------------------------------------
|
|
1474
|
+
# Component query methods — System-level convenience
|
|
1475
|
+
# ------------------------------------------------------------------
|
|
1476
|
+
|
|
1477
|
+
def get_manufacturer(self, system_id: Optional[str] = None) -> str:
|
|
1478
|
+
"""
|
|
1479
|
+
Get the server manufacturer name.
|
|
1480
|
+
|
|
1481
|
+
Args:
|
|
1482
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
1483
|
+
|
|
1484
|
+
Returns:
|
|
1485
|
+
Manufacturer name string (e.g., "Huawei", "Inspur", "H3C", "Lenovo")
|
|
1486
|
+
|
|
1487
|
+
Raises:
|
|
1488
|
+
RedfishException: If manufacturer field is not available
|
|
1489
|
+
"""
|
|
1490
|
+
system = self._systems.get(system_id)
|
|
1491
|
+
if not system.manufacturer:
|
|
1492
|
+
raise RedfishException(500, "Manufacturer field not found in system resource")
|
|
1493
|
+
return system.manufacturer
|
|
1494
|
+
|
|
1495
|
+
# ------------------------------------------------------------------
|
|
1496
|
+
# Resource metadata methods (ETag / @odata.id)
|
|
1497
|
+
# ------------------------------------------------------------------
|
|
1498
|
+
|
|
1499
|
+
def get_etag(self, path: str) -> Optional[str]:
|
|
1500
|
+
"""
|
|
1501
|
+
Get the cached ETag for a Redfish resource path.
|
|
1502
|
+
|
|
1503
|
+
ETag is used for concurrency control in Redfish:
|
|
1504
|
+
- GET responses include an ETag header (or @odata.etag in JSON body)
|
|
1505
|
+
- PATCH/PUT requests should include If-Match header with the ETag
|
|
1506
|
+
- This prevents "lost update" problems when multiple clients modify the same resource
|
|
1507
|
+
|
|
1508
|
+
If the resource has not been fetched yet (no cached ETag), this method
|
|
1509
|
+
will perform a HEAD-like GET to retrieve and cache the ETag.
|
|
1510
|
+
|
|
1511
|
+
Args:
|
|
1512
|
+
path: Redfish resource path (e.g., "/redfish/v1/Systems/1")
|
|
1513
|
+
|
|
1514
|
+
Returns:
|
|
1515
|
+
ETag string (e.g., '"W/12345"'), or None if the server does not provide ETags
|
|
1516
|
+
"""
|
|
1517
|
+
# Check cached ETag first
|
|
1518
|
+
cached = self._http_client._last_etag.get(path)
|
|
1519
|
+
if cached:
|
|
1520
|
+
return cached
|
|
1521
|
+
|
|
1522
|
+
# Fetch the resource to populate the ETag cache
|
|
1523
|
+
try:
|
|
1524
|
+
self._http_client.get_raw(path)
|
|
1525
|
+
except RedfishException:
|
|
1526
|
+
return None
|
|
1527
|
+
|
|
1528
|
+
return self._http_client._last_etag.get(path)
|
|
1529
|
+
|
|
1530
|
+
def get_odata_id(self, key: RedfishResource) -> Optional[str]:
|
|
1531
|
+
"""
|
|
1532
|
+
Look up the @odata.id for a Redfish resource by its ``RedfishResource`` key.
|
|
1533
|
+
|
|
1534
|
+
Automatically searches the Redfish resource tree in a fixed order:
|
|
1535
|
+
|
|
1536
|
+
1. RootService (``/redfish/v1/``)
|
|
1537
|
+
2. First member of the Systems collection
|
|
1538
|
+
3. First member of the Chassis collection
|
|
1539
|
+
4. First member of the Managers collection
|
|
1540
|
+
|
|
1541
|
+
The first match wins and is returned immediately.
|
|
1542
|
+
|
|
1543
|
+
Args:
|
|
1544
|
+
key: A ``RedfishResource`` enum member identifying the resource
|
|
1545
|
+
(e.g., ``RedfishResource.PROCESSORS``, ``RedfishResource.THERMAL``)
|
|
1546
|
+
|
|
1547
|
+
Returns:
|
|
1548
|
+
The @odata.id string (e.g., ``"/redfish/v1/Systems/1/Processors"``),
|
|
1549
|
+
or ``None`` if the key was not found in any layer.
|
|
1550
|
+
|
|
1551
|
+
Example::
|
|
1552
|
+
|
|
1553
|
+
from redfish_sdk import RedfishClient, RedfishResource
|
|
1554
|
+
|
|
1555
|
+
client = RedfishClient(host="10.0.0.1", username="admin", password="pwd")
|
|
1556
|
+
url = client.get_odata_id(RedfishResource.PROCESSORS)
|
|
1557
|
+
# → "/redfish/v1/Systems/1/Processors"
|
|
1558
|
+
"""
|
|
1559
|
+
field_name = key.value # e.g. "Processors", "Thermal"
|
|
1560
|
+
|
|
1561
|
+
# Step 1: Search in RootService
|
|
1562
|
+
try:
|
|
1563
|
+
root_data = self._http_client.get_raw("/redfish/v1/")
|
|
1564
|
+
result = self._extract_odata_id(root_data, field_name)
|
|
1565
|
+
if result is not None:
|
|
1566
|
+
return result
|
|
1567
|
+
except Exception as exc:
|
|
1568
|
+
logger.warning("get_odata_id: failed to fetch RootService: %s", exc)
|
|
1569
|
+
|
|
1570
|
+
# Step 2–4: Search in first member of Systems, Chassis, Managers
|
|
1571
|
+
collection_keys = ["Systems", "Chassis", "Managers"]
|
|
1572
|
+
for col_key in collection_keys:
|
|
1573
|
+
try:
|
|
1574
|
+
first_member_data = self._get_first_collection_member_raw(col_key)
|
|
1575
|
+
if first_member_data is None:
|
|
1576
|
+
continue
|
|
1577
|
+
result = self._extract_odata_id(first_member_data, field_name)
|
|
1578
|
+
if result is not None:
|
|
1579
|
+
return result
|
|
1580
|
+
except Exception as exc:
|
|
1581
|
+
logger.warning(
|
|
1582
|
+
"get_odata_id: failed to search in %s collection: %s",
|
|
1583
|
+
col_key, exc,
|
|
1584
|
+
)
|
|
1585
|
+
|
|
1586
|
+
return None
|
|
1587
|
+
|
|
1588
|
+
@staticmethod
|
|
1589
|
+
def get_resource_odata_id(resource) -> Optional[str]:
|
|
1590
|
+
"""
|
|
1591
|
+
Extract the @odata.id from a Redfish resource object.
|
|
1592
|
+
|
|
1593
|
+
This is a backward-compatible convenience method that works with any
|
|
1594
|
+
SDK model object (Entity, Link, or any pydantic model with an odata_id field).
|
|
1595
|
+
|
|
1596
|
+
Args:
|
|
1597
|
+
resource: Any Redfish resource model instance (e.g., System, Chassis, Processor)
|
|
1598
|
+
|
|
1599
|
+
Returns:
|
|
1600
|
+
The @odata.id string (e.g., "/redfish/v1/Systems/1"), or None if not present
|
|
1601
|
+
"""
|
|
1602
|
+
return getattr(resource, "odata_id", None)
|
|
1603
|
+
|
|
1604
|
+
# ------------------------------------------------------------------
|
|
1605
|
+
# get_odata_id internal helpers
|
|
1606
|
+
# ------------------------------------------------------------------
|
|
1607
|
+
|
|
1608
|
+
def _get_first_collection_member_raw(self, collection_key: str) -> Optional[dict]:
|
|
1609
|
+
"""
|
|
1610
|
+
Fetch the raw JSON of the first member in a top-level collection.
|
|
1611
|
+
|
|
1612
|
+
Args:
|
|
1613
|
+
collection_key: Top-level collection name (e.g., "Systems", "Chassis", "Managers")
|
|
1614
|
+
|
|
1615
|
+
Returns:
|
|
1616
|
+
Raw JSON dict of the first member, or None if not available
|
|
1617
|
+
"""
|
|
1618
|
+
# Get collection @odata.id from root
|
|
1619
|
+
root_data = self._http_client.get_raw("/redfish/v1/")
|
|
1620
|
+
col_ref = root_data.get(collection_key)
|
|
1621
|
+
if col_ref is None:
|
|
1622
|
+
return None
|
|
1623
|
+
|
|
1624
|
+
col_odata_id = col_ref.get("@odata.id") if isinstance(col_ref, dict) else None
|
|
1625
|
+
if not col_odata_id:
|
|
1626
|
+
return None
|
|
1627
|
+
|
|
1628
|
+
# Get collection members list
|
|
1629
|
+
col_data = self._http_client.get_raw(col_odata_id)
|
|
1630
|
+
members = col_data.get("Members", [])
|
|
1631
|
+
if not members:
|
|
1632
|
+
return None
|
|
1633
|
+
|
|
1634
|
+
# Get first member
|
|
1635
|
+
first_member_id = members[0].get("@odata.id")
|
|
1636
|
+
if not first_member_id:
|
|
1637
|
+
return None
|
|
1638
|
+
|
|
1639
|
+
return self._http_client.get_raw(first_member_id)
|
|
1640
|
+
|
|
1641
|
+
@staticmethod
|
|
1642
|
+
def _extract_odata_id(data: dict, field_name: str) -> Optional[str]:
|
|
1643
|
+
"""
|
|
1644
|
+
Extract @odata.id for a given field name from a resource JSON dict.
|
|
1645
|
+
|
|
1646
|
+
Search order:
|
|
1647
|
+
1. Top-level fields
|
|
1648
|
+
2. Links section
|
|
1649
|
+
3. Oem section (recursive)
|
|
1650
|
+
|
|
1651
|
+
Args:
|
|
1652
|
+
data: Raw JSON dict of a Redfish resource
|
|
1653
|
+
field_name: The field name to look for (e.g., "Processors", "Thermal")
|
|
1654
|
+
|
|
1655
|
+
Returns:
|
|
1656
|
+
The @odata.id string, or None if not found
|
|
1657
|
+
"""
|
|
1658
|
+
# 1. Top-level field
|
|
1659
|
+
value = data.get(field_name)
|
|
1660
|
+
if value is not None:
|
|
1661
|
+
odata_id = RedfishClient._resolve_odata_id(value)
|
|
1662
|
+
if odata_id:
|
|
1663
|
+
return odata_id
|
|
1664
|
+
|
|
1665
|
+
# 2. Links section
|
|
1666
|
+
links = data.get("Links")
|
|
1667
|
+
if isinstance(links, dict):
|
|
1668
|
+
value = links.get(field_name)
|
|
1669
|
+
if value is not None:
|
|
1670
|
+
odata_id = RedfishClient._resolve_odata_id(value)
|
|
1671
|
+
if odata_id:
|
|
1672
|
+
return odata_id
|
|
1673
|
+
|
|
1674
|
+
# 3. Oem section (recursive search)
|
|
1675
|
+
oem = data.get("Oem")
|
|
1676
|
+
if isinstance(oem, dict):
|
|
1677
|
+
odata_id = RedfishClient._search_oem_for_key(oem, field_name)
|
|
1678
|
+
if odata_id:
|
|
1679
|
+
return odata_id
|
|
1680
|
+
|
|
1681
|
+
return None
|
|
1682
|
+
|
|
1683
|
+
@staticmethod
|
|
1684
|
+
def _resolve_odata_id(value) -> Optional[str]:
|
|
1685
|
+
"""
|
|
1686
|
+
Resolve @odata.id from a field value.
|
|
1687
|
+
|
|
1688
|
+
Handles:
|
|
1689
|
+
- dict with "@odata.id" key
|
|
1690
|
+
- str (direct path)
|
|
1691
|
+
- list of dicts (takes first element's @odata.id)
|
|
1692
|
+
"""
|
|
1693
|
+
if isinstance(value, dict):
|
|
1694
|
+
return value.get("@odata.id")
|
|
1695
|
+
if isinstance(value, str) and value.startswith("/"):
|
|
1696
|
+
return value
|
|
1697
|
+
if isinstance(value, list) and value:
|
|
1698
|
+
first = value[0]
|
|
1699
|
+
if isinstance(first, dict):
|
|
1700
|
+
return first.get("@odata.id")
|
|
1701
|
+
return None
|
|
1702
|
+
|
|
1703
|
+
@staticmethod
|
|
1704
|
+
def _search_oem_for_key(oem_data: dict, field_name: str) -> Optional[str]:
|
|
1705
|
+
"""
|
|
1706
|
+
Recursively search the Oem section for a field matching field_name.
|
|
1707
|
+
"""
|
|
1708
|
+
for k, v in oem_data.items():
|
|
1709
|
+
if k == field_name:
|
|
1710
|
+
return RedfishClient._resolve_odata_id(v)
|
|
1711
|
+
if isinstance(v, dict):
|
|
1712
|
+
result = RedfishClient._search_oem_for_key(v, field_name)
|
|
1713
|
+
if result:
|
|
1714
|
+
return result
|
|
1715
|
+
return None
|
|
1716
|
+
|
|
1717
|
+
# ------------------------------------------------------------------
|
|
1718
|
+
# Raw JSON access (generic CRUD)
|
|
1719
|
+
# ------------------------------------------------------------------
|
|
1720
|
+
|
|
1721
|
+
def get_raw(self, odata_id: str) -> dict:
|
|
1722
|
+
"""
|
|
1723
|
+
Fetch the raw JSON data for any Redfish resource by its @odata.id.
|
|
1724
|
+
|
|
1725
|
+
Unlike typed getter methods (e.g., ``get_system()``), this returns the
|
|
1726
|
+
unprocessed JSON dict exactly as the BMC returns it — useful for
|
|
1727
|
+
inspecting vendor-specific (OEM) fields, debugging, or accessing
|
|
1728
|
+
resources that the SDK does not yet model.
|
|
1729
|
+
|
|
1730
|
+
Args:
|
|
1731
|
+
odata_id: The @odata.id path of the resource
|
|
1732
|
+
(e.g., ``"/redfish/v1/Systems/1"``)
|
|
1733
|
+
|
|
1734
|
+
Returns:
|
|
1735
|
+
Raw JSON dict of the resource
|
|
1736
|
+
|
|
1737
|
+
Raises:
|
|
1738
|
+
RedfishException: On HTTP errors (404, 500, etc.)
|
|
1739
|
+
|
|
1740
|
+
Example::
|
|
1741
|
+
|
|
1742
|
+
from redfish_sdk import RedfishClient
|
|
1743
|
+
|
|
1744
|
+
client = RedfishClient(host="10.0.0.1", username="admin", password="pwd")
|
|
1745
|
+
data = client.get_raw("/redfish/v1/Systems/1")
|
|
1746
|
+
print(data["Manufacturer"])
|
|
1747
|
+
# → "Huawei"
|
|
1748
|
+
"""
|
|
1749
|
+
return self._http_client.get_raw(odata_id)
|
|
1750
|
+
|
|
1751
|
+
def patch(self, odata_id: str, body: dict) -> dict:
|
|
1752
|
+
"""
|
|
1753
|
+
Send a PATCH request to partially update a Redfish resource.
|
|
1754
|
+
|
|
1755
|
+
Automatically handles ETag-based concurrency control:
|
|
1756
|
+
if no ETag is cached for the target path, a GET is issued first
|
|
1757
|
+
to obtain one. The ETag is then sent as the ``If-Match`` header.
|
|
1758
|
+
|
|
1759
|
+
Args:
|
|
1760
|
+
odata_id: The @odata.id path of the resource to update
|
|
1761
|
+
(e.g., ``"/redfish/v1/Systems/1"``)
|
|
1762
|
+
body: A dict containing only the fields to modify
|
|
1763
|
+
|
|
1764
|
+
Returns:
|
|
1765
|
+
Raw JSON dict of the BMC response (empty dict ``{}`` on 204 No Content)
|
|
1766
|
+
|
|
1767
|
+
Raises:
|
|
1768
|
+
RedfishException: On HTTP errors (412 Precondition Failed, 500, etc.)
|
|
1769
|
+
|
|
1770
|
+
Example::
|
|
1771
|
+
|
|
1772
|
+
client.patch("/redfish/v1/Systems/1", {
|
|
1773
|
+
"Boot": {
|
|
1774
|
+
"BootSourceOverrideEnabled": "Once",
|
|
1775
|
+
"BootSourceOverrideTarget": "Pxe",
|
|
1776
|
+
}
|
|
1777
|
+
})
|
|
1778
|
+
"""
|
|
1779
|
+
# Ensure ETag is cached before PATCH (auto-GET if missing)
|
|
1780
|
+
if odata_id not in self._http_client._last_etag:
|
|
1781
|
+
try:
|
|
1782
|
+
self._http_client.get_raw(odata_id)
|
|
1783
|
+
except RedfishException:
|
|
1784
|
+
pass # proceed with '*' wildcard if GET fails
|
|
1785
|
+
|
|
1786
|
+
response = self._http_client.patch_raw(odata_id, body)
|
|
1787
|
+
|
|
1788
|
+
if response.status_code == 204 or not response.text.strip():
|
|
1789
|
+
return {}
|
|
1790
|
+
return response.json()
|
|
1791
|
+
|
|
1792
|
+
def post(self, odata_id: str, body: Optional[dict] = None) -> dict:
|
|
1793
|
+
"""
|
|
1794
|
+
Send a POST request to create a resource or trigger an action.
|
|
1795
|
+
|
|
1796
|
+
Args:
|
|
1797
|
+
odata_id: The target path
|
|
1798
|
+
(e.g., ``"/redfish/v1/AccountService/Accounts"`` or
|
|
1799
|
+
``"/redfish/v1/Systems/1/Actions/ComputerSystem.Reset"``)
|
|
1800
|
+
body: Optional request body dict
|
|
1801
|
+
|
|
1802
|
+
Returns:
|
|
1803
|
+
Raw JSON dict of the BMC response (empty dict ``{}`` on 204 No Content)
|
|
1804
|
+
|
|
1805
|
+
Raises:
|
|
1806
|
+
RedfishException: On HTTP errors (400, 500, etc.)
|
|
1807
|
+
|
|
1808
|
+
Example::
|
|
1809
|
+
|
|
1810
|
+
# Trigger a system reset
|
|
1811
|
+
client.post(
|
|
1812
|
+
"/redfish/v1/Systems/1/Actions/ComputerSystem.Reset",
|
|
1813
|
+
{"ResetType": "GracefulRestart"},
|
|
1814
|
+
)
|
|
1815
|
+
|
|
1816
|
+
# Create a user account
|
|
1817
|
+
client.post("/redfish/v1/AccountService/Accounts", {
|
|
1818
|
+
"UserName": "operator",
|
|
1819
|
+
"Password": "Op3r@tor!",
|
|
1820
|
+
"RoleId": "Operator",
|
|
1821
|
+
})
|
|
1822
|
+
"""
|
|
1823
|
+
response = self._http_client.post_raw(odata_id, body)
|
|
1824
|
+
|
|
1825
|
+
if response.status_code == 204 or not response.text.strip():
|
|
1826
|
+
return {}
|
|
1827
|
+
return response.json()
|
|
1828
|
+
|
|
1829
|
+
def delete(self, odata_id: str) -> None:
|
|
1830
|
+
"""
|
|
1831
|
+
Send a DELETE request to remove a Redfish resource.
|
|
1832
|
+
|
|
1833
|
+
Args:
|
|
1834
|
+
odata_id: The @odata.id path of the resource to delete
|
|
1835
|
+
(e.g., ``"/redfish/v1/SessionService/Sessions/abc123"``)
|
|
1836
|
+
|
|
1837
|
+
Returns:
|
|
1838
|
+
None
|
|
1839
|
+
|
|
1840
|
+
Raises:
|
|
1841
|
+
RedfishException: On HTTP errors (404, 500, etc.)
|
|
1842
|
+
|
|
1843
|
+
Example::
|
|
1844
|
+
|
|
1845
|
+
client.delete("/redfish/v1/SessionService/Sessions/abc123")
|
|
1846
|
+
"""
|
|
1847
|
+
self._http_client.delete(odata_id)
|
|
1848
|
+
|
|
1849
|
+
# ------------------------------------------------------------------
|
|
1850
|
+
# Convenience aggregation method
|
|
1851
|
+
# ------------------------------------------------------------------
|
|
1852
|
+
|
|
1853
|
+
def get_all_components_summary(
|
|
1854
|
+
self, system_id: Optional[str] = None, chassis_id: str = "1"
|
|
1855
|
+
) -> dict:
|
|
1856
|
+
"""
|
|
1857
|
+
Get a summary of all hardware components in a single call.
|
|
1858
|
+
|
|
1859
|
+
Args:
|
|
1860
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
1861
|
+
chassis_id: Chassis ID (default "1")
|
|
1862
|
+
|
|
1863
|
+
Returns:
|
|
1864
|
+
Dictionary with all component lists/resources::
|
|
1865
|
+
|
|
1866
|
+
{
|
|
1867
|
+
"processors": List[Processor],
|
|
1868
|
+
"memory": List[Memory],
|
|
1869
|
+
"storages": List[Storage],
|
|
1870
|
+
"gpus": List[Gpu],
|
|
1871
|
+
"drives": List[Drive],
|
|
1872
|
+
"network_adapters": List[NetworkAdapter],
|
|
1873
|
+
"pcie_devices": List[PCIeDevice],
|
|
1874
|
+
"power": Power,
|
|
1875
|
+
"thermal": Thermal,
|
|
1876
|
+
"fans": List[Fan],
|
|
1877
|
+
"power_supplies": List[PowerSupply],
|
|
1878
|
+
"firmware_inventory": List[FirmwareInventory],
|
|
1879
|
+
}
|
|
1880
|
+
"""
|
|
1881
|
+
return {
|
|
1882
|
+
"processors": self.get_processors(system_id),
|
|
1883
|
+
"memory": self.get_memory(system_id),
|
|
1884
|
+
"storages": self.get_storages(system_id),
|
|
1885
|
+
"gpus": self.get_gpus(system_id),
|
|
1886
|
+
"drives": self.get_drives(chassis_id),
|
|
1887
|
+
"network_adapters": self.get_network_adapters(chassis_id),
|
|
1888
|
+
"pcie_devices": self.get_pcie_devices(chassis_id),
|
|
1889
|
+
"power": self.get_power(chassis_id),
|
|
1890
|
+
"thermal": self.get_thermal(chassis_id),
|
|
1891
|
+
"fans": self.get_fan(chassis_id),
|
|
1892
|
+
"power_supplies": self.get_power_supplies(chassis_id),
|
|
1893
|
+
"firmware_inventory": self.get_firmware_inventory(),
|
|
1894
|
+
}
|