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