1#!/usr/bin/env python3 2 3r""" 4BMC redfish utility functions. 5""" 6 7import json 8import re 9from json.decoder import JSONDecodeError 10 11import gen_print as gp 12from robot.libraries.BuiltIn import BuiltIn 13 14MTLS_ENABLED = BuiltIn().get_variable_value("${MTLS_ENABLED}") 15 16 17class bmc_redfish_utils(object): 18 ROBOT_LIBRARY_SCOPE = "TEST SUITE" 19 20 def __init__(self): 21 r""" 22 Initialize the bmc_redfish_utils object. 23 """ 24 # Obtain a reference to the global redfish object. 25 self.__inited__ = False 26 self._redfish_ = BuiltIn().get_library_instance("redfish") 27 28 if MTLS_ENABLED == "True": 29 self.__inited__ = True 30 else: 31 # There is a possibility that a given driver support both redfish and 32 # legacy REST. 33 self._redfish_.login() 34 self._rest_response_ = self._redfish_.get( 35 "/xyz/openbmc_project/", valid_status_codes=[200, 404] 36 ) 37 38 # If REST URL /xyz/openbmc_project/ is supported. 39 if self._rest_response_.status == 200: 40 self.__inited__ = True 41 42 BuiltIn().set_global_variable( 43 "${REDFISH_REST_SUPPORTED}", self.__inited__ 44 ) 45 46 def get_redfish_session_info(self): 47 r""" 48 Returns redfish sessions info dictionary. 49 50 { 51 'key': 'yLXotJnrh5nDhXj5lLiH' , 52 'location': '/redfish/v1/SessionService/Sessions/nblYY4wlz0' 53 } 54 """ 55 session_dict = { 56 "key": self._redfish_.get_session_key(), 57 "location": self._redfish_.get_session_location(), 58 } 59 return session_dict 60 61 def get_attribute(self, resource_path, attribute, verify=None): 62 r""" 63 Get resource attribute. 64 65 Description of argument(s): 66 resource_path URI resource absolute path (e.g. 67 "/redfish/v1/Systems/1"). 68 attribute Name of the attribute (e.g. 'PowerState'). 69 """ 70 try: 71 resp = self._redfish_.get(resource_path) 72 except JSONDecodeError as e: 73 BuiltIn().log_to_console( 74 "get_attribute: JSONDecodeError, re-trying" 75 ) 76 resp = self._redfish_.get(resource_path) 77 78 if verify: 79 if resp.dict[attribute] == verify: 80 return resp.dict[attribute] 81 else: 82 raise ValueError("Attribute value is not equal") 83 elif attribute in resp.dict: 84 return resp.dict[attribute] 85 86 return None 87 88 def get_properties(self, resource_path): 89 r""" 90 Returns dictionary of attributes for the resource. 91 92 Description of argument(s): 93 resource_path URI resource absolute path (e.g. 94 /redfish/v1/Systems/1"). 95 """ 96 97 resp = self._redfish_.get(resource_path) 98 return resp.dict 99 100 def get_members_uri(self, resource_path, attribute): 101 r""" 102 Returns the list of valid path which has a given attribute. 103 104 Description of argument(s): 105 resource_path URI resource base path (e.g. 106 '/redfish/v1/Systems/', 107 '/redfish/v1/Chassis/'). 108 attribute Name of the attribute (e.g. 'PowerSupplies'). 109 """ 110 111 # Set quiet variable to keep subordinate get() calls quiet. 112 quiet = 1 113 114 # Get the member id list. 115 # e.g. ['/redfish/v1/Chassis/foo', '/redfish/v1/Chassis/bar'] 116 resource_path_list = self.get_member_list(resource_path) 117 118 valid_path_list = [] 119 120 for path_idx in resource_path_list: 121 # Get all the child object path under the member id e.g. 122 # ['/redfish/v1/Chassis/foo/Power','/redfish/v1/Chassis/bar/Power'] 123 child_path_list = self.list_request(path_idx) 124 125 # Iterate and check if path object has the attribute. 126 for child_path_idx in child_path_list: 127 if ( 128 ("JsonSchemas" in child_path_idx) 129 or ("SessionService" in child_path_idx) 130 or ("#" in child_path_idx) 131 ): 132 continue 133 if self.get_attribute(child_path_idx, attribute): 134 valid_path_list.append(child_path_idx) 135 136 BuiltIn().log_to_console(valid_path_list) 137 return valid_path_list 138 139 def get_endpoint_path_list(self, resource_path, end_point_prefix): 140 r""" 141 Returns list with entries ending in "/endpoint". 142 143 Description of argument(s): 144 resource_path URI resource base path (e.g. "/redfish/v1/Chassis/"). 145 end_point_prefix Name of the endpoint (e.g. 'Power'). 146 147 Find all list entries ending in "/endpoint" combination such as 148 /redfish/v1/Chassis/<foo>/Power 149 /redfish/v1/Chassis/<bar>/Power 150 """ 151 152 end_point_list = self.list_request(resource_path) 153 154 # Regex to match entries ending in "/prefix" with optional underscore. 155 regex = ".*/" + end_point_prefix + "[_]?[0-9]*$" 156 return [x for x in end_point_list if re.match(regex, x, re.IGNORECASE)] 157 158 def get_target_actions(self, resource_path, target_attribute): 159 r""" 160 Returns resource target entry of the searched target attribute. 161 162 Description of argument(s): 163 resource_path URI resource absolute path 164 (e.g. "/redfish/v1/Systems/system"). 165 target_attribute Name of the attribute (e.g. 'ComputerSystem.Reset'). 166 167 Example: 168 "Actions": { 169 "#ComputerSystem.Reset": { 170 "ResetType@Redfish.AllowableValues": [ 171 "On", 172 "ForceOff", 173 "GracefulRestart", 174 "GracefulShutdown" 175 ], 176 "target": "/redfish/v1/Systems/system/Actions/ComputerSystem.Reset" 177 } 178 } 179 """ 180 181 global target_list 182 target_list = [] 183 184 resp_dict = self.get_attribute(resource_path, "Actions") 185 if resp_dict is None: 186 return None 187 188 # Recursively search the "target" key in the nested dictionary. 189 # Populate the target_list of target entries. 190 self.get_key_value_nested_dict(resp_dict, "target") 191 # Return the matching target URL entry. 192 for target in target_list: 193 # target "/redfish/v1/Systems/system/Actions/ComputerSystem.Reset" 194 attribute_in_uri = target.rsplit("/", 1)[-1] 195 # attribute_in_uri "ComputerSystem.Reset" 196 if target_attribute == attribute_in_uri: 197 return target 198 199 return None 200 201 def get_member_list(self, resource_path): 202 r""" 203 Perform a GET list request and return available members entries. 204 205 Description of argument(s): 206 resource_path URI resource absolute path 207 (e.g. "/redfish/v1/SessionService/Sessions"). 208 209 "Members": [ 210 { 211 "@odata.id": "/redfish/v1/SessionService/Sessions/Z5HummWPZ7" 212 } 213 { 214 "@odata.id": "/redfish/v1/SessionService/Sessions/46CmQmEL7H" 215 } 216 ], 217 """ 218 219 member_list = [] 220 resp_list_dict = self.get_attribute(resource_path, "Members") 221 if resp_list_dict is None: 222 return member_list 223 224 for member_id in range(0, len(resp_list_dict)): 225 member_list.append(resp_list_dict[member_id]["@odata.id"]) 226 227 return member_list 228 229 def list_request(self, resource_path): 230 r""" 231 Perform a GET list request and return available resource paths. 232 Description of argument(s): 233 resource_path URI resource absolute path 234 (e.g. "/redfish/v1/SessionService/Sessions"). 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 self.__pending_enumeration = set() 240 self._rest_response_ = self._redfish_.get( 241 resource_path, valid_status_codes=[200, 404, 500] 242 ) 243 244 # Return empty list. 245 if self._rest_response_.status != 200: 246 return self.__pending_enumeration 247 self.walk_nested_dict(self._rest_response_.dict) 248 if not self.__pending_enumeration: 249 return resource_path 250 for resource in self.__pending_enumeration.copy(): 251 self._rest_response_ = self._redfish_.get( 252 resource, valid_status_codes=[200, 404, 500] 253 ) 254 255 if self._rest_response_.status != 200: 256 continue 257 self.walk_nested_dict(self._rest_response_.dict) 258 return list(sorted(self.__pending_enumeration)) 259 260 def enumerate_request( 261 self, resource_path, return_json=1, include_dead_resources=False 262 ): 263 r""" 264 Perform a GET enumerate request and return available resource paths. 265 266 Description of argument(s): 267 resource_path URI resource absolute path (e.g. 268 "/redfish/v1/SessionService/Sessions"). 269 return_json Indicates whether the result should be 270 returned as a json string or as a 271 dictionary. 272 include_dead_resources Check and return a list of dead/broken URI 273 resources. 274 """ 275 276 gp.qprint_executing(style=gp.func_line_style_short) 277 278 return_json = int(return_json) 279 280 # Set quiet variable to keep subordinate get() calls quiet. 281 quiet = 1 282 283 # Variable to hold enumerated data. 284 self.__result = {} 285 286 # Variable to hold the pending list of resources for which enumeration. 287 # is yet to be obtained. 288 self.__pending_enumeration = set() 289 290 self.__pending_enumeration.add(resource_path) 291 292 # Variable having resources for which enumeration is completed. 293 enumerated_resources = set() 294 295 if include_dead_resources: 296 dead_resources = {} 297 298 resources_to_be_enumerated = (resource_path,) 299 300 while resources_to_be_enumerated: 301 for resource in resources_to_be_enumerated: 302 # JsonSchemas, SessionService or URLs containing # are not 303 # required in enumeration. 304 # Example: '/redfish/v1/JsonSchemas/' and sub resources. 305 # '/redfish/v1/SessionService' 306 # '/redfish/v1/Managers/${MANAGER_ID}#/Oem' 307 if ( 308 ("JsonSchemas" in resource) 309 or ("SessionService" in resource) 310 or ("PostCodes" in resource) 311 or ("Registries" in resource) 312 or ("Journal" in resource) 313 or ("#" in resource) 314 ): 315 continue 316 317 try: 318 self._rest_response_ = self._redfish_.get( 319 resource, valid_status_codes=[200, 404, 405, 500] 320 ) 321 except JSONDecodeError as e: 322 BuiltIn().log_to_console( 323 "enumerate_request: JSONDecodeError, re-trying" 324 ) 325 self._rest_response_ = self._redfish_.get( 326 resource, valid_status_codes=[200, 404, 405, 500] 327 ) 328 329 # Enumeration is done for available resources ignoring the 330 # ones for which response is not obtained. 331 if self._rest_response_.status != 200: 332 if include_dead_resources: 333 try: 334 dead_resources[self._rest_response_.status].append( 335 resource 336 ) 337 except KeyError: 338 dead_resources[self._rest_response_.status] = [ 339 resource 340 ] 341 continue 342 343 self.walk_nested_dict(self._rest_response_.dict, url=resource) 344 345 enumerated_resources.update(set(resources_to_be_enumerated)) 346 resources_to_be_enumerated = tuple( 347 self.__pending_enumeration - enumerated_resources 348 ) 349 350 if return_json: 351 if include_dead_resources: 352 return ( 353 json.dumps( 354 self.__result, 355 sort_keys=True, 356 indent=4, 357 separators=(",", ": "), 358 ), 359 dead_resources, 360 ) 361 else: 362 return json.dumps( 363 self.__result, 364 sort_keys=True, 365 indent=4, 366 separators=(",", ": "), 367 ) 368 else: 369 if include_dead_resources: 370 return self.__result, dead_resources 371 else: 372 return self.__result 373 374 def walk_nested_dict(self, data, url=""): 375 r""" 376 Parse through the nested dictionary and get the resource id paths. 377 Description of argument(s): 378 data Nested dictionary data from response message. 379 url Resource for which the response is obtained in data. 380 """ 381 url = url.rstrip("/") 382 383 for key, value in data.items(): 384 # Recursion if nested dictionary found. 385 if isinstance(value, dict): 386 self.walk_nested_dict(value) 387 else: 388 # Value contains a list of dictionaries having member data. 389 if "Members" == key: 390 if isinstance(value, list): 391 for memberDict in value: 392 if isinstance(memberDict, str): 393 self.__pending_enumeration.add(memberDict) 394 else: 395 self.__pending_enumeration.add( 396 memberDict["@odata.id"] 397 ) 398 399 if "@odata.id" == key: 400 value = value.rstrip("/") 401 # Data for the given url. 402 if value == url: 403 self.__result[url] = data 404 # Data still needs to be looked up, 405 else: 406 self.__pending_enumeration.add(value) 407 408 def get_key_value_nested_dict(self, data, key): 409 r""" 410 Parse through the nested dictionary and get the searched key value. 411 412 Description of argument(s): 413 data Nested dictionary data from response message. 414 key Search dictionary key element. 415 """ 416 417 for k, v in data.items(): 418 if isinstance(v, dict): 419 self.get_key_value_nested_dict(v, key) 420 421 if k == key: 422 target_list.append(v) 423