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
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Systems manager — manages server system resources.
|
|
3
|
+
|
|
4
|
+
Provides access to:
|
|
5
|
+
- System info (power state, model, serial number, etc.)
|
|
6
|
+
- Processors (CPUs)
|
|
7
|
+
- Memory (DIMMs)
|
|
8
|
+
- Storage (controllers + drives + volumes)
|
|
9
|
+
- GPUs (via GraphicsControllers or PCIeDevices fallback)
|
|
10
|
+
- BIOS settings
|
|
11
|
+
- Log services and log entries
|
|
12
|
+
- FRU information
|
|
13
|
+
- Boot source control
|
|
14
|
+
- System reset (power on/off/restart)
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
21
|
+
|
|
22
|
+
from ..exceptions import RedfishException, RedfishValidationError
|
|
23
|
+
from ..models.chassis import PCIeDevice
|
|
24
|
+
from ..models.drive import Drive
|
|
25
|
+
from ..models.fru import Fru
|
|
26
|
+
from ..models.logs import Log, LogEntry
|
|
27
|
+
from ..models.systems import (
|
|
28
|
+
Bios,
|
|
29
|
+
BootOption,
|
|
30
|
+
BootSetting,
|
|
31
|
+
Gpu,
|
|
32
|
+
GpuOEM,
|
|
33
|
+
Memory,
|
|
34
|
+
Processor,
|
|
35
|
+
Storage,
|
|
36
|
+
System,
|
|
37
|
+
SystemPatchSetting,
|
|
38
|
+
Volume,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from ..client import RedfishClient
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
# Boot source override constants
|
|
47
|
+
BOOT_OVERRIDE_ENABLED_ONCE = "Once"
|
|
48
|
+
BOOT_OVERRIDE_MODE_UEFI = "UEFI"
|
|
49
|
+
|
|
50
|
+
# PowerState values
|
|
51
|
+
_ON_STATES = {"On", "PoweringOn"}
|
|
52
|
+
_OFF_STATES = {"Off", "PoweringOff"}
|
|
53
|
+
|
|
54
|
+
# ResetType -> valid PowerState mapping
|
|
55
|
+
_RESET_ALLOWED = {
|
|
56
|
+
"On": _OFF_STATES, # Can only turn On when currently Off
|
|
57
|
+
"ForceOff": _ON_STATES, # Can only force off when currently On
|
|
58
|
+
"GracefulShutdown": _ON_STATES,
|
|
59
|
+
"GracefulRestart": _ON_STATES,
|
|
60
|
+
"ForceRestart": _ON_STATES,
|
|
61
|
+
"Nmi": _ON_STATES,
|
|
62
|
+
"ForceOn": _OFF_STATES,
|
|
63
|
+
"PushPowerButton": {"On", "Off", "PoweringOn", "PoweringOff"},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SystemsManager:
|
|
68
|
+
"""
|
|
69
|
+
Manages Redfish System resources.
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, client: RedfishClient):
|
|
75
|
+
self._client = client
|
|
76
|
+
self._http = client._http_client
|
|
77
|
+
|
|
78
|
+
def get(self, system_id: Optional[str] = None) -> System:
|
|
79
|
+
"""
|
|
80
|
+
Get a system resource.
|
|
81
|
+
|
|
82
|
+
If system_id is None and there is exactly one system, returns it automatically.
|
|
83
|
+
If system_id is None and there are multiple systems, raises an error.
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
system_id: System ID (e.g., "1"). If None, auto-selects the sole system.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
System resource
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
RedfishException: If system not found or ambiguous
|
|
95
|
+
"""
|
|
96
|
+
if system_id is None:
|
|
97
|
+
systems = self._client._get_systems_collection()
|
|
98
|
+
if not systems:
|
|
99
|
+
raise RedfishException(404, "No system found")
|
|
100
|
+
if len(systems) > 1:
|
|
101
|
+
raise RedfishValidationError(
|
|
102
|
+
f"Multiple systems found, please specify system_id. "
|
|
103
|
+
f"Available: {[m.id for m in systems]}"
|
|
104
|
+
)
|
|
105
|
+
return systems[0]
|
|
106
|
+
|
|
107
|
+
systems_odata_id = self._client._get_systems_collection_odata_id()
|
|
108
|
+
return self._http.get(
|
|
109
|
+
f"{systems_odata_id}/{system_id}", System
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def processors(self, system_id: Optional[str] = None) -> List[Processor]:
|
|
113
|
+
"""
|
|
114
|
+
Get the list of processors (CPUs) for a system.
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
system = self.get(system_id)
|
|
119
|
+
return self._client._get_collection(system.processors.odata_id, Processor)
|
|
120
|
+
|
|
121
|
+
def memory(self, system_id: Optional[str] = None) -> List[Memory]:
|
|
122
|
+
"""
|
|
123
|
+
Get the list of memory modules (DIMMs) for a system.
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
system = self.get(system_id)
|
|
128
|
+
return self._client._get_collection(system.odata_id + "/Memory", Memory)
|
|
129
|
+
|
|
130
|
+
def storages(self, system_id: Optional[str] = None) -> List[Storage]:
|
|
131
|
+
"""
|
|
132
|
+
Get the list of storage controllers for a system.
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
"""
|
|
136
|
+
system = self.get(system_id)
|
|
137
|
+
return self._client._get_collection(system.storage.odata_id, Storage)
|
|
138
|
+
|
|
139
|
+
def volumes(self, storage_id: str, system_id: Optional[str] = None) -> List[Volume]:
|
|
140
|
+
"""
|
|
141
|
+
Get the list of volumes for a given storage controller.
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
"""
|
|
145
|
+
system = self.get(system_id)
|
|
146
|
+
path = f"{system.storage.odata_id}/{storage_id}/Volumes"
|
|
147
|
+
return self._client._get_collection(path, Volume)
|
|
148
|
+
|
|
149
|
+
def bios(self, system_id: Optional[str] = None) -> Bios:
|
|
150
|
+
"""
|
|
151
|
+
Get BIOS information for a system.
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
"""
|
|
155
|
+
system = self.get(system_id)
|
|
156
|
+
return self._http.get(f"{system.odata_id}/Bios", Bios)
|
|
157
|
+
|
|
158
|
+
def log_services(self, system_id: Optional[str] = None) -> List[Log]:
|
|
159
|
+
"""
|
|
160
|
+
Get the list of log services for a system.
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
"""
|
|
164
|
+
from ._log_helpers import require_log_services_link
|
|
165
|
+
|
|
166
|
+
system = self.get(system_id)
|
|
167
|
+
odata_id = require_log_services_link(system, f"System {system.id!r}")
|
|
168
|
+
return self._client._get_collection(odata_id, Log)
|
|
169
|
+
|
|
170
|
+
def log_entries(
|
|
171
|
+
self,
|
|
172
|
+
log_id: Optional[str] = None,
|
|
173
|
+
system_id: Optional[str] = None,
|
|
174
|
+
) -> List[LogEntry]:
|
|
175
|
+
"""
|
|
176
|
+
Get log entries for a system log service.
|
|
177
|
+
|
|
178
|
+
``log_id`` is optional. When omitted and there is exactly one
|
|
179
|
+
log service on the system, it is auto-selected. Multiple services
|
|
180
|
+
raise :class:`RedfishValidationError` listing the available IDs.
|
|
181
|
+
|
|
182
|
+
The Entries URL is discovered from ``Log.entries.odata_id`` rather
|
|
183
|
+
than hard-coded as ``f"{log_services}/{log_id}/Entries"``.
|
|
184
|
+
"""
|
|
185
|
+
from ._log_helpers import (
|
|
186
|
+
fetch_log_entries,
|
|
187
|
+
require_log_services_link,
|
|
188
|
+
resolve_log_service,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
system = self.get(system_id)
|
|
192
|
+
odata_id = require_log_services_link(system, f"System {system.id!r}")
|
|
193
|
+
log = resolve_log_service(self._client, odata_id, log_id)
|
|
194
|
+
return fetch_log_entries(self._client, log)
|
|
195
|
+
|
|
196
|
+
# ------------------------------------------------------------------
|
|
197
|
+
# Log service single-resource access + ClearLog action
|
|
198
|
+
# ------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
def log_service(
|
|
201
|
+
self,
|
|
202
|
+
log_id: Optional[str] = None,
|
|
203
|
+
system_id: Optional[str] = None,
|
|
204
|
+
) -> Log:
|
|
205
|
+
"""
|
|
206
|
+
Get a single LogService resource (includes the Actions block).
|
|
207
|
+
|
|
208
|
+
Use this when you need ``Log.actions`` (e.g. to read the ClearLog
|
|
209
|
+
target); ``log_services()`` only returns collection members which
|
|
210
|
+
may omit Actions on some BMCs.
|
|
211
|
+
|
|
212
|
+
``log_id`` is optional; auto-selected when there is exactly
|
|
213
|
+
one log service. The single LogService URL is **discovered dynamically**
|
|
214
|
+
from the ``LogServices`` collection members rather than built by string
|
|
215
|
+
concatenation. Vendors that publish a non-standard child path
|
|
216
|
+
(e.g. ``.../LogServices/SystemEventLog`` instead of ``.../Sel``)
|
|
217
|
+
are supported correctly.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
log_id: Log service ID (e.g. "Sel", "Log1"). Optional.
|
|
221
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Fully populated :class:`Log` resource.
|
|
225
|
+
|
|
226
|
+
Raises:
|
|
227
|
+
RedfishException: 404 when the system has no LogServices.
|
|
228
|
+
RedfishValidationError: When ``log_id`` is None and multiple
|
|
229
|
+
services exist.
|
|
230
|
+
RedfishNotFoundError: When ``log_id`` is not in the collection.
|
|
231
|
+
"""
|
|
232
|
+
from ._log_helpers import require_log_services_link, resolve_log_service
|
|
233
|
+
|
|
234
|
+
system = self.get(system_id)
|
|
235
|
+
odata_id = require_log_services_link(system, f"System {system.id!r}")
|
|
236
|
+
return resolve_log_service(self._client, odata_id, log_id)
|
|
237
|
+
|
|
238
|
+
def clear_system_log(
|
|
239
|
+
self,
|
|
240
|
+
log_id: Optional[str] = None,
|
|
241
|
+
system_id: Optional[str] = None,
|
|
242
|
+
) -> None:
|
|
243
|
+
"""
|
|
244
|
+
Invoke the ``#LogService.ClearLog`` action on a system log service.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
log_id: Log service ID (e.g. "Sel", "Log1"). Optional
|
|
248
|
+
— auto-selected when there is exactly one log
|
|
249
|
+
service.
|
|
250
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
251
|
+
|
|
252
|
+
Raises:
|
|
253
|
+
RedfishValidationError: If the log service does not expose ClearLog.
|
|
254
|
+
"""
|
|
255
|
+
from ..models.common import RedfishResponse
|
|
256
|
+
|
|
257
|
+
log = self.log_service(log_id, system_id)
|
|
258
|
+
target = _extract_action_target(log.actions, "#LogService.ClearLog")
|
|
259
|
+
if not target:
|
|
260
|
+
raise RedfishValidationError(
|
|
261
|
+
f"Log service {log.id!r} does not expose #LogService.ClearLog action"
|
|
262
|
+
)
|
|
263
|
+
logger.info("POST ClearLog -> %s", target)
|
|
264
|
+
self._http.post(target, RedfishResponse, raw_body={})
|
|
265
|
+
|
|
266
|
+
# ------------------------------------------------------------------
|
|
267
|
+
# Drive helpers
|
|
268
|
+
# ------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
def drive_by_odata_id(self, odata_id: str) -> Drive:
|
|
271
|
+
"""
|
|
272
|
+
Fetch a single Drive resource directly by its ``@odata.id``.
|
|
273
|
+
|
|
274
|
+
Useful when a Storage controller exposes only Link references and
|
|
275
|
+
the caller needs the full Drive detail without enumerating chassis
|
|
276
|
+
drives.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
odata_id: Full ``@odata.id`` of the drive.
|
|
280
|
+
|
|
281
|
+
Raises:
|
|
282
|
+
RedfishNotFoundError: If the drive resource does not exist.
|
|
283
|
+
"""
|
|
284
|
+
return self._http.get(odata_id, Drive)
|
|
285
|
+
|
|
286
|
+
def drive_reset(self, drive_odata_id: str, reset_type: str) -> None:
|
|
287
|
+
"""
|
|
288
|
+
Invoke the ``#Drive.Reset`` action on a drive (e.g. NVMe power cycle).
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
drive_odata_id: Full ``@odata.id`` of the drive.
|
|
292
|
+
reset_type: Drive reset type, e.g. ``"GracefulShutdown"`` /
|
|
293
|
+
``"ForceOn"`` / ``"PowerCycle"``. Subject to the drive's
|
|
294
|
+
``ResetType@Redfish.AllowableValues``.
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
RedfishValidationError: If the drive does not expose #Drive.Reset
|
|
298
|
+
or ``reset_type`` is not in the drive's allowable values.
|
|
299
|
+
"""
|
|
300
|
+
from ..models.common import RedfishResponse
|
|
301
|
+
|
|
302
|
+
drive = self.drive_by_odata_id(drive_odata_id)
|
|
303
|
+
action = _extract_action(drive.actions, "#Drive.Reset")
|
|
304
|
+
if not action or not action.get("target"):
|
|
305
|
+
raise RedfishValidationError(
|
|
306
|
+
f"Drive {drive_odata_id} does not expose #Drive.Reset action"
|
|
307
|
+
)
|
|
308
|
+
target = action["target"]
|
|
309
|
+
allowable = action.get("ResetType@Redfish.AllowableValues")
|
|
310
|
+
if allowable and reset_type not in allowable:
|
|
311
|
+
raise RedfishValidationError(
|
|
312
|
+
f"Drive reset type '{reset_type}' not in allowable values {allowable}"
|
|
313
|
+
)
|
|
314
|
+
logger.info("POST Drive.Reset (%s) -> %s", reset_type, target)
|
|
315
|
+
self._http.post(target, RedfishResponse, raw_body={"ResetType": reset_type})
|
|
316
|
+
|
|
317
|
+
# ------------------------------------------------------------------
|
|
318
|
+
# BootOptions collection
|
|
319
|
+
# ------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
def boot_options(self, system_id: Optional[str] = None) -> List[BootOption]:
|
|
322
|
+
"""
|
|
323
|
+
Get the BootOptions collection (modern boot model).
|
|
324
|
+
|
|
325
|
+
Returns an empty list when the System does not expose a BootOptions
|
|
326
|
+
link (vendor still uses the legacy ``BootSourceOverrideTarget`` model).
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
330
|
+
"""
|
|
331
|
+
system = self.get(system_id)
|
|
332
|
+
if not system.boot or not system.boot.boot_options:
|
|
333
|
+
return []
|
|
334
|
+
return self._client._get_collection(
|
|
335
|
+
system.boot.boot_options.odata_id, BootOption
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def boot_option(self, option_id: str, system_id: Optional[str] = None) -> BootOption:
|
|
339
|
+
"""
|
|
340
|
+
Get a single BootOption resource.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
option_id: BootOption ID (the trailing segment of its odata_id).
|
|
344
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
345
|
+
|
|
346
|
+
Raises:
|
|
347
|
+
RedfishValidationError: If the System has no BootOptions collection.
|
|
348
|
+
RedfishNotFoundError: If the option does not exist.
|
|
349
|
+
"""
|
|
350
|
+
system = self.get(system_id)
|
|
351
|
+
if not system.boot or not system.boot.boot_options:
|
|
352
|
+
raise RedfishValidationError(
|
|
353
|
+
"System does not expose a BootOptions collection"
|
|
354
|
+
)
|
|
355
|
+
path = f"{system.boot.boot_options.odata_id}/{option_id}"
|
|
356
|
+
return self._http.get(path, BootOption)
|
|
357
|
+
|
|
358
|
+
def set_boot_option_enabled(
|
|
359
|
+
self,
|
|
360
|
+
option_id: str,
|
|
361
|
+
enabled: bool,
|
|
362
|
+
system_id: Optional[str] = None,
|
|
363
|
+
) -> BootOption:
|
|
364
|
+
"""
|
|
365
|
+
Toggle a single BootOption's ``BootOptionEnabled`` flag via PATCH.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
option_id: BootOption ID.
|
|
369
|
+
enabled: New value for BootOptionEnabled.
|
|
370
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
The BootOption re-read after the patch.
|
|
374
|
+
"""
|
|
375
|
+
# Refresh ETag via GET before PATCH.
|
|
376
|
+
option = self.boot_option(option_id, system_id)
|
|
377
|
+
logger.info(
|
|
378
|
+
"PATCH BootOptionEnabled=%s on %s", enabled, option.odata_id
|
|
379
|
+
)
|
|
380
|
+
self._http.patch_raw(
|
|
381
|
+
option.odata_id, {"BootOptionEnabled": enabled}
|
|
382
|
+
)
|
|
383
|
+
return self._http.get(option.odata_id, BootOption)
|
|
384
|
+
|
|
385
|
+
def fru_info(self, system_id: Optional[str] = None) -> Optional[Fru]:
|
|
386
|
+
"""
|
|
387
|
+
Get FRU (Field Replaceable Unit) information for a system.
|
|
388
|
+
Returns None if not available.
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
"""
|
|
392
|
+
system = self.get(system_id)
|
|
393
|
+
if system.oem is None or system.oem.bmc is None or system.oem.bmc.fru is None:
|
|
394
|
+
return None
|
|
395
|
+
fru_link = system.oem.bmc.fru
|
|
396
|
+
return self._http.get(fru_link.odata_id, Fru)
|
|
397
|
+
|
|
398
|
+
def gpus(self, system_id: Optional[str] = None) -> List[Gpu]:
|
|
399
|
+
"""
|
|
400
|
+
Get GPU information for a system.
|
|
401
|
+
|
|
402
|
+
This method uses a multi-step fallback strategy to handle different vendor
|
|
403
|
+
implementations:
|
|
404
|
+
|
|
405
|
+
1. Try /redfish/v1/Systems/{id}/GraphicsControllers (standard path)
|
|
406
|
+
2. If empty, try /redfish/v1/Chassis/1/PCIeDevices and filter by name containing "GPU"
|
|
407
|
+
3. If Chassis PCIeDevices is empty, try System.Links.PCIeDevices
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
"""
|
|
411
|
+
system = self.get(system_id or "1")
|
|
412
|
+
gpu_members: List[Gpu] = []
|
|
413
|
+
|
|
414
|
+
# Step 1: Try GraphicsControllers (standard Redfish path)
|
|
415
|
+
if system.graphics_controllers is not None:
|
|
416
|
+
try:
|
|
417
|
+
gpu_members = self._client._get_collection(
|
|
418
|
+
f"{system.odata_id}/GraphicsControllers", Gpu
|
|
419
|
+
)
|
|
420
|
+
except RedfishException as exc:
|
|
421
|
+
logger.warning("GraphicsControllers fetch failed: %s", exc)
|
|
422
|
+
|
|
423
|
+
if gpu_members:
|
|
424
|
+
return gpu_members
|
|
425
|
+
|
|
426
|
+
# Step 2: Try Chassis PCIeDevices (华为 xFusion, H3C etc.)
|
|
427
|
+
from .chassis import ChassisManager
|
|
428
|
+
chassis_mgr = ChassisManager(self._client)
|
|
429
|
+
|
|
430
|
+
pcie_devices: List[PCIeDevice] = []
|
|
431
|
+
try:
|
|
432
|
+
chassis = chassis_mgr.get("1")
|
|
433
|
+
if chassis.pcie_devices is not None:
|
|
434
|
+
pcie_devices = self._client._get_collection(
|
|
435
|
+
chassis.pcie_devices.odata_id, PCIeDevice
|
|
436
|
+
)
|
|
437
|
+
except RedfishException as exc:
|
|
438
|
+
logger.warning("Chassis PCIeDevices fetch failed: %s", exc)
|
|
439
|
+
|
|
440
|
+
# Step 3: Fall back to System.Links.PCIeDevices
|
|
441
|
+
if not pcie_devices and system.links and system.links.pcie_devices:
|
|
442
|
+
for link in system.links.pcie_devices:
|
|
443
|
+
try:
|
|
444
|
+
device = self._http.get(link.odata_id, PCIeDevice)
|
|
445
|
+
if device:
|
|
446
|
+
pcie_devices.append(device)
|
|
447
|
+
except RedfishException as exc:
|
|
448
|
+
logger.warning("PCIeDevice fetch failed for %s: %s", link.odata_id, exc)
|
|
449
|
+
|
|
450
|
+
# Filter GPU devices by name and convert to Gpu model
|
|
451
|
+
for device in pcie_devices:
|
|
452
|
+
if device.name and "GPU" in device.name:
|
|
453
|
+
gpu = Gpu.model_construct(
|
|
454
|
+
odata_id=device.odata_id,
|
|
455
|
+
name=device.name,
|
|
456
|
+
manufacturer=device.manufacturer,
|
|
457
|
+
model=device.model,
|
|
458
|
+
power_watts="0",
|
|
459
|
+
version=device.part_number,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
# Extract power watts from OEM if available
|
|
463
|
+
if (device.oem and device.oem.gpu_oem_public and
|
|
464
|
+
device.oem.gpu_oem_public.power_watts > 0):
|
|
465
|
+
gpu.power_watts = str(device.oem.gpu_oem_public.power_watts)
|
|
466
|
+
|
|
467
|
+
# Prefer card_model over part_number for version
|
|
468
|
+
if device.card_model:
|
|
469
|
+
gpu.version = device.card_model
|
|
470
|
+
|
|
471
|
+
# Set serial number from device
|
|
472
|
+
if device.serial_number:
|
|
473
|
+
gpu.oem = GpuOEM.model_construct(serial_number=device.serial_number)
|
|
474
|
+
|
|
475
|
+
gpu_members.append(gpu)
|
|
476
|
+
|
|
477
|
+
return gpu_members
|
|
478
|
+
|
|
479
|
+
def pcie_device(self, odata_id: str) -> PCIeDevice:
|
|
480
|
+
"""
|
|
481
|
+
Get a specific PCIe device by its @odata.id.
|
|
482
|
+
|
|
483
|
+
"""
|
|
484
|
+
return self._http.get(odata_id, PCIeDevice)
|
|
485
|
+
|
|
486
|
+
def change_boot_source(
|
|
487
|
+
self,
|
|
488
|
+
target: str,
|
|
489
|
+
system_id: Optional[str] = None,
|
|
490
|
+
mode: str = BOOT_OVERRIDE_MODE_UEFI,
|
|
491
|
+
enabled: str = BOOT_OVERRIDE_ENABLED_ONCE,
|
|
492
|
+
) -> SystemPatchSetting:
|
|
493
|
+
"""
|
|
494
|
+
Change the boot source override target.
|
|
495
|
+
|
|
496
|
+
Validates the target against the system's allowable values,
|
|
497
|
+
then sends a PATCH request with the new boot settings.
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
target: Boot target (e.g., "Pxe", "Hdd", "Cd", "BiosSetup")
|
|
503
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
504
|
+
mode: Boot source override mode ("UEFI" or "Legacy")
|
|
505
|
+
enabled: Override enable mode ("Once", "Continuous", "Disabled")
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
Updated SystemPatchSetting
|
|
509
|
+
|
|
510
|
+
Raises:
|
|
511
|
+
RedfishValidationError: If target is not in allowable values
|
|
512
|
+
"""
|
|
513
|
+
system = self.get(system_id)
|
|
514
|
+
boot = system.boot
|
|
515
|
+
|
|
516
|
+
# Validate target against allowable values if provided by server
|
|
517
|
+
if boot and boot.allowable_values and target not in boot.allowable_values:
|
|
518
|
+
raise RedfishValidationError(
|
|
519
|
+
f"Boot target '{target}' is not supported. "
|
|
520
|
+
f"Allowable values: {boot.allowable_values}"
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
# If already set to the desired target, return current settings (no-op)
|
|
524
|
+
if boot and boot.boot_source_override_target == target:
|
|
525
|
+
logger.info("Boot source override target is already '%s', no change needed", target)
|
|
526
|
+
req = SystemPatchSetting.model_construct(
|
|
527
|
+
boot=BootSetting.model_construct(
|
|
528
|
+
boot_source_override_enabled=boot.boot_source_override_enabled,
|
|
529
|
+
boot_source_override_mode=boot.boot_source_override_mode,
|
|
530
|
+
boot_source_override_target=boot.boot_source_override_target,
|
|
531
|
+
)
|
|
532
|
+
)
|
|
533
|
+
return req
|
|
534
|
+
|
|
535
|
+
# Build PATCH request
|
|
536
|
+
req = SystemPatchSetting.model_construct(
|
|
537
|
+
boot=BootSetting.model_construct(
|
|
538
|
+
boot_source_override_enabled=enabled,
|
|
539
|
+
boot_source_override_mode=mode,
|
|
540
|
+
boot_source_override_target=target,
|
|
541
|
+
)
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
extra_headers = {}
|
|
545
|
+
if system.odata_etag:
|
|
546
|
+
extra_headers["If-Match"] = system.odata_etag
|
|
547
|
+
|
|
548
|
+
return self._http.patch(
|
|
549
|
+
system.odata_id, SystemPatchSetting, req, extra_headers
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
def reset(
|
|
553
|
+
self,
|
|
554
|
+
reset_type: str,
|
|
555
|
+
system_id: Optional[str] = None,
|
|
556
|
+
skip_power_state_check: bool = False,
|
|
557
|
+
):
|
|
558
|
+
"""
|
|
559
|
+
Perform a system reset (power on/off/restart etc.).
|
|
560
|
+
|
|
561
|
+
Validates that the requested reset type is compatible with the current
|
|
562
|
+
power state before sending the request.
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
reset_type: Reset type string (e.g., "GracefulRestart", "ForceOff", "On")
|
|
568
|
+
system_id: System ID. Auto-selected if only one system exists.
|
|
569
|
+
skip_power_state_check: Skip power state compatibility check (some vendors
|
|
570
|
+
don't properly report power state)
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
RedfishResponse
|
|
574
|
+
|
|
575
|
+
Raises:
|
|
576
|
+
RedfishValidationError: If reset_type is incompatible with current power state
|
|
577
|
+
"""
|
|
578
|
+
from ..models.common import RedfishResponse
|
|
579
|
+
|
|
580
|
+
system = self.get(system_id)
|
|
581
|
+
|
|
582
|
+
# Validate reset type against current power state
|
|
583
|
+
if not skip_power_state_check and system.power_state:
|
|
584
|
+
allowed_states = _RESET_ALLOWED.get(reset_type)
|
|
585
|
+
if allowed_states and system.power_state not in allowed_states:
|
|
586
|
+
raise RedfishValidationError(
|
|
587
|
+
f"Reset type '{reset_type}' is not compatible with current "
|
|
588
|
+
f"power state '{system.power_state}'. "
|
|
589
|
+
f"Allowed power states: {allowed_states}"
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
# Get the reset target URL from actions
|
|
593
|
+
if not system.actions or not system.actions.computer_system_reset:
|
|
594
|
+
raise RedfishException(400, "System reset action not found in system resource")
|
|
595
|
+
|
|
596
|
+
reset_target = system.actions.computer_system_reset.target
|
|
597
|
+
if not reset_target:
|
|
598
|
+
raise RedfishException(400, "Reset action target URL is empty")
|
|
599
|
+
|
|
600
|
+
logger.info("Resetting system %s with type=%s via %s",
|
|
601
|
+
system.id, reset_type, reset_target)
|
|
602
|
+
|
|
603
|
+
return self._http.post(
|
|
604
|
+
reset_target,
|
|
605
|
+
RedfishResponse,
|
|
606
|
+
raw_body={"ResetType": reset_type},
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
# ---------------------------------------------------------------------------
|
|
611
|
+
# Internal helpers — Actions block parsing
|
|
612
|
+
# ---------------------------------------------------------------------------
|
|
613
|
+
|
|
614
|
+
def _extract_action(actions: Optional[dict], action_name: str) -> Optional[dict]:
|
|
615
|
+
"""Return the Action sub-dict for ``action_name`` or None."""
|
|
616
|
+
if not actions or not isinstance(actions, dict):
|
|
617
|
+
return None
|
|
618
|
+
action = actions.get(action_name)
|
|
619
|
+
if not isinstance(action, dict):
|
|
620
|
+
return None
|
|
621
|
+
return action
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _extract_action_target(actions: Optional[dict], action_name: str) -> Optional[str]:
|
|
625
|
+
"""Return the Action target URL for ``action_name`` or None."""
|
|
626
|
+
action = _extract_action(actions, action_name)
|
|
627
|
+
if not action:
|
|
628
|
+
return None
|
|
629
|
+
target = action.get("target")
|
|
630
|
+
return target if isinstance(target, str) and target else None
|