Setup and check honeycomb on all DUTs 75/575/11
authorselias <samuel.elias@pantheon.tech>
Mon, 21 Mar 2016 12:06:49 +0000 (13:06 +0100)
committerGerrit Code Review <gerrit@fd.io>
Tue, 5 Apr 2016 14:09:02 +0000 (14:09 +0000)
- methods implementing HTTP requests (PUT,GET,POST,DELETE)
- methods for parsing HTTP responses
- methods for honeycomb setup on DUT
- updated constants.py
- keywords for honeycomb setup and communication
- simple honeycomb sanity test (not enabled for jenkins job runs)

Change-Id: I589f0ca56cc01072b92fe9363aed16a4098aee40
Signed-off-by: selias <samuel.elias@pantheon.tech>
12 files changed:
requirements.txt
resources/libraries/python/HTTPRequest.py [new file with mode: 0644]
resources/libraries/python/HoneycombSetup.py [new file with mode: 0644]
resources/libraries/python/HoneycombUtil.py [new file with mode: 0644]
resources/libraries/python/constants.py
resources/libraries/robot/honeycomb.robot [new file with mode: 0644]
resources/templates/honeycomb/add_vpp_to_topology.xml [new file with mode: 0644]
resources/templates/honeycomb/config_topology.url [new file with mode: 0644]
resources/templates/honeycomb/config_topology_node.url [new file with mode: 0644]
resources/templates/honeycomb/vpp_version.url [new file with mode: 0644]
resources/topology_schemas/topology.sch.yaml
tests/suites/honeycomb/sanity.robot [new file with mode: 0644]

index 1fe9286..6b6db32 100644 (file)
@@ -7,3 +7,4 @@ PyYAML==3.11
 pykwalify==1.5.0
 scapy==2.3.1
 enum34==1.1.2
 pykwalify==1.5.0
 scapy==2.3.1
 enum34==1.1.2
