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