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:
6 # http://www.apache.org/licenses/LICENSE-2.0
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.
14 """Implementation of low level functionality used in communication with
17 Exception HoneycombError is used in all methods and in all modules with
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.
25 from json import loads, dumps
26 from enum import Enum, unique
28 from robot.api import logger
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
36 class DataRepresentation(Enum):
37 """Representation of data sent by PUT and POST requests."""
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"}
59 class HoneycombError(Exception):
61 """Exception(s) raised by methods working with Honeycomb.
63 When raising this exception, put this information to the message in this
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).
71 The logging is performed on two levels: 1. error - short description of the
72 problem; 2. debug - detailed information.
75 def __init__(self, msg, details='', enable_logging=True):
76 """Sets the exception message and enables / disables logging.
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.
82 :param msg: Message to be displayed and logged.
83 :param enable_logging: When True, logging is enabled, otherwise
86 :type enable_logging: bool
88 super(HoneycombError, self).__init__()
89 self._msg = "{0}: {1}".format(self.__class__.__name__, msg)
90 self._details = details
92 logger.debug(self._details)
95 return repr(self._msg)
101 class HoneycombUtil(object):
102 """Implements low level functionality used in communication with Honeycomb.
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.
108 It is possible to PUT the data represented as XML or JSON structures or as
110 Data received in the response of GET are always represented as a JSON
113 There are also two supportive methods implemented:
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.
125 def read_path_from_url_file(url_file):
126 """Read path from .url file.
128 For more information about .url file see docs/honeycomb_url_files.rst
130 :param url_file: URL file. The argument contains only the name of file
131 without extension, not the full path.
133 :returns: Requested path.
137 url_file = "{0}/{1}.url".format(Const.RESOURCES_TPL_HC, url_file)
138 with open(url_file) as template:
139 path = template.readline()
143 def find_item(data, path):
144 """Find a data item (single leaf or sub-tree) in data received from
148 The path is a tuple with items navigating to requested data. The items
149 can be strings or tuples:
151 - string item represents a dictionary key in data,
152 - tuple item represents list item in data.
161 "name": "GigabitEthernet0/8/0",
163 "type": "iana-if-type:ethernetCsmacd",
168 "type": "iana-if-type:ethernetCsmacd",
174 path = ("interfaces", ("interface", "name", "local0"), "enabled")
175 This path points to "false".
177 The tuple ("interface", "name", "local0") consists of::
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.
184 :param data: Data received from Honeycomb REST API.
185 :param path: Path to data we want to find.
188 :returns: Data represented by path.
189 :rtype: str, dict, or list
190 :raises HoneycombError: If the data has not been found.
193 for path_item in path:
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]:
201 except KeyError as err:
202 raise HoneycombError("Data not found: {0}".format(err))
207 def remove_item(data, path):
208 """Remove a data item (single leaf or sub-tree) in data received from
211 :param data: Data received from Honeycomb REST API.
212 :param path: Path to data we want to remove.
215 :returns: Original data without removed part.
219 origin_data = previous_data = data
221 for path_item in path:
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]:
229 except KeyError as err:
230 logger.debug("Data not found: {0}".format(err))
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])
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.
247 If the item is not present in the data structure, it is created.
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.
254 :type new_value: str, dict or list
255 :returns: Original data with the new value.
260 for path_item in path[:-1]:
261 if isinstance(path_item, str):
263 data = data[path_item]
266 data = data[path_item]
267 elif isinstance(path_item, tuple):
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]
278 data[path_item[0]].append({path_item[1]: path_item[2]})
279 data = data[path_item[0]][-1]
283 if not path[-1] in data.keys():
286 if isinstance(new_value, list) and isinstance(data[path[-1]], list):
287 for value in new_value:
288 data[path[-1]].append(value)
290 data[path[-1]] = new_value
295 def get_honeycomb_data(node, url_file, path=""):
296 """Retrieve data from Honeycomb according to given URL.
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.
305 :returns: Status code and content of response.
309 base_path = HoneycombUtil.read_path_from_url_file(url_file)
310 path = base_path + path
311 status_code, resp = HTTPRequest.get(node, path)
316 logger.debug("Failed to deserialize JSON data.")
319 return status_code, data
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.
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.
335 :type data: dict, str
337 :type data_representation: DataRepresentation
338 :returns: Status code and content of response.
340 :raises HoneycombError: If the given data representation is not defined
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:
354 base_path = HoneycombUtil.read_path_from_url_file(url_file)
355 path = base_path + path
357 return HTTPRequest.put(
358 node=node, path=path, headers=header, payload=data)
361 def post_honeycomb_data(node, url_file, data=None,
362 data_representation=DataRepresentation.JSON,
364 """Send a POST request and return the status code and response content.
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
375 :type data: dict, str
376 :type data_representation: DataRepresentation
378 :returns: Status code and content of response.
380 :raises HoneycombError: If the given data representation is not defined
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:
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)
397 def delete_honeycomb_data(node, url_file, path=""):
398 """Delete data from Honeycomb according to given URL.
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.
407 :returns: Status code and content of response.
411 base_path = HoneycombUtil.read_path_from_url_file(url_file)
412 path = base_path + path
413 return HTTPRequest.delete(node, path)
416 def append_honeycomb_log(node, suite_name):
417 """Append Honeycomb log for the current test suite to the full log.
419 :param node: Honeycomb node.
420 :param suite_name: Name of the current test suite. ${SUITE_NAME}
421 variable in robotframework.
423 :type suite_name: str
430 "echo '{separator}' >> /tmp/honeycomb.log".format(separator="="*80))
432 "echo 'Log for suite: {suite}' >> /tmp/honeycomb.log".format(
435 "cat {hc_log} >> /tmp/honeycomb.log".format(
436 hc_log=Const.REMOTE_HC_LOG))
439 def append_odl_log(node, odl_name, suite_name):
440 """Append ODL karaf log for the current test suite to the full log.
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.
448 :type suite_name: str
455 "echo '{separator}' >> /tmp/karaf.log".format(separator="="*80))
457 "echo 'Log for suite: {suite}' >> /tmp/karaf.log".format(
460 "cat /tmp/karaf_{odl_name}/data/log/karaf.log >> /tmp/karaf.log"
461 .format(odl_name=odl_name))
464 def clear_honeycomb_log(node):
465 """Delete the Honeycomb log file for the current test suite.
467 :param node: Honeycomb node.
474 ssh.exec_command("sudo rm {hc_log}".format(hc_log=Const.REMOTE_HC_LOG))
477 def archive_honeycomb_log(node, perf=False):
478 """Copy honeycomb log file from DUT node to VIRL for archiving.
480 :param node: Honeycomb node.
481 :param perf: Alternate handling, for use with performance test topology.
490 cmd = "cp /tmp/honeycomb.log /scratch/"
491 ssh.exec_command_sudo(cmd, timeout=60)
495 "/tmp/honeycomb.log",
498 ssh.exec_command("rm /tmp/honeycomb.log")
501 def archive_odl_log(node):
502 """Copy ODL karaf log file from DUT node to VIRL for archiving.
504 :param node: Honeycomb node.
511 cmd = "cp /tmp/karaf.log /scratch/"
512 ssh.exec_command_sudo(cmd, timeout=60)