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,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)
|