Honeycomb setup and utils 89/689/9
authorTibor <tifrank@cisco.com>
Wed, 6 Apr 2016 11:33:42 +0000 (13:33 +0200)
committerGerrit Code Review <gerrit@fd.io>
Wed, 13 Apr 2016 16:00:37 +0000 (16:00 +0000)
- re-implement HTTPCodes as IntEnum rather then dictionary
- add methods to manipulate data using honeycomb - get, set, delete
- change the name of url file from vpp_version.url to oper_vpp_version.url
- improve checking of startup and shutdown state of honeycomb
- PEP8 fixes
- add docstrings in all modules and classes
- move logging to the lowest possible level
- improve logging in exceptions
- add method exec_command_sudo_log to resources.libraries.python.ssh module

Change-Id: I54e0c6b45313e3a3c11bafa475488ae2b1e605c2
Signed-off-by: Tibor Frank <tifrank@cisco.com>
docs/honeycomb_url_files.rst [new file with mode: 0644]
resources/libraries/python/HTTPRequest.py
resources/libraries/python/HoneycombSetup.py
resources/libraries/python/HoneycombUtil.py
resources/templates/honeycomb/oper_vpp_version.url [moved from resources/templates/honeycomb/vpp_version.url with 100% similarity]

diff --git a/docs/honeycomb_url_files.rst b/docs/honeycomb_url_files.rst
new file mode 100644 (file)
index 0000000..4dfa10c
--- /dev/null
@@ -0,0 +1,22 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+Documentation to files used to store URLs to resources in Honeycomb.
+====================================================================
+
+A URL file is a text file encoded in utf-8 with a path to a resource in
+Honeycomb. There is only one line in each file.
+
+The URL is stored without host and port with leading slash. There is no slash at
+the end, e.g.:
+    /restconf/config/v3po:vpp/bridge-domains
index 7b21f5a..fd2925c 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""Implements HTTP requests GET, PUT, POST, DELETE used in communication with
-honeycomb.
+"""Implementation of HTTP requests GET, PUT, POST and DELETE used in
+communication with Honeycomb.
+
+The HTTP requests are implemented in the class HTTPRequest which uses
+requests.request.
 """
 
+from enum import IntEnum, unique
+
+from robot.api.deco import keyword
+from robot.api import logger
+
 from requests import request, RequestException, Timeout, TooManyRedirects, \
     HTTPError, ConnectionError
 from requests.auth import HTTPBasicAuth
 
-from robot.api import logger
-from robot.api.deco import keyword
-
 
-HTTP_CODES = {"OK": 200,
-              "UNAUTHORIZED": 401,
-              "FORBIDDEN": 403,
-              "NOT_FOUND": 404,
-              "SERVICE_UNAVAILABLE": 503}
+@unique
+class HTTPCodes(IntEnum):
+    """HTTP status codes"""
+    OK = 200
+    UNAUTHORIZED = 401
+    FORBIDDEN = 403
+    NOT_FOUND = 404
+    SERVICE_UNAVAILABLE = 503
 
 
 class HTTPRequestError(Exception):
-    """Exception raised by HTTPRequest objects."""
+    """Exception raised by HTTPRequest objects.
+
+    When raising this exception, put this information to the message in this
+    order:
+    - short description of the encountered problem,
+    - relevant messages if there are any collected, e.g., from caught
+      exception,
+    - relevant data if there are any collected.
+    The logging is performed on two levels: 1. error - short description of the
+    problem; 2. debug - detailed information.
+    """
 
