diff options
Diffstat (limited to 'resources/libraries/python')
-rw-r--r-- | resources/libraries/python/HTTPRequest.py | 258 | ||||
-rw-r--r-- | resources/libraries/python/HoneycombSetup.py | 301 | ||||
-rw-r--r-- | resources/libraries/python/HoneycombUtil.py | 97 | ||||
-rw-r--r-- | resources/libraries/python/constants.py | 17 |
4 files changed, 671 insertions, 2 deletions
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.: + <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 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' |