From: selias Date: Mon, 21 Mar 2016 12:06:49 +0000 (+0100) Subject: Setup and check honeycomb on all DUTs X-Git-Url: https://gerrit.fd.io/r/gitweb?p=csit.git;a=commitdiff_plain;h=a1383e569b184808780fbe0405402dea584902a9 Setup and check honeycomb on all DUTs - 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 --- diff --git a/requirements.txt b/requirements.txt index 1fe928674a..6b6db32a3b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ PyYAML==3.11 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 index 0000000000..7b21f5a761 --- /dev/null +++ b/resources/libraries/python/HTTPRequest.py @@ -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 index 0000000000..de05eff6ed --- /dev/null +++ b/resources/libraries/python/HoneycombSetup.py @@ -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.: + + + {vpp_host} + + + {vpp_ip} + + + {vpp_port} + + + {user} + + + {passwd} + + + false + + + 0 + + + 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 index 0000000000..c4dc3a067a --- /dev/null +++ b/resources/libraries/python/HoneycombUtil.py @@ -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 diff --git a/resources/libraries/python/constants.py b/resources/libraries/python/constants.py index f9bbc46a95..b3a61da16a 100644 --- a/resources/libraries/python/constants.py +++ b/resources/libraries/python/constants.py @@ -10,10 +10,23 @@ # 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): - #OpenVPP testing directory location at topology nodes + # OpenVPP testing directory location at topology nodes REMOTE_FW_DIR = '/tmp/openvpp-testing' + + # shell scripts location RESOURCES_LIB_SH = 'resources/libraries/bash' + + # vat templates location RESOURCES_TPL_VAT = 'resources/templates/vat' - #OpenVPP VAT binary name + + # OpenVPP VAT binary name 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 index 0000000000..98b8e23ae8 --- /dev/null +++ b/resources/libraries/robot/honeycomb.robot @@ -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 index 0000000000..46a84f28e8 --- /dev/null +++ b/resources/templates/honeycomb/add_vpp_to_topology.xml @@ -0,0 +1,9 @@ + + {vpp_host} + {vpp_ip} + {vpp_port} + {user} + {passwd} + false + 0 + \ 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 index 0000000000..e76f47ce8c --- /dev/null +++ b/resources/templates/honeycomb/config_topology.url @@ -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 index 0000000000..89b7aa851e --- /dev/null +++ b/resources/templates/honeycomb/config_topology_node.url @@ -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 index 0000000000..59759be8ea --- /dev/null +++ b/resources/templates/honeycomb/vpp_version.url @@ -0,0 +1 @@ +/restconf/operational/v3po:vpp-state/version \ No newline at end of file diff --git a/resources/topology_schemas/topology.sch.yaml b/resources/topology_schemas/topology.sch.yaml index 33b4e7bfc0..1c20055e0f 100644 --- a/resources/topology_schemas/topology.sch.yaml +++ b/resources/topology_schemas/topology.sch.yaml @@ -70,6 +70,22 @@ schema;type_interface_tg: &type_interface_tg <<: *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 @@ -108,6 +124,8 @@ schema;type_dut: type: map mapping: <<: *type_node_mapping + honeycomb: + <<: *type_honeycomb_mapping 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 index 0000000000..5264f997f4 --- /dev/null +++ b/tests/suites/honeycomb/sanity.robot @@ -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