+requests==2.9.1
diff --git a/resources/libraries/python/HTTPRequest.py b/resources/libraries/python/HTTPRequest.py
new file mode 100644 (file)
index 0000000..7b21f5a
--- /dev/null
@@ -0,0 +1,258 @@
+# 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.
+
+"""Implements HTTP requests GET, PUT, POST, DELETE used in communication with
+honeycomb.
+"""
+
+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}
+
+
+class HTTPRequestError(Exception):
+    """Exception raised by HTTPRequest objects."""
+
+    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(HTTPRequestError, 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)
+
+
+class HTTPRequest(object):
+    """A class implementing HTTP requests."""
+
+    def __init__(self):
+        pass
+
+    @staticmethod
+    def create_full_url(ip_addr, port, path):
+        """Creates full url including IP, port, and 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
+        :return: full url
+        :rtype: str
+        """
+        return "http://{ip}:{port}{path}".format(ip=ip_addr, port=port,
+                                                 path=path)
+
+    @staticmethod
+    def _http_request(method, node, path, enable_logging=True, **kwargs):
+        """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:
+            params -- (optional) Dictionary or bytes to be sent in the query
+            string for the Request.
+            data -- (optional) Dictionary, bytes, or file-like object to
+            send in the body of the Request.
+            json -- (optional) json data to send in the body of the Request.
+            headers -- (optional) Dictionary of HTTP Headers to send with
+            the Request.
+            cookies -- (optional) Dict or CookieJar object to send with the
+            Request.
+            files -- (optional) Dictionary of 'name': file-like-objects
+            (or {'name': ('filename', fileobj)}) for multipart encoding upload.
+            timeout (float or tuple) -- (optional) How long to wait for the
+            server to send data before giving up, as a float, or a (connect
+            timeout, read timeout) tuple.
+            allow_redirects (bool) -- (optional) Boolean. Set to True if POST/
+            PUT/DELETE redirect following is allowed.
+            proxies -- (optional) Dictionary mapping protocol to the URL of
+            the proxy.
+            verify -- (optional) whether the SSL cert will be verified.
+            A CA_BUNDLE path can also be provided. Defaults to True.
+            stream -- (optional) if False, the response content will be
+            immediately downloaded.
+            cert -- (optional) if String, path to ssl client cert file (.pem).
+            If Tuple, ('cert', 'key') pair.
+        :type method: str
+        :type node: dict
+        :type path: str
+        :type enable_logging: bool
+        :type kwargs: dict
+        :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
+        """
+        timeout = kwargs["timeout"]
+        url = HTTPRequest.create_full_url(node['host'],
+                                          node['honeycomb']['port'],
+                                          path)
+        try:
+            auth = HTTPBasicAuth(node['honeycomb']['user'],
+                                 node['honeycomb']['passwd'])
+            rsp = request(method, url, auth=auth, **kwargs)
+            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)
+        except HTTPError as err:
+            raise HTTPRequestError("Invalid HTTP response from {0}\n".
+                                   format(url) + repr(err))
+        except TooManyRedirects as err:
+            raise HTTPRequestError("Request exceeded the configured number "
+                                   "of maximum re-directions\n" + repr(err))
+        except Timeout as err:
+            raise HTTPRequestError("Request timed out. Timeout is set to "
+                                   "{0}\n".format(timeout) + repr(err))
+        except RequestException as err:
+            raise HTTPRequestError("Unexpected HTTP request exception.\n" +
+                                   repr(err))
+
+    @staticmethod
+    @keyword(name="HTTP Get")
+    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 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.
+        :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
+        :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):
+        """Sends a PUT request and returns the response and status code.
+
+        :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 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 timeout: float or tuple
+        :return: Status code and content of response
+        :rtype: tuple
+        """
+        return HTTPRequest._http_request('PUT', node, path, headers=headers,
+                                         data=payload, 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 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 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
+        :rtype: tuple
+        """
+        return HTTPRequest._http_request('POST', node, path, headers=headers,
+                                         data=payload, json=json,
+                                         timeout=timeout)
+
+    @staticmethod
+    @keyword(name="HTTP Delete")
+    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 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
+        :rtype: tuple
+        """
+        return HTTPRequest._http_request('DELETE', node, path, timeout=timeout)
diff --git a/resources/libraries/python/HoneycombSetup.py b/resources/libraries/python/HoneycombSetup.py
new file mode 100644 (file)
index 0000000..de05eff
--- /dev/null
@@ -0,0 +1,301 @@
+# 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.
+
+"""Implements 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)
+
+
+class HoneycombSetup(object):
+    """Implements keywords for Honeycomb setup."""
+
+    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
+        :type nodes: dict
+        """
+        logger.console("Starting honeycomb service")
+
+        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
+
+    @staticmethod
+    def stop_honeycomb_on_all_duts(nodes):
+        """Stop the honeycomb service on all DUTs.
+
+        :param nodes: nodes in topology
+        :type nodes: dict
+        :return: ret_code, stdout, stderr
+        :rtype: tuple
+        :raises HoneycombError: if Honeycomb failed to stop.
+        """
+        logger.console("Shutting down honeycomb service")
+        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)
+                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']))
+        if errors:
+            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.
+
+        Reads html path from template file 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
+        :type nodes: dict
+        :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"])
+
+        for node in nodes.values():
+            if node['type'] == NodeType.DUT:
+                status_code, _ = HTTPRequest.get(node, data, timeout=10,
+                                                 enable_logging=False)
+                if status_code == HTTP_CODES["OK"]:
+                    pass
+                elif status_code in expected_status_codes:
+                    if status_code == HTTP_CODES["UNAUTHORIZED"]:
+                        logger.info('Unauthorized. If this triggers keyword '
+                                    '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}'.
+                                         format(status_code))
+        return True
+
+    @staticmethod
+    def check_honeycomb_shutdown_state(nodes):
+        """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
+
+        :param nodes: nodes in topology
+        :type nodes: dict
+        :return: True if all GETs fail to connect
+        :rtype bool
+        """
+
+        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"]:
+                        raise HoneycombError('Honeycomb on node {0} is still '
+                                             'running'.format(node['host']),
+                                             enable_logging=False)
+                    elif status_code == HTTP_CODES["NOT_FOUND"]:
+                        raise HoneycombError('Honeycomb on node {0} is shutting'
+                                             ' down'.format(node['host']),
+                                             enable_logging=False)
+                    else:
+                        raise HoneycombError('Unexpected return code: {'
+                                             '0}'.format(status_code))
+                except HTTPRequestError:
+                    logger.debug('Connection refused')
+
+        return True
+
+
+    @staticmethod
+    def add_vpp_to_honeycomb_network_topology(nodes, headers):
+        """Add vpp node to Honeycomb network topology.
+
+        :param nodes: all nodes in test topology
+        :param headers: headers to be used with PUT requests
+        :type nodes: dict
+        :type headers: dict
+        :return: status code and response from PUT requests
+        :rtype: tuple
+        :raises HoneycombError: if a node was not added to honeycomb topology
+
+        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.
+
+        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>
+                {vpp_host}
+            </node-id>
+            <host xmlns="urn:opendaylight:netconf-node-topology">
+                {vpp_ip}
+            </host>
+            <port xmlns="urn:opendaylight:netconf-node-topology">
+                {vpp_port}
+            </port>
+            <username xmlns="urn:opendaylight:netconf-node-topology">
+                {user}
+            </username>
+            <password xmlns="urn:opendaylight:netconf-node-topology">
+                {passwd}
+            </password>
+            <tcp-only xmlns="urn:opendaylight:netconf-node-topology">
+                false
+            </tcp-only>
+            <keepalive-delay xmlns="urn:opendaylight:netconf-node-topology">
+                0
+            </keepalive-delay>
+        </node>
+        NOTE: The placeholders:
+            {vpp_host}
+            {vpp_ip}
+            {vpp_port}
+            {user}
+            {passwd}
+        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()
+
+        try:
+            xml_data = ET.parse(os.path.join(C.RESOURCES_TPL_HC,
+                                             "add_vpp_to_topology.xml"))
+        except ET.ParseError as err:
+            raise HoneycombError(repr(err))
+        data = ET.tostring(xml_data.getroot())
+
+        status_codes = []
+        responses = []
+        for node_name, node in nodes.items():
+            if node['type'] == NodeType.DUT:
+                try:
+                    payload = data.format(
+                        vpp_host=node_name,
+                        vpp_ip=node["host"],
+                        vpp_port=node['honeycomb']["netconf_port"],
+                        user=node['honeycomb']["user"],
+                        passwd=node['honeycomb']["passwd"])
+                    status_code, resp = HTTPRequest.put(
+                        node=node,
+                        path=path + '/' + node_name,
+                        headers=headers,
+                        payload=payload)
+                    if status_code != HTTP_CODES["OK"]:
+                        raise HoneycombError(
+                            "VPP {0} was not added to topology. "
+                            "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)))
+
+        return status_codes, responses
diff --git a/resources/libraries/python/HoneycombUtil.py b/resources/libraries/python/HoneycombUtil.py
new file mode 100644 (file)
index 0000000..c4dc3a0
--- /dev/null
@@ -0,0 +1,97 @@
+# 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.
+
+"""Implements keywords used with Honeycomb."""
+
+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
+
+
+class HoneycombUtil(object):
+    """Implements keywords used with Honeycomb."""
+
+    def __init__(self):
+        pass
+
+    def get_configured_topology(self, nodes):
+        """Retrieves topology node IDs from each honeycomb node.
+
+        :param nodes: all nodes in topology
+        :type nodes: dict
+        :return: list of string IDs such as ['vpp1', 'vpp2']
+        :rtype list
+        """
+
+        url_file = os.path.join(C.RESOURCES_TPL_HC, "config_topology.url")
+        with open(url_file) as template:
+            path = template.readline()
+
+        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):
+        """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
+        :type response: string
+        :type path: tuple
+        :return: JSON dictionary/list tree
+        :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]
+
+        return data
+
+    def _parse_json_tree(self, data, path):
+        """Retrieve data from python representation of JSON object.
+
+        :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
+        """
+
+        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
+
+        return data
index f9bbc46..b3a61da 100644 (file)
 # 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.
 # 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.
