1#!/usr/bin/env python 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 if target_attribute in target: 184 return target 185 186 return None 187 188 def get_member_list(self, resource_path): 189 r""" 190 Perform a GET list request and return available members entries. 191 192 Description of argument(s): 193 resource_path URI resource absolute path 194 (e.g. "/redfish/v1/SessionService/Sessions"). 195 196 "Members": [ 197 { 198 "@odata.id": "/redfish/v1/SessionService/Sessions/Z5HummWPZ7" 199 } 200 { 201 "@odata.id": "/redfish/v1/SessionService/Sessions/46CmQmEL7H" 202 } 203 ], 204 """ 205 206 member_list = [] 207 resp_list_dict = self.get_attribute(resource_path, "Members") 208 if resp_list_dict is None: 209 return member_list 210 211 for member_id in range(0, len(resp_list_dict)): 212 member_list.append(resp_list_dict[member_id]["@odata.id"]) 213 214 return member_list 215 216 def list_request(self, resource_path): 217 r""" 218 Perform a GET list request and return available resource paths. 219 Description of argument(s): 220 resource_path URI resource absolute path 221 (e.g. "/redfish/v1/SessionService/Sessions"). 222 """ 223 gp.qprint_executing(style=gp.func_line_style_short) 224 # Set quiet variable to keep subordinate get() calls quiet. 225 quiet = 1 226 self.__pending_enumeration = set() 227 self._rest_response_ = \ 228 self._redfish_.get(resource_path, 229 valid_status_codes=[200, 404, 500]) 230 231 # Return empty list. 232 if self._rest_response_.status != 200: 233 return self.__pending_enumeration 234 self.walk_nested_dict(self._rest_response_.dict) 235 if not self.__pending_enumeration: 236 return resource_path 237 for resource in self.__pending_enumeration.copy(): 238 self._rest_response_ = \ 239 self._redfish_.get(resource, 240 valid_status_codes=[200, 404, 500]) 241 242 if self._rest_response_.status != 200: 243 continue 244 self.walk_nested_dict(self._rest_response_.dict) 245 return list(sorted(self.__pending_enumeration)) 246 247 def enumerate_request(self, resource_path, return_json=1, 248 include_dead_resources=False): 249 r""" 250 Perform a GET enumerate request and return available resource paths. 251 252 Description of argument(s): 253 resource_path URI resource absolute path (e.g. 254 "/redfish/v1/SessionService/Sessions"). 255 return_json Indicates whether the result should be 256 returned as a json string or as a 257 dictionary. 258 include_dead_resources Check and return a list of dead/broken URI 259 resources. 260 """ 261 262 gp.qprint_executing(style=gp.func_line_style_short) 263 264 return_json = int(return_json) 265 266 # Set quiet variable to keep subordinate get() calls quiet. 267 quiet = 1 268 269 # Variable to hold enumerated data. 270 self.__result = {} 271 272 # Variable to hold the pending list of resources for which enumeration. 273 # is yet to be obtained. 274 self.__pending_enumeration = set() 275 276 self.__pending_enumeration.add(resource_path) 277 278 # Variable having resources for which enumeration is completed. 279 enumerated_resources = set() 280 281 if include_dead_resources: 282 dead_resources = {} 283 284 resources_to_be_enumerated = (resource_path,) 285 286 while resources_to_be_enumerated: 287 for resource in resources_to_be_enumerated: 288 # JsonSchemas, SessionService or URLs containing # are not 289 # required in enumeration. 290 # Example: '/redfish/v1/JsonSchemas/' and sub resources. 291 # '/redfish/v1/SessionService' 292 # '/redfish/v1/Managers/bmc#/Oem' 293 if ('JsonSchemas' in resource) or ('SessionService' in resource)\ 294 or ('#' in resource): 295 continue 296 297 self._rest_response_ = \ 298 self._redfish_.get(resource, valid_status_codes=[200, 404, 500]) 299 # Enumeration is done for available resources ignoring the 300 # ones for which response is not obtained. 301 if self._rest_response_.status != 200: 302 if include_dead_resources: 303 try: 304 dead_resources[self._rest_response_.status].append( 305 resource) 306 except KeyError: 307 dead_resources[self._rest_response_.status] = \ 308 [resource] 309 continue 310 311 self.walk_nested_dict(self._rest_response_.dict, url=resource) 312 313 enumerated_resources.update(set(resources_to_be_enumerated)) 314 resources_to_be_enumerated = \ 315 tuple(self.__pending_enumeration - enumerated_resources) 316 317 if return_json: 318 if include_dead_resources: 319 return json.dumps(self.__result, sort_keys=True, 320 indent=4, separators=(',', ': ')), dead_resources 321 else: 322 return json.dumps(self.__result, sort_keys=True, 323 indent=4, separators=(',', ': ')) 324 else: 325 if include_dead_resources: 326 return self.__result, dead_resources 327 else: 328 return self.__result 329 330 def walk_nested_dict(self, data, url=''): 331 r""" 332 Parse through the nested dictionary and get the resource id paths. 333 Description of argument(s): 334 data Nested dictionary data from response message. 335 url Resource for which the response is obtained in data. 336 """ 337 url = url.rstrip('/') 338 339 for key, value in data.items(): 340 341 # Recursion if nested dictionary found. 342 if isinstance(value, dict): 343 self.walk_nested_dict(value) 344 else: 345 # Value contains a list of dictionaries having member data. 346 if 'Members' == key: 347 if isinstance(value, list): 348 for memberDict in value: 349 self.__pending_enumeration.add(memberDict['@odata.id']) 350 if '@odata.id' == key: 351 value = value.rstrip('/') 352 # Data for the given url. 353 if value == url: 354 self.__result[url] = data 355 # Data still needs to be looked up, 356 else: 357 self.__pending_enumeration.add(value) 358 359 def get_key_value_nested_dict(self, data, key): 360 r""" 361 Parse through the nested dictionary and get the searched key value. 362 363 Description of argument(s): 364 data Nested dictionary data from response message. 365 key Search dictionary key element. 366 """ 367 368 for k, v in data.items(): 369 if isinstance(v, dict): 370 self.get_key_value_nested_dict(v, key) 371 372 if k == key: 373 target_list.append(v) 374