diff --git a/calico_containers/tests/st/__init__.py b/calico_containers/tests/st/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/calico_containers/tests/st/test_base.py b/calico_containers/tests/st/test_base.py deleted file mode 100644 index eb4c7f07..00000000 --- a/calico_containers/tests/st/test_base.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2015 Metaswitch Networks -# -# 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. -import subprocess -from unittest import TestCase - -from tests.st.utils.utils import get_ip - - -class TestBase(TestCase): - """ - Base class for test-wide methods. - """ - def setUp(self): - """ - Clean up before every test. - """ - self.ip = get_ip() - # Delete /calico if it exists. This ensures each test has an empty data - # store at start of day. - subprocess.check_output( - "curl -sL http://%s:2379/v2/keys/calico?recursive=true -XDELETE" - % self.ip, shell=True) - - def assert_connectivity(self, pass_list, fail_list=None): - """ - Assert partial connectivity graphs between workloads. - - :param pass_list: Every workload in this list should be able to ping - every other workload in this list. - :param fail_list: Every workload in pass_list should *not* be able to - ping each workload in this list. Interconnectivity is not checked - *within* the fail_list. - """ - if fail_list is None: - fail_list = [] - for source in pass_list: - for dest in pass_list: - source.assert_can_ping(dest.ip) - for dest in fail_list: - source.assert_cant_ping(dest.ip) diff --git a/calico_containers/tests/st/utils/__init__.py b/calico_containers/tests/st/utils/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/calico_containers/tests/st/utils/docker_host.py b/calico_containers/tests/st/utils/docker_host.py deleted file mode 100644 index 77a1c7ca..00000000 --- a/calico_containers/tests/st/utils/docker_host.py +++ /dev/null @@ -1,263 +0,0 @@ -# Copyright 2015 Metaswitch Networks -# -# 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. -import os -from functools import partial -from subprocess import check_output, CalledProcessError, STDOUT - -from sh import docker - -from tests.st.utils.exceptions import CommandExecError -from tests.st.utils import utils -from tests.st.utils.utils import retry_until_success, get_ip -from workload import Workload -from network import DockerNetwork - -CALICO_DRIVER_SOCK = "/run/docker/plugins/calico.sock" - - -class DockerHost(object): - """ - A host container which will hold workload containers to be networked by - Calico. - """ - def __init__(self, name, start_calico=True, dind=True): - self.name = name - self.dind = dind - self.workloads = set() - - # This variable is used to assert on destruction that this object was - # cleaned up. If not used as a context manager, users of this object - self._cleaned = False - - if dind: - # TODO use pydocker - docker.rm("-f", self.name, _ok_code=[0, 1]) - docker.run("--privileged", "-v", os.getcwd()+":/code", "--name", - self.name, - "-e", "DOCKER_DAEMON_ARGS=" - "--kv-store=consul:%s:8500" % utils.get_ip(), - "-tid", "calico/dind") - self.ip = docker.inspect("--format", "{{ .NetworkSettings.IPAddress }}", - self.name).stdout.rstrip() - - self.ip6 = docker.inspect("--format", - "{{ .NetworkSettings." - "GlobalIPv6Address }}", - self.name).stdout.rstrip() - - # Make sure docker is up - docker_ps = partial(self.execute, "docker ps") - retry_until_success(docker_ps, ex_class=CalledProcessError, - retries=100) - self.execute("docker load --input /code/calico_containers/calico-node.tar && " - "docker load --input /code/calico_containers/busybox.tar") - else: - self.ip = get_ip() - - if start_calico: - self.start_calico_node() - self.assert_driver_up() - - def execute(self, command): - """ - Pass a command into a host container. - - Raises a CommandExecError() if the command returns a non-zero - return code. - - :param command: The command to execute. - :return: The output from the command with leading and trailing - whitespace removed. - """ - etcd_auth = "ETCD_AUTHORITY=%s:2379" % get_ip() - # Export the environment, in case the command has multiple parts, e.g. - # use of | or ; - command = "export %s; %s" % (etcd_auth, command) - - if self.dind: - command = self.escape_bash_single_quotes(command) - command = "docker exec -it %s bash -c '%s'" % (self.name, - command) - try: - output = check_output(command, shell=True, stderr=STDOUT) - except CalledProcessError as e: - # Wrap the original exception with one that gives a better error - # message (including command output). - raise CommandExecError(e) - else: - return output.strip() - - def calicoctl(self, command): - """ - Convenience function for abstracting away calling the calicoctl - command. - - Raises a CommandExecError() if the command returns a non-zero - return code. - - :param command: The calicoctl command line parms as a single string. - :return: The output from the command with leading and trailing - whitespace removed. - """ - if os.environ.get("CALICOCTL"): - calicoctl = os.environ["CALICOCTL"] - else: - if self.dind: - calicoctl = "/code/dist/calicoctl" - else: - calicoctl = "dist/calicoctl" - return self.execute(calicoctl + " " + command) - - def start_calico_node(self, as_num=None): - """ - Start calico in a container inside a host by calling through to the - calicoctl node command. - - :param as_num: The AS Number for this node. A value of None uses the - inherited default value. - """ - args = ['node', '--ip=%s' % self.ip] - try: - if self.ip6: - args.append('--ip6=%s' % self.ip6) - except AttributeError: - # No ip6 configured - pass - if as_num: - args.append('--as=%s' % as_num) - - cmd = ' '.join(args) - self.calicoctl(cmd) - - def assert_driver_up(self): - """ - Check that Calico Docker Driver is up by checking the existence of - the unix socket. - """ - sock_exists = partial(self.execute, - "[ -e %s ]" % CALICO_DRIVER_SOCK) - retry_until_success(sock_exists, ex_class=CalledProcessError) - - def remove_workloads(self): - """ - Remove all containers running on this host. - - Useful for test shut down to ensure the host is cleaned up. - :return: None - """ - for workload in self.workloads: - try: - self.execute("docker rm -f %s" % workload.name) - except CalledProcessError: - # Make best effort attempt to clean containers. Don't fail the - # test if a container can't be removed. - pass - - def remove_images(self): - """ - Remove all images running on this host. - - Useful for test shut down to ensure the host is cleaned up. - :return: None - """ - cmd = "docker rmi $(docker images -qa)" - try: - self.execute(cmd) - except CalledProcessError: - # Best effort only. - pass - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - """ - Exit the context of this host. - :return: None - """ - self.cleanup() - - def cleanup(self): - """ - Clean up this host, including removing any containers is created. This - is necessary especially for Docker-in-Docker so we don't leave dangling - volumes. - :return: - """ - self.remove_workloads() - # And remove the calico-node - docker.rm("-f", "calico-node", _ok_code=[0, 1]) - - if self.dind: - # For docker in docker, we need to remove images too. - self.remove_images() - # Remove the outer container for DinD. - docker.rm("-f", self.name, _ok_code=[0, 1]) - self._cleaned = True - - def __del__(self): - """ - This destructor asserts this object was cleaned up before being GC'd. - - Why not just clean up? This object is used in test scripts and we - can't guarantee that GC will happen between test runs. So, un-cleaned - objects may result in confusing behaviour since this object manipulates - Docker containers running on the system. - :return: - """ - assert self._cleaned - - def create_workload(self, name, image="busybox", network=None, - service=None): - """ - Create a workload container inside this host container. - """ - workload = Workload(self, name, image=image, network=network, - service=service) - self.workloads.add(workload) - return workload - - def create_network(self, name, driver="calico"): - """ - Create a Docker network using this host. - - :param name: The name of the network. This must be unique per cluster - and it the user-facing identifier for the network. (Calico itself will - get a UUID for the network via the driver API and will not get the - name). - :param driver: The name of the network driver to use. (The Calico - driver is the default.) - :return: A DockerNetwork object. - """ - return DockerNetwork(self, name, driver=driver) - - @staticmethod - def escape_bash_single_quotes(command): - """ - Escape single quotes in bash string strings. - - Replace ' (single-quote) in the command with an escaped version. - This needs to be done, since the command is passed to "docker - exec" to execute and needs to be single quoted. - Strictly speaking, it's impossible to escape single-quoted - bash script, but there is a workaround - end the single quoted - string, then concatenate a double quoted single quote, - and finally re-open the string with a single quote. Because - this is inside a single quoted python, string, the single - quotes also need escaping. - - :param command: The string to escape. - :return: The escaped string - """ - return command.replace('\'', '\'"\'"\'') diff --git a/calico_containers/tests/st/utils/exceptions.py b/calico_containers/tests/st/utils/exceptions.py deleted file mode 100644 index 4ceedc72..00000000 --- a/calico_containers/tests/st/utils/exceptions.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2015 Metaswitch Networks -# -# 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. -from subprocess import CalledProcessError - - -class CommandExecError(CalledProcessError): - """ - Wrapper for CalledProcessError with an Exception message that gives the - output captured from the failed command. - """ - - def __init__(self, called_process_error): - self.called_process_error = called_process_error - - @property - def returncode(self): - return self.called_process_error.returncode - - @property - def output(self): - return self.called_process_error.output - - @property - def cmd(self): - return self.called_process_error.cmd - - def __str__(self): - return "Command %s failed with RC %s and output:\n%s" % \ - (self.called_process_error.cmd, - self.called_process_error.returncode, - self.called_process_error.output) - diff --git a/calico_containers/tests/st/utils/network.py b/calico_containers/tests/st/utils/network.py deleted file mode 100644 index 912843dd..00000000 --- a/calico_containers/tests/st/utils/network.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright 2015 Metaswitch Networks -# -# 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. - - -class DockerNetwork(object): - """ - A Docker network created by libnetwork. - - Docker networks provide mutual connectivity to the endpoints attached to - them (and endpoints join/leave sandboxes which are network namespaces used - by containers). - """ - - def __init__(self, host, name, driver="calico"): - """ - Create the network. - :param host: The Docker Host which creates the network (note that - networks - :param name: The name of the network. This must be unique per cluster - and it the user-facing identifier for the network. (Calico itself will - get a UUID for the network via the driver API and will not get the - name). - :param driver: The name of the network driver to use. (The Calico - driver is the default.) - :return: A DockerNetwork object. - """ - self.name = name - self.driver = driver - - self.init_host = host - """The host which created the network.""" - - args = [ - "docker", "network", "create", - "--driver=%s" % driver, - name, - ] - command = ' '.join(args) - self.uuid = host.execute(command).rstrip() - - def delete(self): - """ - Delete the network. - :return: Nothing - """ - args = [ - "docker", "network", "rm", - self.name, - ] - command = ' '.join(args) - self.init_host.execute(command).rstrip() - - def __str__(self): - return self.name - - - diff --git a/calico_containers/tests/st/utils/utils.py b/calico_containers/tests/st/utils/utils.py deleted file mode 100644 index 01867fb1..00000000 --- a/calico_containers/tests/st/utils/utils.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2015 Metaswitch Networks -# -# 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. -import socket -from time import sleep -import os - -LOCAL_IP_ENV = "MY_IP" - - -def get_ip(): - """ - Return a string of the IP of the hosts interface. - Try to get the local IP from the environment variables. This allows - testers to specify the IP address in cases where there is more than one - configured IP address for the test system. - """ - try: - ip = os.environ[LOCAL_IP_ENV] - except KeyError: - # No env variable set; try to auto detect. - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(("8.8.8.8", 80)) - ip = s.getsockname()[0] - s.close() - return ip - - -def retry_until_success(function, retries=10, ex_class=Exception): - """ - Retries function until no exception is thrown. If exception continues, - it is reraised. - - :param function: the function to be repeatedly called - :param retries: the maximum number of times to retry the function. - A value of 0 will run the function once with no retries. - :param ex_class: The class of expected exceptions. - :returns: the value returned by function - """ - for retry in range(retries + 1): - try: - result = function() - except ex_class: - if retry < retries: - sleep(1) - else: - raise - else: - # Successfully ran the function - return result diff --git a/calico_containers/tests/st/utils/workload.py b/calico_containers/tests/st/utils/workload.py deleted file mode 100644 index 956d354f..00000000 --- a/calico_containers/tests/st/utils/workload.py +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright 2015 Metaswitch Networks -# -# 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. -from functools import partial -import uuid - -from netaddr import IPAddress - -from utils import retry_until_success -from tests.st.utils.network import DockerNetwork -from tests.st.utils.exceptions import CommandExecError - -NET_NONE = "none" - - -class Workload(object): - """ - A calico workload. - - These are the end-users containers that will run application-level - software. - """ - def __init__(self, host, name, image="busybox", network=None, - service=None): - """ - Create the workload and detect its IPs. - - :param host: The host container on which this workload is instantiated. - All commands executed by this container will be passed through the host - via docker exec. - :param name: The name given to the workload container. This name is - passed to docker and can be used inside docker commands. - :param image: The docker image to be used to instantiate this - container. busybox used by default because it is extremely small and - has ping. - :param network: The DockerNetwork to connect to. Set to None to use - default Docker networking. - :param service: The name of the service to use. Set to None to have - a random one generated. - """ - self.host = host - self.name = name - - args = [ - "docker", "run", - "--tty", - "--interactive", - "--detach", - "--name", name, - ] - if network: - if network is not NET_NONE: - assert isinstance(network, DockerNetwork) - if service is None: - service = str(uuid.uuid4()) - args.append("--publish-service=%s.%s" % (service, network)) - args.append(image) - command = ' '.join(args) - - host.execute(command) - - version_key = "IPAddress" - # TODO Use version_key = "GlobalIPv6Address" for IPv6 - - self.ip = host.execute("docker inspect --format " - "'{{ .NetworkSettings.%s }}' %s" % (version_key, - name), - ).rstrip() - - def execute(self, command): - """ - Execute arbitrary commands on this workload. - """ - # Make sure we've been created in the context of a host. Done here - # instead of in __init__ as we can't exist in the host until we're - # created. - assert self in self.host.workloads - return self.host.execute("docker exec %s %s" % (self.name, command)) - - def _get_ping_function(self, ip): - """ - Return a function to ping the supplied IP address from this workload. - - :param ip: The IPAddress to ping. - :return: A partial function that can be executed to perform the ping. - The function raises a CommandExecError exception if the ping fails, - or returns the output of the ping. - """ - version = IPAddress(ip).version - assert version in [4, 6] - if version == 4: - ping = "ping" - else: # if version == 6: - ping = "ping6" - - args = [ - ping, - "-c", "1", # Number of pings - "-W", "1", # Timeout for each ping - ip, - ] - command = ' '.join(args) - - ping = partial(self.execute, command) - return ping - - def assert_can_ping(self, ip, retries=0): - """ - Execute a ping from this workload to the ip. Assert than a workload - can ping an IP. Use retries to allow for convergence. - - Use of this method assumes the network will be transitioning from a - state where the destination is currently unreachable. - - :param ip: The IP address (str or IPAddress) to ping. - :param retries: The number of retries. - :return: None. - """ - try: - retry_until_success(self._get_ping_function(ip), - retries=retries, - ex_class=CommandExecError) - except CommandExecError: - raise AssertionError("%s cannot ping %s" % (self, ip)) - - def assert_cant_ping(self, ip, retries=0): - """ - Execute a ping from this workload to the ip. Assert that the workload - cannot ping an IP. Use retries to allow for convergence. - - Use of this method assumes the network will be transitioning from a - state where the destination is currently reachable. - - :param ip: The IP address (str or IPAddress) to ping. - :param retries: The number of retries. - :return: None. - """ - ping = self._get_ping_function(ip) - - def cant_ping(): - try: - ping() - except CommandExecError: - pass - else: - raise _PingError() - - try: - retry_until_success(cant_ping, - retries=retries, - ex_class=_PingError) - except _PingError: - raise AssertionError("%s can ping %s" % (self, ip)) - - def __str__(self): - return self.name - - -class _PingError(Exception): - pass