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