blob: 236ff44a840600b297bb794ad02372aa0eb50f61 [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
George Keishing7a465642022-05-02 09:00:23 -0500206 def get_session_response(self):
207 r"""
208 Return session response dictionary data.
209 """
210
211 return self.__dict__
212
Patrick Williams20f38712022-12-08 06:18:26 -0600213 def enumerate(
214 self, resource_path, return_json=1, include_dead_resources=False
215 ):
Michael Walsh1a611fb2020-01-14 17:22:07 -0600216 r"""
217 Perform a GET enumerate request and return available resource paths.
218
219 Description of argument(s):
220 resource_path URI resource absolute path (e.g. "/redfish/v1/SessionService/Sessions").
221 return_json Indicates whether the result should be returned as a json string or as a
222 dictionary.
223 include_dead_resources Check and return a list of dead/broken URI resources.
224 """
225
226 gp.qprint_executing(style=gp.func_line_style_short)
227 # Set quiet variable to keep subordinate get() calls quiet.
228 quiet = 1
229
230 self.__result = {}
231 # Variable to hold the pending list of resources for which enumeration is yet to be obtained.
232 self.__pending_enumeration = set()
233 self.__pending_enumeration.add(resource_path)
234
235 # Variable having resources for which enumeration is completed.
236 enumerated_resources = set()
237 dead_resources = {}
238 resources_to_be_enumerated = (resource_path,)
239 while resources_to_be_enumerated:
240 for resource in resources_to_be_enumerated:
241 # JsonSchemas, SessionService or URLs containing # are not required in enumeration.
242 # Example: '/redfish/v1/JsonSchemas/' and sub resources.
243 # '/redfish/v1/SessionService'
ganesanb4d430282023-04-27 14:33:23 +0000244 # '/redfish/v1/Managers/${MANAGER_ID}#/Oem'
Patrick Williams20f38712022-12-08 06:18:26 -0600245 if (
246 ("JsonSchemas" in resource)
247 or ("SessionService" in resource)
248 or ("#" in resource)
249 ):
Michael Walsh1a611fb2020-01-14 17:22:07 -0600250 continue
251
Patrick Williams20f38712022-12-08 06:18:26 -0600252 self._rest_response_ = self.get(
253 resource, valid_status_codes=[200, 404, 500]
254 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600255 # Enumeration is done for available resources ignoring the ones for which response is not
256 # obtained.
257 if self._rest_response_.status != 200:
258 if include_dead_resources:
259 try:
Patrick Williams20f38712022-12-08 06:18:26 -0600260 dead_resources[self._rest_response_.status].append(
261 resource
262 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600263 except KeyError:
Patrick Williams20f38712022-12-08 06:18:26 -0600264 dead_resources[self._rest_response_.status] = [
265 resource
266 ]
Michael Walsh1a611fb2020-01-14 17:22:07 -0600267 continue
268 self.walk_nested_dict(self._rest_response_.dict, url=resource)
269
270 enumerated_resources.update(set(resources_to_be_enumerated))
Patrick Williams20f38712022-12-08 06:18:26 -0600271 resources_to_be_enumerated = tuple(
272 self.__pending_enumeration - enumerated_resources
273 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600274
275 if return_json:
276 if include_dead_resources:
Patrick Williams20f38712022-12-08 06:18:26 -0600277 return (
278 json.dumps(
279 self.__result,
280 sort_keys=True,
281 indent=4,
282 separators=(",", ": "),
283 ),
284 dead_resources,
285 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600286 else:
Patrick Williams20f38712022-12-08 06:18:26 -0600287 return json.dumps(
288 self.__result,
289 sort_keys=True,
290 indent=4,
291 separators=(",", ": "),
292 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600293 else:
294 if include_dead_resources:
295 return self.__result, dead_resources
296 else:
297 return self.__result
298
Patrick Williams20f38712022-12-08 06:18:26 -0600299 def walk_nested_dict(self, data, url=""):
Michael Walsh1a611fb2020-01-14 17:22:07 -0600300 r"""
301 Parse through the nested dictionary and get the resource id paths.
302
303 Description of argument(s):
304 data Nested dictionary data from response message.
305 url Resource for which the response is obtained in data.
306 """
Patrick Williams20f38712022-12-08 06:18:26 -0600307 url = url.rstrip("/")
Michael Walsh1a611fb2020-01-14 17:22:07 -0600308
309 for key, value in data.items():
Michael Walsh1a611fb2020-01-14 17:22:07 -0600310 # Recursion if nested dictionary found.
311 if isinstance(value, dict):
312 self.walk_nested_dict(value)
313 else:
314 # Value contains a list of dictionaries having member data.
Patrick Williams20f38712022-12-08 06:18:26 -0600315 if "Members" == key:
Michael Walsh1a611fb2020-01-14 17:22:07 -0600316 if isinstance(value, list):
317 for memberDict in value:
Patrick Williams20f38712022-12-08 06:18:26 -0600318 self.__pending_enumeration.add(
319 memberDict["@odata.id"]
320 )
321 if "@odata.id" == key:
322 value = value.rstrip("/")
Michael Walsh1a611fb2020-01-14 17:22:07 -0600323 # Data for the given url.
324 if value == url:
325 self.__result[url] = data
326 # Data still needs to be looked up,
327 else:
328 self.__pending_enumeration.add(value)
George Keishing46191a32021-06-10 13:58:43 -0500329
330 def get_members_list(self, resource_path, filter=None):
331 r"""
332 Return members list in a given URL.
333
334 Description of argument(s):
335 resource_path URI resource absolute path (e.g. "/redfish/v1/AccountService/Accounts").
336 filter strings or regex
337
338 /redfish/v1/AccountService/Accounts/
339 {
340 "@odata.id": "/redfish/v1/AccountService/Accounts",
341 "@odata.type": "#ManagerAccountCollection.ManagerAccountCollection",
342 "Description": "BMC User Accounts",
343 "Members": [
344 {
345 "@odata.id": "/redfish/v1/AccountService/Accounts/root"
346 },
347 {
348 "@odata.id": "/redfish/v1/AccountService/Accounts/admin"
349 }
350 ],
351 "Members@odata.count": 2,
352 "Name": "Accounts Collection"
353 }
354
355 Return list of members if no filter is applied as:
356 ['/redfish/v1/AccountService/Accounts/root', "/redfish/v1/AccountService/Accounts/admin"]
357
358 Return list of members if filter (e.g "root") is applied as:
359 ['/redfish/v1/AccountService/Accounts/root']
360
361
362 Calling from robot code:
363 ${resp}= Redfish.Get Members List /redfish/v1/AccountService/Accounts
364 ${resp}= Redfish.Get Members List /redfish/v1/AccountService/Accounts filter=root
365 """
366
367 member_list = []
Patrick Williams20f38712022-12-08 06:18:26 -0600368 self._rest_response_ = self.get(
369 resource_path, valid_status_codes=[200]
370 )
George Keishing46191a32021-06-10 13:58:43 -0500371
372 try:
373 for member in self._rest_response_.dict["Members"]:
374 member_list.append(member["@odata.id"])
375 except KeyError:
376 # Non Members child objects at the top level, ignore.
377 pass
378
379 # Filter elements in the list and return matched elements.
380 if filter is not None:
Patrick Williams20f38712022-12-08 06:18:26 -0600381 regex = ".*/" + filter + "[^/]*$"
George Keishing46191a32021-06-10 13:58:43 -0500382 return [x for x in member_list if re.match(regex, x)]
383
384 return member_list