-    def __init__(self, msg, enable_logging=True):
-        """Sets the exception message and enables / disables logging
+    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".
+        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 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(HTTPRequestError, self).__init__()
-        self._msg = msg
-        self._repr_msg = self.__module__ + '.' + \
-            self.__class__.__name__ + ": " + self._msg
-
+        self._msg = "{0}: {1}".format(self.__class__.__name__, msg)
+        self._details = details
         if enable_logging:
             logger.error(self._msg)
-            logger.debug(self._repr_msg)
+            logger.debug(self._details)
 
     def __repr__(self):
-        return repr(self._repr_msg)
+        return repr(self._msg)
 
     def __str__(self):
-        return str(self._repr_msg)
+        return str(self._msg)
 
 
 class HTTPRequest(object):
-    """A class implementing HTTP requests."""
+    """A class implementing HTTP requests GET, PUT, POST and DELETE used in
+    communication with Honeycomb.
+
+    The communication with Honeycomb and processing of all exceptions is done in
+    the method _http_request which uses requests.request to send requests and
+    receive responses. The received status code and content of response are
+    logged on the debug level.
+    All possible exceptions raised by requests.request are also processed there.
+
+    The other methods (get, put, post and delete) use _http_request to send
+    corresponding request.
+
+    These methods must not be used as keywords in tests. Use keywords
+    implemented in the module HoneycombAPIKeywords instead.
+    """
 
     def __init__(self):
         pass
 
     @staticmethod
     def create_full_url(ip_addr, port, path):
-        """Creates full url including IP, port, and path to data.
+        """Creates full url including host, port, and path to data.
 
-        :param ip_addr: Server IP
-        :param port: Communication port
-        :param path: Path to data
+        :param ip_addr: Server IP.
+        :param port: Communication port.
+        :param path: Path to data.
         :type ip_addr: str
         :type port: str or int
         :type path: str
@@ -85,16 +116,16 @@ class HTTPRequest(object):
 
     @staticmethod
     def _http_request(method, node, path, enable_logging=True, **kwargs):
-        """Sends specified HTTP request and returns status code and
-        response content
+        """Sends specified HTTP request and returns status code and response
+        content.
 
         :param method: The method to be performed on the resource identified by
-        the given request URI
-        :param node: honeycomb node
-        :param path: URL path, e.g. /index.html
-        :param enable_logging: used to suppress errors when checking
-        honeycomb state during suite setup and teardown
-        :param kwargs: named parameters accepted by request.request:
+        the given request URI.
+        :param node: Honeycomb node.
+        :param path: URL path, e.g. /index.html.
+        :param enable_logging: Used to suppress errors when checking Honeycomb
+        state during suite setup and teardown.
+        :param kwargs: Named parameters accepted by request.request:
             params -- (optional) Dictionary or bytes to be sent in the query
             string for the Request.
             data -- (optional) Dictionary, bytes, or file-like object to
@@ -127,11 +158,11 @@ class HTTPRequest(object):
         :return: Status code and content of response
         :rtype: tuple
         :raises HTTPRequestError: If
-        1. it is not possible to connect
-        2. invalid HTTP response comes from server
-        3. request exceeded the configured number of maximum re-directions
-        4. request timed out
-        5. there is any other unexpected HTTP request exception
+        1. it is not possible to connect,
+        2. invalid HTTP response comes from server,
+        3. request exceeded the configured number of maximum re-directions,
+        4. request timed out,
+        5. there is any other unexpected HTTP request exception.
         """
         timeout = kwargs["timeout"]
         url = HTTPRequest.create_full_url(node['host'],
@@ -141,29 +172,30 @@ class HTTPRequest(object):
             auth = HTTPBasicAuth(node['honeycomb']['user'],
                                  node['honeycomb']['passwd'])
             rsp = request(method, url, auth=auth, **kwargs)
+
+            logger.debug("Status code: {0}".format(rsp.status_code))
+            logger.debug("Response: {0}".format(rsp.content))
+
             return rsp.status_code, rsp.content
 
         except ConnectionError as err:
             # Switching the logging on / off is needed only for
             # "requests.ConnectionError"
-            if enable_logging:
-                raise HTTPRequestError("Not possible to connect to {0}\n".
-                                       format(url) + repr(err))
-            else:
-                raise HTTPRequestError("Not possible to connect to {0}\n".
-                                       format(url) + repr(err),
-                                       enable_logging=False)
+            raise HTTPRequestError("Not possible to connect to {0}:{1}.".
+                                   format(node['host'],
+                                          node['honeycomb']['port']),
+                                   repr(err), enable_logging=enable_logging)
         except HTTPError as err:
-            raise HTTPRequestError("Invalid HTTP response from {0}\n".
-                                   format(url) + repr(err))
+            raise HTTPRequestError("Invalid HTTP response from {0}.".
+                                   format(node['host']), repr(err))
         except TooManyRedirects as err:
             raise HTTPRequestError("Request exceeded the configured number "
-                                   "of maximum re-directions\n" + repr(err))
+                                   "of maximum re-directions.", repr(err))
         except Timeout as err:
