blob: 7570c9bf76d7d3132e9eb3d5f0ac0563b81e7e6b [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)
Michael Walsh5cc89192019-03-12 16:43:38 -0500126
Tony Lee05aa70b2021-01-28 19:18:27 +0800127 def logout(self):
Patrick Williams20f38712022-12-08 06:18:26 -0600128 if MTLS_ENABLED == "True":
Tony Lee05aa70b2021-01-28 19:18:27 +0800129 return None
130 else:
131 super(bmc_redfish, self).logout()
132
Michael Walsh5cc89192019-03-12 16:43:38 -0500133 def get_properties(self, *args, **kwargs):
134 r"""
135 Return dictionary of attributes for a given path.
136
137 The difference between calling this function and calling get()
138 directly is that this function returns ONLY the dictionary portion of
139 the response object.
140
141 Example robot code:
142
143 ${properties}= Get Properties /redfish/v1/Systems/system/
Michael Walsh39c00512019-07-17 10:54:06 -0500144 Rprint Vars properties
Michael Walsh5cc89192019-03-12 16:43:38 -0500145
146 Output:
147
148 properties:
149 [PowerState]: Off
150 [Processors]:
151 [@odata.id]: /redfish/v1/Systems/system/Processors
152 [SerialNumber]: 1234567
153 ...
154
155 Description of argument(s):
156 args See parent class get() prolog for details.
157 kwargs See parent class get() prolog for details.
158 """
159
160 resp = self.get(*args, **kwargs)
Patrick Williams20f38712022-12-08 06:18:26 -0600161 return resp.dict if hasattr(resp, "dict") else {}
Michael Walsh5cc89192019-03-12 16:43:38 -0500162
163 def get_attribute(self, path, attribute, default=None, *args, **kwargs):
164 r"""
165 Get and return the named attribute from the properties for a given
166 path.
167
168 This method has the following advantages over calling get_properties
169 directly:
170 - The caller can specify a default value to be returned if the
171 attribute does not exist.
172
173 Example robot code:
174
175 ${attribute}= Get Attribute /redfish/v1/AccountService
176 ... MaxPasswordLength default=600
177 Rprint Vars attribute
178
179 Output:
180
181 attribute: 31
182
183 Description of argument(s):
184 path The path (e.g.
185 "/redfish/v1/AccountService").
186 attribute The name of the attribute to be retrieved
187 (e.g. "MaxPasswordLength").
188 default The default value to be returned if the
189 attribute does not exist (e.g. "").
190 args See parent class get() prolog for details.
191 kwargs See parent class get() prolog for details.
192 """
193
Patrick Williams20f38712022-12-08 06:18:26 -0600194 return self.get_properties(path, *args, **kwargs).get(
195 attribute, default
196 )
Michael Walsh5cc89192019-03-12 16:43:38 -0500197
198 def get_session_info(self):
199 r"""
200 Get and return session info as a tuple consisting of session_key and
201 session_location.
202 """
203
204 return self.get_session_key(), self.get_session_location()
Michael Walsh1a611fb2020-01-14 17:22:07 -0600205
Patrick Williams20f38712022-12-08 06:18:26 -0600206 def enumerate(
207 self, resource_path, return_json=1, include_dead_resources=False
208 ):
Michael Walsh1a611fb2020-01-14 17:22:07 -0600209 r"""
210 Perform a GET enumerate request and return available resource paths.
211
212 Description of argument(s):
213 resource_path URI resource absolute path (e.g. "/redfish/v1/SessionService/Sessions").
214 return_json Indicates whether the result should be returned as a json string or as a
215 dictionary.
216 include_dead_resources Check and return a list of dead/broken URI resources.
217 """
218
219 gp.qprint_executing(style=gp.func_line_style_short)
220 # Set quiet variable to keep subordinate get() calls quiet.
221 quiet = 1
222
223 self.__result = {}
224 # Variable to hold the pending list of resources for which enumeration is yet to be obtained.
225 self.__pending_enumeration = set()
226 self.__pending_enumeration.add(resource_path)
227
228 # Variable having resources for which enumeration is completed.
229 enumerated_resources = set()
230 dead_resources = {}
231 resources_to_be_enumerated = (resource_path,)
232 while resources_to_be_enumerated:
233 for resource in resources_to_be_enumerated:
234 # JsonSchemas, SessionService or URLs containing # are not required in enumeration.
235 # Example: '/redfish/v1/JsonSchemas/' and sub resources.
236 # '/redfish/v1/SessionService'
ganesanb4d430282023-04-27 14:33:23 +0000237 # '/redfish/v1/Managers/${MANAGER_ID}#/Oem'
Patrick Williams20f38712022-12-08 06:18:26 -0600238 if (
239 ("JsonSchemas" in resource)
240 or ("SessionService" in resource)
241 or ("#" in resource)
242 ):
Michael Walsh1a611fb2020-01-14 17:22:07 -0600243 continue
244
Patrick Williams20f38712022-12-08 06:18:26 -0600245 self._rest_response_ = self.get(
246 resource, valid_status_codes=[200, 404, 500]
247 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600248 # Enumeration is done for available resources ignoring the ones for which response is not
249 # obtained.
250 if self._rest_response_.status != 200:
251 if include_dead_resources:
252 try:
Patrick Williams20f38712022-12-08 06:18:26 -0600253 dead_resources[self._rest_response_.status].append(
254 resource
255 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600256 except KeyError:
Patrick Williams20f38712022-12-08 06:18:26 -0600257 dead_resources[self._rest_response_.status] = [
258 resource
259 ]
Michael Walsh1a611fb2020-01-14 17:22:07 -0600260 continue
261 self.walk_nested_dict(self._rest_response_.dict, url=resource)
262
263 enumerated_resources.update(set(resources_to_be_enumerated))
Patrick Williams20f38712022-12-08 06:18:26 -0600264 resources_to_be_enumerated = tuple(
265 self.__pending_enumeration - enumerated_resources
266 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600267
268 if return_json:
269 if include_dead_resources:
Patrick Williams20f38712022-12-08 06:18:26 -0600270 return (
271 json.dumps(
272 self.__result,
273 sort_keys=True,
274 indent=4,
275 separators=(",", ": "),
276 ),
277 dead_resources,
278 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600279 else:
Patrick Williams20f38712022-12-08 06:18:26 -0600280 return json.dumps(
281 self.__result,
282 sort_keys=True,
283 indent=4,
284 separators=(",", ": "),
285 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600286 else:
287 if include_dead_resources:
288 return self.__result, dead_resources
289 else:
290 return self.__result
291
Patrick Williams20f38712022-12-08 06:18:26 -0600292 def walk_nested_dict(self, data, url=""):
Michael Walsh1a611fb2020-01-14 17:22:07 -0600293 r"""
294 Parse through the nested dictionary and get the resource id paths.
295
296 Description of argument(s):
297 data Nested dictionary data from response message.
298 url Resource for which the response is obtained in data.
299 """
Patrick Williams20f38712022-12-08 06:18:26 -0600300 url = url.rstrip("/")
Michael Walsh1a611fb2020-01-14 17:22:07 -0600301
302 for key, value in data.items():
Michael Walsh1a611fb2020-01-14 17:22:07 -0600303 # Recursion if nested dictionary found.
304 if isinstance(value, dict):
305 self.walk_nested_dict(value)
306 else:
307 # Value contains a list of dictionaries having member data.
Patrick Williams20f38712022-12-08 06:18:26 -0600308 if "Members" == key:
Michael Walsh1a611fb2020-01-14 17:22:07 -0600309 if isinstance(value, list):
310 for memberDict in value:
Patrick Williams20f38712022-12-08 06:18:26 -0600311 self.__pending_enumeration.add(
312 memberDict["@odata.id"]
313 )
314 if "@odata.id" == key:
315 value = value.rstrip("/")
Michael Walsh1a611fb2020-01-14 17:22:07 -0600316 # Data for the given url.
317 if value == url:
318 self.__result[url] = data
319 # Data still needs to be looked up,
320 else:
321 self.__pending_enumeration.add(value)
George Keishing46191a32021-06-10 13:58:43 -0500322
323 def get_members_list(self, resource_path, filter=None):
324 r"""
325 Return members list in a given URL.
326
327 Description of argument(s):
328 resource_path URI resource absolute path (e.g. "/redfish/v1/AccountService/Accounts").
329 filter strings or regex
330
331 /redfish/v1/AccountService/Accounts/
332 {
333 "@odata.id": "/redfish/v1/AccountService/Accounts",
334 "@odata.type": "#ManagerAccountCollection.ManagerAccountCollection",
335 "Description": "BMC User Accounts",
336 "Members": [
337 {
338 "@odata.id": "/redfish/v1/AccountService/Accounts/root"
339 },
340 {
341 "@odata.id": "/redfish/v1/AccountService/Accounts/admin"
342 }
343 ],
344 "Members@odata.count": 2,
345 "Name": "Accounts Collection"
346 }
347
348 Return list of members if no filter is applied as:
349 ['/redfish/v1/AccountService/Accounts/root', "/redfish/v1/AccountService/Accounts/admin"]
350
351 Return list of members if filter (e.g "root") is applied as:
352 ['/redfish/v1/AccountService/Accounts/root']
353
354
355 Calling from robot code:
356 ${resp}= Redfish.Get Members List /redfish/v1/AccountService/Accounts
357 ${resp}= Redfish.Get Members List /redfish/v1/AccountService/Accounts filter=root
358 """
359
360 member_list = []
Patrick Williams20f38712022-12-08 06:18:26 -0600361 self._rest_response_ = self.get(
362 resource_path, valid_status_codes=[200]
363 )
George Keishing46191a32021-06-10 13:58:43 -0500364
365 try:
366 for member in self._rest_response_.dict["Members"]:
367 member_list.append(member["@odata.id"])
368 except KeyError:
369 # Non Members child objects at the top level, ignore.
370 pass
371
372 # Filter elements in the list and return matched elements.
373 if filter is not None:
Patrick Williams20f38712022-12-08 06:18:26 -0600374 regex = ".*/" + filter + "[^/]*$"
George Keishing46191a32021-06-10 13:58:43 -0500375 return [x for x in member_list if re.match(regex, x)]
376
377 return member_list