Improve test tag string parsing
[csit.git] / resources / libraries / python / honeycomb / HoneycombUtil.py
1 # Copyright (c) 2018 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
66      - short description of the encountered problem (parameter msg),
67      - relevant messages if there are any collected, e.g., from caught
68        exception (optional parameter details),
69      - relevant data if there are any collected (optional parameter details).
70
71     The logging is performed on two levels: 1. error - short description of the
72     problem; 2. debug - detailed information.
73     """
74
75     def __init__(self, msg, details='', enable_logging=True):
76         """Sets the exception message and enables / disables logging.
77
78         It is not wanted to log errors when using these keywords together
79         with keywords like "Wait until keyword succeeds". So you can disable
80         logging by setting enable_logging to False.
81
82         :param msg: Message to be displayed and logged.
83         :param enable_logging: When True, logging is enabled, otherwise
84             logging is disabled.
85         :type msg: str
86         :type enable_logging: bool
87         """
88         super(HoneycombError, self).__init__()
89         self._msg = "{0}: {1}".format(self.__class__.__name__, msg)
90         self._details = details
91         if enable_logging:
92             logger.debug(self._details)
93
94     def __repr__(self):
95         return repr(self._msg)
96
97     def __str__(self):
98         return str(self._msg)
99
100
101 class HoneycombUtil(object):
102     """Implements low level functionality used in communication with Honeycomb.
103
104     There are implemented methods to get, put and delete data to/from Honeycomb.
105     They are based on functionality implemented in the module HTTPRequests which
106     uses HTTP requests GET, PUT, POST and DELETE to communicate with Honeycomb.
107
108     It is possible to PUT the data represented as XML or JSON structures or as
109     plain text.
110     Data received in the response of GET are always represented as a JSON
111     structure.
112
113     There are also two supportive methods implemented:
114
115       - read_path_from_url_file which reads URL file and returns a path (see
116         docs/honeycomb_url_files.rst).
117       - parse_json_response which parses data from response in JSON
118         representation according to given path.
119     """
120
121     def __init__(self):
122         pass
123
124     @staticmethod
125     def read_path_from_url_file(url_file):
126         """Read path from .url file.
127
128         For more information about .url file see docs/honeycomb_url_files.rst
129
130         :param url_file: URL file. The argument contains only the name of file
131             without extension, not the full path.
132         :type url_file: str
133         :returns: Requested path.
134         :rtype: str
135         """
136
137         url_file = "{0}/{1}.url".format(Const.RESOURCES_TPL_HC, url_file)
138         with open(url_file) as template:
139             path = template.readline()
140         return path
141
142     @staticmethod
143     def find_item(data, path):
144         """Find a data item (single leaf or sub-tree) in data received from
145         Honeycomb REST API.
146
147         Path format:
148         The path is a tuple with items navigating to requested data. The items
149         can be strings or tuples:
150
151           - string item represents a dictionary key in data,
152           - tuple item represents list item in data.
153
154         Example::
155
156           data = \
157           {
158               "interfaces": {
159                   "interface": [
160                       {
161                           "name": "GigabitEthernet0/8/0",
162                           "enabled": "true",
163                           "type": "iana-if-type:ethernetCsmacd",
164                       },
165                       {
166                           "name": "local0",
167                           "enabled": "false",
168                           "type": "iana-if-type:ethernetCsmacd",
169                       }
170                   ]
171               }
172           }
173
174         path = ("interfaces", ("interface", "name", "local0"), "enabled")
175         This path points to "false".
176
177         The tuple ("interface", "name", "local0") consists of::
178
179           index 0 - dictionary key pointing to a list,
180           index 1 - key which identifies an item in the list, it is also marked
181                     as the key in corresponding yang file.
182           index 2 - key value.
183
184         :param data: Data received from Honeycomb REST API.
185         :param path: Path to data we want to find.
186         :type data: dict
187         :type path: tuple
188         :returns: Data represented by path.
189         :rtype: str, dict, or list
190         :raises HoneycombError: If the data has not been found.
191         """
192
193         for path_item in path:
194             try:
195                 if isinstance(path_item, str):
196                     data = data[path_item]
197                 elif isinstance(path_item, tuple):
198                     for data_item in data[path_item[0]]:
199                         if data_item[path_item[1]] == path_item[2]:
200                             data = data_item
201             except KeyError as err:
202                 raise HoneycombError("Data not found: {0}".format(err))
203
204         return data
205
206     @staticmethod
207     def remove_item(data, path):
208         """Remove a data item (single leaf or sub-tree) in data received from
209         Honeycomb REST API.
210
211         :param data: Data received from Honeycomb REST API.
212         :param path: Path to data we want to remove.
213         :type data: dict
214         :type path: tuple
215         :returns: Original data without removed part.
216         :rtype: dict
217         """
218
219         origin_data = previous_data = data
220         try:
221             for path_item in path:
222                 previous_data = data
223                 if isinstance(path_item, str):
224                     data = data[path_item]
225                 elif isinstance(path_item, tuple):
226                     for data_item in data[path_item[0]]:
227                         if data_item[path_item[1]] == path_item[2]:
228                             data = data_item
229         except KeyError as err:
230             logger.debug("Data not found: {0}".format(err))
231             return origin_data
232
233         if isinstance(path[-1], str):
234             previous_data.pop(path[-1])
235         elif isinstance(path[-1], tuple):
236             previous_data[path[-1][0]].remove(data)
237             if not previous_data[path[-1][0]]:
238                 previous_data.pop(path[-1][0])
239
240         return origin_data
241
242     @staticmethod
243     def set_item_value(data, path, new_value):
244         """Set or change the value (single leaf or sub-tree) in data received
245         from Honeycomb REST API.
246
247         If the item is not present in the data structure, it is created.
248
249         :param data: Data received from Honeycomb REST API.
250         :param path: Path to data we want to change or create.
251         :param new_value: The value to be set.
252         :type data: dict
253         :type path: tuple
254         :type new_value: str, dict or list
255         :returns: Original data with the new value.
256         :rtype: dict
257         """
258
259         origin_data = data
260         for path_item in path[:-1]:
261             if isinstance(path_item, str):
262                 try:
263                     data = data[path_item]
264                 except KeyError:
265                     data[path_item] = {}
266                     data = data[path_item]
267             elif isinstance(path_item, tuple):
268                 try:
269                     flag = False
270                     index = 0
271                     for data_item in data[path_item[0]]:
272                         if data_item[path_item[1]] == path_item[2]:
273                             data = data[path_item[0]][index]
274                             flag = True
275                             break
276                         index += 1
277                     if not flag:
278                         data[path_item[0]].append({path_item[1]: path_item[2]})
279                         data = data[path_item[0]][-1]
280                 except KeyError:
281                     data[path_item] = []
282
283         if not path[-1] in data.keys():
284             data[path[-1]] = {}
285
286         if isinstance(new_value, list) and isinstance(data[path[-1]], list):
287             for value in new_value:
288                 data[path[-1]].append(value)
289         else:
290             data[path[-1]] = new_value
291
292         return origin_data
293
294     @staticmethod
295     def get_honeycomb_data(node, url_file, path=""):
296         """Retrieve data from Honeycomb according to given URL.
297
298         :param node: Honeycomb node.
299         :param url_file: URL file. The argument contains only the name of file
300             without extension, not the full path.
301         :param path: Path which is added to the base path to identify the data.
302         :type node: dict
303         :type url_file: str
304         :type path: str
305         :returns: Status code and content of response.
306         :rtype: tuple
307         """
308
309         base_path = HoneycombUtil.read_path_from_url_file(url_file)
310         path = base_path + path
311         status_code, resp = HTTPRequest.get(node, path)
312
313         try:
314             data = loads(resp)
315         except ValueError:
316             logger.debug("Failed to deserialize JSON data.")
317             data = None
318
319         return status_code, data
320
321     @staticmethod
322     def put_honeycomb_data(node, url_file, data, path="",
323                            data_representation=DataRepresentation.JSON):
324         """Send configuration data using PUT request and return the status code
325         and response content.
326
327         :param node: Honeycomb node.
328         :param url_file: URL file. The argument contains only the name of file
329             without extension, not the full path.
330         :param data: Configuration data to be sent to Honeycomb.
331         :param path: Path which is added to the base path to identify the data.
332         :param data_representation: How the data is represented.
333         :type node: dict
334         :type url_file: str
335         :type data: dict, str
336         :type path: str
337         :type data_representation: DataRepresentation
338         :returns: Status code and content of response.
339         :rtype: tuple
340         :raises HoneycombError: If the given data representation is not defined
341             in HEADERS.
342         """
343
344         try:
345             header = HEADERS[data_representation]
346         except AttributeError as err:
347             raise HoneycombError("Wrong data representation: {0}.".
348                                  format(data_representation), repr(err))
349         if data_representation == DataRepresentation.JSON:
350             data = dumps(data)
351
352         logger.trace(data)
353
354         base_path = HoneycombUtil.read_path_from_url_file(url_file)
355         path = base_path + path
356         logger.trace(path)
357         return HTTPRequest.put(
358             node=node, path=path, headers=header, payload=data)
359
360     @staticmethod
361     def post_honeycomb_data(node, url_file, data=None,
362                             data_representation=DataRepresentation.JSON,
363                             timeout=10):
364         """Send a POST request and return the status code and response content.
365
366         :param node: Honeycomb node.
367         :param url_file: URL file. The argument contains only the name of file
368             without extension, not the full path.
369         :param data: Configuration data to be sent to Honeycomb.
370         :param data_representation: How the data is represented.
371         :param timeout: How long to wait for the server to send data before
372             giving up.
373         :type node: dict
374         :type url_file: str
375         :type data: dict, str
376         :type data_representation: DataRepresentation
377         :type timeout: int
378         :returns: Status code and content of response.
379         :rtype: tuple
380         :raises HoneycombError: If the given data representation is not defined
381             in HEADERS.
382         """
383
384         try:
385             header = HEADERS[data_representation]
386         except AttributeError as err:
387             raise HoneycombError("Wrong data representation: {0}.".
388                                  format(data_representation), repr(err))
389         if data_representation == DataRepresentation.JSON:
390             data = dumps(data)
391
392         path = HoneycombUtil.read_path_from_url_file(url_file)
393         return HTTPRequest.post(
394             node=node, path=path, headers=header, payload=data, timeout=timeout)
395
396     @staticmethod
397     def delete_honeycomb_data(node, url_file, path=""):
398         """Delete data from Honeycomb according to given URL.
399
400         :param node: Honeycomb node.
401         :param url_file: URL file. The argument contains only the name of file
402             without extension, not the full path.
403         :param path: Path which is added to the base path to identify the data.
404         :type node: dict
405         :type url_file: str
406         :type path: str
407         :returns: Status code and content of response.
408         :rtype: tuple
409         """
410
411         base_path = HoneycombUtil.read_path_from_url_file(url_file)
412         path = base_path + path
413         return HTTPRequest.delete(node, path)
414
415     @staticmethod
416     def append_honeycomb_log(node, suite_name):
417         """Append Honeycomb log for the current test suite to the full log.
418
419         :param node: Honeycomb node.
420         :param suite_name: Name of the current test suite. ${SUITE_NAME}
421             variable in robotframework.
422         :type node: dict
423         :type suite_name: str
424         """
425
426         ssh = SSH()
427         ssh.connect(node)
428
429         ssh.exec_command(
430             "echo '{separator}' >> /tmp/honeycomb.log".format(separator="="*80))
431         ssh.exec_command(
432             "echo 'Log for suite: {suite}' >> /tmp/honeycomb.log".format(
433                 suite=suite_name))
434         ssh.exec_command(
435             "cat {hc_log} >> /tmp/honeycomb.log".format(
436                 hc_log=Const.REMOTE_HC_LOG))
437
438     @staticmethod
439     def append_odl_log(node, odl_name, suite_name):
440         """Append ODL karaf log for the current test suite to the full log.
441
442         :param node: Honeycomb node.
443         :param odl_name: Name of ODL client version to use.
444         :param suite_name: Name of the current test suite. ${SUITE_NAME}
445             variable in robotframework.
446         :type node: dict
447         :type odl_name: str
448         :type suite_name: str
449         """
450
451         ssh = SSH()
452         ssh.connect(node)
453
454         ssh.exec_command(
455             "echo '{separator}' >> /tmp/karaf.log".format(separator="="*80))
456         ssh.exec_command(
457             "echo 'Log for suite: {suite}' >> /tmp/karaf.log".format(
458                 suite=suite_name))
459         ssh.exec_command(
460             "cat /tmp/karaf_{odl_name}/data/log/karaf.log >> /tmp/karaf.log"
461             .format(odl_name=odl_name))
462
463     @staticmethod
464     def clear_honeycomb_log(node):
465         """Delete the Honeycomb log file for the current test suite.
466
467         :param node: Honeycomb node.
468         :type node: dict
469         """
470
471         ssh = SSH()
472         ssh.connect(node)
473
474         ssh.exec_command("sudo rm {hc_log}".format(hc_log=Const.REMOTE_HC_LOG))
475
476     @staticmethod
477     def archive_honeycomb_log(node, perf=False):
478         """Copy honeycomb log file from DUT node to VIRL for archiving.
479
480         :param node: Honeycomb node.
481         :param perf: Alternate handling, for use with performance test topology.
482         :type node: dict
483         :type perf: bool
484         """
485
486         ssh = SSH()
487         ssh.connect(node)
488
489         if not perf:
490             cmd = "cp /tmp/honeycomb.log /scratch/"
491             ssh.exec_command_sudo(cmd, timeout=60)
492         else:
493             ssh.scp(
494                 ".",
495                 "/tmp/honeycomb.log",
496                 get=True,
497                 timeout=60)
498             ssh.exec_command("rm /tmp/honeycomb.log")
499
500     @staticmethod
501     def archive_odl_log(node):
502         """Copy ODL karaf log file from DUT node to VIRL for archiving.
503
504         :param node: Honeycomb node.
505         :type node: dict
506         """
507
508         ssh = SSH()
509         ssh.connect(node)
510
511         cmd = "cp /tmp/karaf.log /scratch/"
512         ssh.exec_command_sudo(cmd, timeout=60)