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