blob: 1fe66c322c8ff053a2535cd217741318eeaa9148 [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__)
George Keishingb131ff32020-10-13 09:18:24 -050068 BuiltIn().set_global_variable("${REDFISH_REST_SUPPORTED}", True)
Michael Walshce7c4b52019-03-20 17:33:15 -050069
George Keishinge62d8b02018-11-29 12:01:56 -060070 def login(self, *args, **kwargs):
71 r"""
George Keishing97c93942019-03-04 12:45:07 -060072 Assign BMC default values for username, password and auth arguments
73 and call parent class login method.
George Keishinge62d8b02018-11-29 12:01:56 -060074
75 Description of argument(s):
Michael Walsh5cc89192019-03-12 16:43:38 -050076 args See parent class method prolog for details.
77 kwargs See parent class method prolog for details.
George Keishinge62d8b02018-11-29 12:01:56 -060078 """
George Keishing4c394012019-02-01 06:03:02 -060079
Patrick Williams20f38712022-12-08 06:18:26 -060080 if MTLS_ENABLED == "True":
Tony Lee05aa70b2021-01-28 19:18:27 +080081 return None
Michael Walshce7c4b52019-03-20 17:33:15 -050082 if not self.__inited__:
Michael Walsh707ed0e2019-05-17 15:27:25 -050083 message = "bmc_redfish.__init__() was never successfully run. It "
84 message += "is likely that the target BMC firmware code level "
85 message += "does not support redfish.\n"
Michael Walshce7c4b52019-03-20 17:33:15 -050086 raise ValueError(message)
George Keishing97c93942019-03-04 12:45:07 -060087 # Assign default values for username, password, auth where necessary.
Michael Walsh5cc89192019-03-12 16:43:38 -050088 openbmc_username = BuiltIn().get_variable_value("${OPENBMC_USERNAME}")
89 openbmc_password = BuiltIn().get_variable_value("${OPENBMC_PASSWORD}")
90 username, args, kwargs = fa.pop_arg(openbmc_username, *args, **kwargs)
91 password, args, kwargs = fa.pop_arg(openbmc_password, *args, **kwargs)
Patrick Williams20f38712022-12-08 06:18:26 -060092 auth, args, kwargs = fa.pop_arg("session", *args, **kwargs)
George Keishing4c394012019-02-01 06:03:02 -060093
George Keishing36a10752022-02-24 04:14:17 -060094 try:
Patrick Williams20f38712022-12-08 06:18:26 -060095 super(bmc_redfish, self).login(
96 username, password, auth, *args, **kwargs
97 )
George Keishing36a10752022-02-24 04:14:17 -060098 # Handle InvalidCredentialsError.
99 # (raise redfish.rest.v1.InvalidCredentialsError if not [200, 201, 202, 204])
100 except InvalidCredentialsError:
101 except_type, except_value, except_traceback = sys.exc_info()
102 BuiltIn().log_to_console(str(except_type))
103 BuiltIn().log_to_console(str(except_value))
104 e_message = "Re-try login due to exception and "
105 e_message += "it is likely error response from server side."
106 BuiltIn().log_to_console(e_message)
Patrick Williams20f38712022-12-08 06:18:26 -0600107 super(bmc_redfish, self).login(
108 username, password, auth, *args, **kwargs
109 )
George Keishing36a10752022-02-24 04:14:17 -0600110 # Handle JSONDecodeError and others.
111 except JSONDecodeError:
112 except_type, except_value, except_traceback = sys.exc_info()
113 BuiltIn().log_to_console(str(except_type))
114 BuiltIn().log_to_console(str(except_value))
115 e_message = "Re-try login due to JSONDecodeError exception and "
116 e_message += "it is likely error response from server side."
117 BuiltIn().log_to_console(e_message)
Patrick Williams20f38712022-12-08 06:18:26 -0600118 super(bmc_redfish, self).login(
119 username, password, auth, *args, **kwargs
120 )
George Keishing36a10752022-02-24 04:14:17 -0600121 except ValueError:
122 except_type, except_value, except_traceback = sys.exc_info()
123 BuiltIn().log_to_console(str(except_type))
124 BuiltIn().log_to_console(str(except_value))
125 e_message = "Unexpected exception."
126 BuiltIn().log_to_console(e_message)
Michael Walsh5cc89192019-03-12 16:43:38 -0500127
Tony Lee05aa70b2021-01-28 19:18:27 +0800128 def logout(self):
Patrick Williams20f38712022-12-08 06:18:26 -0600129 if MTLS_ENABLED == "True":
Tony Lee05aa70b2021-01-28 19:18:27 +0800130 return None
131 else:
132 super(bmc_redfish, self).logout()
133
Michael Walsh5cc89192019-03-12 16:43:38 -0500134 def get_properties(self, *args, **kwargs):
135 r"""
136 Return dictionary of attributes for a given path.
137
138 The difference between calling this function and calling get()
139 directly is that this function returns ONLY the dictionary portion of
140 the response object.
141
142 Example robot code:
143
144 ${properties}= Get Properties /redfish/v1/Systems/system/
Michael Walsh39c00512019-07-17 10:54:06 -0500145 Rprint Vars properties
Michael Walsh5cc89192019-03-12 16:43:38 -0500146
147 Output:
148
149 properties:
150 [PowerState]: Off
151 [Processors]:
152 [@odata.id]: /redfish/v1/Systems/system/Processors
153 [SerialNumber]: 1234567
154 ...
155
156 Description of argument(s):
157 args See parent class get() prolog for details.
158 kwargs See parent class get() prolog for details.
159 """
160
161 resp = self.get(*args, **kwargs)
Patrick Williams20f38712022-12-08 06:18:26 -0600162 return resp.dict if hasattr(resp, "dict") else {}
Michael Walsh5cc89192019-03-12 16:43:38 -0500163
164 def get_attribute(self, path, attribute, default=None, *args, **kwargs):
165 r"""
166 Get and return the named attribute from the properties for a given
167 path.
168
169 This method has the following advantages over calling get_properties
170 directly:
171 - The caller can specify a default value to be returned if the
172 attribute does not exist.
173
174 Example robot code:
175
176 ${attribute}= Get Attribute /redfish/v1/AccountService
177 ... MaxPasswordLength default=600
178 Rprint Vars attribute
179
180 Output:
181
182 attribute: 31
183
184 Description of argument(s):
185 path The path (e.g.
186 "/redfish/v1/AccountService").
187 attribute The name of the attribute to be retrieved
188 (e.g. "MaxPasswordLength").
189 default The default value to be returned if the
190 attribute does not exist (e.g. "").
191 args See parent class get() prolog for details.
192 kwargs See parent class get() prolog for details.
193 """
194
Patrick Williams20f38712022-12-08 06:18:26 -0600195 return self.get_properties(path, *args, **kwargs).get(
196 attribute, default
197 )
Michael Walsh5cc89192019-03-12 16:43:38 -0500198
199 def get_session_info(self):
200 r"""
201 Get and return session info as a tuple consisting of session_key and
202 session_location.
203 """
204
205 return self.get_session_key(), self.get_session_location()
Michael Walsh1a611fb2020-01-14 17:22:07 -0600206
Patrick Williams20f38712022-12-08 06:18:26 -0600207 def enumerate(
208 self, resource_path, return_json=1, include_dead_resources=False
209 ):
Michael Walsh1a611fb2020-01-14 17:22:07 -0600210 r"""
211 Perform a GET enumerate request and return available resource paths.
212
213 Description of argument(s):
214 resource_path URI resource absolute path (e.g. "/redfish/v1/SessionService/Sessions").
215 return_json Indicates whether the result should be returned as a json string or as a
216 dictionary.
217 include_dead_resources Check and return a list of dead/broken URI resources.
218 """
219
220 gp.qprint_executing(style=gp.func_line_style_short)
221 # Set quiet variable to keep subordinate get() calls quiet.
222 quiet = 1
223
224 self.__result = {}
225 # Variable to hold the pending list of resources for which enumeration is yet to be obtained.
226 self.__pending_enumeration = set()
227 self.__pending_enumeration.add(resource_path)
228
229 # Variable having resources for which enumeration is completed.
230 enumerated_resources = set()
231 dead_resources = {}
232 resources_to_be_enumerated = (resource_path,)
233 while resources_to_be_enumerated:
234 for resource in resources_to_be_enumerated:
235 # JsonSchemas, SessionService or URLs containing # are not required in enumeration.
236 # Example: '/redfish/v1/JsonSchemas/' and sub resources.
237 # '/redfish/v1/SessionService'
ganesanb4d430282023-04-27 14:33:23 +0000238 # '/redfish/v1/Managers/${MANAGER_ID}#/Oem'
Patrick Williams20f38712022-12-08 06:18:26 -0600239 if (
240 ("JsonSchemas" in resource)
241 or ("SessionService" in resource)
242 or ("#" in resource)
243 ):
Michael Walsh1a611fb2020-01-14 17:22:07 -0600244 continue
245
Patrick Williams20f38712022-12-08 06:18:26 -0600246 self._rest_response_ = self.get(
247 resource, valid_status_codes=[200, 404, 500]
248 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600249 # Enumeration is done for available resources ignoring the ones for which response is not
250 # obtained.
251 if self._rest_response_.status != 200:
252 if include_dead_resources:
253 try:
Patrick Williams20f38712022-12-08 06:18:26 -0600254 dead_resources[self._rest_response_.status].append(
255 resource
256 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600257 except KeyError:
Patrick Williams20f38712022-12-08 06:18:26 -0600258 dead_resources[self._rest_response_.status] = [
259 resource
260 ]
Michael Walsh1a611fb2020-01-14 17:22:07 -0600261 continue
262 self.walk_nested_dict(self._rest_response_.dict, url=resource)
263
264 enumerated_resources.update(set(resources_to_be_enumerated))
Patrick Williams20f38712022-12-08 06:18:26 -0600265 resources_to_be_enumerated = tuple(
266 self.__pending_enumeration - enumerated_resources
267 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600268
269 if return_json:
270 if include_dead_resources:
Patrick Williams20f38712022-12-08 06:18:26 -0600271 return (
272 json.dumps(
273 self.__result,
274 sort_keys=True,
275 indent=4,
276 separators=(",", ": "),
277 ),
278 dead_resources,
279 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600280 else:
Patrick Williams20f38712022-12-08 06:18:26 -0600281 return json.dumps(
282 self.__result,
283 sort_keys=True,
284 indent=4,
285 separators=(",", ": "),
286 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600287 else:
288 if include_dead_resources:
289 return self.__result, dead_resources
290 else:
291 return self.__result
292
Patrick Williams20f38712022-12-08 06:18:26 -0600293 def walk_nested_dict(self, data, url=""):
Michael Walsh1a611fb2020-01-14 17:22:07 -0600294 r"""
295 Parse through the nested dictionary and get the resource id paths.
296
297 Description of argument(s):
298 data Nested dictionary data from response message.
299 url Resource for which the response is obtained in data.
300 """
Patrick Williams20f38712022-12-08 06:18:26 -0600301 url = url.rstrip("/")
Michael Walsh1a611fb2020-01-14 17:22:07 -0600302
303 for key, value in data.items():
Michael Walsh1a611fb2020-01-14 17:22:07 -0600304 # Recursion if nested dictionary found.
305 if isinstance(value, dict):
306 self.walk_nested_dict(value)
307 else:
308 # Value contains a list of dictionaries having member data.
Patrick Williams20f38712022-12-08 06:18:26 -0600309 if "Members" == key:
Michael Walsh1a611fb2020-01-14 17:22:07 -0600310 if isinstance(value, list):
311 for memberDict in value:
Patrick Williams20f38712022-12-08 06:18:26 -0600312 self.__pending_enumeration.add(
313 memberDict["@odata.id"]
314 )
315 if "@odata.id" == key:
316 value = value.rstrip("/")
Michael Walsh1a611fb2020-01-14 17:22:07 -0600317 # Data for the given url.
318 if value == url:
319 self.__result[url] = data
320 # Data still needs to be looked up,
321 else:
322 self.__pending_enumeration.add(value)
George Keishing46191a32021-06-10 13:58:43 -0500323
324 def get_members_list(self, resource_path, filter=None):
325 r"""
326 Return members list in a given URL.
327
328 Description of argument(s):
329 resource_path URI resource absolute path (e.g. "/redfish/v1/AccountService/Accounts").
330 filter strings or regex
331
332 /redfish/v1/AccountService/Accounts/
333 {
334 "@odata.id": "/redfish/v1/AccountService/Accounts",
335 "@odata.type": "#ManagerAccountCollection.ManagerAccountCollection",
336 "Description": "BMC User Accounts",
337 "Members": [
338 {
339 "@odata.id": "/redfish/v1/AccountService/Accounts/root"
340 },
341 {
342 "@odata.id": "/redfish/v1/AccountService/Accounts/admin"
343 }
344 ],
345 "Members@odata.count": 2,
346 "Name": "Accounts Collection"
347 }
348
349 Return list of members if no filter is applied as:
350 ['/redfish/v1/AccountService/Accounts/root', "/redfish/v1/AccountService/Accounts/admin"]
351
352 Return list of members if filter (e.g "root") is applied as:
353 ['/redfish/v1/AccountService/Accounts/root']
354
355
356 Calling from robot code:
357 ${resp}= Redfish.Get Members List /redfish/v1/AccountService/Accounts
358 ${resp}= Redfish.Get Members List /redfish/v1/AccountService/Accounts filter=root
359 """
360
361 member_list = []
Patrick Williams20f38712022-12-08 06:18:26 -0600362 self._rest_response_ = self.get(
363 resource_path, valid_status_codes=[200]
364 )
George Keishing46191a32021-06-10 13:58:43 -0500365
366 try:
367 for member in self._rest_response_.dict["Members"]:
368 member_list.append(member["@odata.id"])
369 except KeyError:
370 # Non Members child objects at the top level, ignore.
371 pass
372
373 # Filter elements in the list and return matched elements.
374 if filter is not None:
Patrick Williams20f38712022-12-08 06:18:26 -0600375 regex = ".*/" + filter + "[^/]*$"
George Keishing46191a32021-06-10 13:58:43 -0500376 return [x for x in member_list if re.match(regex, x)]
377
378 return member_list