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