blob: 8ee31b6ebeea90f76e6df763491b12f7378b9cb9 [file] [log] [blame]
George Keishinge7e91712021-09-03 11:28:44 -05001#!/usr/bin/env python3
George Keishinge62d8b02018-11-29 12:01:56 -06002
3r"""
George Keishing97c93942019-03-04 12:45:07 -06004See class prolog below for details.
George Keishinge62d8b02018-11-29 12:01:56 -06005"""
6
George Keishinge635ddc2022-12-08 07:38:02 -06007import json
Patrick Williams20f38712022-12-08 06:18:26 -06008import re
9import sys
George Keishing36a10752022-02-24 04:14:17 -060010from json.decoder import JSONDecodeError
George Keishinge62d8b02018-11-29 12:01:56 -060011
Michael Walsh5cc89192019-03-12 16:43:38 -050012import func_args as fa
Michael Walshce7c4b52019-03-20 17:33:15 -050013import gen_print as gp
Patrick Williams20f38712022-12-08 06:18:26 -060014from redfish.rest.v1 import InvalidCredentialsError
15from redfish_plus import redfish_plus
16from robot.libraries.BuiltIn import BuiltIn
George Keishinge62d8b02018-11-29 12:01:56 -060017
Tony Lee05aa70b2021-01-28 19:18:27 +080018MTLS_ENABLED = BuiltIn().get_variable_value("${MTLS_ENABLED}")
19
20
George Keishing97c93942019-03-04 12:45:07 -060021class bmc_redfish(redfish_plus):
George Keishinge62d8b02018-11-29 12:01:56 -060022 r"""
Michael Walsh5cc89192019-03-12 16:43:38 -050023 bmc_redfish is a child class of redfish_plus that is designed to provide
George Keishing97c93942019-03-04 12:45:07 -060024 benefits specifically for using redfish to communicate with an OpenBMC.
25
26 See the prologs of the methods below for details.
George Keishinge62d8b02018-11-29 12:01:56 -060027 """
George Keishinge62d8b02018-11-29 12:01:56 -060028
Michael Walshce7c4b52019-03-20 17:33:15 -050029 def __init__(self, *args, **kwargs):
30 r"""
31 Do BMC-related redfish initialization.
32
33 Presently, older versions of BMC code may not support redfish
34 requests. This can lead to unsightly error text being printed out for
35 programs that may use lib/bmc_redfish_resource.robot even though they
36 don't necessarily intend to make redfish requests.
37
38 This class method will make an attempt to tolerate this situation. At
39 some future point, when all BMCs can be expected to support redfish,
40 this class method may be considered for deletion. If it is deleted,
41 the self.__inited__ test code in the login() class method below should
42 likewise be deleted.
43 """
44 self.__inited__ = False
45 try:
Patrick Williams20f38712022-12-08 06:18:26 -060046 if MTLS_ENABLED == "True":
Tony Lee05aa70b2021-01-28 19:18:27 +080047 self.__inited__ = True
48 else:
49 super(bmc_redfish, self).__init__(*args, **kwargs)
50 self.__inited__ = True
Michael Walshce7c4b52019-03-20 17:33:15 -050051 except ValueError as get_exception:
52 except_type, except_value, except_traceback = sys.exc_info()
53 regex = r"The HTTP status code was not valid:[\r\n]+status:[ ]+502"
54 result = re.match(regex, str(except_value), flags=re.MULTILINE)
55 if not result:
56 gp.lprint_var(except_type)
57 gp.lprint_varx("except_value", str(except_value))
George Keishing62246352022-08-01 01:20:06 -050058 raise (get_exception)
George Keishing7049fba2025-01-16 10:44:46 +053059 except AttributeError as e:
60 BuiltIn().log_to_console(
61 "AttributeError: Error response from server"
62 )
63 except Exception as e:
64 error_response = type(e).__name__ + " from bmc_redfish class"
65 BuiltIn().log_to_console(error_response)
66
Michael Walshe58df1c2019-08-07 09:57:43 -050067 BuiltIn().set_global_variable("${REDFISH_SUPPORTED}", self.__inited__)
Michael Walshce7c4b52019-03-20 17:33:15 -050068
George Keishinge62d8b02018-11-29 12:01:56 -060069 def login(self, *args, **kwargs):
70 r"""
George Keishing97c93942019-03-04 12:45:07 -060071 Assign BMC default values for username, password and auth arguments
72 and call parent class login method.
George Keishinge62d8b02018-11-29 12:01:56 -060073
74 Description of argument(s):
Michael Walsh5cc89192019-03-12 16:43:38 -050075 args See parent class method prolog for details.
76 kwargs See parent class method prolog for details.
George Keishinge62d8b02018-11-29 12:01:56 -060077 """
George Keishing4c394012019-02-01 06:03:02 -060078
Patrick Williams20f38712022-12-08 06:18:26 -060079 if MTLS_ENABLED == "True":
Tony Lee05aa70b2021-01-28 19:18:27 +080080 return None
Michael Walshce7c4b52019-03-20 17:33:15 -050081 if not self.__inited__:
Michael Walsh707ed0e2019-05-17 15:27:25 -050082 message = "bmc_redfish.__init__() was never successfully run. It "
83 message += "is likely that the target BMC firmware code level "
84 message += "does not support redfish.\n"
Michael Walshce7c4b52019-03-20 17:33:15 -050085 raise ValueError(message)
George Keishing97c93942019-03-04 12:45:07 -060086 # Assign default values for username, password, auth where necessary.
Michael Walsh5cc89192019-03-12 16:43:38 -050087 openbmc_username = BuiltIn().get_variable_value("${OPENBMC_USERNAME}")
88 openbmc_password = BuiltIn().get_variable_value("${OPENBMC_PASSWORD}")
89 username, args, kwargs = fa.pop_arg(openbmc_username, *args, **kwargs)
90 password, args, kwargs = fa.pop_arg(openbmc_password, *args, **kwargs)
Patrick Williams20f38712022-12-08 06:18:26 -060091 auth, args, kwargs = fa.pop_arg("session", *args, **kwargs)
George Keishing4c394012019-02-01 06:03:02 -060092
George Keishing36a10752022-02-24 04:14:17 -060093 try:
Patrick Williams20f38712022-12-08 06:18:26 -060094 super(bmc_redfish, self).login(
95 username, password, auth, *args, **kwargs
96 )
George Keishing36a10752022-02-24 04:14:17 -060097 # Handle InvalidCredentialsError.
98 # (raise redfish.rest.v1.InvalidCredentialsError if not [200, 201, 202, 204])
99 except InvalidCredentialsError:
100 except_type, except_value, except_traceback = sys.exc_info()
101 BuiltIn().log_to_console(str(except_type))
102 BuiltIn().log_to_console(str(except_value))
103 e_message = "Re-try login due to exception and "
104 e_message += "it is likely error response from server side."
105 BuiltIn().log_to_console(e_message)
Patrick Williams20f38712022-12-08 06:18:26 -0600106 super(bmc_redfish, self).login(
107 username, password, auth, *args, **kwargs
108 )
George Keishing36a10752022-02-24 04:14:17 -0600109 # Handle JSONDecodeError and others.
110 except JSONDecodeError:
111 except_type, except_value, except_traceback = sys.exc_info()
112 BuiltIn().log_to_console(str(except_type))
113 BuiltIn().log_to_console(str(except_value))
114 e_message = "Re-try login due to JSONDecodeError exception and "
115 e_message += "it is likely error response from server side."
116 BuiltIn().log_to_console(e_message)
Patrick Williams20f38712022-12-08 06:18:26 -0600117 super(bmc_redfish, self).login(
118 username, password, auth, *args, **kwargs
119 )
George Keishing36a10752022-02-24 04:14:17 -0600120 except ValueError:
121 except_type, except_value, except_traceback = sys.exc_info()
122 BuiltIn().log_to_console(str(except_type))
123 BuiltIn().log_to_console(str(except_value))
124 e_message = "Unexpected exception."
125 BuiltIn().log_to_console(e_message)
George Keishing16920a52025-03-25 22:04:30 +0530126 except KeyError:
127 except_type, except_value, except_traceback = sys.exc_info()
128 BuiltIn().log_to_console(str(except_type))
129 BuiltIn().log_to_console(str(except_value))
130 e_message = "Login failed, something went wrong during request. "
131 e_message += "Error usually due to SessionCreationError or "
132 e_message += "InvalidCredentialsError resulting in failure"
133 BuiltIn().log_to_console(e_message)
134 # Custom BaseException error message from KeyError exception.
135 raise KeyError("SessionCreationError or InvalidCredentialsError")
Michael Walsh5cc89192019-03-12 16:43:38 -0500136
Tony Lee05aa70b2021-01-28 19:18:27 +0800137 def logout(self):
Patrick Williams20f38712022-12-08 06:18:26 -0600138 if MTLS_ENABLED == "True":
Tony Lee05aa70b2021-01-28 19:18:27 +0800139 return None
140 else:
141 super(bmc_redfish, self).logout()
142
Michael Walsh5cc89192019-03-12 16:43:38 -0500143 def get_properties(self, *args, **kwargs):
144 r"""
145 Return dictionary of attributes for a given path.
146
147 The difference between calling this function and calling get()
148 directly is that this function returns ONLY the dictionary portion of
149 the response object.
150
151 Example robot code:
152
153 ${properties}= Get Properties /redfish/v1/Systems/system/
Michael Walsh39c00512019-07-17 10:54:06 -0500154 Rprint Vars properties
Michael Walsh5cc89192019-03-12 16:43:38 -0500155
156 Output:
157
158 properties:
159 [PowerState]: Off
160 [Processors]:
161 [@odata.id]: /redfish/v1/Systems/system/Processors
162 [SerialNumber]: 1234567
163 ...
164
165 Description of argument(s):
166 args See parent class get() prolog for details.
167 kwargs See parent class get() prolog for details.
168 """
169
170 resp = self.get(*args, **kwargs)
Patrick Williams20f38712022-12-08 06:18:26 -0600171 return resp.dict if hasattr(resp, "dict") else {}
Michael Walsh5cc89192019-03-12 16:43:38 -0500172
173 def get_attribute(self, path, attribute, default=None, *args, **kwargs):
174 r"""
175 Get and return the named attribute from the properties for a given
176 path.
177
178 This method has the following advantages over calling get_properties
179 directly:
180 - The caller can specify a default value to be returned if the
181 attribute does not exist.
182
183 Example robot code:
184
185 ${attribute}= Get Attribute /redfish/v1/AccountService
186 ... MaxPasswordLength default=600
187 Rprint Vars attribute
188
189 Output:
190
191 attribute: 31
192
193 Description of argument(s):
194 path The path (e.g.
195 "/redfish/v1/AccountService").
196 attribute The name of the attribute to be retrieved
197 (e.g. "MaxPasswordLength").
198 default The default value to be returned if the
199 attribute does not exist (e.g. "").
200 args See parent class get() prolog for details.
201 kwargs See parent class get() prolog for details.
202 """
203
Patrick Williams20f38712022-12-08 06:18:26 -0600204 return self.get_properties(path, *args, **kwargs).get(
205 attribute, default
206 )
Michael Walsh5cc89192019-03-12 16:43:38 -0500207
208 def get_session_info(self):
209 r"""
210 Get and return session info as a tuple consisting of session_key and
211 session_location.
212 """
213
214 return self.get_session_key(), self.get_session_location()
Michael Walsh1a611fb2020-01-14 17:22:07 -0600215
George Keishing7a465642022-05-02 09:00:23 -0500216 def get_session_response(self):
217 r"""
218 Return session response dictionary data.
219 """
220
221 return self.__dict__
222
Patrick Williams20f38712022-12-08 06:18:26 -0600223 def enumerate(
224 self, resource_path, return_json=1, include_dead_resources=False
225 ):
Michael Walsh1a611fb2020-01-14 17:22:07 -0600226 r"""
227 Perform a GET enumerate request and return available resource paths.
228
229 Description of argument(s):
230 resource_path URI resource absolute path (e.g. "/redfish/v1/SessionService/Sessions").
231 return_json Indicates whether the result should be returned as a json string or as a
232 dictionary.
233 include_dead_resources Check and return a list of dead/broken URI resources.
234 """
235
236 gp.qprint_executing(style=gp.func_line_style_short)
237 # Set quiet variable to keep subordinate get() calls quiet.
238 quiet = 1
239
240 self.__result = {}
241 # Variable to hold the pending list of resources for which enumeration is yet to be obtained.
242 self.__pending_enumeration = set()
243 self.__pending_enumeration.add(resource_path)
244
245 # Variable having resources for which enumeration is completed.
246 enumerated_resources = set()
247 dead_resources = {}
248 resources_to_be_enumerated = (resource_path,)
249 while resources_to_be_enumerated:
250 for resource in resources_to_be_enumerated:
251 # JsonSchemas, SessionService or URLs containing # are not required in enumeration.
252 # Example: '/redfish/v1/JsonSchemas/' and sub resources.
253 # '/redfish/v1/SessionService'
ganesanb4d430282023-04-27 14:33:23 +0000254 # '/redfish/v1/Managers/${MANAGER_ID}#/Oem'
Patrick Williams20f38712022-12-08 06:18:26 -0600255 if (
256 ("JsonSchemas" in resource)
257 or ("SessionService" in resource)
258 or ("#" in resource)
259 ):
Michael Walsh1a611fb2020-01-14 17:22:07 -0600260 continue
261
Patrick Williams20f38712022-12-08 06:18:26 -0600262 self._rest_response_ = self.get(
263 resource, valid_status_codes=[200, 404, 500]
264 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600265 # Enumeration is done for available resources ignoring the ones for which response is not
266 # obtained.
267 if self._rest_response_.status != 200:
268 if include_dead_resources:
269 try:
Patrick Williams20f38712022-12-08 06:18:26 -0600270 dead_resources[self._rest_response_.status].append(
271 resource
272 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600273 except KeyError:
Patrick Williams20f38712022-12-08 06:18:26 -0600274 dead_resources[self._rest_response_.status] = [
275 resource
276 ]
Michael Walsh1a611fb2020-01-14 17:22:07 -0600277 continue
278 self.walk_nested_dict(self._rest_response_.dict, url=resource)
279
280 enumerated_resources.update(set(resources_to_be_enumerated))
Patrick Williams20f38712022-12-08 06:18:26 -0600281 resources_to_be_enumerated = tuple(
282 self.__pending_enumeration - enumerated_resources
283 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600284
285 if return_json:
286 if include_dead_resources:
Patrick Williams20f38712022-12-08 06:18:26 -0600287 return (
288 json.dumps(
289 self.__result,
290 sort_keys=True,
291 indent=4,
292 separators=(",", ": "),
293 ),
294 dead_resources,
295 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600296 else:
Patrick Williams20f38712022-12-08 06:18:26 -0600297 return json.dumps(
298 self.__result,
299 sort_keys=True,
300 indent=4,
301 separators=(",", ": "),
302 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600303 else:
304 if include_dead_resources:
305 return self.__result, dead_resources
306 else:
307 return self.__result
308
Patrick Williams20f38712022-12-08 06:18:26 -0600309 def walk_nested_dict(self, data, url=""):
Michael Walsh1a611fb2020-01-14 17:22:07 -0600310 r"""
311 Parse through the nested dictionary and get the resource id paths.
312
313 Description of argument(s):
314 data Nested dictionary data from response message.
315 url Resource for which the response is obtained in data.
316 """
Patrick Williams20f38712022-12-08 06:18:26 -0600317 url = url.rstrip("/")
Michael Walsh1a611fb2020-01-14 17:22:07 -0600318
319 for key, value in data.items():
Michael Walsh1a611fb2020-01-14 17:22:07 -0600320 # Recursion if nested dictionary found.
321 if isinstance(value, dict):
322 self.walk_nested_dict(value)
323 else:
324 # Value contains a list of dictionaries having member data.
Patrick Williams20f38712022-12-08 06:18:26 -0600325 if "Members" == key:
Michael Walsh1a611fb2020-01-14 17:22:07 -0600326 if isinstance(value, list):
327 for memberDict in value:
Patrick Williams20f38712022-12-08 06:18:26 -0600328 self.__pending_enumeration.add(
329 memberDict["@odata.id"]
330 )
331 if "@odata.id" == key:
332 value = value.rstrip("/")
Michael Walsh1a611fb2020-01-14 17:22:07 -0600333 # Data for the given url.
334 if value == url:
335 self.__result[url] = data
336 # Data still needs to be looked up,
337 else:
338 self.__pending_enumeration.add(value)
George Keishing46191a32021-06-10 13:58:43 -0500339
340 def get_members_list(self, resource_path, filter=None):
341 r"""
342 Return members list in a given URL.
343
344 Description of argument(s):
345 resource_path URI resource absolute path (e.g. "/redfish/v1/AccountService/Accounts").
346 filter strings or regex
347
348 /redfish/v1/AccountService/Accounts/
349 {
350 "@odata.id": "/redfish/v1/AccountService/Accounts",
351 "@odata.type": "#ManagerAccountCollection.ManagerAccountCollection",
352 "Description": "BMC User Accounts",
353 "Members": [
354 {
355 "@odata.id": "/redfish/v1/AccountService/Accounts/root"
356 },
357 {
358 "@odata.id": "/redfish/v1/AccountService/Accounts/admin"
359 }
360 ],
361 "Members@odata.count": 2,
362 "Name": "Accounts Collection"
363 }
364
365 Return list of members if no filter is applied as:
366 ['/redfish/v1/AccountService/Accounts/root', "/redfish/v1/AccountService/Accounts/admin"]
367
368 Return list of members if filter (e.g "root") is applied as:
369 ['/redfish/v1/AccountService/Accounts/root']
370
371
372 Calling from robot code:
373 ${resp}= Redfish.Get Members List /redfish/v1/AccountService/Accounts
374 ${resp}= Redfish.Get Members List /redfish/v1/AccountService/Accounts filter=root
375 """
376
377 member_list = []
Patrick Williams20f38712022-12-08 06:18:26 -0600378 self._rest_response_ = self.get(
379 resource_path, valid_status_codes=[200]
380 )
George Keishing46191a32021-06-10 13:58:43 -0500381
382 try:
383 for member in self._rest_response_.dict["Members"]:
384 member_list.append(member["@odata.id"])
385 except KeyError:
386 # Non Members child objects at the top level, ignore.
387 pass
388
389 # Filter elements in the list and return matched elements.
390 if filter is not None:
Patrick Williams20f38712022-12-08 06:18:26 -0600391 regex = ".*/" + filter + "[^/]*$"
George Keishing46191a32021-06-10 13:58:43 -0500392 return [x for x in member_list if re.match(regex, x)]
393
394 return member_list