1#!/usr/bin/env python
2
3r"""
4BMC redfish utility functions.
5"""
6
7import json
8from robot.libraries.BuiltIn import BuiltIn
9import gen_print as gp
10
11
12class bmc_redfish_utils(object):
13
14    ROBOT_LIBRARY_SCOPE = 'TEST SUITE'
15
16    def __init__(self):
17        r"""
18        Initialize the bmc_redfish_utils object.
19        """
20        # Obtain a reference to the global redfish object.
21        self.__inited__ = False
22        self._redfish_ = BuiltIn().get_library_instance('redfish')
23
24        # There is a possibility that a given driver support both redfish and
25        # legacy REST.
26        self._redfish_.login()
27        self._rest_response_ = \
28            self._redfish_.get("/xyz/openbmc_project/", valid_status_codes=[200, 404])
29
30        # If REST URL /xyz/openbmc_project/ is supported.
31        if self._rest_response_.status == 200:
32            self.__inited__ = True
33
34        BuiltIn().set_global_variable("${REDFISH_REST_SUPPORTED}", self.__inited__)
35
36    def get_redfish_session_info(self):
37        r"""
38        Returns redfish sessions info dictionary.
39
40        {
41            'key': 'yLXotJnrh5nDhXj5lLiH' ,
42            'location': '/redfish/v1/SessionService/Sessions/nblYY4wlz0'
43        }
44        """
45        session_dict = {
46            "key": self._redfish_.get_session_key(),
47            "location": self._redfish_.get_session_location()
48        }
49        return session_dict
50
51    def get_attribute(self, resource_path, attribute, verify=None):
52        r"""
53        Get resource attribute.
54
55        Description of argument(s):
56        resource_path               URI resource absolute path (e.g.
57                                    "/redfish/v1/Systems/1").
58        attribute                   Name of the attribute (e.g. 'PowerState').
59        """
60
61        resp = self._redfish_.get(resource_path)
62
63        if verify:
64            if resp.dict[attribute] == verify:
65                return resp.dict[attribute]
66            else:
67                raise ValueError("Attribute value is not equal")
68        elif attribute in resp.dict:
69            return resp.dict[attribute]
70
71        return None
72
73    def get_properties(self, resource_path):
74        r"""
75        Returns dictionary of attributes for the resource.
76
77        Description of argument(s):
78        resource_path               URI resource absolute path (e.g.
79                                    /redfish/v1/Systems/1").
80        """
81
82        resp = self._redfish_.get(resource_path)
83        return resp.dict
84
85    def get_target_actions(self, resource_path, target_attribute):
86        r"""
87        Returns resource target entry of the searched target attribute.
88
89        Description of argument(s):
90        resource_path      URI resource absolute path
91                           (e.g. "/redfish/v1/Systems/system").
92        target_attribute   Name of the attribute (e.g. 'ComputerSystem.Reset').
93
94        Example:
95        "Actions": {
96        "#ComputerSystem.Reset": {
97        "ResetType@Redfish.AllowableValues": [
98            "On",
99            "ForceOff",
100            "GracefulRestart",
101            "GracefulShutdown"
102        ],
103        "target": "/redfish/v1/Systems/system/Actions/ComputerSystem.Reset"
104        }
105        }
106        """
107
108        global target_list
109        target_list = []
110
111        resp_dict = self.get_attribute(resource_path, "Actions")
112        if resp_dict is None:
113            return None
114
115        # Recursively search the "target" key in the nested dictionary.
116        # Populate the target_list of target entries.
117        self.get_key_value_nested_dict(resp_dict, "target")
118        # Return the matching target URL entry.
119        for target in target_list:
120            # target "/redfish/v1/Systems/system/Actions/ComputerSystem.Reset"
121            if target_attribute in target:
122                return target
123
124        return None
125
126    def get_member_list(self, resource_path):
127        r"""
128        Perform a GET list request and return available members entries.
129
130        Description of argument(s):
131        resource_path  URI resource absolute path
132                       (e.g. "/redfish/v1/SessionService/Sessions").
133
134        "Members": [
135            {
136             "@odata.id": "/redfish/v1/SessionService/Sessions/Z5HummWPZ7"
137            }
138            {
139             "@odata.id": "/redfish/v1/SessionService/Sessions/46CmQmEL7H"
140            }
141        ],
142        """
143
144        member_list = []
145        resp_list_dict = self.get_attribute(resource_path, "Members")
146        if resp_list_dict is None:
147            return member_list
148
149        for member_id in range(0, len(resp_list_dict)):
150            member_list.append(resp_list_dict[member_id]["@odata.id"])
151
152        return member_list
153
154    def list_request(self, resource_path):
155        r"""
156        Perform a GET list request and return available resource paths.
157        Description of argument(s):
158        resource_path  URI resource absolute path
159                       (e.g. "/redfish/v1/SessionService/Sessions").
160        """
161        gp.qprint_executing(style=gp.func_line_style_short)
162        # Set quiet variable to keep subordinate get() calls quiet.
163        quiet = 1
164        self.__pending_enumeration = set()
165        self._rest_response_ = \
166            self._redfish_.get(resource_path,
167                               valid_status_codes=[200, 404, 500])
168
169        # Return empty list.
170        if self._rest_response_.status != 200:
171            return self.__pending_enumeration
172        self.walk_nested_dict(self._rest_response_.dict)
173        if not self.__pending_enumeration:
174            return resource_path
175        for resource in self.__pending_enumeration.copy():
176            self._rest_response_ = \
177                self._redfish_.get(resource,
178                                   valid_status_codes=[200, 404, 500])
179
180            if self._rest_response_.status != 200:
181                continue
182            self.walk_nested_dict(self._rest_response_.dict)
183        return list(sorted(self.__pending_enumeration))
184
185    def enumerate_request(self, resource_path, return_json=1,
186                          include_dead_resources=False):
187        r"""
188        Perform a GET enumerate request and return available resource paths.
189
190        Description of argument(s):
191        resource_path               URI resource absolute path (e.g.
192                                    "/redfish/v1/SessionService/Sessions").
193        return_json                 Indicates whether the result should be
194                                    returned as a json string or as a
195                                    dictionary.
196        include_dead_resources      Check and return a list of dead/broken URI
197                                    resources.
198        """
199
200        gp.qprint_executing(style=gp.func_line_style_short)
201
202        return_json = int(return_json)
203
204        # Set quiet variable to keep subordinate get() calls quiet.
205        quiet = 1
206
207        # Variable to hold enumerated data.
208        self.__result = {}
209
210        # Variable to hold the pending list of resources for which enumeration.
211        # is yet to be obtained.
212        self.__pending_enumeration = set()
213
214        self.__pending_enumeration.add(resource_path)
215
216        # Variable having resources for which enumeration is completed.
217        enumerated_resources = set()
218
219        if include_dead_resources:
220            dead_resources = {}
221
222        resources_to_be_enumerated = (resource_path,)
223
224        while resources_to_be_enumerated:
225            for resource in resources_to_be_enumerated:
226                # JsonSchemas, SessionService or URLs containing # are not
227                # required in enumeration.
228                # Example: '/redfish/v1/JsonSchemas/' and sub resources.
229                #          '/redfish/v1/SessionService'
230                #          '/redfish/v1/Managers/bmc#/Oem'
231                if ('JsonSchemas' in resource) or ('SessionService' in resource)\
232                        or ('#' in resource):
233                    continue
234
235                self._rest_response_ = \
236                    self._redfish_.get(resource, valid_status_codes=[200, 404, 500])
237                # Enumeration is done for available resources ignoring the
238                # ones for which response is not obtained.
239                if self._rest_response_.status != 200:
240                    if include_dead_resources:
241                        try:
242                            dead_resources[self._rest_response_.status].append(
243                                resource)
244                        except KeyError:
245                            dead_resources[self._rest_response_.status] = \
246                                [resource]
247                    continue
248
249                self.walk_nested_dict(self._rest_response_.dict, url=resource)
250
251            enumerated_resources.update(set(resources_to_be_enumerated))
252            resources_to_be_enumerated = \
253                tuple(self.__pending_enumeration - enumerated_resources)
254
255        if return_json:
256            if include_dead_resources:
257                return json.dumps(self.__result, sort_keys=True,
258                                  indent=4, separators=(',', ': ')), dead_resources
259            else:
260                return json.dumps(self.__result, sort_keys=True,
261                                  indent=4, separators=(',', ': '))
262        else:
263            if include_dead_resources:
264                return self.__result, dead_resources
265            else:
266                return self.__result
267
268    def walk_nested_dict(self, data, url=''):
269        r"""
270        Parse through the nested dictionary and get the resource id paths.
271        Description of argument(s):
272        data    Nested dictionary data from response message.
273        url     Resource for which the response is obtained in data.
274        """
275        url = url.rstrip('/')
276
277        for key, value in data.items():
278
279            # Recursion if nested dictionary found.
280            if isinstance(value, dict):
281                self.walk_nested_dict(value)
282            else:
283                # Value contains a list of dictionaries having member data.
284                if 'Members' == key:
285                    if isinstance(value, list):
286                        for memberDict in value:
287                            self.__pending_enumeration.add(memberDict['@odata.id'])
288                if '@odata.id' == key:
289                    value = value.rstrip('/')
290                    # Data for the given url.
291                    if value == url:
292                        self.__result[url] = data
293                    # Data still needs to be looked up,
294                    else:
295                        self.__pending_enumeration.add(value)
296
297    def get_key_value_nested_dict(self, data, key):
298        r"""
299        Parse through the nested dictionary and get the searched key value.
300
301        Description of argument(s):
302        data    Nested dictionary data from response message.
303        key     Search dictionary key element.
304        """
305
306        for k, v in data.items():
307            if isinstance(v, dict):
308                self.get_key_value_nested_dict(v, key)
309
310            if k == key:
311                target_list.append(v)
312