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,96 @@
1
+ """
2
+ Account service manager — manages user accounts and roles.
3
+
4
+ Provides:
5
+ - List accounts
6
+ - List roles
7
+ - Add account
8
+ - Update account
9
+ - Delete account
10
+
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from typing import TYPE_CHECKING, List
16
+
17
+ from ..models.account import Account, Role
18
+
19
+ if TYPE_CHECKING:
20
+ from ..client import RedfishClient
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class AccountServiceManager:
26
+ """
27
+ Manages Redfish Account resources.
28
+
29
+ """
30
+
31
+ def __init__(self, client: RedfishClient):
32
+ self._client = client
33
+ self._http = client._http_client
34
+
35
+ def accounts(self) -> List[Account]:
36
+ """
37
+ Get the list of user accounts.
38
+
39
+ """
40
+ account_service = self._client._get_account_service()
41
+ return self._client._get_collection(account_service.accounts.odata_id, Account)
42
+
43
+ def roles(self) -> List[Role]:
44
+ """
45
+ Get the list of user roles.
46
+
47
+ """
48
+ account_service = self._client._get_account_service()
49
+ return self._client._get_collection(account_service.roles.odata_id, Role)
50
+
51
+ def add(self, account: Account) -> Account:
52
+ """
53
+ Create a new user account.
54
+
55
+
56
+ Args:
57
+ account: Account model with UserName, Password, RoleId, Enabled fields
58
+
59
+ Returns:
60
+ Created account resource
61
+ """
62
+ account_service = self._client._get_account_service()
63
+ return self._http.post(
64
+ account_service.accounts.odata_id, Account, body=account
65
+ )
66
+
67
+ def update(self, username: str, account: Account) -> Account:
68
+ """
69
+ Update an existing user account.
70
+
71
+
72
+ Args:
73
+ username: Username of the account to update
74
+ account: Account model with fields to update
75
+
76
+ Returns:
77
+ Updated account resource
78
+ """
79
+ account_service = self._client._get_account_service()
80
+ path = f"{account_service.accounts.odata_id}/{username}"
81
+ return self._http.patch(path, Account, account)
82
+
83
+ def delete(self, username: str) -> str:
84
+ """
85
+ Delete a user account.
86
+
87
+
88
+ Args:
89
+ username: Username of the account to delete
90
+
91
+ Returns:
92
+ Response body (usually empty)
93
+ """
94
+ account_service = self._client._get_account_service()
95
+ path = f"{account_service.accounts.odata_id}/{username}"
96
+ return self._http.delete(path)
@@ -0,0 +1,327 @@
1
+ """
2
+ Chassis manager — manages physical server enclosure resources.
3
+
4
+ Provides access to:
5
+ - Chassis info (manufacturer, model, serial number, power state)
6
+ - Thermal data (fan speeds, temperatures)
7
+ - Power data (PSUs, voltage, power control)
8
+ - Drives (HDDs/SSDs/NVMe with multi-vendor fallback)
9
+ - Network adapters (NICs)
10
+ - PCIe devices (GPU, HBA, etc.)
11
+ - FRU service data
12
+
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ from typing import TYPE_CHECKING, List, Optional
18
+
19
+ from ..exceptions import (
20
+ RedfishException,
21
+ RedfishNotFoundError,
22
+ RedfishValidationError,
23
+ )
24
+ from ..models.chassis import (
25
+ Chassis,
26
+ Drive,
27
+ NetworkAdapter,
28
+ PCIeDevice,
29
+ Power,
30
+ Thermal,
31
+ )
32
+ from ..models.thermal import InletHistoryTemperature
33
+
34
+ if TYPE_CHECKING:
35
+ from ..client import RedfishClient
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+ # Allowed IndicatorLED states per Redfish spec.
40
+ _INDICATOR_LED_STATES = ("Lit", "Blinking", "Off")
41
+
42
+
43
+ class ChassisManager:
44
+ """
45
+ Manages Redfish Chassis resources.
46
+
47
+
48
+ """
49
+
50
+ def __init__(self, client: RedfishClient):
51
+ self._client = client
52
+ self._http = client._http_client
53
+
54
+ def get(self, chassis_id: str = "1") -> Chassis:
55
+ """
56
+ Get a chassis resource by ID.
57
+
58
+ Special handling: if the chassis collection URL already ends with "/{id}",
59
+ use it directly to avoid double-appending (some vendors return the direct path).
60
+
61
+
62
+
63
+ Args:
64
+ chassis_id: Chassis ID (default "1")
65
+
66
+ Returns:
67
+ Chassis resource
68
+ """
69
+ odata_id = self._client._get_chassis_collection_odata_id()
70
+
71
+ # If the collection odata_id already points to a specific chassis (ends with /1)
72
+ # use it directly; otherwise append the chassis_id
73
+ if odata_id.endswith(f"/{chassis_id}"):
74
+ return self._http.get(odata_id, Chassis)
75
+
76
+ return self._http.get(f"{odata_id}/{chassis_id}", Chassis)
77
+
78
+ def thermal(self, chassis_id: str = "1") -> Thermal:
79
+ """
80
+ Get thermal information (fans and temperatures) for a chassis.
81
+
82
+
83
+ """
84
+ chassis = self.get(chassis_id)
85
+ return self._http.get(chassis.thermal.odata_id, Thermal)
86
+
87
+ def inlet_history_temperature(self, chassis_id: str = "1") -> Optional[InletHistoryTemperature]:
88
+ """
89
+ Get air inlet historical temperature samples for a chassis.
90
+
91
+ Discovery flow:
92
+ 1. Fetch the Thermal resource via ``thermal(chassis_id)``.
93
+ 2. Read ``Thermal.inlet_history_temperature.odata_id``.
94
+ 3. GET that URL and parse as :class:`InletHistoryTemperature`.
95
+
96
+ Returns ``None`` when the sub-resource is not advertised by the BMC
97
+ or returns 404 (vendor does not implement it). Other transport errors
98
+ (auth failure, network issues, etc.) propagate to the caller.
99
+
100
+ Args:
101
+ chassis_id: Chassis ID (default "1")
102
+
103
+ Returns:
104
+ InletHistoryTemperature model, or None when not supported.
105
+ """
106
+ thermal = self.thermal(chassis_id)
107
+ if thermal.inlet_history_temperature is None:
108
+ logger.debug("Thermal resource has no InletHistoryTemperature link")
109
+ return None
110
+
111
+ odata_id = thermal.inlet_history_temperature.odata_id
112
+ try:
113
+ return self._http.get(odata_id, InletHistoryTemperature)
114
+ except RedfishNotFoundError as exc:
115
+ logger.debug("InletHistoryTemperature not found at %s: %s", odata_id, exc)
116
+ return None
117
+
118
+ def power(self, chassis_id: str = "1") -> Power:
119
+ """
120
+ Get power information (PSUs and power controls) for a chassis.
121
+
122
+
123
+ """
124
+ chassis = self.get(chassis_id)
125
+ return self._http.get(chassis.power.odata_id, Power)
126
+
127
+ def drives(self, chassis_id: str = "1") -> List[Drive]:
128
+ """
129
+ Get the list of physical drives in a chassis.
130
+
131
+ Multi-vendor fallback strategy:
132
+ 1. Try Chassis.Links.Drives[0] as a collection endpoint
133
+ 2. If that returns empty/no members, fetch each link individually
134
+ 3. Fall back to Chassis.Drives if Links.Drives is not set
135
+
136
+
137
+ multi-vendor fallback logic.
138
+
139
+ Returns:
140
+ List of Drive objects
141
+ """
142
+ chassis = self.get(chassis_id)
143
+
144
+ # Strategy 1: Use Chassis.Links.Drives
145
+ if chassis.links and chassis.links.drives:
146
+ drive_links = chassis.links.drives
147
+ if drive_links:
148
+ # Try first link as a collection endpoint
149
+ first_link = drive_links[0]
150
+ try:
151
+ drives = self._client._get_collection(first_link.odata_id, Drive)
152
+ if drives:
153
+ return drives
154
+ except RedfishException:
155
+ pass
156
+
157
+ # If collection is empty, fetch each link individually
158
+ result = []
159
+ for link in drive_links:
160
+ try:
161
+ drive = self._http.get(link.odata_id, Drive)
162
+ if drive:
163
+ result.append(drive)
164
+ except RedfishException as exc:
165
+ logger.warning("Failed to fetch drive %s: %s", link.odata_id, exc)
166
+
167
+ if result:
168
+ return result
169
+
170
+ # Strategy 2: Use Chassis.Drives collection endpoint
171
+ if chassis.drives:
172
+ try:
173
+ return self._client._get_collection(chassis.drives.odata_id, Drive)
174
+ except RedfishException as exc:
175
+ logger.warning("Chassis.Drives collection failed: %s", exc)
176
+
177
+ return []
178
+
179
+ def network_adapters(self, chassis_id: str = "1") -> List[NetworkAdapter]:
180
+ """
181
+ Get the list of network adapters (NICs) in a chassis.
182
+
183
+
184
+ """
185
+ chassis = self.get(chassis_id)
186
+ return self._client._get_collection(chassis.network_adapters.odata_id, NetworkAdapter)
187
+
188
+ def pcie_devices(self, chassis_id: str = "1") -> List[PCIeDevice]:
189
+ """
190
+ Get the list of PCIe devices in a chassis.
191
+
192
+ Multi-vendor fallback strategy:
193
+ 1. Try Chassis.PCIeDevices collection endpoint
194
+ 2. Fall back to Chassis.Links.PCIeDevices (individual links)
195
+
196
+
197
+
198
+ Returns:
199
+ List of PCIeDevice objects
200
+ """
201
+ chassis = self.get(chassis_id)
202
+
203
+ # Strategy 1: Use Chassis.PCIeDevices collection endpoint
204
+ if chassis.pcie_devices:
205
+ try:
206
+ return self._client._get_collection(chassis.pcie_devices.odata_id, PCIeDevice)
207
+ except RedfishException as exc:
208
+ logger.warning("Chassis.PCIeDevices collection failed: %s", exc)
209
+
210
+ # Strategy 2: Use Chassis.Links.PCIeDevices individual links
211
+ if chassis.links and chassis.links.pcie_devices:
212
+ return self._client._get_list(chassis.links.pcie_devices, PCIeDevice)
213
+
214
+ return []
215
+
216
+ def fru_service(self, chassis_id: str = "1") -> List[dict]:
217
+ """
218
+ Get FRU service data from the chassis OEM extension.
219
+
220
+ This is a vendor-specific (华为 iBMC) feature that provides
221
+ detailed FRU board info via /redfish/v1/Chassis/{id}/FruService.
222
+
223
+
224
+
225
+ Returns:
226
+ List of raw FRU service data dicts
227
+ """
228
+ chassis = self.get(chassis_id)
229
+ if chassis.oem is None or chassis.oem.fru_service is None:
230
+ return []
231
+
232
+ fru_service_url = chassis.oem.fru_service
233
+ try:
234
+ collection_raw = self._http.get_raw(fru_service_url)
235
+ members = collection_raw.get("Members", [])
236
+ results = []
237
+ for member in members:
238
+ member_id = member.get("@odata.id")
239
+ if member_id:
240
+ try:
241
+ fru_data = self._http.get_raw(member_id)
242
+ results.append(fru_data)
243
+ except RedfishException as exc:
244
+ logger.warning("Failed to fetch FRU service %s: %s", member_id, exc)
245
+ return results
246
+ except RedfishException as exc:
247
+ logger.warning("FRU service collection failed: %s", exc)
248
+ return []
249
+
250
+ def fru_service_board(self, chassis_id: str = "1") -> Optional[dict]:
251
+ """
252
+ Get the primary FRU board info (the '/0' member of the FRU service collection).
253
+
254
+
255
+ """
256
+ chassis = self.get(chassis_id)
257
+ if chassis.oem is None or chassis.oem.fru_service is None:
258
+ return None
259
+
260
+ fru_service_url = chassis.oem.fru_service
261
+ try:
262
+ collection_raw = self._http.get_raw(fru_service_url)
263
+ members = collection_raw.get("Members", [])
264
+ for member in members:
265
+ member_id = member.get("@odata.id", "")
266
+ if member_id.endswith("/0"):
267
+ return self._http.get_raw(member_id)
268
+ except RedfishException as exc:
269
+ logger.warning("FRU service board fetch failed: %s", exc)
270
+
271
+ return None
272
+
273
+ # ------------------------------------------------------------------
274
+ # IndicatorLED write helpers
275
+ # ------------------------------------------------------------------
276
+
277
+ def set_indicator_led(self, state: str, chassis_id: str = "1") -> str:
278
+ """
279
+ Set the chassis IndicatorLED state via PATCH.
280
+
281
+ Reads the chassis first so the HTTP layer has a fresh ETag for If-Match.
282
+
283
+ Args:
284
+ state: One of "Lit", "Blinking", "Off".
285
+ chassis_id: Chassis ID (default "1").
286
+
287
+ Returns:
288
+ The IndicatorLED value after the patch (re-read from BMC).
289
+
290
+ Raises:
291
+ RedfishValidationError: If ``state`` is not an allowed value.
292
+ """
293
+ _validate_indicator_led_state(state)
294
+ chassis = self.get(chassis_id)
295
+ logger.info("PATCH IndicatorLED=%s on %s", state, chassis.odata_id)
296
+ self._http.patch_raw(chassis.odata_id, {"IndicatorLED": state})
297
+ return self.get(chassis_id).indicator_led
298
+
299
+ def set_drive_indicator_led(self, drive_odata_id: str, state: str) -> str:
300
+ """
301
+ Set a Drive IndicatorLED state via PATCH on the drive resource.
302
+
303
+ Args:
304
+ drive_odata_id: Full ``@odata.id`` of the drive resource.
305
+ state: One of "Lit", "Blinking", "Off".
306
+
307
+ Returns:
308
+ The IndicatorLED value after the patch (re-read from BMC).
309
+
310
+ Raises:
311
+ RedfishValidationError: If ``state`` is not an allowed value.
312
+ """
313
+ _validate_indicator_led_state(state)
314
+ # Refresh ETag before PATCH.
315
+ drive_before = self._http.get(drive_odata_id, Drive)
316
+ logger.info("PATCH IndicatorLED=%s on %s", state, drive_before.odata_id)
317
+ self._http.patch_raw(drive_odata_id, {"IndicatorLED": state})
318
+ drive_after = self._http.get(drive_odata_id, Drive)
319
+ return drive_after.indicator_led
320
+
321
+
322
+ def _validate_indicator_led_state(state: str) -> None:
323
+ if state not in _INDICATOR_LED_STATES:
324
+ raise RedfishValidationError(
325
+ f"Invalid IndicatorLED state '{state}'. "
326
+ f"Allowed values: {list(_INDICATOR_LED_STATES)}"
327
+ )
@@ -0,0 +1,264 @@
1
+ """
2
+ Event service manager — manages event subscriptions.
3
+
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
9
+
10
+ from ..exceptions import RedfishValidationError
11
+ from ..models.event import EventService, Subscription
12
+
13
+ if TYPE_CHECKING:
14
+ from ..client import RedfishClient
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class EventServiceManager:
20
+ """
21
+ Manages Redfish Event subscriptions.
22
+
23
+ """
24
+
25
+ def __init__(self, client: RedfishClient):
26
+ self._client = client
27
+ self._http = client._http_client
28
+
29
+ # ------------------------------------------------------------------
30
+ # internal helpers
31
+ # ------------------------------------------------------------------
32
+
33
+ def _resolve_subscription_path(self, id_or_uri: str) -> str:
34
+ """
35
+ Accept either a bare subscription Id (e.g. ``"1"``) or a full
36
+ ``@odata.id`` path (e.g. ``"/redfish/v1/EventService/Subscriptions/1"``)
37
+ and return the absolute Redfish path to GET/DELETE against.
38
+
39
+ The dual-form is needed because some callers (e.g. those iterating
40
+ over the ``Members`` block of the Subscriptions collection) already
41
+ hold the absolute path.
42
+ """
43
+ if not isinstance(id_or_uri, str) or not id_or_uri:
44
+ raise RedfishValidationError(
45
+ "subscription id_or_uri must be a non-empty string"
46
+ )
47
+ if id_or_uri.startswith("/redfish/"):
48
+ return id_or_uri
49
+ event_service = self._client._get_event_service()
50
+ return f"{event_service.subscriptions.odata_id}/{id_or_uri}"
51
+
52
+ # ------------------------------------------------------------------
53
+ # EventService resource
54
+ # ------------------------------------------------------------------
55
+
56
+ def service(self) -> EventService:
57
+ """
58
+ Get the full :class:`EventService` resource (incl. the Actions block).
59
+
60
+ Differs from :meth:`subscriptions` which only returns the list of
61
+ active subscriptions. Use this when you need the SubmitTestEvent
62
+ action target or the EventTypesForSubscription allowable values.
63
+ """
64
+ return self._client._get_event_service()
65
+
66
+ # ------------------------------------------------------------------
67
+ # Subscription CRUD
68
+ # ------------------------------------------------------------------
69
+
70
+ def subscriptions(self) -> List[Subscription]:
71
+ """
72
+ Get the list of event subscriptions (collection-expanded).
73
+
74
+ Internally lists the ``Subscriptions`` collection and fetches each
75
+ member by its ``@odata.id``; members that fail to fetch are skipped
76
+ with a warning.
77
+ """
78
+ event_service = self._client._get_event_service()
79
+ return self._client._get_collection(
80
+ event_service.subscriptions.odata_id, Subscription
81
+ )
82
+
83
+ def get_subscription(self, id_or_uri: str) -> Subscription:
84
+ """
85
+ Get a single subscription by its Id or full ``@odata.id``.
86
+
87
+ Args:
88
+ id_or_uri: Either a bare subscription Id (e.g. ``"1"``) or the
89
+ full ``@odata.id`` path
90
+ (e.g. ``"/redfish/v1/EventService/Subscriptions/1"``).
91
+
92
+ Returns:
93
+ The :class:`Subscription` resource.
94
+ """
95
+ path = self._resolve_subscription_path(id_or_uri)
96
+ return self._http.get(path, Subscription)
97
+
98
+ def subscribe(
99
+ self,
100
+ destination: str,
101
+ event_types: Optional[List[str]] = None,
102
+ context: Optional[str] = None,
103
+ *,
104
+ protocol: str = "Redfish",
105
+ http_headers: Optional[Any] = None,
106
+ origin_resources: Optional[List[Dict[str, Any]]] = None,
107
+ subscription_type: Optional[str] = None,
108
+ registry_prefixes: Optional[List[str]] = None,
109
+ resource_types: Optional[List[str]] = None,
110
+ message_ids: Optional[List[str]] = None,
111
+ delivery_retry_policy: Optional[str] = None,
112
+ event_format_type: Optional[str] = None,
113
+ severities: Optional[List[str]] = None,
114
+ oem_subscription_type: Optional[str] = None,
115
+ extra: Optional[Dict[str, Any]] = None,
116
+ raw_body: Optional[Dict[str, Any]] = None,
117
+ ) -> Subscription:
118
+ """
119
+ Create a new event subscription (webhook).
120
+
121
+ The signature is intentionally vendor-agnostic: every Redfish
122
+ Subscription field observed in the wild is exposed as a
123
+ keyword-only parameter, and ``extra`` / ``raw_body`` give a final
124
+ escape hatch for OEM-specific payloads. The SDK does **not** apply
125
+ any vendor-default values; callers may attempt multiple payload
126
+ shapes in sequence if a BMC rejects the first one.
127
+
128
+ Args:
129
+ destination: URL to receive events (e.g. ``"https://my-server/events"``).
130
+ event_types: Optional list of event types (e.g. ``["Alert"]``).
131
+ context: Optional context string identifying the subscription.
132
+ protocol: Wire protocol; defaults to ``"Redfish"``.
133
+ http_headers: Optional headers to be sent on the callback POST.
134
+ Pass a ``dict`` (``{"X-Auth-Token": "..."}``) or a
135
+ ``list[dict]`` — both forms are common across BMC vendors.
136
+ origin_resources: Optional list of ``{"@odata.id": "..."}``
137
+ filtering the resources of interest.
138
+ subscription_type: Optional ``SubscriptionType`` value
139
+ (e.g. ``"RedfishEvent"``, ``"SSE"``).
140
+ registry_prefixes: Optional message-registry filter list.
141
+ resource_types: Optional list of resource-type filters.
142
+ message_ids: Optional list of message Ids to filter on.
143
+ delivery_retry_policy: Optional retry policy value.
144
+ event_format_type: Optional event format type.
145
+ severities: Optional severity filter list.
146
+ oem_subscription_type: Optional vendor-specific subscription type.
147
+ extra: Optional dict shallow-merged into the request body —
148
+ useful for one-off OEM fields without bypassing field hints.
149
+ raw_body: If provided, **replaces** the auto-generated body
150
+ entirely. Use this when a BMC accepts only a non-standard
151
+ payload shape.
152
+
153
+ Returns:
154
+ The created :class:`Subscription` resource (as echoed by the BMC).
155
+ """
156
+ event_service = self._client._get_event_service()
157
+
158
+ if raw_body is not None:
159
+ body: Dict[str, Any] = dict(raw_body)
160
+ else:
161
+ body = {"Destination": destination, "Protocol": protocol}
162
+ if event_types is not None:
163
+ body["EventTypes"] = event_types
164
+ if context is not None:
165
+ body["Context"] = context
166
+ if http_headers is not None:
167
+ body["HttpHeaders"] = http_headers
168
+ if origin_resources is not None:
169
+ body["OriginResources"] = origin_resources
170
+ if subscription_type is not None:
171
+ body["SubscriptionType"] = subscription_type
172
+ if registry_prefixes is not None:
173
+ body["RegistryPrefixes"] = registry_prefixes
174
+ if resource_types is not None:
175
+ body["ResourceTypes"] = resource_types
176
+ if message_ids is not None:
177
+ body["MessageIds"] = message_ids
178
+ if delivery_retry_policy is not None:
179
+ body["DeliveryRetryPolicy"] = delivery_retry_policy
180
+ if event_format_type is not None:
181
+ body["EventFormatType"] = event_format_type
182
+ if severities is not None:
183
+ body["Severities"] = severities
184
+ if oem_subscription_type is not None:
185
+ body["OemSubscriptionType"] = oem_subscription_type
186
+ if extra:
187
+ body.update(extra)
188
+
189
+ return self._http.post(
190
+ event_service.subscriptions.odata_id,
191
+ Subscription,
192
+ raw_body=body,
193
+ )
194
+
195
+ def delete(self, id_or_uri: str) -> str:
196
+ """
197
+ Delete an event subscription.
198
+
199
+ Args:
200
+ id_or_uri: Either a bare subscription Id (e.g. ``"1"``) or the
201
+ full ``@odata.id`` path
202
+ (e.g. ``"/redfish/v1/EventService/Subscriptions/1"``).
203
+
204
+ Returns:
205
+ Raw response body (typically empty on 204).
206
+ """
207
+ path = self._resolve_subscription_path(id_or_uri)
208
+ return self._http.delete(path)
209
+
210
+ # ------------------------------------------------------------------
211
+ # Actions
212
+ # ------------------------------------------------------------------
213
+
214
+ def submit_test_event(
215
+ self,
216
+ event_type: str,
217
+ message: Optional[str] = None,
218
+ message_id: Optional[str] = None,
219
+ severity: Optional[str] = None,
220
+ message_args: Optional[List[str]] = None,
221
+ ) -> None:
222
+ """
223
+ Invoke ``#EventService.SubmitTestEvent`` on the BMC.
224
+
225
+ Args:
226
+ event_type: Event type, e.g. ``"Alert"`` / ``"StatusChange"``.
227
+ Subject to ``EventType@Redfish.AllowableValues`` on the BMC.
228
+ message: Optional event message string.
229
+ message_id: Optional Redfish MessageId.
230
+ severity: Optional severity (e.g. ``"OK"`` / ``"Warning"`` / ``"Critical"``).
231
+ message_args: Optional message argument list.
232
+
233
+ Raises:
234
+ RedfishValidationError: If EventService does not expose
235
+ SubmitTestEvent or ``event_type`` is not in the allowable list.
236
+ """
237
+ from ..models.common import RedfishResponse
238
+
239
+ es = self.service()
240
+ actions = es.actions or {}
241
+ action = actions.get("#EventService.SubmitTestEvent")
242
+ if not isinstance(action, dict) or not action.get("target"):
243
+ raise RedfishValidationError(
244
+ "EventService does not expose #EventService.SubmitTestEvent action"
245
+ )
246
+ target = action["target"]
247
+ allowable = action.get("EventType@Redfish.AllowableValues")
248
+ if allowable and event_type not in allowable:
249
+ raise RedfishValidationError(
250
+ f"Event type '{event_type}' not in allowable values {allowable}"
251
+ )
252
+
253
+ body: dict = {"EventType": event_type}
254
+ if message is not None:
255
+ body["Message"] = message
256
+ if message_id is not None:
257
+ body["MessageId"] = message_id
258
+ if severity is not None:
259
+ body["Severity"] = severity
260
+ if message_args is not None:
261
+ body["MessageArgs"] = message_args
262
+
263
+ logger.info("POST SubmitTestEvent (%s) -> %s", event_type, target)
264
+ self._http.post(target, RedfishResponse, raw_body=body)