1#!/usr/bin/env python3 2 3r""" 4See class prolog below for details. 5""" 6 7import json 8import re 9import sys 10from json.decoder import JSONDecodeError 11 12import func_args as fa 13import gen_print as gp 14from redfish.rest.v1 import InvalidCredentialsError 15from redfish_plus import redfish_plus 16from robot.libraries.BuiltIn import BuiltIn 17 18MTLS_ENABLED = BuiltIn().get_variable_value("${MTLS_ENABLED}") 19 20 21class bmc_redfish(redfish_plus): 22 r""" 23 bmc_redfish is a child class of redfish_plus that is designed to provide 24 benefits specifically for using redfish to communicate with an OpenBMC. 25 26 See the prologs of the methods below for details. 27 """ 28 29 def __init__(self, *args, **kwargs): 30 r""" 31 Do BMC-related redfish initialization. 32 33 Presently, older versions of BMC code may not support redfish 34 requests. This can lead to unsightly error text being printed out for 35 programs that may use lib/bmc_redfish_resource.robot even though they 36 don't necessarily intend to make redfish requests. 37 38 This class method will make an attempt to tolerate this situation. At 39 some future point, when all BMCs can be expected to support redfish, 40 this class method may be considered for deletion. If it is deleted, 41 the self.__inited__ test code in the login() class method below should 42 likewise be deleted. 43 """ 44 self.__inited__ = False 45 try: 46 if MTLS_ENABLED == "True": 47 self.__inited__ = True 48 else: 49 super(bmc_redfish, self).__init__(*args, **kwargs) 50 self.__inited__ = True 51 except ValueError as get_exception: 52 except_type, except_value, except_traceback = sys.exc_info() 53 regex = r"The HTTP status code was not valid:[\r\n]+status:[ ]+502" 54 result = re.match(regex, str(except_value), flags=re.MULTILINE) 55 if not result: 56 gp.lprint_var(except_type) 57 gp.lprint_varx("except_value", str(except_value)) 58 raise (get_exception) 59 except AttributeError as e: 60 BuiltIn().log_to_console( 61 "AttributeError: Error response from server" 62 ) 63 except Exception as e: 64 error_response = type(e).__name__ + " from bmc_redfish class" 65 BuiltIn().log_to_console(error_response) 66 67 BuiltIn().set_global_variable("${REDFISH_SUPPORTED}", self.__inited__) 68 BuiltIn().set_global_variable("${REDFISH_REST_SUPPORTED}", True) 69 70 def login(self, *args, **kwargs): 71 r""" 72 Assign BMC default values for username, password and auth arguments 73 and call parent class login method. 74 75 Description of argument(s): 76 args See parent class method prolog for details. 77 kwargs See parent class method prolog for details. 78 """ 79 80 if MTLS_ENABLED == "True": 81 return None 82 if not self.__inited__: 83 message = "bmc_redfish.__init__() was never successfully run. It " 84 message += "is likely that the target BMC firmware code level " 85 message += "does not support redfish.\n" 86 raise ValueError(message) 87 # Assign default values for username, password, auth where necessary. 88 openbmc_username = BuiltIn().get_variable_value("${OPENBMC_USERNAME}") 89 openbmc_password = BuiltIn().get_variable_value("${OPENBMC_PASSWORD}") 90 username, args, kwargs = fa.pop_arg(openbmc_username, *args, **kwargs) 91 password, args, kwargs = fa.pop_arg(openbmc_password, *args, **kwargs) 92 auth, args, kwargs = fa.pop_arg("session", *args, **kwargs) 93 94 try: 95 super(bmc_redfish, self).login( 96 username, password, auth, *args, **kwargs 97 ) 98 # Handle InvalidCredentialsError. 99 # (raise redfish.rest.v1.InvalidCredentialsError if not [200, 201, 202, 204]) 100 except InvalidCredentialsError: 101 except_type, except_value, except_traceback = sys.exc_info() 102 BuiltIn().log_to_console(str(except_type)) 103 BuiltIn().log_to_console(str(except_value)) 104 e_message = "Re-try login due to exception and " 105 e_message += "it is likely error response from server side." 106 BuiltIn().log_to_console(e_message) 107 super(bmc_redfish, self).login( 108 username, password, auth, *args, **kwargs 109 ) 110 # Handle JSONDecodeError and others. 111 except JSONDecodeError: 112 except_type, except_value, except_traceback = sys.exc_info() 113 BuiltIn().log_to_console(str(except_type)) 114 BuiltIn().log_to_console(str(except_value)) 115 e_message = "Re-try login due to JSONDecodeError exception and " 116 e_message += "it is likely error response from server side." 117 BuiltIn().log_to_console(e_message) 118 super(bmc_redfish, self).login( 119 username, password, auth, *args, **kwargs 120 ) 121 except ValueError: 122 except_type, except_value, except_traceback = sys.exc_info() 123 BuiltIn().log_to_console(str(except_type)) 124 BuiltIn().log_to_console(str(except_value)) 125 e_message = "Unexpected exception." 126 BuiltIn().log_to_console(e_message) 127 128 def logout(self): 129 if MTLS_ENABLED == "True": 130 return None 131 else: 132 super(bmc_redfish, self).logout() 133 134 def get_properties(self, *args, **kwargs): 135 r""" 136 Return dictionary of attributes for a given path. 137 138 The difference between calling this function and calling get() 139 directly is that this function returns ONLY the dictionary portion of 140 the response object. 141 142 Example robot code: 143 144 ${properties}= Get Properties /redfish/v1/Systems/system/ 145 Rprint Vars properties 146 147 Output: 148 149 properties: 150 [PowerState]: Off 151 [Processors]: 152 [@odata.id]: /redfish/v1/Systems/system/Processors 153 [SerialNumber]: 1234567 154 ... 155 156 Description of argument(s): 157 args See parent class get() prolog for details. 158 kwargs See parent class get() prolog for details. 159 """ 160 161 resp = self.get(*args, **kwargs) 162 return resp.dict if hasattr(resp, "dict") else {} 163 164 def get_attribute(self, path, attribute, default=None, *args, **kwargs): 165 r""" 166 Get and return the named attribute from the properties for a given 167 path. 168 169 This method has the following advantages over calling get_properties 170 directly: 171 - The caller can specify a default value to be returned if the 172 attribute does not exist. 173 174 Example robot code: 175 176 ${attribute}= Get Attribute /redfish/v1/AccountService 177 ... MaxPasswordLength default=600 178 Rprint Vars attribute 179 180 Output: 181 182 attribute: 31 183 184 Description of argument(s): 185 path The path (e.g. 186 "/redfish/v1/AccountService"). 187 attribute The name of the attribute to be retrieved 188 (e.g. "MaxPasswordLength"). 189 default The default value to be returned if the 190 attribute does not exist (e.g. ""). 191 args See parent class get() prolog for details. 192 kwargs See parent class get() prolog for details. 193 """ 194 195 return self.get_properties(path, *args, **kwargs).get( 196 attribute, default 197 ) 198 199 def get_session_info(self): 200 r""" 201 Get and return session info as a tuple consisting of session_key and 202 session_location. 203 """ 204 205 return self.get_session_key(), self.get_session_location() 206 207 def enumerate( 208 self, resource_path, return_json=1, include_dead_resources=False 209 ): 210 r""" 211 Perform a GET enumerate request and return available resource paths. 212 213 Description of argument(s): 214 resource_path URI resource absolute path (e.g. "/redfish/v1/SessionService/Sessions"). 215 return_json Indicates whether the result should be returned as a json string or as a 216 dictionary. 217 include_dead_resources Check and return a list of dead/broken URI resources. 218 """ 219 220 gp.qprint_executing(style=gp.func_line_style_short) 221 # Set quiet variable to keep subordinate get() calls quiet. 222 quiet = 1 223 224 self.__result = {} 225 # Variable to hold the pending list of resources for which enumeration is yet to be obtained. 226 self.__pending_enumeration = set() 227 self.__pending_enumeration.add(resource_path) 228 229 # Variable having resources for which enumeration is completed. 230 enumerated_resources = set() 231 dead_resources = {} 232 resources_to_be_enumerated = (resource_path,) 233 while resources_to_be_enumerated: 234 for resource in resources_to_be_enumerated: 235 # JsonSchemas, SessionService or URLs containing # are not required in enumeration. 236 # Example: '/redfish/v1/JsonSchemas/' and sub resources. 237 # '/redfish/v1/SessionService' 238 # '/redfish/v1/Managers/${MANAGER_ID}#/Oem' 239 if ( 240 ("JsonSchemas" in resource) 241 or ("SessionService" in resource) 242 or ("#" in resource) 243 ): 244 continue 245 246 self._rest_response_ = self.get( 247 resource, valid_status_codes=[200, 404, 500] 248 ) 249 # Enumeration is done for available resources ignoring the ones for which response is not 250 # obtained. 251 if self._rest_response_.status != 200: 252 if include_dead_resources: 253 try: 254 dead_resources[self._rest_response_.status].append( 255 resource 256 ) 257 except KeyError: 258 dead_resources[self._rest_response_.status] = [ 259 resource 260 ] 261 continue 262 self.walk_nested_dict(self._rest_response_.dict, url=resource) 263 264 enumerated_resources.update(set(resources_to_be_enumerated)) 265 resources_to_be_enumerated = tuple( 266 self.__pending_enumeration - enumerated_resources 267 ) 268 269 if return_json: 270 if include_dead_resources: 271 return ( 272 json.dumps( 273 self.__result, 274 sort_keys=True, 275 indent=4, 276 separators=(",", ": "), 277 ), 278 dead_resources, 279 ) 280 else: 281 return json.dumps( 282 self.__result, 283 sort_keys=True, 284 indent=4, 285 separators=(",", ": "), 286 ) 287 else: 288 if include_dead_resources: 289 return self.__result, dead_resources 290 else: 291 return self.__result 292 293 def walk_nested_dict(self, data, url=""): 294 r""" 295 Parse through the nested dictionary and get the resource id paths. 296 297 Description of argument(s): 298 data Nested dictionary data from response message. 299 url Resource for which the response is obtained in data. 300 """ 301 url = url.rstrip("/") 302 303 for key, value in data.items(): 304 # Recursion if nested dictionary found. 305 if isinstance(value, dict): 306 self.walk_nested_dict(value) 307 else: 308 # Value contains a list of dictionaries having member data. 309 if "Members" == key: 310 if isinstance(value, list): 311 for memberDict in value: 312 self.__pending_enumeration.add( 313 memberDict["@odata.id"] 314 ) 315 if "@odata.id" == key: 316 value = value.rstrip("/") 317 # Data for the given url. 318 if value == url: 319 self.__result[url] = data 320 # Data still needs to be looked up, 321 else: 322 self.__pending_enumeration.add(value) 323 324 def get_members_list(self, resource_path, filter=None): 325 r""" 326 Return members list in a given URL. 327 328 Description of argument(s): 329 resource_path URI resource absolute path (e.g. "/redfish/v1/AccountService/Accounts"). 330 filter strings or regex 331 332 /redfish/v1/AccountService/Accounts/ 333 { 334 "@odata.id": "/redfish/v1/AccountService/Accounts", 335 "@odata.type": "#ManagerAccountCollection.ManagerAccountCollection", 336 "Description": "BMC User Accounts", 337 "Members": [ 338 { 339 "@odata.id": "/redfish/v1/AccountService/Accounts/root" 340 }, 341 { 342 "@odata.id": "/redfish/v1/AccountService/Accounts/admin" 343 } 344 ], 345 "Members@odata.count": 2, 346 "Name": "Accounts Collection" 347 } 348 349 Return list of members if no filter is applied as: 350 ['/redfish/v1/AccountService/Accounts/root', "/redfish/v1/AccountService/Accounts/admin"] 351 352 Return list of members if filter (e.g "root") is applied as: 353 ['/redfish/v1/AccountService/Accounts/root'] 354 355 356 Calling from robot code: 357 ${resp}= Redfish.Get Members List /redfish/v1/AccountService/Accounts 358 ${resp}= Redfish.Get Members List /redfish/v1/AccountService/Accounts filter=root 359 """ 360 361 member_list = [] 362 self._rest_response_ = self.get( 363 resource_path, valid_status_codes=[200] 364 ) 365 366 try: 367 for member in self._rest_response_.dict["Members"]: 368 member_list.append(member["@odata.id"]) 369 except KeyError: 370 # Non Members child objects at the top level, ignore. 371 pass 372 373 # Filter elements in the list and return matched elements. 374 if filter is not None: 375 regex = ".*/" + filter + "[^/]*$" 376 return [x for x in member_list if re.match(regex, x)] 377 378 return member_list 379