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