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