-            raise HTTPRequestError("Request timed out. Timeout is set to "
-                                   "{0}\n".format(timeout) + repr(err))
+            raise HTTPRequestError("Request timed out. Timeout is set to {0}.".
+                                   format(timeout), repr(err))
         except RequestException as err:
-            raise HTTPRequestError("Unexpected HTTP request exception.\n" +
+            raise HTTPRequestError("Unexpected HTTP request exception.",
                                    repr(err))
 
     @staticmethod
@@ -171,60 +203,64 @@ class HTTPRequest(object):
     def get(node, path, headers=None, timeout=10, enable_logging=True):
         """Sends a GET request and returns the response and status code.
 
-        :param node: honeycomb node
-        :param path: URL path, e.g. /index.html
+        :param node: Honeycomb node.
+        :param path: URL path, e.g. /index.html.
         :param headers: Dictionary of HTTP Headers to send with the Request.
         :param timeout: How long to wait for the server to send data before
         giving up, as a float, or a (connect timeout, read timeout) tuple.
-        :param enable_logging: Used to suppress errors when checking
-        honeycomb state during suite setup and teardown. When True, logging
-        is enabled, otherwise logging is disabled.
+        :param enable_logging: Used to suppress errors when checking Honeycomb
+        state during suite setup and teardown. When True, logging is enabled,
+        otherwise logging is disabled.
         :type node: dict
         :type path: str
         :type headers: dict
         :type timeout: float or tuple
         :type enable_logging: bool
-        :return: Status code and content of response
+        :return: Status code and content of response.
         :rtype: tuple
         """
+
         return HTTPRequest._http_request('GET', node, path,
                                          enable_logging=enable_logging,
                                          headers=headers, timeout=timeout)
 
     @staticmethod
     @keyword(name="HTTP Put")
-    def put(node, path, headers=None, payload=None, timeout=10):
+    def put(node, path, headers=None, payload=None, json=None, timeout=10):
         """Sends a PUT request and returns the response and status code.
 
-        :param node: honeycomb node
-        :param path: URL path, e.g. /index.html
+        :param node: Honeycomb node.
+        :param path: URL path, e.g. /index.html.
         :param headers: Dictionary of HTTP Headers to send with the Request.
         :param payload: Dictionary, bytes, or file-like object to send in
         the body of the Request.
+        :param json: JSON formatted string to send in the body of the Request.
         :param timeout: How long to wait for the server to send data before
         giving up, as a float, or a (connect timeout, read timeout) tuple.
         :type node: dict
         :type path: str
         :type headers: dict
         :type payload: dict, bytes, or file-like object
+        :type json: str
         :type timeout: float or tuple
-        :return: Status code and content of response
+        :return: Status code and content of response.
         :rtype: tuple
         """
         return HTTPRequest._http_request('PUT', node, path, headers=headers,
-                                         data=payload, timeout=timeout)
+                                         data=payload, json=json,
+                                         timeout=timeout)
 
     @staticmethod
     @keyword(name="HTTP Post")
     def post(node, path, headers=None, payload=None, json=None, timeout=10):
         """Sends a POST request and returns the response and status code.
 
-        :param node: honeycomb node
-        :param path: URL path, e.g. /index.html
+        :param node: Honeycomb node.
+        :param path: URL path, e.g. /index.html.
         :param headers: Dictionary of HTTP Headers to send with the Request.
         :param payload: Dictionary, bytes, or file-like object to send in
         the body of the Request.
-        :param json: json data to send in the body of the Request
+        :param json: JSON formatted string to send in the body of the Request.
         :param timeout: How long to wait for the server to send data before
         giving up, as a float, or a (connect timeout, read timeout) tuple.
         :type node: dict
@@ -233,7 +269,7 @@ class HTTPRequest(object):
         :type payload: dict, bytes, or file-like object
         :type json: str
         :type timeout: float or tuple
-        :return: Status code and content of response
+        :return: Status code and content of response.
         :rtype: tuple
         """
         return HTTPRequest._http_request('POST', node, path, headers=headers,
@@ -245,14 +281,14 @@ class HTTPRequest(object):
     def delete(node, path, timeout=10):
         """Sends a DELETE request and returns the response and status code.
 
-        :param node: honeycomb node
-        :param path: URL path, e.g. /index.html
+        :param node: Honeycomb node.
+        :param path: URL path, e.g. /index.html.
         :param timeout: How long to wait for the server to send data before
         giving up, as a float, or a (connect timeout, read timeout) tuple.
         :type node: dict
         :type path: str
         :type timeout: float or tuple
-        :return: Status code and content of response
+        :return: Status code and content of response.
         :rtype: tuple
         """
         return HTTPRequest._http_request('DELETE', node, path, timeout=timeout)
index de05eff..384c294 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""Implements keywords for Honeycomb setup."""
+"""Implementation of keywords for Honeycomb setup."""
 
-import os.path
 from xml.etree import ElementTree as ET
 
 from robot.api import logger
 
 from resources.libraries.python.topology import NodeType
 from resources.libraries.python.ssh import SSH
-from resources.libraries.python.HTTPRequest import HTTPRequest, \
-    HTTPRequestError, HTTP_CODES
-from resources.libraries.python.constants import Constants as C
-
-
-class HoneycombError(Exception):
-    """Exception(s) raised by methods working with Honeycomb."""
-
-    def __init__(self, msg, 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".
-
-        :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 = msg
-        self._repr_msg = self.__module__ + '.' + \
-                         self.__class__.__name__ + ": " + self._msg
-        if enable_logging:
-            logger.error(self._msg)
-            logger.debug(self._repr_msg)
-
-    def __repr__(self):
-        return repr(self._repr_msg)
-
-    def __str__(self):
-        return str(self._repr_msg)
+from resources.libraries.python.HTTPRequest import HTTPRequest, HTTPCodes, \
+    HTTPRequestError
+from resources.libraries.python.HoneycombUtil import HoneycombUtil as HcUtil
+from resources.libraries.python.HoneycombUtil import HoneycombError
+from resources.libraries.python.constants import Constants as Const
 
 
 class HoneycombSetup(object):
-    """Implements keywords for Honeycomb setup."""
+    """Implements keywords for Honeycomb setup.
+
+    The keywords implemented in this class make possible to:
+    - start Honeycomb,
+    - stop Honeycomb,
+    - check the Honeycomb start-up state,
+    - check the Honeycomb shutdown state,
+    - add VPP to the topology.
+    """
 
     def __init__(self):
         pass
 
     @staticmethod
     def start_honeycomb_on_all_duts(nodes):
-        """Start honeycomb on all DUT nodes in topology.
-
-        :param nodes: all nodes in topology
+        """Start Honeycomb on all DUT nodes in topology.
+
+        This keyword starts the Honeycomb service on all DUTs. The keyword just
+        starts the Honeycomb and does not check its startup state. Use the
+        keyword "Check Honeycomb Startup State" to check if the Honeycomb is up
+        and running.
+        Honeycomb must be installed in "/opt" directory, otherwise the start
+        will fail.
+        :param nodes: All nodes in topology.
         :type nodes: dict
+        :raises HoneycombError: If Honeycomb fails to start.
         """
-        logger.console("Starting honeycomb service")
+        logger.console("Starting Honeycomb service ...")
+
+        cmd = "{0}/start".format(Const.REMOTE_HC_DIR)
 
         for node in nodes.values():
             if node['type'] == NodeType.DUT:
-                HoneycombSetup.start_honeycomb(node)
-
-    @staticmethod
-    def start_honeycomb(node):
-        """Start up honeycomb on DUT node.
-
-        :param node: DUT node with honeycomb
-        :type node: dict
-        :return: ret_code, stdout, stderr
-        :rtype: tuple
-        :raises HoneycombError: if Honeycomb fails to start.
-        """
-
-        ssh = SSH()
-        ssh.connect(node)
-        cmd = os.path.join(C.REMOTE_HC_DIR, "start")
-        (ret_code, stdout, stderr) = ssh.exec_command_sudo(cmd)
-        if int(ret_code) != 0:
-            logger.debug('stdout: {0}'.format(stdout))
-            logger.debug('stderr: {0}'.format(stderr))
-            raise HoneycombError('Node {0} failed to start honeycomb'.
-                                 format(node['host']))
-        return ret_code, stdout, stderr
+                ssh = SSH()
+                ssh.connect(node)
+                (ret_code, _, _) = ssh.exec_command_sudo(cmd)
+                if int(ret_code) != 0:
+                    raise HoneycombError('Node {0} failed to start Honeycomb.'.
+                                         format(node['host']))
+                else:
+                    logger.info("Starting the Honeycomb service on node {0} is "
+                                "in progress ...".format(node['host']))
 
     @staticmethod
     def stop_honeycomb_on_all_duts(nodes):
-        """Stop the honeycomb service on all DUTs.
+        """Stop the Honeycomb service on all DUTs.
 
-        :param nodes: nodes in topology
+        This keyword stops the Honeycomb service on all nodes. It just stops the
+        Honeycomb and does not check its shutdown state. Use the keyword "Check
+        Honeycomb Shutdown State" to check if Honeycomb has stopped.
+        :param nodes: Nodes in topology.
         :type nodes: dict
-        :return: ret_code, stdout, stderr
-        :rtype: tuple
-        :raises HoneycombError: if Honeycomb failed to stop.
+        :raises HoneycombError: If Honeycomb failed to stop.
         """
-        logger.console("Shutting down honeycomb service")
+        logger.console("Shutting down Honeycomb service ...")
+
+        cmd = "{0}/stop".format(Const.REMOTE_HC_DIR)
         errors = []
+
         for node in nodes.values():
             if node['type'] == NodeType.DUT:
-
                 ssh = SSH()
                 ssh.connect(node)
-                cmd = os.path.join(C.REMOTE_HC_DIR, "stop")
-                (ret_code, stdout, stderr) = ssh.exec_command_sudo(cmd)
+                (ret_code, _, _) = ssh.exec_command_sudo(cmd)
                 if int(ret_code) != 0:
-                    logger.debug('stdout: {0}'.format(stdout))
-                    logger.debug('stderr: {0}'.format(stderr))
                     errors.append(node['host'])
-                    continue
-                logger.info("Honeycomb was successfully stopped on node {0}.".
-                            format(node['host']))
+                else:
+                    logger.info("Stopping the Honeycomb service on node {0} is "
+                                "in progress ...".format(node['host']))
         if errors:
-            raise HoneycombError('Node(s) {0} failed to stop honeycomb.'.
+            raise HoneycombError('Node(s) {0} failed to stop Honeycomb.'.
                                  format(errors))
 
     @staticmethod
     def check_honeycomb_startup_state(nodes):
-        """Check state of honeycomb service during startup.
+        """Check state of Honeycomb service during startup.
 
-        Reads html path from template file vpp_version.url
+        Reads html path from template file oper_vpp_version.url.
 
         Honeycomb node replies with connection refused or the following status
         codes depending on startup progress: codes 200, 401, 403, 404, 503
 
-        :param nodes: nodes in topology
+        :param nodes: Nodes in topology.
         :type nodes: dict
-        :return: True if all GETs returned code 200(OK)
+        :return: True if all GETs returned code 200(OK).
         :rtype bool
         """
 
-        url_file = os.path.join(C.RESOURCES_TPL_HC, "vpp_version.url")
-        with open(url_file) as template:
-            data = template.readline()
-
-        expected_status_codes = (HTTP_CODES["UNAUTHORIZED"],
-                                 HTTP_CODES["FORBIDDEN"],
-                                 HTTP_CODES["NOT_FOUND"],
-                                 HTTP_CODES["SERVICE_UNAVAILABLE"])
+        path = HcUtil.read_path_from_url_file("oper_vpp_version")
+        expected_status_codes = (HTTPCodes.UNAUTHORIZED,
+                                 HTTPCodes.FORBIDDEN,
+                                 HTTPCodes.NOT_FOUND,
+                                 HTTPCodes.SERVICE_UNAVAILABLE)
 
         for node in nodes.values():
             if node['type'] == NodeType.DUT:
-                status_code, _ = HTTPRequest.get(node, data, timeout=10,
+                status_code, _ = HTTPRequest.get(node, path, timeout=10,
                                                  enable_logging=False)
-                if status_code == HTTP_CODES["OK"]:
-                    pass
+                if status_code == HTTPCodes.OK:
+                    logger.info("Honeycomb on node {0} is up and running".
+                                format(node['host']))
                 elif status_code in expected_status_codes:
-                    if status_code == HTTP_CODES["UNAUTHORIZED"]:
+                    if status_code == HTTPCodes.UNAUTHORIZED:
                         logger.info('Unauthorized. If this triggers keyword '
-                                    'timeout, verify honeycomb '
-                                    'username and password')
+                                    'timeout, verify Honeycomb username and '
+                                    'password.')
                     raise HoneycombError('Honeycomb on node {0} running but '
                                          'not yet ready.'.format(node['host']),
                                          enable_logging=False)
                 else:
-                    raise HoneycombError('Unexpected return code: {0}'.
+                    raise HoneycombError('Unexpected return code: {0}.'.
                                          format(status_code))
         return True
 
     @staticmethod
     def check_honeycomb_shutdown_state(nodes):
-        """Check state of honeycomb service during shutdown.
+        """Check state of Honeycomb service during shutdown.
 
         Honeycomb node replies with connection refused or the following status
-        codes depending on shutdown progress: codes 200, 404
+        codes depending on shutdown progress: codes 200, 404.
 
-        :param nodes: nodes in topology
+        :param nodes: Nodes in topology.
         :type nodes: dict
-        :return: True if all GETs fail to connect
+        :return: True if all GETs fail to connect.
         :rtype bool
         """
 
+        cmd = "ps -ef | grep -v grep | grep karaf"
         for node in nodes.values():
             if node['type'] == NodeType.DUT:
                 try:
                     status_code, _ = HTTPRequest.get(node, '/index.html',
                                                      timeout=5,
                                                      enable_logging=False)
-                    if status_code == HTTP_CODES["OK"]:
+                    if status_code == HTTPCodes.OK:
                         raise HoneycombError('Honeycomb on node {0} is still '
-                                             'running'.format(node['host']),
+                                             'running.'.format(node['host']),
                                              enable_logging=False)
-                    elif status_code == HTTP_CODES["NOT_FOUND"]:
+                    elif status_code == HTTPCodes.NOT_FOUND:
                         raise HoneycombError('Honeycomb on node {0} is shutting'
-                                             ' down'.format(node['host']),
+                                             ' down.'.format(node['host']),
                                              enable_logging=False)
                     else:
-                        raise HoneycombError('Unexpected return code: {'
-                                             '0}'.format(status_code))
+                        raise HoneycombError('Unexpected return code: {0}.'.
+                                             format(status_code))
                 except HTTPRequestError:
-                    logger.debug('Connection refused')
-
+                    logger.debug('Connection refused, checking the process '
+                                 'state ...')
+                    ssh = SSH()
+                    ssh.connect(node)
+                    (ret_code, _, _) = ssh.exec_command_sudo(cmd)
+                    if ret_code == 0:
+                        raise HoneycombError('Honeycomb on node {0} is still '
+                                             'running.'.format(node['host']),
+                                             enable_logging=False)
+                    else:
+                        logger.info("Honeycomb on node {0} has stopped".
+                                    format(node['host']))
         return True
 
-
     @staticmethod
-    def add_vpp_to_honeycomb_network_topology(nodes, headers):
+    def add_vpp_to_honeycomb_network_topology(nodes):
         """Add vpp node to Honeycomb network topology.
 
-        :param nodes: all nodes in test topology
-        :param headers: headers to be used with PUT requests
+        :param nodes: All nodes in test topology.
         :type nodes: dict
-        :type headers: dict
-        :return: status code and response from PUT requests
+        :return: Status code and response content from PUT requests.
         :rtype: tuple
-        :raises HoneycombError: if a node was not added to honeycomb topology
+        :raises HoneycombError: If a node was not added to Honeycomb topology.
 
-        Reads HTML path from template file config_topology_node.url
+        Reads HTML path from template file config_topology_node.url.
         Path to the node to be added, e.g.:
         ("/restconf/config/network-topology:network-topology"
          "/topology/topology-netconf/node/")
-        There must be "/" at the end, as generated node name is added
-        at the end.
+        There must be "/" at the end, as generated node name is added at the
+        end.
 
-        Reads payload data from template file add_vpp_to_topology.xml
+        Reads payload data from template file add_vpp_to_topology.xml.
         Information about node as XML structure, e.g.:
         <node xmlns="urn:TBD:params:xml:ns:yang:network-topology">
             <node-id>
@@ -258,17 +238,16 @@ class HoneycombSetup(object):
         MUST be there as they are replaced by correct values.
         """
 
-        with open(os.path.join(C.RESOURCES_TPL_HC, "config_topology_node.url"))\
-                as template:
-            path = template.readline()
-
+        path = HcUtil.read_path_from_url_file("config_topology_node")
         try:
-            xml_data = ET.parse(os.path.join(C.RESOURCES_TPL_HC,
-                                             "add_vpp_to_topology.xml"))
+            xml_data = ET.parse("{0}/add_vpp_to_topology.xml".
+                                format(Const.RESOURCES_TPL_HC))
         except ET.ParseError as err:
             raise HoneycombError(repr(err))
         data = ET.tostring(xml_data.getroot())
 
+        headers = {"Content-Type": "application/xml"}
+
         status_codes = []
         responses = []
         for node_name, node in nodes.items():
@@ -282,20 +261,19 @@ class HoneycombSetup(object):
                         passwd=node['honeycomb']["passwd"])
                     status_code, resp = HTTPRequest.put(
                         node=node,
-                        path=path + '/' + node_name,
+                        path="{0}/{1}".format(path, node_name),
                         headers=headers,
                         payload=payload)
-                    if status_code != HTTP_CODES["OK"]:
+                    if status_code != HTTPCodes.OK:
                         raise HoneycombError(
                             "VPP {0} was not added to topology. "
-                            "Status code: {1}".format(node["host"],
-                                                      status_code))
+                            "Status code: {1}.".format(node["host"],
+                                                       status_code))
 
                     status_codes.append(status_code)
                     responses.append(resp)
 
                 except HTTPRequestError as err:
-                    raise HoneycombError("VPP {0} was not added to topology.\n"
-                                         "{1}".format(node["host"], repr(err)))
-
+                    raise HoneycombError("VPP {0} was not added to topology.".
+                                         format(node["host"]), repr(err))
         return status_codes, responses
index c4dc3a0..86c25ad 100644 (file)
 # 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.
+
+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.
+"""
 
-import os.path
 from json import loads
 
 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
+
+
+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
 
-        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")))
-
-        return data
-
-    def parse_json_response(self, response, path=None):
+    @staticmethod
+    def parse_json_response(response, path=None):
         """Parse data from response string in JSON format according to given
         path.
 
-        :param response: JSON formatted string
-        :param path: Path to navigate down the data structure
+        :param response: JSON formatted string.
+        :param path: Path to navigate down the data structure.
         :type response: string
         :type path: tuple
-        :return: JSON dictionary/list tree
-        :rtype: dict
+        :return: JSON dictionary/list tree.
+        :rtype: list
         """
         data = loads(response)
 
         if path:
-            data = self._parse_json_tree(data, path)
-            while isinstance(data, list) and len(data) == 1:
-                data = data[0]
+            data = HoneycombUtil._parse_json_tree(data, path)
+            if not isinstance(data, list):
+                data = [data, ]
 
         return data
 
-    def _parse_json_tree(self, data, path):
-        """Retrieve data from python representation of JSON object.
+    @staticmethod
+    def _parse_json_tree(data, path):
+        """Retrieve data addressed by path from python representation of JSON
+        object.
 
-        :param data: parsed JSON dictionary tree
-        :param path: Path to navigate down the dictionary tree
+        :param data: Parsed JSON dictionary tree.
+        :param path: Path to navigate down the dictionary tree.
         :type data: dict
         :type path: tuple
-        :return: data from specified path
-        :rtype: list or str
+        :return: Data from specified path.
+        :rtype: list, dict or str
         """
 
         count = 0
@@ -91,7 +152,80 @@ class HoneycombUtil(object):
             elif isinstance(data, list):
                 result = []
                 for item in data:
-                    result.append(self._parse_json_tree(item, path[count:]))
+                    result.append(HoneycombUtil._parse_json_tree(item,
+                                                                 path[count:]))
                     return result
-
         return 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: Requested information.
+        :rtype list
+        """
+
+        path = HoneycombUtil.read_path_from_url_file(url_file)
+        status_code, resp = HTTPRequest.get(node, path)
+        return status_code, resp
+
+    @staticmethod
+    def put_honeycomb_data(node, url_file, data, data_representation='json'):
+        """Send configuration data using PUT request and return the status code
+        and response.
+
+        :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. Supported types
+        of representation are: json, xml and txt.
+        :type node: dict
+        :type url_file: str
+        :type data: str
+        :type data_representation: str
+        :return: Status code and content of response.
+        :rtype: tuple
+        """
+
+        headers = {'json':
+                       {"Content-Type": "application/json",
+                        'Accept': 'text/plain'},
+                   'xml':
+                       {"Content-Type": "application/xml",
+                        'Accept': 'text/plain'},
+                   'txt':
+                       {"Content-Type": "text/plain",
+                        'Accept': 'text/plain'}
+                  }
+        try:
+            header = headers[data_representation]
+        except KeyError as err:
+            raise HoneycombError("Wrong data type: {0}.".
+                                 format(data_representation), repr(err))
+
+        path = HoneycombUtil.read_path_from_url_file(url_file)
+        status_code, resp = HTTPRequest.put(node=node, path=path,
+                                            headers=header, payload=data)
+        return status_code, resp
+
+    @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 response.
+        :rtype tuple
+        """
+
+        path = HoneycombUtil.read_path_from_url_file(url_file)
+        return HTTPRequest.delete(node, path)