2015-07-16 02:00:18 -04:00
|
|
|
"""Provides a common base for configurator proxies"""
|
2015-07-14 21:04:43 -04:00
|
|
|
import logging
|
|
|
|
|
import os
|
2015-07-21 21:14:57 -04:00
|
|
|
import shutil
|
2015-07-16 02:00:18 -04:00
|
|
|
import tempfile
|
2015-07-16 19:57:47 -04:00
|
|
|
import threading
|
2015-07-14 21:04:43 -04:00
|
|
|
|
|
|
|
|
import docker
|
|
|
|
|
|
|
|
|
|
from tests.compatibility import errors
|
|
|
|
|
from tests.compatibility import util
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
2015-07-16 02:00:18 -04:00
|
|
|
class Proxy(object):
|
2015-07-14 21:04:43 -04:00
|
|
|
# pylint: disable=too-many-instance-attributes
|
|
|
|
|
"""A common base for compatibility test configurators"""
|
|
|
|
|
|
|
|
|
|
_NOT_ADDED_ARGS = True
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def add_parser_arguments(cls, parser):
|
|
|
|
|
"""Adds command line arguments needed by the plugin"""
|
2015-07-16 02:00:18 -04:00
|
|
|
if Proxy._NOT_ADDED_ARGS:
|
|
|
|
|
group = parser.add_argument_group("docker")
|
2015-07-14 21:04:43 -04:00
|
|
|
group.add_argument(
|
2015-07-16 02:00:18 -04:00
|
|
|
"--docker-url", default="unix://var/run/docker.sock",
|
|
|
|
|
help="URL of the docker server")
|
2015-07-14 21:04:43 -04:00
|
|
|
group.add_argument(
|
2015-07-16 02:00:18 -04:00
|
|
|
"--no-remove", action="store_true",
|
|
|
|
|
help="do not delete container on program exit")
|
|
|
|
|
Proxy._NOT_ADDED_ARGS = False
|
2015-07-14 21:04:43 -04:00
|
|
|
|
|
|
|
|
def __init__(self, args):
|
|
|
|
|
"""Initializes the plugin with the given command line args"""
|
2015-07-21 21:14:57 -04:00
|
|
|
self._temp_dir = tempfile.mkdtemp()
|
|
|
|
|
self.le_config = util.create_le_config(self._temp_dir)
|
|
|
|
|
config_dir = util.extract_configs(args.configs, self._temp_dir)
|
|
|
|
|
self._configs = [
|
|
|
|
|
os.path.join(config_dir, config)
|
|
|
|
|
for config in os.listdir(config_dir)]
|
2015-07-14 21:04:43 -04:00
|
|
|
|
|
|
|
|
self.args = args
|
|
|
|
|
self._docker_client = docker.Client(
|
2015-07-16 02:00:18 -04:00
|
|
|
base_url=self.args.docker_url, version="auto")
|
2015-07-14 21:04:43 -04:00
|
|
|
self.http_port, self.https_port = util.get_two_free_ports()
|
2015-07-16 19:57:47 -04:00
|
|
|
self._container_id = self._log_thread = None
|
2015-07-14 21:04:43 -04:00
|
|
|
|
|
|
|
|
def has_more_configs(self):
|
|
|
|
|
"""Returns true if there are more configs to test"""
|
|
|
|
|
return bool(self._configs)
|
|
|
|
|
|
|
|
|
|
def cleanup_from_tests(self):
|
|
|
|
|
"""Performs any necessary cleanup from running plugin tests"""
|
|
|
|
|
self._docker_client.stop(self._container_id)
|
2015-07-16 19:57:47 -04:00
|
|
|
self._log_thread.join()
|
2015-07-14 21:04:43 -04:00
|
|
|
if not self.args.no_remove:
|
|
|
|
|
self._docker_client.remove_container(self._container_id)
|
|
|
|
|
|
2015-07-17 19:21:17 -04:00
|
|
|
def load_config(self):
|
2015-07-14 21:04:43 -04:00
|
|
|
"""Returns the next config directory to be tested"""
|
2015-07-21 21:14:57 -04:00
|
|
|
return self._configs.pop()
|
2015-07-14 21:04:43 -04:00
|
|
|
|
2015-07-21 21:14:57 -04:00
|
|
|
def start_docker(self, image_name, command):
|
2015-07-14 21:04:43 -04:00
|
|
|
"""Creates and runs a Docker container with the specified image"""
|
2015-07-21 21:14:57 -04:00
|
|
|
logger.info("Pulling Docker image. This may take a minute.")
|
2015-07-14 21:04:43 -04:00
|
|
|
for line in self._docker_client.pull(image_name, stream=True):
|
|
|
|
|
logger.debug(line)
|
|
|
|
|
|
|
|
|
|
host_config = docker.utils.create_host_config(
|
|
|
|
|
binds={
|
2015-07-21 21:14:57 -04:00
|
|
|
self._temp_dir : {"bind" : self._temp_dir, "mode" : "rw"}},
|
2015-07-14 21:04:43 -04:00
|
|
|
port_bindings={
|
2015-07-16 02:00:18 -04:00
|
|
|
80 : ("127.0.0.1", self.http_port),
|
|
|
|
|
443 : ("127.0.0.1", self.https_port)},)
|
2015-07-14 21:04:43 -04:00
|
|
|
container = self._docker_client.create_container(
|
2015-07-21 21:14:57 -04:00
|
|
|
image_name, command, ports=[80, 443], volumes=self._temp_dir,
|
2015-07-14 21:04:43 -04:00
|
|
|
host_config=host_config)
|
2015-07-16 02:00:18 -04:00
|
|
|
if container["Warnings"]:
|
|
|
|
|
logger.warning(container["Warnings"])
|
|
|
|
|
self._container_id = container["Id"]
|
2015-07-14 21:04:43 -04:00
|
|
|
self._docker_client.start(self._container_id)
|
|
|
|
|
|
2015-07-16 19:57:47 -04:00
|
|
|
self._log_thread = threading.Thread(target=self._start_log_thread)
|
|
|
|
|
self._log_thread.start()
|
2015-07-14 21:04:43 -04:00
|
|
|
|
|
|
|
|
def _start_log_thread(self):
|
2015-07-16 02:00:18 -04:00
|
|
|
client = docker.Client(base_url=self.args.docker_url, version="auto")
|
2015-07-14 21:04:43 -04:00
|
|
|
for line in client.logs(self._container_id, stream=True):
|
2015-07-17 19:21:17 -04:00
|
|
|
logger.debug(line.rstrip())
|
2015-07-16 02:00:18 -04:00
|
|
|
|
|
|
|
|
def check_call_in_docker(
|
|
|
|
|
self, command, *args, **kwargs): # pylint: disable=unused-argument
|
|
|
|
|
"""Simulates a call to check_call but executes the command in the
|
|
|
|
|
running docker image
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
if self.popen_in_docker(command).returncode:
|
|
|
|
|
raise errors.Error(
|
|
|
|
|
"{0} exited with a nonzero value".format(command))
|
|
|
|
|
|
|
|
|
|
def popen_in_docker(
|
|
|
|
|
self, command, *args, **kwargs): # pylint: disable=unused-argument
|
|
|
|
|
"""Simulates a call to Popen but executes the command in the
|
|
|
|
|
running docker image
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
class SimplePopen(object):
|
|
|
|
|
# pylint: disable=too-few-public-methods
|
|
|
|
|
"""Simplified Popen object"""
|
|
|
|
|
def __init__(self, returncode, output):
|
|
|
|
|
self.returncode = returncode
|
|
|
|
|
self._stdout = output
|
|
|
|
|
self._stderr = output
|
|
|
|
|
|
|
|
|
|
def communicate(self):
|
|
|
|
|
"""Returns stdout and stderr"""
|
|
|
|
|
return self._stdout, self._stderr
|
|
|
|
|
|
|
|
|
|
if isinstance(command, list):
|
|
|
|
|
command = " ".join(command)
|
|
|
|
|
|
2015-07-16 19:57:47 -04:00
|
|
|
returncode, output = self.execute_in_docker(command)
|
2015-07-16 02:00:18 -04:00
|
|
|
return SimplePopen(returncode, output)
|
|
|
|
|
|
2015-07-16 19:57:47 -04:00
|
|
|
def execute_in_docker(self, command):
|
2015-07-16 02:00:18 -04:00
|
|
|
"""Executes command inside the running docker image"""
|
2015-07-21 21:14:57 -04:00
|
|
|
logger.debug("Executing '%s'", command)
|
2015-07-16 02:00:18 -04:00
|
|
|
exec_id = self._docker_client.exec_create(self._container_id, command)
|
|
|
|
|
output = self._docker_client.exec_start(exec_id)
|
|
|
|
|
returncode = self._docker_client.exec_inspect(exec_id)["ExitCode"]
|
|
|
|
|
return returncode, output
|
2015-07-21 21:14:57 -04:00
|
|
|
|
|
|
|
|
def copy_certs_and_keys(self, cert_path, key_path, chain_path=None):
|
|
|
|
|
"""Copies certs and keys into the temporary directory"""
|
|
|
|
|
cert_and_key_dir = os.path.join(self._temp_dir, "certs_and_keys")
|
|
|
|
|
os.mkdir(cert_and_key_dir)
|
|
|
|
|
|
|
|
|
|
cert = os.path.join(cert_and_key_dir, "cert")
|
|
|
|
|
shutil.copy(cert_path, cert)
|
|
|
|
|
key = os.path.join(cert_and_key_dir, "key")
|
|
|
|
|
shutil.copy(key_path, key)
|
|
|
|
|
if chain_path:
|
|
|
|
|
chain = os.path.join(cert_and_key_dir, "chain")
|
|
|
|
|
shutil.copy(chain_path, chain)
|
|
|
|
|
else:
|
|
|
|
|
chain = None
|
|
|
|
|
|
|
|
|
|
return cert, key, chain
|