+
+
 class Constants(object):
 class Constants(object):
-    #OpenVPP testing directory location at topology nodes
+    # OpenVPP testing directory location at topology nodes
     REMOTE_FW_DIR = '/tmp/openvpp-testing'
     REMOTE_FW_DIR = '/tmp/openvpp-testing'
+
+    # shell scripts location
     RESOURCES_LIB_SH = 'resources/libraries/bash'
     RESOURCES_LIB_SH = 'resources/libraries/bash'
+
+    # vat templates location
     RESOURCES_TPL_VAT = 'resources/templates/vat'
     RESOURCES_TPL_VAT = 'resources/templates/vat'
-    #OpenVPP VAT binary name
+
+    # OpenVPP VAT binary name
     VAT_BIN_NAME = 'vpp_api_test'
     VAT_BIN_NAME = 'vpp_api_test'
+
+    # Honeycomb directory location at topology nodes:
+    REMOTE_HC_DIR = '/opt/honeycomb/v3po-karaf-1.0.0-SNAPSHOT/bin'
+
+    # Honeycomb templates location
+    RESOURCES_TPL_HC = 'resources/templates/honeycomb'
diff --git a/resources/libraries/robot/honeycomb.robot b/resources/libraries/robot/honeycomb.robot
new file mode 100644 (file)
index 0000000..98b8e23
--- /dev/null
@@ -0,0 +1,68 @@
+# 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.
+
+*** Settings ***
+| Library | resources/libraries/python/HoneycombSetup.py
+| Library | resources/libraries/python/HoneycombUtil.py
+| Library | resources/libraries/python/HTTPRequest.py
+
+*** Keywords ***
+| Setup Honeycomb service
+| | [Documentation] | *Setup environment for honeycomb testing*
+| | ...
+| | ... | _Setup steps:_
+| | ... | - 1. Login to each honeycomb node using ssh
+| | ... | - 2. Startup honeycomb service
+| | ... | - 3. Monitor service startup using HTTP GET request loop
+| | ... | Expected sequence of HTTP replies: connection refused -> 404 -> 401 -> 503 -> 200 (pass)
+| | ... | - 4. Configure honeycomb nodes using HTTP PUT request
+| | ...
+| | ... | _Used global constants and variables:_
+| | ... | - RESOURCES_TPL_HC - path to honeycomb templates directory
+| | ... | - HTTPCodes - HTTP protocol status codes
+| | ... | - ${nodes} - dictionary of all nodes in topology.YAML file
+| | ...
+| | Start Honeycomb on all DUTs | ${nodes}
+| | Wait until keyword succeeds | 3min | 10sec | Check honeycomb startup state | ${nodes}
+| | &{Header}= | Create dictionary | Content-Type=application/xml
+| | Add VPP to honeycomb network topology | ${nodes} | ${header}
+
+| Stop honeycomb service
+| | [Documentation] | *Cleanup environment after honeycomb testing*
+| | ...
+| | ... | _Teardown steps:_
+| | ... | - 1. Login to each honeycomb node using ssh
+| | ... | - 2. Stop honeycomb service
+| | ... | - 3. Monitor service shutdown using HTTP GET request loop
+| | ... | Expected sequence of HTTP replies: 200 -> 404 -> connection refused (pass)
+| | ...
+| | ... | _Used global constants and variables:_
+| | ... | - RESOURCES_TPL_HC - path to honeycomb templates directory
+| | ... | - HTTPCodes - HTTP protocol status codes
+| | ... | - ${nodes} - dictionary of all nodes in topology.YAML file
+| | ...
+| | Stop honeycomb on all DUTs | ${nodes}
+| | Wait until keyword succeeds | 1m | 5s | Check honeycomb shutdown state | ${nodes}
+
+| Honeycomb checks VPP node configuration
+| | [Documentation] | *Check configuration of honeycomb nodes*
+| | ...
+| | ... | _Arguments:_
+| | ... | - None
+| | ...
+| | ... | _Return value:_
+| | ... | - None
+| | ...
+| | ${reply}= | Get configured topology | ${nodes}
+| | :FOR | ${item} | IN | @{reply}
+| | | Should match regexp | ${item} | ^DUT\\d{1,2}$
\ No newline at end of file
diff --git a/resources/templates/honeycomb/add_vpp_to_topology.xml b/resources/templates/honeycomb/add_vpp_to_topology.xml
new file mode 100644 (file)
index 0000000..46a84f2
--- /dev/null
@@ -0,0 +1,9 @@
+<node xmlns="urn:TBD:params:xml:ns:yang:network-topology">
+    <node-id>{vpp_host}</node-id>
+    <host xmlns="urn:opendaylight:netconf-node-topology">{vpp_ip}</host>
+    <port xmlns="urn:opendaylight:netconf-node-topology">{vpp_port}</port>
+    <username xmlns="urn:opendaylight:netconf-node-topology">{user}</username>
+    <password xmlns="urn:opendaylight:netconf-node-topology">{passwd}</password>
+    <tcp-only xmlns="urn:opendaylight:netconf-node-topology">false</tcp-only>
+    <keepalive-delay xmlns="urn:opendaylight:netconf-node-topology">0</keepalive-delay>
+</node>
\ No newline at end of file
diff --git a/resources/templates/honeycomb/config_topology.url b/resources/templates/honeycomb/config_topology.url
new file mode 100644 (file)
index 0000000..e76f47c
--- /dev/null
@@ -0,0 +1 @@
+/restconf/config/network-topology:network-topology/topology/topology-netconf
\ No newline at end of file
diff --git a/resources/templates/honeycomb/config_topology_node.url b/resources/templates/honeycomb/config_topology_node.url
new file mode 100644 (file)
index 0000000..89b7aa8
--- /dev/null
@@ -0,0 +1 @@
+/restconf/config/network-topology:network-topology/topology/topology-netconf/node
\ No newline at end of file
diff --git a/resources/templates/honeycomb/vpp_version.url b/resources/templates/honeycomb/vpp_version.url
new file mode 100644 (file)
index 0000000..59759be
--- /dev/null
@@ -0,0 +1 @@
+/restconf/operational/v3po:vpp-state/version
\ No newline at end of file
index 33b4e7b..1c20055 100644 (file)
@@ -70,6 +70,22 @@ schema;type_interface_tg: &type_interface_tg
           <<: *type_interface_mapping_driver
           required: yes
 
           <<: *type_interface_mapping_driver
           required: yes
 
