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