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.
Files changed (56) hide show
  1. redfish_python_sdk-1.0.0.dist-info/METADATA +164 -0
  2. redfish_python_sdk-1.0.0.dist-info/RECORD +56 -0
  3. redfish_python_sdk-1.0.0.dist-info/WHEEL +5 -0
  4. redfish_python_sdk-1.0.0.dist-info/licenses/LICENSE +29 -0
  5. redfish_python_sdk-1.0.0.dist-info/top_level.txt +1 -0
  6. redfish_sdk/__init__.py +63 -0
  7. redfish_sdk/client.py +1894 -0
  8. redfish_sdk/exceptions.py +56 -0
  9. redfish_sdk/http_client.py +452 -0
  10. redfish_sdk/managers/__init__.py +21 -0
  11. redfish_sdk/managers/_log_helpers.py +144 -0
  12. redfish_sdk/managers/account.py +96 -0
  13. redfish_sdk/managers/chassis.py +327 -0
  14. redfish_sdk/managers/event.py +264 -0
  15. redfish_sdk/managers/managers.py +120 -0
  16. redfish_sdk/managers/registries.py +48 -0
  17. redfish_sdk/managers/session.py +130 -0
  18. redfish_sdk/managers/systems.py +630 -0
  19. redfish_sdk/managers/task.py +89 -0
  20. redfish_sdk/managers/update.py +99 -0
  21. redfish_sdk/managers/update_strategies/__init__.py +51 -0
  22. redfish_sdk/managers/update_strategies/base.py +101 -0
  23. redfish_sdk/managers/update_strategies/h3c.py +140 -0
  24. redfish_sdk/managers/update_strategies/inspur.py +89 -0
  25. redfish_sdk/managers/update_strategies/lenovo.py +59 -0
  26. redfish_sdk/managers/update_strategies/nettrix.py +56 -0
  27. redfish_sdk/managers/update_strategies/registry.py +72 -0
  28. redfish_sdk/managers/update_strategies/vendor_detect.py +111 -0
  29. redfish_sdk/managers/update_strategies/xfusion.py +60 -0
  30. redfish_sdk/managers/update_strategies/zte.py +68 -0
  31. redfish_sdk/models/__init__.py +55 -0
  32. redfish_sdk/models/account.py +60 -0
  33. redfish_sdk/models/chassis.py +86 -0
  34. redfish_sdk/models/check.py +231 -0
  35. redfish_sdk/models/common.py +102 -0
  36. redfish_sdk/models/drive.py +53 -0
  37. redfish_sdk/models/event.py +64 -0
  38. redfish_sdk/models/fru.py +59 -0
  39. redfish_sdk/models/gpu.py +33 -0
  40. redfish_sdk/models/logs.py +56 -0
  41. redfish_sdk/models/managers.py +153 -0
  42. redfish_sdk/models/memory.py +50 -0
  43. redfish_sdk/models/network_adapter.py +76 -0
  44. redfish_sdk/models/oem.py +173 -0
  45. redfish_sdk/models/pcie_device.py +89 -0
  46. redfish_sdk/models/power.py +92 -0
  47. redfish_sdk/models/processor.py +50 -0
  48. redfish_sdk/models/registry.py +34 -0
  49. redfish_sdk/models/resource_key.py +70 -0
  50. redfish_sdk/models/root.py +50 -0
  51. redfish_sdk/models/session.py +42 -0
  52. redfish_sdk/models/storage.py +77 -0
  53. redfish_sdk/models/systems.py +194 -0
  54. redfish_sdk/models/task.py +54 -0
  55. redfish_sdk/models/thermal.py +111 -0
  56. 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