# See the License for the specific language governing permissions and
# limitations under the License.
-"""Implements keywords used with Honeycomb."""
+"""Implementation of low level functionality used in communication with
+Honeycomb.
-import os.path
-from json import loads
+Exception HoneycombError is used in all methods and in all modules with
+Honeycomb keywords.
+
+Class HoneycombUtil implements methods used by Honeycomb keywords. They must not
+be used directly in tests. Use keywords implemented in the module
+HoneycombAPIKeywords instead.
+"""
+
+from json import loads, dumps
+from enum import Enum, unique
from robot.api import logger
-from resources.libraries.python.topology import NodeType
from resources.libraries.python.HTTPRequest import HTTPRequest
-from resources.libraries.python.constants import Constants as C
+from resources.libraries.python.constants import Constants as Const
+
+
+@unique
+class DataRepresentation(Enum):
+ """Representation of data sent by PUT and POST requests."""
+ NO_DATA = 0
+ JSON = 1
+ XML = 2
+ TXT = 3
+
+
+# Headers used in requests. Key - content representation, value - header.
+HEADERS = {DataRepresentation.NO_DATA:
+ {}, # It must be empty dictionary.
+ DataRepresentation.JSON:
+ {"Content-Type": "application/json",
+ "Accept": "text/plain"},
+ DataRepresentation.XML:
+ {"Content-Type": "application/xml",
+ "Accept": "text/plain"},
+ DataRepresentation.TXT:
+ {"Content-Type": "text/plain",
+ "Accept": "text/plain"}
+ }
+
+
+class HoneycombError(Exception):
+
+ """Exception(s) raised by methods working with Honeycomb.
+
+ When raising this exception, put this information to the message in this
+ order:
+ - short description of the encountered problem (parameter msg),
+ - relevant messages if there are any collected, e.g., from caught
+ exception (optional parameter details),
+ - relevant data if there are any collected (optional parameter details).
+ The logging is performed on two levels: 1. error - short description of the
+ problem; 2. debug - detailed information.
+ """
+
+ def __init__(self, msg, details='', enable_logging=True):
+ """Sets the exception message and enables / disables logging.
+
+ It is not wanted to log errors when using these keywords together
+ with keywords like "Wait until keyword succeeds". So you can disable
+ logging by setting enable_logging to False.
+
+ :param msg: Message to be displayed and logged.
+ :param enable_logging: When True, logging is enabled, otherwise
+ logging is disabled.
+ :type msg: str
+ :type enable_logging: bool
+ """
+ super(HoneycombError, self).__init__()
+ self._msg = "{0}: {1}".format(self.__class__.__name__, msg)
+ self._details = details
+ if enable_logging:
+ logger.error(self._msg)
+ logger.debug(self._details)
+
+ def __repr__(self):
+ return repr(self._msg)
+
+ def __str__(self):
+ return str(self._msg)
class HoneycombUtil(object):
- """Implements keywords used with Honeycomb."""
+ """Implements low level functionality used in communication with Honeycomb.
+
+ There are implemented methods to get, put and delete data to/from Honeycomb.
+ They are based on functionality implemented in the module HTTPRequests which
+ uses HTTP requests GET, PUT, POST and DELETE to communicate with Honeycomb.
+
+ It is possible to PUT the data represented as XML or JSON structures or as
+ plain text.
+ Data received in the response of GET are always represented as a JSON
+ structure.
+
+ There are also two supportive methods implemented:
+ - read_path_from_url_file which reads URL file and returns a path (see
+ docs/honeycomb_url_files.rst).
+ - parse_json_response which parses data from response in JSON representation
+ according to given path.
+ """
def __init__(self):
pass
- def get_configured_topology(self, nodes):
- """Retrieves topology node IDs from each honeycomb node.
+ @staticmethod
+ def read_path_from_url_file(url_file):
+ """Read path from *.url file.
- :param nodes: all nodes in topology
- :type nodes: dict
- :return: list of string IDs such as ['vpp1', 'vpp2']
- :rtype list
+ For more information about *.url file see docs/honeycomb_url_files.rst
+ :param url_file: URL file. The argument contains only the name of file
+ without extension, not the full path.
+ :type url_file: str
+ :return: Requested path.
+ :rtype: str
"""
- url_file = os.path.join(C.RESOURCES_TPL_HC, "config_topology.url")
+ url_file = "{0}/{1}.url".format(Const.RESOURCES_TPL_HC, url_file)
with open(url_file) as template:
path = template.readline()
+ return path
+
+ @staticmethod
+ def find_item(data, path):
+ """Find a data item (single leaf or sub-tree) in data received from
+ Honeycomb REST API.
+
+ Path format:
+ The path is a tuple with items navigating to requested data. The items
+ can be strings or tuples:
+ - string item represents a dictionary key in data,
+ - tuple item represents list item in data.
+
+ Example:
+ data = \
+ {
+ "interfaces": {
+ "interface": [
+ {
+ "name": "GigabitEthernet0/8/0",
+ "enabled": "true",
+ "type": "iana-if-type:ethernetCsmacd",
+ },
+ {
+ "name": "local0",
+ "enabled": "false",
+ "type": "iana-if-type:ethernetCsmacd",
+ }
+ ]
+ }
+ }
+
+ path = ("interfaces", ("interface", "name", "local0"), "enabled")
+ This path points to "false".
+
+ The tuple ("interface", "name", "local0") consists of:
+ index 0 - dictionary key pointing to a list,
+ index 1 - key which identifies an item in the list, it is also marked as
+ the key in corresponding yang file.
+ index 2 - key value.
+
+ :param data: Data received from Honeycomb REST API.
+ :param path: Path to data we want to find.
+ :type data: dict
+ :type path: tuple
+ :return: Data represented by path.
+ :rtype: str, dict, or list
+ :raises HoneycombError: If the data has not been found.
+ """
- data = []
- for node in nodes.values():
- if node['type'] == NodeType.DUT:
- _, ret = HTTPRequest.get(node, path)
- logger.debug('return: {0}'.format(ret))
- data.append(self.parse_json_response(ret, ("topology",
- "node", "node-id")))
+ for path_item in path:
+ try:
+ if isinstance(path_item, str):
+ data = data[path_item]
+ elif isinstance(path_item, tuple):
+ for data_item in data[path_item[0]]:
+ if data_item[path_item[1]] == path_item[2]:
+ data = data_item
+ except KeyError as err:
+ raise HoneycombError("Data not found: {0}".format(err))
return data
- def parse_json_response(self, response, path=None):
- """Parse data from response string in JSON format according to given
- path.
+ @staticmethod
+ def remove_item(data, path):
+ """Remove a data item (single leaf or sub-tree) in data received from
+ Honeycomb REST API.
- :param response: JSON formatted string
- :param path: Path to navigate down the data structure
- :type response: string
+ :param data: Data received from Honeycomb REST API.
+ :param path: Path to data we want to remove.
+ :type data: dict
:type path: tuple
- :return: JSON dictionary/list tree
+ :return: Original data without removed part.
:rtype: dict
"""
- data = loads(response)
- if path:
- data = self._parse_json_tree(data, path)
- while isinstance(data, list) and len(data) == 1:
- data = data[0]
+ origin_data = previous_data = data
+ try:
+ for path_item in path:
+ previous_data = data
+ if isinstance(path_item, str):
+ data = data[path_item]
+ elif isinstance(path_item, tuple):
+ for data_item in data[path_item[0]]:
+ if data_item[path_item[1]] == path_item[2]:
+ data = data_item
+ except KeyError as err:
+ logger.debug("Data not found: {0}".format(err))
+ return origin_data
- return data
+ if isinstance(path[-1], str):
+ previous_data.pop(path[-1])
+ elif isinstance(path[-1], tuple):
+ previous_data[path[-1][0]].remove(data)
+ if not previous_data[path[-1][0]]:
+ previous_data.pop(path[-1][0])
+
+ return origin_data
- def _parse_json_tree(self, data, path):
- """Retrieve data from python representation of JSON object.
+ @staticmethod
+ def set_item_value(data, path, new_value):
+ """Set or change the value (single leaf or sub-tree) in data received
+ from Honeycomb REST API.
- :param data: parsed JSON dictionary tree
- :param path: Path to navigate down the dictionary tree
+ If the item is not present in the data structure, it is created.
+
+ :param data: Data received from Honeycomb REST API.
+ :param path: Path to data we want to change or create.
+ :param new_value: The value to be set.
:type data: dict
:type path: tuple
- :return: data from specified path
- :rtype: list or str
+ :type new_value: str, dict or list
+ :return: Original data with the new value.
+ :rtype: dict
"""
- count = 0
- for key in path:
- if isinstance(data, dict):
- data = data[key]
- count += 1
- elif isinstance(data, list):
- result = []
- for item in data:
- result.append(self._parse_json_tree(item, path[count:]))
- return result
+ origin_data = data
+ for path_item in path[:-1]:
+ if isinstance(path_item, str):
+ try:
+ data = data[path_item]
+ except KeyError:
+ data[path_item] = {}
+ data = data[path_item]
+ elif isinstance(path_item, tuple):
+ try:
+ flag = False
+ index = 0
+ for data_item in data[path_item[0]]:
+ if data_item[path_item[1]] == path_item[2]:
+ data = data[path_item[0]][index]
+ flag = True
+ break
+ index += 1
+ if not flag:
+ data[path_item[0]].append({path_item[1]: path_item[2]})
+ data = data[path_item[0]][-1]
+ except KeyError:
+ data[path_item] = []
- return data
+ if not path[-1] in data.keys():
+ data[path[-1]] = {}
+
+ if isinstance(new_value, list) and isinstance(data[path[-1]], list):
+ for value in new_value:
+ data[path[-1]].append(value)
+ else:
+ data[path[-1]] = new_value
+
+ return origin_data
+
+ @staticmethod
+ def get_honeycomb_data(node, url_file):
+ """Retrieve data from Honeycomb according to given URL.
+
+ :param node: Honeycomb node.
+ :param url_file: URL file. The argument contains only the name of file
+ without extension, not the full path.
+ :type node: dict
+ :type url_file: str
+ :return: Status code and content of response.
+ :rtype tuple
+ """
+
+ path = HoneycombUtil.read_path_from_url_file(url_file)
+ status_code, resp = HTTPRequest.get(node, path)
+ return status_code, loads(resp)
+
+ @staticmethod
+ def put_honeycomb_data(node, url_file, data,
+ data_representation=DataRepresentation.JSON):
+ """Send configuration data using PUT request and return the status code
+ and response content.
+
+ :param node: Honeycomb node.
+ :param url_file: URL file. The argument contains only the name of file
+ without extension, not the full path.
+ :param data: Configuration data to be sent to Honeycomb.
+ :param data_representation: How the data is represented.
+ :type node: dict
+ :type url_file: str
+ :type data: dict, str
+ :type data_representation: DataRepresentation
+ :return: Status code and content of response.
+ :rtype: tuple
+ :raises HoneycombError: If the given data representation is not defined
+ in HEADERS.
+ """
+
+ try:
+ header = HEADERS[data_representation]
+ except AttributeError as err:
+ raise HoneycombError("Wrong data representation: {0}.".
+ format(data_representation), repr(err))
+ if data_representation == DataRepresentation.JSON:
+ data = dumps(data)
+
+ path = HoneycombUtil.read_path_from_url_file(url_file)
+ return HTTPRequest.put(node=node, path=path, headers=header,
+ payload=data)
+
+ @staticmethod
+ def post_honeycomb_data(node, url_file, data=None,
+ data_representation=DataRepresentation.JSON,
+ timeout=10):
+ """Send a POST request and return the status code and response content.
+
+ :param node: Honeycomb node.
+ :param url_file: URL file. The argument contains only the name of file
+ without extension, not the full path.
+ :param data: Configuration data to be sent to Honeycomb.
+ :param data_representation: How the data is represented.
+ :param timeout: How long to wait for the server to send data before
+ giving up.
+ :type node: dict
+ :type url_file: str
+ :type data: dict, str
+ :type data_representation: DataRepresentation
+ :type timeout: int
+ :return: Status code and content of response.
+ :rtype: tuple
+ :raises HoneycombError: If the given data representation is not defined
+ in HEADERS.
+ """
+
+ try:
+ header = HEADERS[data_representation]
+ except AttributeError as err:
+ raise HoneycombError("Wrong data representation: {0}.".
+ format(data_representation), repr(err))
+ if data_representation == DataRepresentation.JSON:
+ data = dumps(data)
+
+ path = HoneycombUtil.read_path_from_url_file(url_file)
+ return HTTPRequest.post(node=node, path=path, headers=header,
+ payload=data, timeout=timeout)
+
+ @staticmethod
+ def delete_honeycomb_data(node, url_file):
+ """Delete data from Honeycomb according to given URL.
+
+ :param node: Honeycomb node.
+ :param url_file: URL file. The argument contains only the name of file
+ without extension, not the full path.
+ :type node: dict
+ :type url_file: str
+ :return: Status code and content of response.
+ :rtype tuple
+ """
+
+ path = HoneycombUtil.read_path_from_url_file(url_file)
+ return HTTPRequest.delete(node, path)