xref: /openbmc/openbmc-test-automation/lib/bmc_redfish.py (revision f719f24fc618dfbe05d0a001cca8e4702fb486d4)
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 enumerate(
207        self, resource_path, return_json=1, include_dead_resources=False
208    ):
209        r"""
210        Perform a GET enumerate request and return available resource paths.
211
212        Description of argument(s):
213        resource_path               URI resource absolute path (e.g. "/redfish/v1/SessionService/Sessions").
214        return_json                 Indicates whether the result should be returned as a json string or as a
215                                    dictionary.
216        include_dead_resources      Check and return a list of dead/broken URI resources.
217        """
218
219        gp.qprint_executing(style=gp.func_line_style_short)
220        # Set quiet variable to keep subordinate get() calls quiet.
221        quiet = 1
222
223        self.__result = {}
224        # Variable to hold the pending list of resources for which enumeration is yet to be obtained.
225        self.__pending_enumeration = set()
226        self.__pending_enumeration.add(resource_path)
227
228        # Variable having resources for which enumeration is completed.
229        enumerated_resources = set()
230        dead_resources = {}
231        resources_to_be_enumerated = (resource_path,)
232        while resources_to_be_enumerated:
233            for resource in resources_to_be_enumerated:
234                # JsonSchemas, SessionService or URLs containing # are not required in enumeration.
235                # Example: '/redfish/v1/JsonSchemas/' and sub resources.
236                #          '/redfish/v1/SessionService'
237                #          '/redfish/v1/Managers/${MANAGER_ID}#/Oem'
238                if (
239                    ("JsonSchemas" in resource)
240                    or ("SessionService" in resource)
241                    or ("#" in resource)
242                ):
243                    continue
244
245                self._rest_response_ = self.get(
246                    resource, valid_status_codes=[200, 404, 500]
247                )
248                # Enumeration is done for available resources ignoring the ones for which response is not
249                # obtained.
250                if self._rest_response_.status != 200:
251                    if include_dead_resources:
252                        try:
253                            dead_resources[self._rest_response_.status].append(
254                                resource
255                            )
256                        except KeyError:
257                            dead_resources[self._rest_response_.status] = [
258                                resource
259                            ]
260                    continue
261                self.walk_nested_dict(self._rest_response_.dict, url=resource)
262
263            enumerated_resources.update(set(resources_to_be_enumerated))
264            resources_to_be_enumerated = tuple(
265                self.__pending_enumeration - enumerated_resources
266            )
267
268        if return_json:
269            if include_dead_resources:
270                return (
271                    json.dumps(
272                        self.__result,
273                        sort_keys=True,
274                        indent=4,
275                        separators=(",", ": "),
276                    ),
277                    dead_resources,
278                )
279            else:
280                return json.dumps(
281                    self.__result,
282                    sort_keys=True,
283                    indent=4,
284                    separators=(",", ": "),
285                )
286        else:
287            if include_dead_resources:
288                return self.__result, dead_resources
289            else:
290                return self.__result
291
292    def walk_nested_dict(self, data, url=""):
293        r"""
294        Parse through the nested dictionary and get the resource id paths.
295
296        Description of argument(s):
297        data                        Nested dictionary data from response message.
298        url                         Resource for which the response is obtained in data.
299        """
300        url = url.rstrip("/")
301
302        for key, value in data.items():
303            # Recursion if nested dictionary found.
304            if isinstance(value, dict):
305                self.walk_nested_dict(value)
306            else:
307                # Value contains a list of dictionaries having member data.
308                if "Members" == key:
309                    if isinstance(value, list):
310                        for memberDict in value:
311                            self.__pending_enumeration.add(
312                                memberDict["@odata.id"]
313                            )
314                if "@odata.id" == key:
315                    value = value.rstrip("/")
316                    # Data for the given url.
317                    if value == url:
318                        self.__result[url] = data
319                    # Data still needs to be looked up,
320                    else:
321                        self.__pending_enumeration.add(value)
322
323    def get_members_list(self, resource_path, filter=None):
324        r"""
325        Return members list in a given URL.
326
327        Description of argument(s):
328        resource_path    URI resource absolute path (e.g. "/redfish/v1/AccountService/Accounts").
329        filter           strings or regex
330
331        /redfish/v1/AccountService/Accounts/
332        {
333            "@odata.id": "/redfish/v1/AccountService/Accounts",
334            "@odata.type": "#ManagerAccountCollection.ManagerAccountCollection",
335            "Description": "BMC User Accounts",
336            "Members": [
337                {
338                    "@odata.id": "/redfish/v1/AccountService/Accounts/root"
339                },
340                {
341                    "@odata.id": "/redfish/v1/AccountService/Accounts/admin"
342                }
343           ],
344           "Members@odata.count": 2,
345           "Name": "Accounts Collection"
346        }
347
348        Return list of members if no filter is applied as:
349        ['/redfish/v1/AccountService/Accounts/root', "/redfish/v1/AccountService/Accounts/admin"]
350
351        Return list of members if filter (e.g "root") is applied as:
352        ['/redfish/v1/AccountService/Accounts/root']
353
354
355        Calling from robot code:
356           ${resp}=  Redfish.Get Members List  /redfish/v1/AccountService/Accounts
357           ${resp}=  Redfish.Get Members List  /redfish/v1/AccountService/Accounts  filter=root
358        """
359
360        member_list = []
361        self._rest_response_ = self.get(
362            resource_path, valid_status_codes=[200]
363        )
364
365        try:
366            for member in self._rest_response_.dict["Members"]:
367                member_list.append(member["@odata.id"])
368        except KeyError:
369            # Non Members child objects at the top level, ignore.
370            pass
371
372        # Filter elements in the list and return matched elements.
373        if filter is not None:
374            regex = ".*/" + filter + "[^/]*$"
375            return [x for x in member_list if re.match(regex, x)]
376
377        return member_list
378