c290af9a6e1ff7fc93495528ed25d9d49ded217f
[csit.git] / resources / libraries / python / honeycomb / HoneycombUtil.py
1 # Copyright (c) 2016 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Implementation of low level functionality used in communication with
15 Honeycomb.
16
17 Exception HoneycombError is used in all methods and in all modules with
18 Honeycomb keywords.
19
20 Class HoneycombUtil implements methods used by Honeycomb keywords. They must not
21 be used directly in tests. Use keywords implemented in the module
22 HoneycombAPIKeywords instead.
23 """
24
25 from json import loads, dumps
26 from enum import Enum, unique
27
28 from robot.api import logger
29
30 from resources.libraries.python.HTTPRequest import HTTPRequest
31 from resources.libraries.python.constants import Constants as Const
32
33
34 @unique
35 class DataRepresentation(Enum):
36     """Representation of data sent by PUT and POST requests."""
37     NO_DATA = 0
38     JSON = 1
39     XML = 2
40     TXT = 3
41
42
43 # Headers used in requests. Key - content representation, value - header.
44 HEADERS = {DataRepresentation.NO_DATA:
45                {},  # It must be empty dictionary.
46            DataRepresentation.JSON:
47                {"Content-Type": "application/json",
48                 "Accept": "text/plain"},
49            DataRepresentation.XML:
50                {"Content-Type": "application/xml",
51                 "Accept": "text/plain"},
52            DataRepresentation.TXT:
53                {"Content-Type": "text/plain",
54                 "Accept": "text/plain"}
55           }
56
57
58 class HoneycombError(Exception):
59
60     """Exception(s) raised by methods working with Honeycomb.
61
62     When raising this exception, put this information to the message in this
63     order:
64      - short description of the encountered problem (parameter msg),
65      - relevant messages if there are any collected, e.g., from caught
66        exception (optional parameter details),
67      - relevant data if there are any collected (optional parameter details).
68      The logging is performed on two levels: 1. error - short description of the
69      problem; 2. debug - detailed information.
70     """
71
72     def __init__(self, msg, details='', enable_logging=True):
73         """Sets the exception message and enables / disables logging.
74
75         It is not wanted to log errors when using these keywords together
76         with keywords like "Wait until keyword succeeds". So you can disable
77         logging by setting enable_logging to False.
78
79         :param msg: Message to be displayed and logged.
80         :param enable_logging: When True, logging is enabled, otherwise
81         logging is disabled.
82         :type msg: str
83         :type enable_logging: bool
84         """
85         super(HoneycombError, self).__init__()
86         self._msg = "{0}: {1}".format(self.__class__.__name__, msg)
87         self._details = details
88         if enable_logging:
89             logger.debug(self._details)
90
91     def __repr__(self):
92         return repr(self._msg)
93
94     def __str__(self):
95         return str(self._msg)
96
97
98 class HoneycombUtil(object):
99     """Implements low level functionality used in communication with Honeycomb.
100
101     There are implemented methods to get, put and delete data to/from Honeycomb.
102     They are based on functionality implemented in the module HTTPRequests which
103     uses HTTP requests GET, PUT, POST and DELETE to communicate with Honeycomb.
104
105     It is possible to PUT the data represented as XML or JSON structures or as
106     plain text.
107     Data received in the response of GET are always represented as a JSON
108     structure.
109
110     There are also two supportive methods implemented:
111     - read_path_from_url_file which reads URL file and returns a path (see
112       docs/honeycomb_url_files.rst).
113     - parse_json_response which parses data from response in JSON representation
114       according to given path.
115     """
116
117     def __init__(self):
118         pass
119
120     @staticmethod
121     def read_path_from_url_file(url_file):
122         """Read path from *.url file.
123
124         For more information about *.url file see docs/honeycomb_url_files.rst
125         :param url_file: URL file. The argument contains only the name of file
126         without extension, not the full path.
127         :type url_file: str
128         :returns: Requested path.
129         :rtype: str
130         """
131
132         url_file = "{0}/{1}.url".format(Const.RESOURCES_TPL_HC, url_file)
133         with open(url_file) as template:
134             path = template.readline()
135         return path
136
137     @staticmethod
138     def find_item(data, path):
139         """Find a data item (single leaf or sub-tree) in data received from
140         Honeycomb REST API.
141
142         Path format:
143         The path is a tuple with items navigating to requested data. The items
144         can be strings or tuples:
145         - string item represents a dictionary key in data,
146         - tuple item represents list item in data.
147
148         Example:
149         data = \
150         {
151             "interfaces": {
152                 "interface": [
153                     {
154                         "name": "GigabitEthernet0/8/0",
155                         "enabled": "true",
156                         "type": "iana-if-type:ethernetCsmacd",
157                     },
158                     {
159                         "name": "local0",
160                         "enabled": "false",
161                         "type": "iana-if-type:ethernetCsmacd",
162                     }
163                 ]
164             }
165         }
166
167         path = ("interfaces", ("interface", "name", "local0"), "enabled")
168         This path points to "false".
169
170         The tuple ("interface", "name", "local0") consists of:
171         index 0 - dictionary key pointing to a list,
172         index 1 - key which identifies an item in the list, it is also marked as
173                   the key in corresponding yang file.
174         index 2 - key value.
175
176         :param data: Data received from Honeycomb REST API.
177         :param path: Path to data we want to find.
178         :type data: dict
179         :type path: tuple
180         :returns: Data represented by path.
181         :rtype: str, dict, or list
182         :raises HoneycombError: If the data has not been found.
183         """
184
185         for path_item in path:
186             try:
187                 if isinstance(path_item, str):
188                     data = data[path_item]
189                 elif isinstance(path_item, tuple):
190                     for data_item in data[path_item[0]]:
191                         if data_item[path_item[1]] == path_item[2]:
192                             data = data_item
193             except KeyError as err:
194                 raise HoneycombError("Data not found: {0}".format(err))
195
196         return data
197
198     @staticmethod
199     def remove_item(data, path):
200         """Remove a data item (single leaf or sub-tree) in data received from
201         Honeycomb REST API.
202
203         :param data: Data received from Honeycomb REST API.
204         :param path: Path to data we want to remove.
205         :type data: dict
206         :type path: tuple
207         :returns: Original data without removed part.
208         :rtype: dict
209         """
210
211         origin_data = previous_data = data
212         try:
213             for path_item in path:
214                 previous_data = data
215                 if isinstance(path_item, str):
216                     data = data[path_item]
217                 elif isinstance(path_item, tuple):
218                     for data_item in data[path_item[0]]:
219                         if data_item[path_item[1]] == path_item[2]:
220                             data = data_item
221         except KeyError as err:
222             logger.debug("Data not found: {0}".format(err))
223             return origin_data
224
225         if isinstance(path[-1], str):
226             previous_data.pop(path[-1])
227         elif isinstance(path[-1], tuple):
228             previous_data[path[-1][0]].remove(data)
229             if not previous_data[path[-1][0]]:
230                 previous_data.pop(path[-1][0])
231
232         return origin_data
233
234     @staticmethod
235     def set_item_value(data, path, new_value):
236         """Set or change the value (single leaf or sub-tree) in data received
237         from Honeycomb REST API.
238
239         If the item is not present in the data structure, it is created.
240
241         :param data: Data received from Honeycomb REST API.
242         :param path: Path to data we want to change or create.
243         :param new_value: The value to be set.
244         :type data: dict
245         :type path: tuple
246         :type new_value: str, dict or list
247         :returns: Original data with the new value.
248         :rtype: dict
249         """
250
251         origin_data = data
252         for path_item in path[:-1]:
253             if isinstance(path_item, str):
254                 try:
255                     data = data[path_item]
256                 except KeyError:
257                     data[path_item] = {}
258                     data = data[path_item]
259             elif isinstance(path_item, tuple):
260                 try:
261                     flag = False
262                     index = 0
263                     for data_item in data[path_item[0]]:
264                         if data_item[path_item[1]] == path_item[2]:
265                             data = data[path_item[0]][index]
266                             flag = True
267                             break
268                         index += 1
269                     if not flag:
270                         data[path_item[0]].append({path_item[1]: path_item[2]})
271                         data = data[path_item[0]][-1]
272                 except KeyError:
273                     data[path_item] = []
274
275         if not path[-1] in data.keys():
276             data[path[-1]] = {}
277
278         if isinstance(new_value, list) and isinstance(data[path[-1]], list):
279             for value in new_value:
280                 data[path[-1]].append(value)
281         else:
282             data[path[-1]] = new_value
283
284         return origin_data
285
286     @staticmethod
287     def get_honeycomb_data(node, url_file, path=""):
288         """Retrieve data from Honeycomb according to given URL.
289
290         :param node: Honeycomb node.
291         :param url_file: URL file. The argument contains only the name of file
292         without extension, not the full path.
293         :param path: Path which is added to the base path to identify the data.
294         :type node: dict
295         :type url_file: str
296         :type path: str
297         :returns: Status code and content of response.
298         :rtype tuple
299         """
300
301         base_path = HoneycombUtil.read_path_from_url_file(url_file)
302         path = base_path + path
303         status_code, resp = HTTPRequest.get(node, path)
304         return status_code, loads(resp)
305
306     @staticmethod
307     def put_honeycomb_data(node, url_file, data, path="",
308                            data_representation=DataRepresentation.JSON):
309         """Send configuration data using PUT request and return the status code
310         and response content.
311
312         :param node: Honeycomb node.
313         :param url_file: URL file. The argument contains only the name of file
314         without extension, not the full path.
315         :param data: Configuration data to be sent to Honeycomb.
316         :param path: Path which is added to the base path to identify the data.
317         :param data_representation: How the data is represented.
318         :type node: dict
319         :type url_file: str
320         :type data: dict, str
321         :type path: str
322         :type data_representation: DataRepresentation
323         :returns: Status code and content of response.
324         :rtype: tuple
325         :raises HoneycombError: If the given data representation is not defined
326         in HEADERS.
327         """
328
329         try:
330             header = HEADERS[data_representation]
331         except AttributeError as err:
332             raise HoneycombError("Wrong data representation: {0}.".
333                                  format(data_representation), repr(err))
334         if data_representation == DataRepresentation.JSON:
335             data = dumps(data)
336
337         logger.trace(data)
338
339         base_path = HoneycombUtil.read_path_from_url_file(url_file)
340         path = base_path + path
341         logger.trace(path)
342         return HTTPRequest.put(
343             node=node, path=path, headers=header, payload=data)
344
345     @staticmethod
346     def post_honeycomb_data(node, url_file, data=None,
347                             data_representation=DataRepresentation.JSON,
348                             timeout=10):
349         """Send a POST request and return the status code and response content.
350
351         :param node: Honeycomb node.
352         :param url_file: URL file. The argument contains only the name of file
353         without extension, not the full path.
354         :param data: Configuration data to be sent to Honeycomb.
355         :param data_representation: How the data is represented.
356         :param timeout: How long to wait for the server to send data before
357         giving up.
358         :type node: dict
359         :type url_file: str
360         :type data: dict, str
361         :type data_representation: DataRepresentation
362         :type timeout: int
363         :returns: Status code and content of response.
364         :rtype: tuple
365         :raises HoneycombError: If the given data representation is not defined
366         in HEADERS.
367         """
368
369         try:
370             header = HEADERS[data_representation]
371         except AttributeError as err:
372             raise HoneycombError("Wrong data representation: {0}.".
373                                  format(data_representation), repr(err))
374         if data_representation == DataRepresentation.JSON:
375             data = dumps(data)
376
377         path = HoneycombUtil.read_path_from_url_file(url_file)
378         return HTTPRequest.post(
379             node=node, path=path, headers=header, payload=data, timeout=timeout)
380
381     @staticmethod
382     def delete_honeycomb_data(node, url_file, path=""):
383         """Delete data from Honeycomb according to given URL.
384
385         :param node: Honeycomb node.
386         :param url_file: URL file. The argument contains only the name of file
387         without extension, not the full path.
388         :param path: Path which is added to the base path to identify the data.
389         :type node: dict
390         :type url_file: str
391         :type path: str
392         :returns: Status code and content of response.
393         :rtype tuple
394         """
395
396         base_path = HoneycombUtil.read_path_from_url_file(url_file)
397         path = base_path + path
398         return HTTPRequest.delete(node, path)