1#!/usr/bin/env python3 2 3r""" 4See class prolog below for details. 5""" 6 7import json 8import re 9import sys 10from json.decoder import JSONDecodeError 11 12import func_args as fa 13import gen_print as gp 14from redfish.rest.v1 import InvalidCredentialsError 15from redfish_plus import redfish_plus 16from robot.libraries.BuiltIn import BuiltIn 17 18MTLS_ENABLED = BuiltIn().get_variable_value("${MTLS_ENABLED}") 19 20 21class bmc_redfish(redfish_plus): 22 r""" 23 bmc_redfish is a child class of redfish_plus that is designed to provide 24 benefits specifically for using redfish to communicate with an OpenBMC. 25 26 See the prologs of the methods below for details. 27 """ 28 29 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: 46 if MTLS_ENABLED == "True": 47 self.__inited__ = True 48 else: 49 super(bmc_redfish, self).__init__(*args, **kwargs) 50 self.__inited__ = True 51 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)) 58 raise (get_exception) 59 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 67 BuiltIn().set_global_variable("${REDFISH_SUPPORTED}", self.__inited__) 68 69 def login(self, *args, **kwargs): 70 r""" 71 Assign BMC default values for username, password and auth arguments 72 and call parent class login method. 73 74 Description of argument(s): 75 args See parent class method prolog for details. 76 kwargs See parent class method prolog for details. 77 """ 78 79 if MTLS_ENABLED == "True": 80 return None 81 if not self.__inited__: 82 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" 85 raise ValueError(message) 86 # Assign default values for username, password, auth where necessary. 87 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) 91 auth, args, kwargs = fa.pop_arg("session", *args, **kwargs) 92 93 try: 94 super(bmc_redfish, self).login( 95 username, password, auth, *args, **kwargs 96 ) 97 # 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) 106 super(bmc_redfish, self).login( 107 username, password, auth, *args, **kwargs 108 ) 109 # 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) 117 super(bmc_redfish, self).login( 118 username, password, auth, *args, **kwargs 119 ) 120 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) 126 except KeyError: 127 except_type, except_value, except_traceback = sys.exc_info() 128 BuiltIn().log_to_console(str(except_type)) 129 BuiltIn().log_to_console(str(except_value)) 130 e_message = "Login failed, something went wrong during request. " 131 e_message += "Error usually due to SessionCreationError or " 132 e_message += "InvalidCredentialsError resulting in failure" 133 BuiltIn().log_to_console(e_message) 134 # Custom BaseException error message from KeyError exception. 135 raise KeyError("SessionCreationError or InvalidCredentialsError") 136 137 def logout(self): 138 if MTLS_ENABLED == "True": 139 return None 140 else: 141 super(bmc_redfish, self).logout() 142 143 def get_properties(self, *args, **kwargs): 144 r""" 145 Return dictionary of attributes for a given path. 146 147 The difference between calling this function and calling get() 148 directly is that this function returns ONLY the dictionary portion of 149 the response object. 150 151 Example robot code: 152 153 ${properties}= Get Properties /redfish/v1/Systems/system/ 154 Rprint Vars properties 155 156 Output: 157 158 properties: 159 [PowerState]: Off 160 [Processors]: 161 [@odata.id]: /redfish/v1/Systems/system/Processors 162 [SerialNumber]: 1234567 163 ... 164 165 Description of argument(s): 166 args See parent class get() prolog for details. 167 kwargs See parent class get() prolog for details. 168 """ 169 170 resp = self.get(*args, **kwargs) 171 return resp.dict if hasattr(resp, "dict") else {} 172 173 def get_attribute(self, path, attribute, default=None, *args, **kwargs): 174 r""" 175 Get and return the named attribute from the properties for a given 176 path. 177 178 This method has the following advantages over calling get_properties 179 directly: 180 - The caller can specify a default value to be returned if the 181 attribute does not exist. 182 183 Example robot code: 184 185 ${attribute}= Get Attribute /redfish/v1/AccountService 186 ... MaxPasswordLength default=600 187 Rprint Vars attribute 188 189 Output: 190 191 attribute: 31 192 193 Description of argument(s): 194 path The path (e.g. 195 "/redfish/v1/AccountService"). 196 attribute The name of the attribute to be retrieved 197 (e.g. "MaxPasswordLength"). 198 default The default value to be returned if the 199 attribute does not exist (e.g. ""). 200 args See parent class get() prolog for details. 201 kwargs See parent class get() prolog for details. 202 """ 203 204 return self.get_properties(path, *args, **kwargs).get( 205 attribute, default 206 ) 207 208 def get_session_info(self): 209 r""" 210 Get and return session info as a tuple consisting of session_key and 211 session_location. 212 """ 213 214 return self.get_session_key(), self.get_session_location() 215 216 def get_session_response(self): 217 r""" 218 Return session response dictionary data. 219 """ 220 221 return self.__dict__ 222 223 def enumerate( 224 self, resource_path, return_json=1, include_dead_resources=False 225 ): 226 r""" 227 Perform a GET enumerate request and return available resource paths. 228 229 Description of argument(s): 230 resource_path URI resource absolute path (e.g. "/redfish/v1/SessionService/Sessions"). 231 return_json Indicates whether the result should be returned as a json string or as a 232 dictionary. 233 include_dead_resources Check and return a list of dead/broken URI resources. 234 """ 235 236 gp.qprint_executing(style=gp.func_line_style_short) 237 # Set quiet variable to keep subordinate get() calls quiet. 238 quiet = 1 239 240 self.__result = {} 241 # Variable to hold the pending list of resources for which enumeration is yet to be obtained. 242 self.__pending_enumeration = set() 243 self.__pending_enumeration.add(resource_path) 244 245 # Variable having resources for which enumeration is completed. 246 enumerated_resources = set() 247 dead_resources = {} 248 resources_to_be_enumerated = (resource_path,) 249 while resources_to_be_enumerated: 250 for resource in resources_to_be_enumerated: 251 # JsonSchemas, SessionService or URLs containing # are not required in enumeration. 252 # Example: '/redfish/v1/JsonSchemas/' and sub resources. 253 # '/redfish/v1/SessionService' 254 # '/redfish/v1/Managers/${MANAGER_ID}#/Oem' 255 if ( 256 ("JsonSchemas" in resource) 257 or ("SessionService" in resource) 258 or ("#" in resource) 259 ): 260 continue 261 262 self._rest_response_ = self.get( 263 resource, valid_status_codes=[200, 404, 500] 264 ) 265 # Enumeration is done for available resources ignoring the ones for which response is not 266 # obtained. 267 if self._rest_response_.status != 200: 268 if include_dead_resources: 269 try: 270 dead_resources[self._rest_response_.status].append( 271 resource 272 ) 273 except KeyError: 274 dead_resources[self._rest_response_.status] = [ 275 resource 276 ] 277 continue 278 self.walk_nested_dict(self._rest_response_.dict, url=resource) 279 280 enumerated_resources.update(set(resources_to_be_enumerated)) 281 resources_to_be_enumerated = tuple( 282 self.__pending_enumeration - enumerated_resources 283 ) 284 285 if return_json: 286 if include_dead_resources: 287 return ( 288 json.dumps( 289 self.__result, 290 sort_keys=True, 291 indent=4, 292 separators=(",", ": "), 293 ), 294 dead_resources, 295 ) 296 else: 297 return json.dumps( 298 self.__result, 299 sort_keys=True, 300 indent=4, 301 separators=(",", ": "), 302 ) 303 else: 304 if include_dead_resources: 305 return self.__result, dead_resources 306 else: 307 return self.__result 308 309 def walk_nested_dict(self, data, url=""): 310 r""" 311 Parse through the nested dictionary and get the resource id paths. 312 313 Description of argument(s): 314 data Nested dictionary data from response message. 315 url Resource for which the response is obtained in data. 316 """ 317 url = url.rstrip("/") 318 319 for key, value in data.items(): 320 # Recursion if nested dictionary found. 321 if isinstance(value, dict): 322 self.walk_nested_dict(value) 323 else: 324 # Value contains a list of dictionaries having member data. 325 if "Members" == key: 326 if isinstance(value, list): 327 for memberDict in value: 328 self.__pending_enumeration.add( 329 memberDict["@odata.id"] 330 ) 331 if "@odata.id" == key: 332 value = value.rstrip("/") 333 # Data for the given url. 334 if value == url: 335 self.__result[url] = data 336 # Data still needs to be looked up, 337 else: 338 self.__pending_enumeration.add(value) 339 340 def get_members_list(self, resource_path, filter=None): 341 r""" 342 Return members list in a given URL. 343 344 Description of argument(s): 345 resource_path URI resource absolute path (e.g. "/redfish/v1/AccountService/Accounts"). 346 filter strings or regex 347 348 /redfish/v1/AccountService/Accounts/ 349 { 350 "@odata.id": "/redfish/v1/AccountService/Accounts", 351 "@odata.type": "#ManagerAccountCollection.ManagerAccountCollection", 352 "Description": "BMC User Accounts", 353 "Members": [ 354 { 355 "@odata.id": "/redfish/v1/AccountService/Accounts/root" 356 }, 357 { 358 "@odata.id": "/redfish/v1/AccountService/Accounts/admin" 359 } 360 ], 361 "Members@odata.count": 2, 362 "Name": "Accounts Collection" 363 } 364 365 Return list of members if no filter is applied as: 366 ['/redfish/v1/AccountService/Accounts/root', "/redfish/v1/AccountService/Accounts/admin"] 367 368 Return list of members if filter (e.g "root") is applied as: 369 ['/redfish/v1/AccountService/Accounts/root'] 370 371 372 Calling from robot code: 373 ${resp}= Redfish.Get Members List /redfish/v1/AccountService/Accounts 374 ${resp}= Redfish.Get Members List /redfish/v1/AccountService/Accounts filter=root 375 """ 376 377 member_list = [] 378 self._rest_response_ = self.get( 379 resource_path, valid_status_codes=[200] 380 ) 381 382 try: 383 for member in self._rest_response_.dict["Members"]: 384 member_list.append(member["@odata.id"]) 385 except KeyError: 386 # Non Members child objects at the top level, ignore. 387 pass 388 389 # Filter elements in the list and return matched elements. 390 if filter is not None: 391 regex = ".*/" + filter + "[^/]*$" 392 return [x for x in member_list if re.match(regex, x)] 393 394 return member_list 395