blob: a7a70c4989f6af557d01aae8d966af16bd63d35c [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)
Michael Walshe58df1c2019-08-07 09:57:43 -050059 BuiltIn().set_global_variable("${REDFISH_SUPPORTED}", self.__inited__)
George Keishingb131ff32020-10-13 09:18:24 -050060 BuiltIn().set_global_variable("${REDFISH_REST_SUPPORTED}", True)
Michael Walshce7c4b52019-03-20 17:33:15 -050061
George Keishinge62d8b02018-11-29 12:01:56 -060062 def login(self, *args, **kwargs):
63 r"""
George Keishing97c93942019-03-04 12:45:07 -060064 Assign BMC default values for username, password and auth arguments
65 and call parent class login method.
George Keishinge62d8b02018-11-29 12:01:56 -060066
67 Description of argument(s):
Michael Walsh5cc89192019-03-12 16:43:38 -050068 args See parent class method prolog for details.
69 kwargs See parent class method prolog for details.
George Keishinge62d8b02018-11-29 12:01:56 -060070 """
George Keishing4c394012019-02-01 06:03:02 -060071
Patrick Williams20f38712022-12-08 06:18:26 -060072 if MTLS_ENABLED == "True":
Tony Lee05aa70b2021-01-28 19:18:27 +080073 return None
Michael Walshce7c4b52019-03-20 17:33:15 -050074 if not self.__inited__:
Michael Walsh707ed0e2019-05-17 15:27:25 -050075 message = "bmc_redfish.__init__() was never successfully run. It "
76 message += "is likely that the target BMC firmware code level "
77 message += "does not support redfish.\n"
Michael Walshce7c4b52019-03-20 17:33:15 -050078 raise ValueError(message)
George Keishing97c93942019-03-04 12:45:07 -060079 # Assign default values for username, password, auth where necessary.
Michael Walsh5cc89192019-03-12 16:43:38 -050080 openbmc_username = BuiltIn().get_variable_value("${OPENBMC_USERNAME}")
81 openbmc_password = BuiltIn().get_variable_value("${OPENBMC_PASSWORD}")
82 username, args, kwargs = fa.pop_arg(openbmc_username, *args, **kwargs)
83 password, args, kwargs = fa.pop_arg(openbmc_password, *args, **kwargs)
Patrick Williams20f38712022-12-08 06:18:26 -060084 auth, args, kwargs = fa.pop_arg("session", *args, **kwargs)
George Keishing4c394012019-02-01 06:03:02 -060085
George Keishing36a10752022-02-24 04:14:17 -060086 try:
Patrick Williams20f38712022-12-08 06:18:26 -060087 super(bmc_redfish, self).login(
88 username, password, auth, *args, **kwargs
89 )
George Keishing36a10752022-02-24 04:14:17 -060090 # Handle InvalidCredentialsError.
91 # (raise redfish.rest.v1.InvalidCredentialsError if not [200, 201, 202, 204])
92 except InvalidCredentialsError:
93 except_type, except_value, except_traceback = sys.exc_info()
94 BuiltIn().log_to_console(str(except_type))
95 BuiltIn().log_to_console(str(except_value))
96 e_message = "Re-try login due to exception and "
97 e_message += "it is likely error response from server side."
98 BuiltIn().log_to_console(e_message)
Patrick Williams20f38712022-12-08 06:18:26 -060099 super(bmc_redfish, self).login(
100 username, password, auth, *args, **kwargs
101 )
George Keishing36a10752022-02-24 04:14:17 -0600102 # Handle JSONDecodeError and others.
103 except JSONDecodeError:
104 except_type, except_value, except_traceback = sys.exc_info()
105 BuiltIn().log_to_console(str(except_type))
106 BuiltIn().log_to_console(str(except_value))
107 e_message = "Re-try login due to JSONDecodeError exception and "
108 e_message += "it is likely error response from server side."
109 BuiltIn().log_to_console(e_message)
Patrick Williams20f38712022-12-08 06:18:26 -0600110 super(bmc_redfish, self).login(
111 username, password, auth, *args, **kwargs
112 )
George Keishing36a10752022-02-24 04:14:17 -0600113 except ValueError:
114 except_type, except_value, except_traceback = sys.exc_info()
115 BuiltIn().log_to_console(str(except_type))
116 BuiltIn().log_to_console(str(except_value))
117 e_message = "Unexpected exception."
118 BuiltIn().log_to_console(e_message)
Michael Walsh5cc89192019-03-12 16:43:38 -0500119
Tony Lee05aa70b2021-01-28 19:18:27 +0800120 def logout(self):
Patrick Williams20f38712022-12-08 06:18:26 -0600121 if MTLS_ENABLED == "True":
Tony Lee05aa70b2021-01-28 19:18:27 +0800122 return None
123 else:
124 super(bmc_redfish, self).logout()
125
Michael Walsh5cc89192019-03-12 16:43:38 -0500126 def get_properties(self, *args, **kwargs):
127 r"""
128 Return dictionary of attributes for a given path.
129
130 The difference between calling this function and calling get()
131 directly is that this function returns ONLY the dictionary portion of
132 the response object.
133
134 Example robot code:
135
136 ${properties}= Get Properties /redfish/v1/Systems/system/
Michael Walsh39c00512019-07-17 10:54:06 -0500137 Rprint Vars properties
Michael Walsh5cc89192019-03-12 16:43:38 -0500138
139 Output:
140
141 properties:
142 [PowerState]: Off
143 [Processors]:
144 [@odata.id]: /redfish/v1/Systems/system/Processors
145 [SerialNumber]: 1234567
146 ...
147
148 Description of argument(s):
149 args See parent class get() prolog for details.
150 kwargs See parent class get() prolog for details.
151 """
152
153 resp = self.get(*args, **kwargs)
Patrick Williams20f38712022-12-08 06:18:26 -0600154 return resp.dict if hasattr(resp, "dict") else {}
Michael Walsh5cc89192019-03-12 16:43:38 -0500155
156 def get_attribute(self, path, attribute, default=None, *args, **kwargs):
157 r"""
158 Get and return the named attribute from the properties for a given
159 path.
160
161 This method has the following advantages over calling get_properties
162 directly:
163 - The caller can specify a default value to be returned if the
164 attribute does not exist.
165
166 Example robot code:
167
168 ${attribute}= Get Attribute /redfish/v1/AccountService
169 ... MaxPasswordLength default=600
170 Rprint Vars attribute
171
172 Output:
173
174 attribute: 31
175
176 Description of argument(s):
177 path The path (e.g.
178 "/redfish/v1/AccountService").
179 attribute The name of the attribute to be retrieved
180 (e.g. "MaxPasswordLength").
181 default The default value to be returned if the
182 attribute does not exist (e.g. "").
183 args See parent class get() prolog for details.
184 kwargs See parent class get() prolog for details.
185 """
186
Patrick Williams20f38712022-12-08 06:18:26 -0600187 return self.get_properties(path, *args, **kwargs).get(
188 attribute, default
189 )
Michael Walsh5cc89192019-03-12 16:43:38 -0500190
191 def get_session_info(self):
192 r"""
193 Get and return session info as a tuple consisting of session_key and
194 session_location.
195 """
196
197 return self.get_session_key(), self.get_session_location()
Michael Walsh1a611fb2020-01-14 17:22:07 -0600198
Patrick Williams20f38712022-12-08 06:18:26 -0600199 def enumerate(
200 self, resource_path, return_json=1, include_dead_resources=False
201 ):
Michael Walsh1a611fb2020-01-14 17:22:07 -0600202 r"""
203 Perform a GET enumerate request and return available resource paths.
204
205 Description of argument(s):
206 resource_path URI resource absolute path (e.g. "/redfish/v1/SessionService/Sessions").
207 return_json Indicates whether the result should be returned as a json string or as a
208 dictionary.
209 include_dead_resources Check and return a list of dead/broken URI resources.
210 """
211
212 gp.qprint_executing(style=gp.func_line_style_short)
213 # Set quiet variable to keep subordinate get() calls quiet.
214 quiet = 1
215
216 self.__result = {}
217 # Variable to hold the pending list of resources for which enumeration is yet to be obtained.
218 self.__pending_enumeration = set()
219 self.__pending_enumeration.add(resource_path)
220
221 # Variable having resources for which enumeration is completed.
222 enumerated_resources = set()
223 dead_resources = {}
224 resources_to_be_enumerated = (resource_path,)
225 while resources_to_be_enumerated:
226 for resource in resources_to_be_enumerated:
227 # JsonSchemas, SessionService or URLs containing # are not required in enumeration.
228 # Example: '/redfish/v1/JsonSchemas/' and sub resources.
229 # '/redfish/v1/SessionService'
ganesanb4d430282023-04-27 14:33:23 +0000230 # '/redfish/v1/Managers/${MANAGER_ID}#/Oem'
Patrick Williams20f38712022-12-08 06:18:26 -0600231 if (
232 ("JsonSchemas" in resource)
233 or ("SessionService" in resource)
234 or ("#" in resource)
235 ):
Michael Walsh1a611fb2020-01-14 17:22:07 -0600236 continue
237
Patrick Williams20f38712022-12-08 06:18:26 -0600238 self._rest_response_ = self.get(
239 resource, valid_status_codes=[200, 404, 500]
240 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600241 # Enumeration is done for available resources ignoring the ones for which response is not
242 # obtained.
243 if self._rest_response_.status != 200:
244 if include_dead_resources:
245 try:
Patrick Williams20f38712022-12-08 06:18:26 -0600246 dead_resources[self._rest_response_.status].append(
247 resource
248 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600249 except KeyError:
Patrick Williams20f38712022-12-08 06:18:26 -0600250 dead_resources[self._rest_response_.status] = [
251 resource
252 ]
Michael Walsh1a611fb2020-01-14 17:22:07 -0600253 continue
254 self.walk_nested_dict(self._rest_response_.dict, url=resource)
255
256 enumerated_resources.update(set(resources_to_be_enumerated))
Patrick Williams20f38712022-12-08 06:18:26 -0600257 resources_to_be_enumerated = tuple(
258 self.__pending_enumeration - enumerated_resources
259 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600260
261 if return_json:
262 if include_dead_resources:
Patrick Williams20f38712022-12-08 06:18:26 -0600263 return (
264 json.dumps(
265 self.__result,
266 sort_keys=True,
267 indent=4,
268 separators=(",", ": "),
269 ),
270 dead_resources,
271 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600272 else:
Patrick Williams20f38712022-12-08 06:18:26 -0600273 return json.dumps(
274 self.__result,
275 sort_keys=True,
276 indent=4,
277 separators=(",", ": "),
278 )
Michael Walsh1a611fb2020-01-14 17:22:07 -0600279 else:
280 if include_dead_resources:
281 return self.__result, dead_resources
282 else:
283 return self.__result
284
Patrick Williams20f38712022-12-08 06:18:26 -0600285 def walk_nested_dict(self, data, url=""):
Michael Walsh1a611fb2020-01-14 17:22:07 -0600286 r"""
287 Parse through the nested dictionary and get the resource id paths.
288
289 Description of argument(s):
290 data Nested dictionary data from response message.
291 url Resource for which the response is obtained in data.
292 """
Patrick Williams20f38712022-12-08 06:18:26 -0600293 url = url.rstrip("/")
Michael Walsh1a611fb2020-01-14 17:22:07 -0600294
295 for key, value in data.items():
Michael Walsh1a611fb2020-01-14 17:22:07 -0600296 # Recursion if nested dictionary found.
297 if isinstance(value, dict):
298 self.walk_nested_dict(value)
299 else:
300 # Value contains a list of dictionaries having member data.
Patrick Williams20f38712022-12-08 06:18:26 -0600301 if "Members" == key:
Michael Walsh1a611fb2020-01-14 17:22:07 -0600302 if isinstance(value, list):
303 for memberDict in value:
Patrick Williams20f38712022-12-08 06:18:26 -0600304 self.__pending_enumeration.add(
305 memberDict["@odata.id"]
306 )
307 if "@odata.id" == key:
308 value = value.rstrip("/")
Michael Walsh1a611fb2020-01-14 17:22:07 -0600309 # Data for the given url.
310 if value == url:
311 self.__result[url] = data
312 # Data still needs to be looked up,
313 else:
314 self.__pending_enumeration.add(value)
George Keishing46191a32021-06-10 13:58:43 -0500315
316 def get_members_list(self, resource_path, filter=None):
317 r"""
318 Return members list in a given URL.
319
320 Description of argument(s):
321 resource_path URI resource absolute path (e.g. "/redfish/v1/AccountService/Accounts").
322 filter strings or regex
323
324 /redfish/v1/AccountService/Accounts/
325 {
326 "@odata.id": "/redfish/v1/AccountService/Accounts",
327 "@odata.type": "#ManagerAccountCollection.ManagerAccountCollection",
328 "Description": "BMC User Accounts",
329 "Members": [
330 {
331 "@odata.id": "/redfish/v1/AccountService/Accounts/root"
332 },
333 {
334 "@odata.id": "/redfish/v1/AccountService/Accounts/admin"
335 }
336 ],
337 "Members@odata.count": 2,
338 "Name": "Accounts Collection"
339 }
340
341 Return list of members if no filter is applied as:
342 ['/redfish/v1/AccountService/Accounts/root', "/redfish/v1/AccountService/Accounts/admin"]
343
344 Return list of members if filter (e.g "root") is applied as:
345 ['/redfish/v1/AccountService/Accounts/root']
346
347
348 Calling from robot code:
349 ${resp}= Redfish.Get Members List /redfish/v1/AccountService/Accounts
350 ${resp}= Redfish.Get Members List /redfish/v1/AccountService/Accounts filter=root
351 """
352
353 member_list = []
Patrick Williams20f38712022-12-08 06:18:26 -0600354 self._rest_response_ = self.get(
355 resource_path, valid_status_codes=[200]
356 )
George Keishing46191a32021-06-10 13:58:43 -0500357
358 try:
359 for member in self._rest_response_.dict["Members"]:
360 member_list.append(member["@odata.id"])
361 except KeyError:
362 # Non Members child objects at the top level, ignore.
363 pass
364
365 # Filter elements in the list and return matched elements.
366 if filter is not None:
Patrick Williams20f38712022-12-08 06:18:26 -0600367 regex = ".*/" + filter + "[^/]*$"
George Keishing46191a32021-06-10 13:58:43 -0500368 return [x for x in member_list if re.match(regex, x)]
369
370 return member_list