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
12
13import func_args as fa
14import gen_print as gp
15
16
17MTLS_ENABLED = BuiltIn().get_variable_value("${MTLS_ENABLED}")
18
19
20class bmc_redfish(redfish_plus):
21    r"""
22    bmc_redfish is a child class of redfish_plus that is designed to provide
23    benefits specifically for using redfish to communicate with an OpenBMC.
24
25    See the prologs of the methods below for details.
26    """
27
28    def __init__(self, *args, **kwargs):
29        r"""
30        Do BMC-related redfish initialization.
31
32        Presently, older versions of BMC code may not support redfish
33        requests.  This can lead to unsightly error text being printed out for
34        programs that may use lib/bmc_redfish_resource.robot even though they
35        don't necessarily intend to make redfish requests.
36
37        This class method will make an attempt to tolerate this situation.  At
38        some future point, when all BMCs can be expected to support redfish,
39        this class method may be considered for deletion.  If it is deleted,
40        the self.__inited__ test code in the login() class method below should
41        likewise be deleted.
42        """
43        self.__inited__ = False
44        try:
45            if MTLS_ENABLED == 'True':
46                self.__inited__ = True
47            else:
48                super(bmc_redfish, self).__init__(*args, **kwargs)
49                self.__inited__ = True
50        except ValueError as get_exception:
51            except_type, except_value, except_traceback = sys.exc_info()
52            regex = r"The HTTP status code was not valid:[\r\n]+status:[ ]+502"
53            result = re.match(regex, str(except_value), flags=re.MULTILINE)
54            if not result:
55                gp.lprint_var(except_type)
56                gp.lprint_varx("except_value", str(except_value))
57                raise(get_exception)
58        BuiltIn().set_global_variable("${REDFISH_SUPPORTED}", self.__inited__)
59        BuiltIn().set_global_variable("${REDFISH_REST_SUPPORTED}", True)
60
61    def login(self, *args, **kwargs):
62        r"""
63        Assign BMC default values for username, password and auth arguments
64        and call parent class login method.
65
66        Description of argument(s):
67        args                        See parent class method prolog for details.
68        kwargs                      See parent class method prolog for details.
69        """
70
71        if MTLS_ENABLED == 'True':
72            return None
73        if not self.__inited__:
74            message = "bmc_redfish.__init__() was never successfully run.  It "
75            message += "is likely that the target BMC firmware code level "
76            message += "does not support redfish.\n"
77            raise ValueError(message)
78        # Assign default values for username, password, auth where necessary.
79        openbmc_username = BuiltIn().get_variable_value("${OPENBMC_USERNAME}")
80        openbmc_password = BuiltIn().get_variable_value("${OPENBMC_PASSWORD}")
81        username, args, kwargs = fa.pop_arg(openbmc_username, *args, **kwargs)
82        password, args, kwargs = fa.pop_arg(openbmc_password, *args, **kwargs)
83        auth, args, kwargs = fa.pop_arg('session', *args, **kwargs)
84
85        super(bmc_redfish, self).login(username, password, auth,
86                                       *args, **kwargs)
87
88    def logout(self):
89
90        if MTLS_ENABLED == 'True':
91            return None
92        else:
93            super(bmc_redfish, self).logout()
94
95    def get_properties(self, *args, **kwargs):
96        r"""
97        Return dictionary of attributes for a given path.
98
99        The difference between calling this function and calling get()
100        directly is that this function returns ONLY the dictionary portion of
101        the response object.
102
103        Example robot code:
104
105        ${properties}=  Get Properties  /redfish/v1/Systems/system/
106        Rprint Vars  properties
107
108        Output:
109
110        properties:
111          [PowerState]:      Off
112          [Processors]:
113            [@odata.id]:     /redfish/v1/Systems/system/Processors
114          [SerialNumber]:    1234567
115          ...
116
117        Description of argument(s):
118        args                        See parent class get() prolog for details.
119        kwargs                      See parent class get() prolog for details.
120        """
121
122        resp = self.get(*args, **kwargs)
123        return resp.dict if hasattr(resp, 'dict') else {}
124
125    def get_attribute(self, path, attribute, default=None, *args, **kwargs):
126        r"""
127        Get and return the named attribute from the properties for a given
128        path.
129
130        This method has the following advantages over calling get_properties
131        directly:
132        - The caller can specify a default value to be returned if the
133          attribute does not exist.
134
135        Example robot code:
136
137        ${attribute}=  Get Attribute  /redfish/v1/AccountService
138        ...  MaxPasswordLength  default=600
139        Rprint Vars  attribute
140
141        Output:
142
143        attribute:           31
144
145        Description of argument(s):
146        path                        The path (e.g.
147                                    "/redfish/v1/AccountService").
148        attribute                   The name of the attribute to be retrieved
149                                    (e.g. "MaxPasswordLength").
150        default                     The default value to be returned if the
151                                    attribute does not exist (e.g. "").
152        args                        See parent class get() prolog for details.
153        kwargs                      See parent class get() prolog for details.
154        """
155
156        return self.get_properties(path, *args, **kwargs).get(attribute,
157                                                              default)
158
159    def get_session_info(self):
160        r"""
161        Get and return session info as a tuple consisting of session_key and
162        session_location.
163        """
164
165        return self.get_session_key(), self.get_session_location()
166
167    def enumerate(self, resource_path, return_json=1, include_dead_resources=False):
168        r"""
169        Perform a GET enumerate request and return available resource paths.
170
171        Description of argument(s):
172        resource_path               URI resource absolute path (e.g. "/redfish/v1/SessionService/Sessions").
173        return_json                 Indicates whether the result should be returned as a json string or as a
174                                    dictionary.
175        include_dead_resources      Check and return a list of dead/broken URI resources.
176        """
177
178        gp.qprint_executing(style=gp.func_line_style_short)
179        # Set quiet variable to keep subordinate get() calls quiet.
180        quiet = 1
181
182        self.__result = {}
183        # Variable to hold the pending list of resources for which enumeration is yet to be obtained.
184        self.__pending_enumeration = set()
185        self.__pending_enumeration.add(resource_path)
186
187        # Variable having resources for which enumeration is completed.
188        enumerated_resources = set()
189        dead_resources = {}
190        resources_to_be_enumerated = (resource_path,)
191        while resources_to_be_enumerated:
192            for resource in resources_to_be_enumerated:
193                # JsonSchemas, SessionService or URLs containing # are not required in enumeration.
194                # Example: '/redfish/v1/JsonSchemas/' and sub resources.
195                #          '/redfish/v1/SessionService'
196                #          '/redfish/v1/Managers/bmc#/Oem'
197                if ('JsonSchemas' in resource) or ('SessionService' in resource) or ('#' in resource):
198                    continue
199
200                self._rest_response_ = self.get(resource, valid_status_codes=[200, 404, 500])
201                # Enumeration is done for available resources ignoring the ones for which response is not
202                # obtained.
203                if self._rest_response_.status != 200:
204                    if include_dead_resources:
205                        try:
206                            dead_resources[self._rest_response_.status].append(resource)
207                        except KeyError:
208                            dead_resources[self._rest_response_.status] = [resource]
209                    continue
210                self.walk_nested_dict(self._rest_response_.dict, url=resource)
211
212            enumerated_resources.update(set(resources_to_be_enumerated))
213            resources_to_be_enumerated = tuple(self.__pending_enumeration - enumerated_resources)
214
215        if return_json:
216            if include_dead_resources:
217                return json.dumps(self.__result, sort_keys=True,
218                                  indent=4, separators=(',', ': ')), dead_resources
219            else:
220                return json.dumps(self.__result, sort_keys=True,
221                                  indent=4, separators=(',', ': '))
222        else:
223            if include_dead_resources:
224                return self.__result, dead_resources
225            else:
226                return self.__result
227
228    def walk_nested_dict(self, data, url=''):
229        r"""
230        Parse through the nested dictionary and get the resource id paths.
231
232        Description of argument(s):
233        data                        Nested dictionary data from response message.
234        url                         Resource for which the response is obtained in data.
235        """
236        url = url.rstrip('/')
237
238        for key, value in data.items():
239
240            # Recursion if nested dictionary found.
241            if isinstance(value, dict):
242                self.walk_nested_dict(value)
243            else:
244                # Value contains a list of dictionaries having member data.
245                if 'Members' == key:
246                    if isinstance(value, list):
247                        for memberDict in value:
248                            self.__pending_enumeration.add(memberDict['@odata.id'])
249                if '@odata.id' == key:
250                    value = value.rstrip('/')
251                    # Data for the given url.
252                    if value == url:
253                        self.__result[url] = data
254                    # Data still needs to be looked up,
255                    else:
256                        self.__pending_enumeration.add(value)
257
258    def get_members_list(self, resource_path, filter=None):
259        r"""
260        Return members list in a given URL.
261
262        Description of argument(s):
263        resource_path    URI resource absolute path (e.g. "/redfish/v1/AccountService/Accounts").
264        filter           strings or regex
265
266        /redfish/v1/AccountService/Accounts/
267        {
268            "@odata.id": "/redfish/v1/AccountService/Accounts",
269            "@odata.type": "#ManagerAccountCollection.ManagerAccountCollection",
270            "Description": "BMC User Accounts",
271            "Members": [
272                {
273                    "@odata.id": "/redfish/v1/AccountService/Accounts/root"
274                },
275                {
276                    "@odata.id": "/redfish/v1/AccountService/Accounts/admin"
277                }
278           ],
279           "Members@odata.count": 2,
280           "Name": "Accounts Collection"
281        }
282
283        Return list of members if no filter is applied as:
284        ['/redfish/v1/AccountService/Accounts/root', "/redfish/v1/AccountService/Accounts/admin"]
285
286        Return list of members if filter (e.g "root") is applied as:
287        ['/redfish/v1/AccountService/Accounts/root']
288
289
290        Calling from robot code:
291           ${resp}=  Redfish.Get Members List  /redfish/v1/AccountService/Accounts
292           ${resp}=  Redfish.Get Members List  /redfish/v1/AccountService/Accounts  filter=root
293        """
294
295        member_list = []
296        self._rest_response_ = self.get(resource_path, valid_status_codes=[200])
297
298        try:
299            for member in self._rest_response_.dict["Members"]:
300                member_list.append(member["@odata.id"])
301        except KeyError:
302            # Non Members child objects at the top level, ignore.
303            pass
304
305        # Filter elements in the list and return matched elements.
306        if filter is not None:
307            regex = '.*/' + filter + '[^/]*$'
308            return [x for x in member_list if re.match(regex, x)]
309
310        return member_list
311