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