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