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