blob: db9e013ada4dfbe98888729ba158fd0ff430417d [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
Michael Walshce7c4b52019-03-20 17:33:15 -05007import sys
8import re
Michael Walsh1a611fb2020-01-14 17:22:07 -06009import json
George Keishing97c93942019-03-04 12:45:07 -060010from redfish_plus import redfish_plus
George Keishing2296e8c2019-02-01 05:49:58 -060011from robot.libraries.BuiltIn import BuiltIn
George Keishing36a10752022-02-24 04:14:17 -060012from json.decoder import JSONDecodeError
13from redfish.rest.v1 import InvalidCredentialsError
George Keishinge62d8b02018-11-29 12:01:56 -060014
Michael Walsh5cc89192019-03-12 16:43:38 -050015import func_args as fa
Michael Walshce7c4b52019-03-20 17:33:15 -050016import gen_print as gp
Michael Walsh5cc89192019-03-12 16:43:38 -050017
George Keishinge62d8b02018-11-29 12:01:56 -060018
Tony Lee05aa70b2021-01-28 19:18:27 +080019MTLS_ENABLED = BuiltIn().get_variable_value("${MTLS_ENABLED}")
20
21
George Keishing97c93942019-03-04 12:45:07 -060022class bmc_redfish(redfish_plus):
George Keishinge62d8b02018-11-29 12:01:56 -060023 r"""
Michael Walsh5cc89192019-03-12 16:43:38 -050024 bmc_redfish is a child class of redfish_plus that is designed to provide
George Keishing97c93942019-03-04 12:45:07 -060025 benefits specifically for using redfish to communicate with an OpenBMC.
26
27 See the prologs of the methods below for details.
George Keishinge62d8b02018-11-29 12:01:56 -060028 """
George Keishinge62d8b02018-11-29 12:01:56 -060029
Michael Walshce7c4b52019-03-20 17:33:15 -050030 def __init__(self, *args, **kwargs):
31 r"""
32 Do BMC-related redfish initialization.
33
34 Presently, older versions of BMC code may not support redfish
35 requests. This can lead to unsightly error text being printed out for
36 programs that may use lib/bmc_redfish_resource.robot even though they
37 don't necessarily intend to make redfish requests.
38
39 This class method will make an attempt to tolerate this situation. At
40 some future point, when all BMCs can be expected to support redfish,
41 this class method may be considered for deletion. If it is deleted,
42 the self.__inited__ test code in the login() class method below should
43 likewise be deleted.
44 """
45 self.__inited__ = False
46 try:
Tony Lee05aa70b2021-01-28 19:18:27 +080047 if MTLS_ENABLED == 'True':
48 self.__inited__ = True
49 else:
50 super(bmc_redfish, self).__init__(*args, **kwargs)
51 self.__inited__ = True
Michael Walshce7c4b52019-03-20 17:33:15 -050052 except ValueError as get_exception:
53 except_type, except_value, except_traceback = sys.exc_info()
54 regex = r"The HTTP status code was not valid:[\r\n]+status:[ ]+502"
55 result = re.match(regex, str(except_value), flags=re.MULTILINE)
56 if not result:
57 gp.lprint_var(except_type)
58 gp.lprint_varx("except_value", str(except_value))
59 raise(get_exception)
Michael Walshe58df1c2019-08-07 09:57:43 -050060 BuiltIn().set_global_variable("${REDFISH_SUPPORTED}", self.__inited__)
George Keishingb131ff32020-10-13 09:18:24 -050061 BuiltIn().set_global_variable("${REDFISH_REST_SUPPORTED}", True)
Michael Walshce7c4b52019-03-20 17:33:15 -050062
George Keishinge62d8b02018-11-29 12:01:56 -060063 def login(self, *args, **kwargs):
64 r"""
George Keishing97c93942019-03-04 12:45:07 -060065 Assign BMC default values for username, password and auth arguments
66 and call parent class login method.
George Keishinge62d8b02018-11-29 12:01:56 -060067
68 Description of argument(s):
Michael Walsh5cc89192019-03-12 16:43:38 -050069 args See parent class method prolog for details.
70 kwargs See parent class method prolog for details.
George Keishinge62d8b02018-11-29 12:01:56 -060071 """
George Keishing4c394012019-02-01 06:03:02 -060072
Tony Lee05aa70b2021-01-28 19:18:27 +080073 if MTLS_ENABLED == 'True':
74 return None
Michael Walshce7c4b52019-03-20 17:33:15 -050075 if not self.__inited__:
Michael Walsh707ed0e2019-05-17 15:27:25 -050076 message = "bmc_redfish.__init__() was never successfully run. It "
77 message += "is likely that the target BMC firmware code level "
78 message += "does not support redfish.\n"
Michael Walshce7c4b52019-03-20 17:33:15 -050079 raise ValueError(message)
George Keishing97c93942019-03-04 12:45:07 -060080 # Assign default values for username, password, auth where necessary.
Michael Walsh5cc89192019-03-12 16:43:38 -050081 openbmc_username = BuiltIn().get_variable_value("${OPENBMC_USERNAME}")
82 openbmc_password = BuiltIn().get_variable_value("${OPENBMC_PASSWORD}")
83 username, args, kwargs = fa.pop_arg(openbmc_username, *args, **kwargs)
84 password, args, kwargs = fa.pop_arg(openbmc_password, *args, **kwargs)
85 auth, args, kwargs = fa.pop_arg('session', *args, **kwargs)
George Keishing4c394012019-02-01 06:03:02 -060086
George Keishing36a10752022-02-24 04:14:17 -060087 try:
88 super(bmc_redfish, self).login(username, password, auth,
89 *args, **kwargs)
90 # 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)
99 super(bmc_redfish, self).login(username, password, auth,
100 *args, **kwargs)
101 # Handle JSONDecodeError and others.
102 except JSONDecodeError:
103 except_type, except_value, except_traceback = sys.exc_info()
104 BuiltIn().log_to_console(str(except_type))
105 BuiltIn().log_to_console(str(except_value))
106 e_message = "Re-try login due to JSONDecodeError exception and "
107 e_message += "it is likely error response from server side."
108 BuiltIn().log_to_console(e_message)
109 super(bmc_redfish, self).login(username, password, auth,
110 *args, **kwargs)
111 except ValueError:
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 = "Unexpected exception."
116 BuiltIn().log_to_console(e_message)
Michael Walsh5cc89192019-03-12 16:43:38 -0500117
Tony Lee05aa70b2021-01-28 19:18:27 +0800118 def logout(self):
119
120 if MTLS_ENABLED == 'True':
121 return None
122 else:
123 super(bmc_redfish, self).logout()
124
Michael Walsh5cc89192019-03-12 16:43:38 -0500125 def get_properties(self, *args, **kwargs):
126 r"""
127 Return dictionary of attributes for a given path.
128
129 The difference between calling this function and calling get()
130 directly is that this function returns ONLY the dictionary portion of
131 the response object.
132
133 Example robot code:
134
135 ${properties}= Get Properties /redfish/v1/Systems/system/
Michael Walsh39c00512019-07-17 10:54:06 -0500136 Rprint Vars properties
Michael Walsh5cc89192019-03-12 16:43:38 -0500137
138 Output:
139
140 properties:
141 [PowerState]: Off
142 [Processors]:
143 [@odata.id]: /redfish/v1/Systems/system/Processors
144 [SerialNumber]: 1234567
145 ...
146
147 Description of argument(s):
148 args See parent class get() prolog for details.
149 kwargs See parent class get() prolog for details.
150 """
151
152 resp = self.get(*args, **kwargs)
153 return resp.dict if hasattr(resp, 'dict') else {}
154
155 def get_attribute(self, path, attribute, default=None, *args, **kwargs):
156 r"""
157 Get and return the named attribute from the properties for a given
158 path.
159
160 This method has the following advantages over calling get_properties
161 directly:
162 - The caller can specify a default value to be returned if the
163 attribute does not exist.
164
165 Example robot code:
166
167 ${attribute}= Get Attribute /redfish/v1/AccountService
168 ... MaxPasswordLength default=600
169 Rprint Vars attribute
170
171 Output:
172
173 attribute: 31
174
175 Description of argument(s):
176 path The path (e.g.
177 "/redfish/v1/AccountService").
178 attribute The name of the attribute to be retrieved
179 (e.g. "MaxPasswordLength").
180 default The default value to be returned if the
181 attribute does not exist (e.g. "").
182 args See parent class get() prolog for details.
183 kwargs See parent class get() prolog for details.
184 """
185
186 return self.get_properties(path, *args, **kwargs).get(attribute,
187 default)
188
189 def get_session_info(self):
190 r"""
191 Get and return session info as a tuple consisting of session_key and
192 session_location.
193 """
194
195 return self.get_session_key(), self.get_session_location()
Michael Walsh1a611fb2020-01-14 17:22:07 -0600196
197 def enumerate(self, resource_path, return_json=1, include_dead_resources=False):
198 r"""
199 Perform a GET enumerate request and return available resource paths.
200
201 Description of argument(s):
202 resource_path URI resource absolute path (e.g. "/redfish/v1/SessionService/Sessions").
203 return_json Indicates whether the result should be returned as a json string or as a
204 dictionary.
205 include_dead_resources Check and return a list of dead/broken URI resources.
206 """
207
208 gp.qprint_executing(style=gp.func_line_style_short)
209 # Set quiet variable to keep subordinate get() calls quiet.
210 quiet = 1
211
212 self.__result = {}
213 # Variable to hold the pending list of resources for which enumeration is yet to be obtained.
214 self.__pending_enumeration = set()
215 self.__pending_enumeration.add(resource_path)
216
217 # Variable having resources for which enumeration is completed.
218 enumerated_resources = set()
219 dead_resources = {}
220 resources_to_be_enumerated = (resource_path,)
221 while resources_to_be_enumerated:
222 for resource in resources_to_be_enumerated:
223 # JsonSchemas, SessionService or URLs containing # are not required in enumeration.
224 # Example: '/redfish/v1/JsonSchemas/' and sub resources.
225 # '/redfish/v1/SessionService'
226 # '/redfish/v1/Managers/bmc#/Oem'
227 if ('JsonSchemas' in resource) or ('SessionService' in resource) or ('#' in resource):
228 continue
229
230 self._rest_response_ = self.get(resource, valid_status_codes=[200, 404, 500])
231 # Enumeration is done for available resources ignoring the ones for which response is not
232 # obtained.
233 if self._rest_response_.status != 200:
234 if include_dead_resources:
235 try:
236 dead_resources[self._rest_response_.status].append(resource)
237 except KeyError:
238 dead_resources[self._rest_response_.status] = [resource]
239 continue
240 self.walk_nested_dict(self._rest_response_.dict, url=resource)
241
242 enumerated_resources.update(set(resources_to_be_enumerated))
243 resources_to_be_enumerated = tuple(self.__pending_enumeration - enumerated_resources)
244
245 if return_json:
246 if include_dead_resources:
247 return json.dumps(self.__result, sort_keys=True,
248 indent=4, separators=(',', ': ')), dead_resources
249 else:
250 return json.dumps(self.__result, sort_keys=True,
251 indent=4, separators=(',', ': '))
252 else:
253 if include_dead_resources:
254 return self.__result, dead_resources
255 else:
256 return self.__result
257
258 def walk_nested_dict(self, data, url=''):
259 r"""
260 Parse through the nested dictionary and get the resource id paths.
261
262 Description of argument(s):
263 data Nested dictionary data from response message.
264 url Resource for which the response is obtained in data.
265 """
266 url = url.rstrip('/')
267
268 for key, value in data.items():
269
270 # Recursion if nested dictionary found.
271 if isinstance(value, dict):
272 self.walk_nested_dict(value)
273 else:
274 # Value contains a list of dictionaries having member data.
275 if 'Members' == key:
276 if isinstance(value, list):
277 for memberDict in value:
278 self.__pending_enumeration.add(memberDict['@odata.id'])
279 if '@odata.id' == key:
280 value = value.rstrip('/')
281 # Data for the given url.
282 if value == url:
283 self.__result[url] = data
284 # Data still needs to be looked up,
285 else:
286 self.__pending_enumeration.add(value)
George Keishing46191a32021-06-10 13:58:43 -0500287
288 def get_members_list(self, resource_path, filter=None):
289 r"""
290 Return members list in a given URL.
291
292 Description of argument(s):
293 resource_path URI resource absolute path (e.g. "/redfish/v1/AccountService/Accounts").
294 filter strings or regex
295
296 /redfish/v1/AccountService/Accounts/
297 {
298 "@odata.id": "/redfish/v1/AccountService/Accounts",
299 "@odata.type": "#ManagerAccountCollection.ManagerAccountCollection",
300 "Description": "BMC User Accounts",
301 "Members": [
302 {
303 "@odata.id": "/redfish/v1/AccountService/Accounts/root"
304 },
305 {
306 "@odata.id": "/redfish/v1/AccountService/Accounts/admin"
307 }
308 ],
309 "Members@odata.count": 2,
310 "Name": "Accounts Collection"
311 }
312
313 Return list of members if no filter is applied as:
314 ['/redfish/v1/AccountService/Accounts/root', "/redfish/v1/AccountService/Accounts/admin"]
315
316 Return list of members if filter (e.g "root") is applied as:
317 ['/redfish/v1/AccountService/Accounts/root']
318
319
320 Calling from robot code:
321 ${resp}= Redfish.Get Members List /redfish/v1/AccountService/Accounts
322 ${resp}= Redfish.Get Members List /redfish/v1/AccountService/Accounts filter=root
323 """
324
325 member_list = []
326 self._rest_response_ = self.get(resource_path, valid_status_codes=[200])
327
328 try:
329 for member in self._rest_response_.dict["Members"]:
330 member_list.append(member["@odata.id"])
331 except KeyError:
332 # Non Members child objects at the top level, ignore.
333 pass
334
335 # Filter elements in the list and return matched elements.
336 if filter is not None:
337 regex = '.*/' + filter + '[^/]*$'
338 return [x for x in member_list if re.match(regex, x)]
339
340 return member_list