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 ('JsonSchemas' in child_path_idx)\
114                        or ('SessionService' in child_path_idx)\
115                        or ('#' in child_path_idx):
116                    continue
117                if self.get_attribute(child_path_idx, attribute):
118                    valid_path_list.append(child_path_idx)
119
120        BuiltIn().log_to_console(valid_path_list)
121        return valid_path_list
122
123    def get_endpoint_path_list(self, resource_path, end_point_prefix):
124        r"""
125        Returns list with entries ending in "/endpoint".
126
127        Description of argument(s):
128        resource_path      URI resource base path (e.g. "/redfish/v1/Chassis/").
129        end_point_prefix   Name of the enpoint (e.g. 'Power').
130
131        Find all list entries ending in "/endpoint" combination such as
132        /redfish/v1/Chassis/<foo>/Power
133        /redfish/v1/Chassis/<bar>/Power
134        """
135
136        end_point_list = self.list_request(resource_path)
137
138        # Regex to match entries ending in "/prefix" with optional underscore.
139        regex = ".*/" + end_point_prefix + "[_]?[0-9]*?"
140        return [x for x in end_point_list if re.match(regex, x, re.IGNORECASE)]
141
142    def get_target_actions(self, resource_path, target_attribute):
143        r"""
144        Returns resource target entry of the searched target attribute.
145
146        Description of argument(s):
147        resource_path      URI resource absolute path
148                           (e.g. "/redfish/v1/Systems/system").
149        target_attribute   Name of the attribute (e.g. 'ComputerSystem.Reset').
150
151        Example:
152        "Actions": {
153        "#ComputerSystem.Reset": {
154        "ResetType@Redfish.AllowableValues": [
155            "On",
156            "ForceOff",
157            "GracefulRestart",
158            "GracefulShutdown"
159        ],
160        "target": "/redfish/v1/Systems/system/Actions/ComputerSystem.Reset"
161        }
162        }
163        """
164
165        global target_list
166        target_list = []
167
168        resp_dict = self.get_attribute(resource_path, "Actions")
169        if resp_dict is None:
170            return None
171
172        # Recursively search the "target" key in the nested dictionary.
173        # Populate the target_list of target entries.
174        self.get_key_value_nested_dict(resp_dict, "target")
175        # Return the matching target URL entry.
176        for target in target_list:
177            # target "/redfish/v1/Systems/system/Actions/ComputerSystem.Reset"
178            if target_attribute in target:
179                return target
180
181        return None
182
183    def get_member_list(self, resource_path):
184        r"""
185        Perform a GET list request and return available members entries.
186
187        Description of argument(s):
188        resource_path  URI resource absolute path
189                       (e.g. "/redfish/v1/SessionService/Sessions").
190
191        "Members": [
192            {
193             "@odata.id": "/redfish/v1/SessionService/Sessions/Z5HummWPZ7"
194            }
195            {
196             "@odata.id": "/redfish/v1/SessionService/Sessions/46CmQmEL7H"
197            }
198        ],
199        """
200
201        member_list = []
202        resp_list_dict = self.get_attribute(resource_path, "Members")
203        if resp_list_dict is None:
204            return member_list
205
206        for member_id in range(0, len(resp_list_dict)):
207            member_list.append(resp_list_dict[member_id]["@odata.id"])
208
209        return member_list
210
211    def list_request(self, resource_path):
212        r"""
213        Perform a GET list request and return available resource paths.
214        Description of argument(s):
215        resource_path  URI resource absolute path
216                       (e.g. "/redfish/v1/SessionService/Sessions").
217        """
218        gp.qprint_executing(style=gp.func_line_style_short)
219        # Set quiet variable to keep subordinate get() calls quiet.
220        quiet = 1
221        self.__pending_enumeration = set()
222        self._rest_response_ = \
223            self._redfish_.get(resource_path,
224                               valid_status_codes=[200, 404, 500])
225
226        # Return empty list.
227        if self._rest_response_.status != 200:
228            return self.__pending_enumeration
229        self.walk_nested_dict(self._rest_response_.dict)
230        if not self.__pending_enumeration:
231            return resource_path
232        for resource in self.__pending_enumeration.copy():
233            self._rest_response_ = \
234                self._redfish_.get(resource,
235                                   valid_status_codes=[200, 404, 500])
236
237            if self._rest_response_.status != 200:
238                continue
239            self.walk_nested_dict(self._rest_response_.dict)
240        return list(sorted(self.__pending_enumeration))
241
242    def enumerate_request(self, resource_path, return_json=1,
243                          include_dead_resources=False):
244        r"""
245        Perform a GET enumerate request and return available resource paths.
246
247        Description of argument(s):
248        resource_path               URI resource absolute path (e.g.
249                                    "/redfish/v1/SessionService/Sessions").
250        return_json                 Indicates whether the result should be
251                                    returned as a json string or as a
252                                    dictionary.
253        include_dead_resources      Check and return a list of dead/broken URI
254                                    resources.
255        """
256
257        gp.qprint_executing(style=gp.func_line_style_short)
258
259        return_json = int(return_json)
260
261        # Set quiet variable to keep subordinate get() calls quiet.
262        quiet = 1
263
264        # Variable to hold enumerated data.
265        self.__result = {}
266
267        # Variable to hold the pending list of resources for which enumeration.
268        # is yet to be obtained.
269        self.__pending_enumeration = set()
270
271        self.__pending_enumeration.add(resource_path)
272
273        # Variable having resources for which enumeration is completed.
274        enumerated_resources = set()
275
276        if include_dead_resources:
277            dead_resources = {}
278
279        resources_to_be_enumerated = (resource_path,)
280
281        while resources_to_be_enumerated:
282            for resource in resources_to_be_enumerated:
283                # JsonSchemas, SessionService or URLs containing # are not
284                # required in enumeration.
285                # Example: '/redfish/v1/JsonSchemas/' and sub resources.
286                #          '/redfish/v1/SessionService'
287                #          '/redfish/v1/Managers/bmc#/Oem'
288                if ('JsonSchemas' in resource) or ('SessionService' in resource)\
289                        or ('#' in resource):
290                    continue
291
292                self._rest_response_ = \
293                    self._redfish_.get(resource, valid_status_codes=[200, 404, 500])
294                # Enumeration is done for available resources ignoring the
295                # ones for which response is not obtained.
296                if self._rest_response_.status != 200:
297                    if include_dead_resources:
298                        try:
299                            dead_resources[self._rest_response_.status].append(
300                                resource)
301                        except KeyError:
302                            dead_resources[self._rest_response_.status] = \
303                                [resource]
304                    continue
305
306                self.walk_nested_dict(self._rest_response_.dict, url=resource)
307
308            enumerated_resources.update(set(resources_to_be_enumerated))
309            resources_to_be_enumerated = \
310                tuple(self.__pending_enumeration - enumerated_resources)
311
312        if return_json:
313            if include_dead_resources:
314                return json.dumps(self.__result, sort_keys=True,
315                                  indent=4, separators=(',', ': ')), dead_resources
316            else:
317                return json.dumps(self.__result, sort_keys=True,
318                                  indent=4, separators=(',', ': '))
319        else:
320            if include_dead_resources:
321                return self.__result, dead_resources
322            else:
323                return self.__result
324
325    def walk_nested_dict(self, data, url=''):
326        r"""
327        Parse through the nested dictionary and get the resource id paths.
328        Description of argument(s):
329        data    Nested dictionary data from response message.
330        url     Resource for which the response is obtained in data.
331        """
332        url = url.rstrip('/')
333
334        for key, value in data.items():
335
336            # Recursion if nested dictionary found.
337            if isinstance(value, dict):
338                self.walk_nested_dict(value)
339            else:
340                # Value contains a list of dictionaries having member data.
341                if 'Members' == key:
342                    if isinstance(value, list):
343                        for memberDict in value:
344                            self.__pending_enumeration.add(memberDict['@odata.id'])
345                if '@odata.id' == key:
346                    value = value.rstrip('/')
347                    # Data for the given url.
348                    if value == url:
349                        self.__result[url] = data
350                    # Data still needs to be looked up,
351                    else:
352                        self.__pending_enumeration.add(value)
353
354    def get_key_value_nested_dict(self, data, key):
355        r"""
356        Parse through the nested dictionary and get the searched key value.
357
358        Description of argument(s):
359        data    Nested dictionary data from response message.
360        key     Search dictionary key element.
361        """
362
363        for k, v in data.items():
364            if isinstance(v, dict):
365                self.get_key_value_nested_dict(v, key)
366
367            if k == key:
368                target_list.append(v)
369