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