f607c2439ac39eb4a9410ea151e240f191684b43
[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.ssh import SSH
31 from resources.libraries.python.HTTPRequest import HTTPRequest, HTTPCodes
32 from resources.libraries.python.constants import Constants as Const
33
34
35 @unique
36 class DataRepresentation(Enum):
37     """Representation of data sent by PUT and POST requests."""
38     NO_DATA = 0
39     JSON = 1
40     XML = 2
41     TXT = 3
42
43
44 # Headers used in requests. Key - content representation, value - header.
45 HEADERS = {DataRepresentation.NO_DATA:
46                {},  # It must be empty dictionary.
47            DataRepresentation.JSON:
48                {"Content-Type": "application/json",
49                 "Accept": "text/plain"},
50            DataRepresentation.XML:
51                {"Content-Type": "application/xml",
52                 "Accept": "text/plain"},
53            DataRepresentation.TXT:
54                {"Content-Type": "text/plain",
55                 "Accept": "text/plain"}
56           }
57
58
59 class HoneycombError(Exception):
60
61     """Exception(s) raised by methods working with Honeycomb.
62
63     When raising this exception, put this information to the message in this
64     order:
65      - short description of the encountered problem (parameter msg),
66      - relevant messages if there are any collected, e.g., from caught
67        exception (optional parameter details),
68      - relevant data if there are any collected (optional parameter details).
69      The logging is performed on two levels: 1. error - short description of the
70      problem; 2. debug - detailed information.
71     """
72
73     def __init__(self, msg, details='', enable_logging=True):
74         """Sets the exception message and enables / disables logging.
75
76         It is not wanted to log errors when using these keywords together
77         with keywords like "Wait until keyword succeeds". So you can disable
78         logging by setting enable_logging to False.
79
80         :param msg: Message to be displayed and logged.
81         :param enable_logging: When True, logging is enabled, otherwise
82         logging is disabled.
83         :type msg: str
84         :type enable_logging: bool
85         """
86         super(HoneycombError, self).__init__()
87         self._msg = "{0}: {1}".format(self.__class__.__name__, msg)
88         self._details = details
89         if enable_logging:
90             logger.debug(self._details)
91
92     def __repr__(self):
93         return repr(self._msg)
94
95     def __str__(self):
96         return str(self._msg)
97
98
99 class HoneycombUtil(object):
100     """Implements low level functionality used in communication with Honeycomb.
101
102     There are implemented methods to get, put and delete data to/from Honeycomb.
103     They are based on functionality implemented in the module HTTPRequests which
104     uses HTTP requests GET, PUT, POST and DELETE to communicate with Honeycomb.
105
106     It is possible to PUT the data represented as XML or JSON structures or as
107     plain text.
108     Data received in the response of GET are always represented as a JSON
109     structure.
110
111     There are also two supportive methods implemented:
112     - read_path_from_url_file which reads URL file and returns a path (see
113       docs/honeycomb_url_files.rst).
114     - parse_json_response which parses data from response in JSON representation
115       according to given path.
116     """
117
118     def __init__(self):
119         pass
120
121     @staticmethod
122     def read_path_from_url_file(url_file):
123         """Read path from *.url file.
124
125         For more information about *.url file see docs/honeycomb_url_files.rst
126         :param url_file: URL file. The argument contains only the name of file
127         without extension, not the full path.
128         :type url_file: str
129         :return: Requested path.
130         :rtype: str
131         """
132
133         url_file = "{0}/{1}.url".format(Const.RESOURCES_TPL_HC, url_file)
134         with open(url_file) as template:
135             path = template.readline()
136         return path
137
138     @staticmethod
139     def find_item(data, path):
140         """Find a data item (single leaf or sub-tree) in data received from
141         Honeycomb REST API.
142
143         Path format:
144         The path is a tuple with items navigating to requested data. The items
145         can be strings or tuples:
146         - string item represents a dictionary key in data,
147         - tuple item represents list item in data.
148
149         Example:
150         data = \
151         {
152             "interfaces": {
153                 "interface": [
154                     {
155                         "name": "GigabitEthernet0/8/0",
156                         "enabled": "true",
157                         "type": "iana-if-type:ethernetCsmacd",
158                     },
159                     {
160                         "name": "local0",
161                         "enabled": "false",
162                         "type": "iana-if-type:ethernetCsmacd",
163                     }
164                 ]
165             }
166         }
167
168         path = ("interfaces", ("interface", "name", "local0"), "enabled")
169         This path points to "false".
170
171         The tuple ("interface", "name", "local0") consists of:
172         index 0 - dictionary key pointing to a list,
173         index 1 - key which identifies an item in the list, it is also marked as
174                   the key in corresponding yang file.
175         index 2 - key value.
176
177         :param data: Data received from Honeycomb REST API.
178         :param path: Path to data we want to find.
179         :type data: dict
180         :type path: tuple
181         :return: Data represented by path.
182         :rtype: str, dict, or list
183         :raises HoneycombError: If the data has not been found.
184         """
185
186         for path_item in path:
187             try:
188                 if isinstance(path_item, str):
189                     data = data[path_item]
190                 elif isinstance(path_item, tuple):
191                     for data_item in data[path_item[0]]:
192                         if data_item[path_item[1]] == path_item[2]:
193                             data = data_item
194             except KeyError as err:
195                 raise HoneycombError("Data not found: {0}".format(err))
196
197         return data
198
199     @staticmethod
200     def remove_item(data, path):
201         """Remove a data item (single leaf or sub-tree) in data received from
202         Honeycomb REST API.
203
204         :param data: Data received from Honeycomb REST API.
205         :param path: Path to data we want to remove.
206         :type data: dict
207         :type path: tuple
208         :return: Original data without removed part.
209         :rtype: dict
210         """
211
212         origin_data = previous_data = data
213         try:
214             for path_item in path:
215                 previous_data = data
216                 if isinstance(path_item, str):
217                     data = data[path_item]
218                 elif isinstance(path_item, tuple):
219                     for data_item in data[path_item[0]]:
220                         if data_item[path_item[1]] == path_item[2]:
221                             data = data_item
222         except KeyError as err:
223             logger.debug("Data not found: {0}".format(err))
224             return origin_data
225
226         if isinstance(path[-1], str):
227             previous_data.pop(path[-1])
228         elif isinstance(path[-1], tuple):
229             previous_data[path[-1][0]].remove(data)
230             if not previous_data[path[-1][0]]:
231                 previous_data.pop(path[-1][0])
232
233         return origin_data
234
235     @staticmethod
236     def set_item_value(data, path, new_value):
237         """Set or change the value (single leaf or sub-tree) in data received
238         from Honeycomb REST API.
239
240         If the item is not present in the data structure, it is created.
241
242         :param data: Data received from Honeycomb REST API.
243         :param path: Path to data we want to change or create.
244         :param new_value: The value to be set.
245         :type data: dict
246         :type path: tuple
247         :type new_value: str, dict or list
248         :return: Original data with the new value.
249         :rtype: dict
250         """
251
252         origin_data = data
253         for path_item in path[:-1]:
254             if isinstance(path_item, str):
255                 try:
256                     data = data[path_item]
257                 except KeyError:
258                     data[path_item] = {}
259                     data = data[path_item]
260             elif isinstance(path_item, tuple):
261                 try:
262                     flag = False
263                     index = 0
264                     for data_item in data[path_item[0]]:
265                         if data_item[path_item[1]] == path_item[2]:
266                             data = data[path_item[0]][index]
267                             flag = True
268                             break
269                         index += 1
270                     if not flag:
271                         data[path_item[0]].append({path_item[1]: path_item[2]})
272                         data = data[path_item[0]][-1]
273                 except KeyError:
274                     data[path_item] = []
275
276         if not path[-1] in data.keys():
277             data[path[-1]] = {}
278
279         if isinstance(new_value, list) and isinstance(data[path[-1]], list):
280             for value in new_value:
281                 data[path[-1]].append(value)
282         else:
283             data[path[-1]] = new_value
284
285         return origin_data
286
287     @staticmethod
288     def get_honeycomb_data(node, url_file, path=""):
289         """Retrieve data from Honeycomb according to given URL.
290
291         :param node: Honeycomb node.
292         :param url_file: URL file. The argument contains only the name of file
293         without extension, not the full path.
294         :param path: Path which is added to the base path to identify the data.
295         :type node: dict
296         :type url_file: str
297         :type path: str
298         :return: Status code and content of response.
299         :rtype tuple
300         """
301
302         base_path = HoneycombUtil.read_path_from_url_file(url_file)
303         path = base_path + path
304         status_code, resp = HTTPRequest.get(node, path)
305         (status_node, response) = status_code, loads(resp)
306         if status_code != HTTPCodes.OK:
307             HoneycombUtil.read_log_tail(node)
308         return status_code, response
309
310     @staticmethod
311     def put_honeycomb_data(node, url_file, data, path="",
312                            data_representation=DataRepresentation.JSON):
313         """Send configuration data using PUT request and return the status code
314         and response content.
315
316         :param node: Honeycomb node.
317         :param url_file: URL file. The argument contains only the name of file
318         without extension, not the full path.
319         :param data: Configuration data to be sent to Honeycomb.
320         :param path: Path which is added to the base path to identify the data.
321         :param data_representation: How the data is represented.
322         :type node: dict
323         :type url_file: str
324         :type data: dict, str
325         :type path: str
326         :type data_representation: DataRepresentation
327         :return: Status code and content of response.
328         :rtype: tuple
329         :raises HoneycombError: If the given data representation is not defined
330         in HEADERS.
331         """
332
333         try:
334             header = HEADERS[data_representation]
335         except AttributeError as err:
336             raise HoneycombError("Wrong data representation: {0}.".
337                                  format(data_representation), repr(err))
338         if data_representation == DataRepresentation.JSON:
339             data = dumps(data)
340
341         logger.trace(data)
342
343         base_path = HoneycombUtil.read_path_from_url_file(url_file)
344         path = base_path + path
345         logger.trace(path)
346         (status_code, response) = HTTPRequest.put(
347             node=node, path=path, headers=header, payload=data)
348
349         if status_code not in (HTTPCodes.OK, HTTPCodes.ACCEPTED):
350             HoneycombUtil.read_log_tail(node)
351         return status_code, response
352
353     @staticmethod
354     def post_honeycomb_data(node, url_file, data=None,
355                             data_representation=DataRepresentation.JSON,
356                             timeout=10):
357         """Send a POST request and return the status code and response content.
358
359         :param node: Honeycomb node.
360         :param url_file: URL file. The argument contains only the name of file
361         without extension, not the full path.
362         :param data: Configuration data to be sent to Honeycomb.
363         :param data_representation: How the data is represented.
364         :param timeout: How long to wait for the server to send data before
365         giving up.
366         :type node: dict
367         :type url_file: str
368         :type data: dict, str
369         :type data_representation: DataRepresentation
370         :type timeout: int
371         :return: Status code and content of response.
372         :rtype: tuple
373         :raises HoneycombError: If the given data representation is not defined
374         in HEADERS.
375         """
376
377         try:
378             header = HEADERS[data_representation]
379         except AttributeError as err:
380             raise HoneycombError("Wrong data representation: {0}.".
381                                  format(data_representation), repr(err))
382         if data_representation == DataRepresentation.JSON:
383             data = dumps(data)
384
385         path = HoneycombUtil.read_path_from_url_file(url_file)
386         (status_code, response) = HTTPRequest.post(
387             node=node, path=path, headers=header, payload=data, timeout=timeout)
388
389         if status_code not in (HTTPCodes.OK, HTTPCodes.ACCEPTED):
390             HoneycombUtil.read_log_tail(node)
391         return status_code, response
392
393     @staticmethod
394     def delete_honeycomb_data(node, url_file, path=""):
395         """Delete data from Honeycomb according to given URL.
396
397         :param node: Honeycomb node.
398         :param url_file: URL file. The argument contains only the name of file
399         without extension, not the full path.
400         :param path: Path which is added to the base path to identify the data.
401         :type node: dict
402         :type url_file: str
403         :type path: str
404         :return: Status code and content of response.
405         :rtype tuple
406         """
407
408         base_path = HoneycombUtil.read_path_from_url_file(url_file)
409         path = base_path + path
410         (status_code, response) = HTTPRequest.delete(node, path)
411
412         if status_code != HTTPCodes.OK:
413             HoneycombUtil.read_log_tail(node)
414         return status_code, response
415
416     @staticmethod
417     def read_log_tail(node, lines=120):
418         """Read  the last N lines of the Honeycomb log file and print them
419         to robot log.
420
421         :param node: Honeycomb node.
422         :param lines: Number of lines to read.
423         :type node: dict
424         :type lines: int
425         :return: Last N log lines.
426         :rtype: str
427         """
428
429         logger.trace(
430             "HTTP request failed, "
431             "obtaining last {0} lines of Honeycomb log...".format(lines))
432
433         ssh = SSH()
434         ssh.connect(node)
435         cmd = "tail -n {0} /var/log/honeycomb/honeycomb.log".format(lines)
436         # ssh also logs the reply on trace level
437         (_, stdout, _) = ssh.exec_command(cmd, timeout=30)
438
439         return stdout
440
441     @staticmethod
442     def archive_honeycomb_log(node):
443         """Copy honeycomb log file from DUT node to VIRL for archiving.
444
445         :param node: Honeycomb node.
446         :type node: dict
447         """
448
449         ssh = SSH()
450         ssh.connect(node)
451
452         cmd = "cp /var/log/honeycomb/honeycomb.log /scratch/"
453
454         ssh.exec_command_sudo(cmd)