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