xref: /openbmc/openbmc-test-automation/lib/bmc_redfish.py (revision 16920a5a5a2f76252947488afa99ba830709dd45)
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        except KeyError:
127            except_type, except_value, except_traceback = sys.exc_info()
128            BuiltIn().log_to_console(str(except_type))
129            BuiltIn().log_to_console(str(except_value))
130            e_message = "Login failed, something went wrong during request. "
131            e_message += "Error usually due to SessionCreationError or "
132            e_message += "InvalidCredentialsError resulting in failure"
133            BuiltIn().log_to_console(e_message)
134            # Custom BaseException error message from KeyError exception.
135            raise KeyError("SessionCreationError or InvalidCredentialsError")
136
137    def logout(self):
138        if MTLS_ENABLED == "True":
139            return None
140        else:
141            super(bmc_redfish, self).logout()
142
143    def get_properties(self, *args, **kwargs):
144        r"""
145        Return dictionary of attributes for a given path.
146
147        The difference between calling this function and calling get()
148        directly is that this function returns ONLY the dictionary portion of
149        the response object.
150
151        Example robot code:
152
153        ${properties}=  Get Properties  /redfish/v1/Systems/system/
154        Rprint Vars  properties
155
156        Output:
157
158        properties:
159          [PowerState]:      Off
160          [Processors]:
161            [@odata.id]:     /redfish/v1/Systems/system/Processors
162          [SerialNumber]:    1234567
163          ...
164
165        Description of argument(s):
166        args                        See parent class get() prolog for details.
167        kwargs                      See parent class get() prolog for details.
168        """
169
170        resp = self.get(*args, **kwargs)
171        return resp.dict if hasattr(resp, "dict") else {}
172
173    def get_attribute(self, path, attribute, default=None, *args, **kwargs):
174        r"""
175        Get and return the named attribute from the properties for a given
176        path.
177
178        This method has the following advantages over calling get_properties
179        directly:
180        - The caller can specify a default value to be returned if the
181          attribute does not exist.
182
183        Example robot code:
184
185        ${attribute}=  Get Attribute  /redfish/v1/AccountService
186        ...  MaxPasswordLength  default=600
187        Rprint Vars  attribute
188
189        Output:
190
191        attribute:           31
192
193        Description of argument(s):
194        path                        The path (e.g.
195                                    "/redfish/v1/AccountService").
196        attribute                   The name of the attribute to be retrieved
197                                    (e.g. "MaxPasswordLength").
198        default                     The default value to be returned if the
199                                    attribute does not exist (e.g. "").
200        args                        See parent class get() prolog for details.
201        kwargs                      See parent class get() prolog for details.
202        """
203
204        return self.get_properties(path, *args, **kwargs).get(
205            attribute, default
206        )
207
208    def get_session_info(self):
209        r"""
210        Get and return session info as a tuple consisting of session_key and
211        session_location.
212        """
213
214        return self.get_session_key(), self.get_session_location()
215
216    def get_session_response(self):
217        r"""
218        Return session response dictionary data.
219        """
220
221        return self.__dict__
222
223    def enumerate(
224        self, resource_path, return_json=1, include_dead_resources=False
225    ):
226        r"""
227        Perform a GET enumerate request and return available resource paths.
228
229        Description of argument(s):
230        resource_path               URI resource absolute path (e.g. "/redfish/v1/SessionService/Sessions").
231        return_json                 Indicates whether the result should be returned as a json string or as a
232                                    dictionary.
233        include_dead_resources      Check and return a list of dead/broken URI resources.
234        """
235
236        gp.qprint_executing(style=gp.func_line_style_short)
237        # Set quiet variable to keep subordinate get() calls quiet.
238        quiet = 1
239
240        self.__result = {}
241        # Variable to hold the pending list of resources for which enumeration is yet to be obtained.
242        self.__pending_enumeration = set()
243        self.__pending_enumeration.add(resource_path)
244
245        # Variable having resources for which enumeration is completed.
246        enumerated_resources = set()
247        dead_resources = {}
248        resources_to_be_enumerated = (resource_path,)
249        while resources_to_be_enumerated:
250            for resource in resources_to_be_enumerated:
251                # JsonSchemas, SessionService or URLs containing # are not required in enumeration.
252                # Example: '/redfish/v1/JsonSchemas/' and sub resources.
253                #          '/redfish/v1/SessionService'
254                #          '/redfish/v1/Managers/${MANAGER_ID}#/Oem'
255                if (
256                    ("JsonSchemas" in resource)
257                    or ("SessionService" in resource)
258                    or ("#" in resource)
259                ):
260                    continue
261
262                self._rest_response_ = self.get(
263                    resource, valid_status_codes=[200, 404, 500]
264                )
265                # Enumeration is done for available resources ignoring the ones for which response is not
266                # obtained.
267                if self._rest_response_.status != 200:
268                    if include_dead_resources:
269                        try:
270                            dead_resources[self._rest_response_.status].append(
271                                resource
272                            )
273                        except KeyError:
274                            dead_resources[self._rest_response_.status] = [
275                                resource
276                            ]
277                    continue
278                self.walk_nested_dict(self._rest_response_.dict, url=resource)
279
280            enumerated_resources.update(set(resources_to_be_enumerated))
281            resources_to_be_enumerated = tuple(
282                self.__pending_enumeration - enumerated_resources
283            )
284
285        if return_json:
286            if include_dead_resources:
287                return (
288                    json.dumps(
289                        self.__result,
290                        sort_keys=True,
291                        indent=4,
292                        separators=(",", ": "),
293                    ),
294                    dead_resources,
295                )
296            else:
297                return json.dumps(
298                    self.__result,
299                    sort_keys=True,
300                    indent=4,
301                    separators=(",", ": "),
302                )
303        else:
304            if include_dead_resources:
305                return self.__result, dead_resources
306            else:
307                return self.__result
308
309    def walk_nested_dict(self, data, url=""):
310        r"""
311        Parse through the nested dictionary and get the resource id paths.
312
313        Description of argument(s):
314        data                        Nested dictionary data from response message.
315        url                         Resource for which the response is obtained in data.
316        """
317        url = url.rstrip("/")
318
319        for key, value in data.items():
320            # Recursion if nested dictionary found.
321            if isinstance(value, dict):
322                self.walk_nested_dict(value)
323            else:
324                # Value contains a list of dictionaries having member data.
325                if "Members" == key:
326                    if isinstance(value, list):
327                        for memberDict in value:
328                            self.__pending_enumeration.add(
329                                memberDict["@odata.id"]
330                            )
331                if "@odata.id" == key:
332                    value = value.rstrip("/")
333                    # Data for the given url.
334                    if value == url:
335                        self.__result[url] = data
336                    # Data still needs to be looked up,
337                    else:
338                        self.__pending_enumeration.add(value)
339
340    def get_members_list(self, resource_path, filter=None):
341        r"""
342        Return members list in a given URL.
343
344        Description of argument(s):
345        resource_path    URI resource absolute path (e.g. "/redfish/v1/AccountService/Accounts").
346        filter           strings or regex
347
348        /redfish/v1/AccountService/Accounts/
349        {
350            "@odata.id": "/redfish/v1/AccountService/Accounts",
351            "@odata.type": "#ManagerAccountCollection.ManagerAccountCollection",
352            "Description": "BMC User Accounts",
353            "Members": [
354                {
355                    "@odata.id": "/redfish/v1/AccountService/Accounts/root"
356                },
357                {
358                    "@odata.id": "/redfish/v1/AccountService/Accounts/admin"
359                }
360           ],
361           "Members@odata.count": 2,
362           "Name": "Accounts Collection"
363        }
364
365        Return list of members if no filter is applied as:
366        ['/redfish/v1/AccountService/Accounts/root', "/redfish/v1/AccountService/Accounts/admin"]
367
368        Return list of members if filter (e.g "root") is applied as:
369        ['/redfish/v1/AccountService/Accounts/root']
370
371
372        Calling from robot code:
373           ${resp}=  Redfish.Get Members List  /redfish/v1/AccountService/Accounts
374           ${resp}=  Redfish.Get Members List  /redfish/v1/AccountService/Accounts  filter=root
375        """
376
377        member_list = []
378        self._rest_response_ = self.get(
379            resource_path, valid_status_codes=[200]
380        )
381
382        try:
383            for member in self._rest_response_.dict["Members"]:
384                member_list.append(member["@odata.id"])
385        except KeyError:
386            # Non Members child objects at the top level, ignore.
387            pass
388
389        # Filter elements in the list and return matched elements.
390        if filter is not None:
391            regex = ".*/" + filter + "[^/]*$"
392            return [x for x in member_list if re.match(regex, x)]
393
394        return member_list
395