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