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            attribute_in_uri = target.rsplit('/', 1)[-1]
184            # attribute_in_uri "ComputerSystem.Reset"
185            if target_attribute == attribute_in_uri:
186                return target
187
188        return None
189
190    def get_member_list(self, resource_path):
191        r"""
192        Perform a GET list request and return available members entries.
193
194        Description of argument(s):
195        resource_path  URI resource absolute path
196                       (e.g. "/redfish/v1/SessionService/Sessions").
197
198        "Members": [
199            {
200             "@odata.id": "/redfish/v1/SessionService/Sessions/Z5HummWPZ7"
201            }
202            {
203             "@odata.id": "/redfish/v1/SessionService/Sessions/46CmQmEL7H"
204            }
205        ],
206        """
207
208        member_list = []
209        resp_list_dict = self.get_attribute(resource_path, "Members")
210        if resp_list_dict is None:
211            return member_list
212
213        for member_id in range(0, len(resp_list_dict)):
214            member_list.append(resp_list_dict[member_id]["@odata.id"])
215
216        return member_list
217
218    def list_request(self, resource_path):
219        r"""
220        Perform a GET list request and return available resource paths.
221        Description of argument(s):
222        resource_path  URI resource absolute path
223                       (e.g. "/redfish/v1/SessionService/Sessions").
224        """
225        gp.qprint_executing(style=gp.func_line_style_short)
226        # Set quiet variable to keep subordinate get() calls quiet.
227        quiet = 1
228        self.__pending_enumeration = set()
229        self._rest_response_ = \
230            self._redfish_.get(resource_path,
231                               valid_status_codes=[200, 404, 500])
232
233        # Return empty list.
234        if self._rest_response_.status != 200:
235            return self.__pending_enumeration
236        self.walk_nested_dict(self._rest_response_.dict)
237        if not self.__pending_enumeration:
238            return resource_path
239        for resource in self.__pending_enumeration.copy():
240            self._rest_response_ = \
241                self._redfish_.get(resource,
242                                   valid_status_codes=[200, 404, 500])
243
244            if self._rest_response_.status != 200:
245                continue
246            self.walk_nested_dict(self._rest_response_.dict)
247        return list(sorted(self.__pending_enumeration))
248
249    def enumerate_request(self, resource_path, return_json=1,
250                          include_dead_resources=False):
251        r"""
252        Perform a GET enumerate request and return available resource paths.
253
254        Description of argument(s):
255        resource_path               URI resource absolute path (e.g.
256                                    "/redfish/v1/SessionService/Sessions").
257        return_json                 Indicates whether the result should be
258                                    returned as a json string or as a
259                                    dictionary.
260        include_dead_resources      Check and return a list of dead/broken URI
261                                    resources.
262        """
263
264        gp.qprint_executing(style=gp.func_line_style_short)
265
266        return_json = int(return_json)
267
268        # Set quiet variable to keep subordinate get() calls quiet.
269        quiet = 1
270
271        # Variable to hold enumerated data.
272        self.__result = {}
273
274        # Variable to hold the pending list of resources for which enumeration.
275        # is yet to be obtained.
276        self.__pending_enumeration = set()
277
278        self.__pending_enumeration.add(resource_path)
279
280        # Variable having resources for which enumeration is completed.
281        enumerated_resources = set()
282
283        if include_dead_resources:
284            dead_resources = {}
285
286        resources_to_be_enumerated = (resource_path,)
287
288        while resources_to_be_enumerated:
289            for resource in resources_to_be_enumerated:
290                # JsonSchemas, SessionService or URLs containing # are not
291                # required in enumeration.
292                # Example: '/redfish/v1/JsonSchemas/' and sub resources.
293                #          '/redfish/v1/SessionService'
294                #          '/redfish/v1/Managers/bmc#/Oem'
295                if ('JsonSchemas' in resource) or ('SessionService' in resource)\
296                        or ('#' in resource):
297                    continue
298
299                self._rest_response_ = \
300                    self._redfish_.get(resource, valid_status_codes=[200, 404, 405, 500])
301                # Enumeration is done for available resources ignoring the
302                # ones for which response is not obtained.
303                if self._rest_response_.status != 200:
304                    if include_dead_resources:
305                        try:
306                            dead_resources[self._rest_response_.status].append(
307                                resource)
308                        except KeyError:
309                            dead_resources[self._rest_response_.status] = \
310                                [resource]
311                    continue
312
313                self.walk_nested_dict(self._rest_response_.dict, url=resource)
314
315            enumerated_resources.update(set(resources_to_be_enumerated))
316            resources_to_be_enumerated = \
317                tuple(self.__pending_enumeration - enumerated_resources)
318
319        if return_json:
320            if include_dead_resources:
321                return json.dumps(self.__result, sort_keys=True,
322                                  indent=4, separators=(',', ': ')), dead_resources
323            else:
324                return json.dumps(self.__result, sort_keys=True,
325                                  indent=4, separators=(',', ': '))
326        else:
327            if include_dead_resources:
328                return self.__result, dead_resources
329            else:
330                return self.__result
331
332    def walk_nested_dict(self, data, url=''):
333        r"""
334        Parse through the nested dictionary and get the resource id paths.
335        Description of argument(s):
336        data    Nested dictionary data from response message.
337        url     Resource for which the response is obtained in data.
338        """
339        url = url.rstrip('/')
340
341        for key, value in data.items():
342
343            # Recursion if nested dictionary found.
344            if isinstance(value, dict):
345                self.walk_nested_dict(value)
346            else:
347                # Value contains a list of dictionaries having member data.
348                if 'Members' == key:
349                    if isinstance(value, list):
350                        for memberDict in value:
351                            if isinstance(memberDict, str):
352                                self.__pending_enumeration.add(memberDict)
353                            else:
354                                self.__pending_enumeration.add(memberDict['@odata.id'])
355
356                if '@odata.id' == key:
357                    value = value.rstrip('/')
358                    # Data for the given url.
359                    if value == url:
360                        self.__result[url] = data
361                    # Data still needs to be looked up,
362                    else:
363                        self.__pending_enumeration.add(value)
364
365    def get_key_value_nested_dict(self, data, key):
366        r"""
367        Parse through the nested dictionary and get the searched key value.
368
369        Description of argument(s):
370        data    Nested dictionary data from response message.
371        key     Search dictionary key element.
372        """
373
374        for k, v in data.items():
375            if isinstance(v, dict):
376                self.get_key_value_nested_dict(v, key)
377
378            if k == key:
379                target_list.append(v)
380