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 127 def logout(self): 128 if MTLS_ENABLED == "True": 129 return None 130 else: 131 super(bmc_redfish, self).logout() 132 133 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/ 144 Rprint Vars properties 145 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) 161 return resp.dict if hasattr(resp, "dict") else {} 162 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 194 return self.get_properties(path, *args, **kwargs).get( 195 attribute, default 196 ) 197 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() 205 206 def get_session_response(self): 207 r""" 208 Return session response dictionary data. 209 """ 210 211 return self.__dict__ 212 213 def enumerate( 214 self, resource_path, return_json=1, include_dead_resources=False 215 ): 216 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' 244 # '/redfish/v1/Managers/${MANAGER_ID}#/Oem' 245 if ( 246 ("JsonSchemas" in resource) 247 or ("SessionService" in resource) 248 or ("#" in resource) 249 ): 250 continue 251 252 self._rest_response_ = self.get( 253 resource, valid_status_codes=[200, 404, 500] 254 ) 255 # 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: 260 dead_resources[self._rest_response_.status].append( 261 resource 262 ) 263 except KeyError: 264 dead_resources[self._rest_response_.status] = [ 265 resource 266 ] 267 continue 268 self.walk_nested_dict(self._rest_response_.dict, url=resource) 269 270 enumerated_resources.update(set(resources_to_be_enumerated)) 271 resources_to_be_enumerated = tuple( 272 self.__pending_enumeration - enumerated_resources 273 ) 274 275 if return_json: 276 if include_dead_resources: 277 return ( 278 json.dumps( 279 self.__result, 280 sort_keys=True, 281 indent=4, 282 separators=(",", ": "), 283 ), 284 dead_resources, 285 ) 286 else: 287 return json.dumps( 288 self.__result, 289 sort_keys=True, 290 indent=4, 291 separators=(",", ": "), 292 ) 293 else: 294 if include_dead_resources: 295 return self.__result, dead_resources 296 else: 297 return self.__result 298 299 def walk_nested_dict(self, data, url=""): 300 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 """ 307 url = url.rstrip("/") 308 309 for key, value in data.items(): 310 # 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. 315 if "Members" == key: 316 if isinstance(value, list): 317 for memberDict in value: 318 self.__pending_enumeration.add( 319 memberDict["@odata.id"] 320 ) 321 if "@odata.id" == key: 322 value = value.rstrip("/") 323 # 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) 329 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 = [] 368 self._rest_response_ = self.get( 369 resource_path, valid_status_codes=[200] 370 ) 371 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: 381 regex = ".*/" + filter + "[^/]*$" 382 return [x for x in member_list if re.match(regex, x)] 383 384 return member_list 385