76bb5b38788a8e230d2c2cc3d181927d48776889
[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
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         :returns: 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         :returns: 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         :returns: 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         :returns: 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         :returns: 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
306         try:
307             data = loads(resp)
308         except ValueError:
309             logger.debug("Failed to deserialize JSON data.")
310             data = None
311
312         return status_code, data
313
314     @staticmethod
315     def put_honeycomb_data(node, url_file, data, path="",
316                            data_representation=DataRepresentation.JSON):
317         """Send configuration data using PUT request and return the status code
318         and response content.
319
320         :param node: Honeycomb node.
321         :param url_file: URL file. The argument contains only the name of file
322         without extension, not the full path.
323         :param data: Configuration data to be sent to Honeycomb.
324         :param path: Path which is added to the base path to identify the data.
325         :param data_representation: How the data is represented.
326         :type node: dict
327         :type url_file: str
328         :type data: dict, str
329         :type path: str
330         :type data_representation: DataRepresentation
331         :returns: Status code and content of response.
332         :rtype: tuple
333         :raises HoneycombError: If the given data representation is not defined
334         in HEADERS.
335         """
336
337         try:
338             header = HEADERS[data_representation]
339         except AttributeError as err:
340             raise HoneycombError("Wrong data representation: {0}.".
341                                  format(data_representation), repr(err))
342         if data_representation == DataRepresentation.JSON:
343             data = dumps(data)
344
345         logger.trace(data)
346
347         base_path = HoneycombUtil.read_path_from_url_file(url_file)
348         path = base_path + path
349         logger.trace(path)
350         return HTTPRequest.put(
351             node=node, path=path, headers=header, payload=data)
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         :returns: 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         return HTTPRequest.post(
387             node=node, path=path, headers=header, payload=data, timeout=timeout)
388
389     @staticmethod
390     def delete_honeycomb_data(node, url_file, path=""):
391         """Delete data from Honeycomb according to given URL.
392
393         :param node: Honeycomb node.
394         :param url_file: URL file. The argument contains only the name of file
395         without extension, not the full path.
396         :param path: Path which is added to the base path to identify the data.
397         :type node: dict
398         :type url_file: str
399         :type path: str
400         :returns: Status code and content of response.
401         :rtype tuple
402         """
403
404         base_path = HoneycombUtil.read_path_from_url_file(url_file)
405         path = base_path + path
406         return HTTPRequest.delete(node, path)
407
408     @staticmethod
409     def append_honeycomb_log(node, suite_name):
410         """Append Honeycomb log for the current test suite to the full log.
411
412         :param node: Honeycomb node.
413         :param suite_name: Name of the current test suite. ${SUITE_NAME}
414         variable in robotframework.
415         :type node: dict
416         :type suite_name: str
417         """
418
419         ssh = SSH()
420         ssh.connect(node)
421
422         ssh.exec_command(
423             "echo '{separator}' >> /tmp/honeycomb.log".format(separator="="*80))
424         ssh.exec_command(
425             "echo 'Log for suite: {suite}' >> /tmp/honeycomb.log".format(
426                 suite=suite_name))
427         ssh.exec_command(
428             "cat {hc_log} >> /tmp/honeycomb.log".format(
429                 hc_log=Const.REMOTE_HC_LOG))
430
431     @staticmethod
432     def clear_honeycomb_log(node):
433         """Delete the Honeycomb log file for the current test suite.
434
435         :param node: Honeycomb node.
436         :type node: dict"""
437
438         ssh = SSH()
439         ssh.connect(node)
440
441         ssh.exec_command("sudo rm {hc_log}".format(hc_log=Const.REMOTE_HC_LOG))
442
443     @staticmethod
444     def archive_honeycomb_log(node, perf=False):
445         """Copy honeycomb log file from DUT node to VIRL for archiving.
446
447         :param node: Honeycomb node.
448         :param perf: Alternate handling, for use with performance test topology.
449         :type node: dict
450         :type perf: bool
451         """
452
453         ssh = SSH()
454         ssh.connect(node)
455
456         if not perf:
457             cmd = "cp /tmp/honeycomb.log /scratch/"
458             ssh.exec_command_sudo(cmd, timeout=60)
459         else:
460             ssh.scp(
461                 ".",
462                 "/tmp/honeycomb.log",
463                 get=True,
464                 timeout=60)
465             ssh.exec_command("rm /tmp/honeycomb.log")