+schema;type_honeycomb: &type_honeycomb
+  type: map
+  mapping: &type_honeycomb_mapping
+    user:
+      type: str
+      required: yes
+    passwd:
+      type: str
+      required: yes
+    port:
+      type: int
+      required: yes
+    netconf_port:
+      type: int
+      required: yes
+
 schema;type_node: &type_node
   type: map
   mapping: &type_node_mapping
 schema;type_node: &type_node
   type: map
   mapping: &type_node_mapping
@@ -108,6 +124,8 @@ schema;type_dut:
   type: map
   mapping:
     <<: *type_node_mapping
   type: map
   mapping:
     <<: *type_node_mapping
+    honeycomb:
+      <<: *type_honeycomb_mapping
     type:
       <<: *type_node_mapping_type
       enum: [DUT]
     type:
       <<: *type_node_mapping_type
       enum: [DUT]
diff --git a/tests/suites/honeycomb/sanity.robot b/tests/suites/honeycomb/sanity.robot
new file mode 100644 (file)
index 0000000..5264f99
--- /dev/null
@@ -0,0 +1,37 @@
+# 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.
+
+*** Settings ***
+| Resource | resources/libraries/robot/honeycomb.robot
+| Suite Setup | Setup Honeycomb service
+| Suite Teardown | Stop Honeycomb service
+
+*** Test Cases ***
+| Honeycomb reports running configuration
+| | [Documentation] | *Check the contents of honeycomb configuration*
+| | ...
+| | ... | _Test steps:_
+| | ... | - 1. Send HTTP GET request to obtain configured topology from all honeycomb nodes
+| | ... | - 2. Retrieve configuration as JSON object
+| | ... | - 3. Parse JSON for VPP instance ID string
+| | ... | - 4. regex match ID string against expected pattern (vpp1, vpp2, vpp3,...)
+| | ...
+| | ... | _Pass criteria:_
+| | ... | The test passes if the ID strings of VPP instances on each honeycomb node match the expected pattern
+| | ...
+| | ... | _Used global constants and variables:_
+| | ... | - RESOURCES_TPL_HC - path to honeycomb templates directory
+| | ... | - nodes - dictionary of all nodes in topology.YAML file
+| | ...
+| | [Tags] | honeycomb_sanity
+| | Honeycomb checks VPP node configuration
\ No newline at end of file