1#!/usr/bin/env python
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