1
0
mirror of https://github.com/certbot/certbot.git synced 2026-01-23 07:20:55 +03:00

Started implementation of Apache base

This commit is contained in:
Brad Warren
2015-07-14 18:04:43 -07:00
parent 44f69c525d
commit 6c6ef2bb40
14 changed files with 318 additions and 72 deletions

View File

@@ -60,7 +60,6 @@ docs_extras = [
testing_extras = [
'coverage',
'docker-py',
'nose',
'nosexcover',
'tox',
@@ -107,7 +106,6 @@ setup(
entry_points={
'console_scripts': [
'compatibility = tests.compatibility.plugin_test:main [testing]',
'letsencrypt = letsencrypt.cli:main',
'letsencrypt-renewer = letsencrypt.renewer:main',
],

View File

@@ -1 +1 @@
"""Let's Encrypt Tests"""
"""Let's Encrypt tests"""

View File

@@ -15,6 +15,6 @@ ENV APACHE_CONFDIR=/tmp/apache2 \
APACHE_LOCK_DIR=/var/lock \
APACHE_LOG_DIR=/usr/local/apache2/logs
COPY a2enmod.sh /usr/local/bin/
COPY tests/compatibility/a2enmod.sh /usr/local/bin/
CMD [ "httpd-foreground" ]

View File

@@ -1 +1 @@
"""Let's Encrypt Compatibility Test"""
"""Let's Encrypt compatibility test"""

View File

@@ -0,0 +1 @@
"""Let's Encrypt compatibility test configurators"""

View File

@@ -0,0 +1 @@
"""Let's Encrypt compatibility test Apache configurators"""

View File

@@ -0,0 +1,34 @@
"""Provides a common base for Apache tests"""
import mock
from tests.compatibilty import configurators
class ApacheConfiguratorCommonTester(configurators.common.ConfiguratorTester):
"""A common base for Apache test configurators"""
def __init__(self, args):
"""Initializes the plugin with the given command line args"""
super(ApacheConfiguratorCommonTester, self).__init__(args)
self._patch = mock.patch('letsencrypt_apache.configurator.subprocess')
self._mock = self._patch.start()
self._mock.check_call = self._check_call
self._apache_configurator = None
def __getattr__(self, name):
"""Wraps the Apache Configurator methods"""
method = getattr(self._apache_configurator, name, None)
if callable(method):
return method
else:
raise AttributeError()
def _check_call(self, command, *args, **kwargs):
"""A function to mock the call to subprocess.check_call"""
def load_config(self):
"""Loads the next configuration for the plugin to test"""
raise NotImplementedError()
def get_test_domain_names(self):
"""Returns a list of domain names to test against the plugin"""
raise NotImplementedError()

View File

@@ -0,0 +1,95 @@
"""Provides a common base for compatibility test configurators"""
import logging
import multiprocessing
import os
import docker
from tests.compatibility import errors
from tests.compatibility import util
logger = logging.getLogger(__name__)
class ConfiguratorTester(object):
# 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"""
if ConfiguratorTester._NOT_ADDED_ARGS:
group = parser.add_argument_group('docker')
group.add_argument(
'--docker-url', default='unix://var/run/docker.sock',
help='URL of the docker server')
group.add_argument(
'--no-remove', action='store_true',
help='do not delete container on program exit')
ConfiguratorTester._NOT_ADDED_ARGS = False
def __init__(self, args):
"""Initializes the plugin with the given command line args"""
self.temp_dir = util.setup_temp_dir(args.configs)
self.config_dir = os.path.join(self.temp_dir, util.CONFIG_DIR)
self._configs = os.listdir(self.config_dir)
self.args = args
self._docker_client = docker.Client(
base_url=self.args.docker_url, version='auto')
self.http_port, self.https_port = util.get_two_free_ports()
self._container_id = self._log_process = None
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)
self._log_process.join()
if not self.args.no_remove:
self._docker_client.remove_container(self._container_id)
def get_next_config(self):
"""Returns the next config directory to be tested"""
return self._configs.pop()
def start_docker(self, image_name):
"""Creates and runs a Docker container with the specified image"""
for line in self._docker_client.pull(image_name, stream=True):
logger.debug(line)
host_config = docker.utils.create_host_config(
binds={
self.config_dir : {'bind' : self.config_dir, 'mode' : 'rw'}},
port_bindings={
80 : ('127.0.0.1', self.http_port),
443 : ('127.0.0.1', self.https_port)},)
container = self._docker_client.create_container(
image_name, ports=[80, 443], volumes=self.config_dir,
host_config=host_config)
if container['Warnings']:
logger.warning(container['Warnings'])
self._container_id = container['Id']
self._docker_client.start(self._container_id)
self._log_process = multiprocessing.Process(
target=self._start_log_thread)
self._log_process.start()
def execute_in_docker(self, command):
"""Executes command inside the running docker image"""
exec_id = self._docker_client.exec_create(self._container_id, command)
output = self._docker_client.exec_start(exec_id)
if self._docker_client.exec_inspect(exec_id)['ExitCode']:
raise errors.Error('Docker command \'{0}\' failed'.format(command))
return output
def _start_log_thread(self):
client = docker.Client(base_url=self.args.docker_url, version='auto')
for line in client.logs(self._container_id, stream=True):
logger.debug(line)

View File

@@ -0,0 +1,5 @@
"""Let's Encrypt compatibility test errors"""
class Error(Exception):
"""Generic Let's Encrypt compatibility test error"""

View File

@@ -0,0 +1,53 @@
"""Let's Encrypt compatibility test interfaces"""
import zope.interface
import letsencrypt.interfaces
class IPluginTester(zope.interface.Interface):
"""Wraps a Let's Encrypt plugin"""
@classmethod
def add_parser_arguments(cls, parser):
"""Adds command line arguments needed by the parser"""
def __init__(self, args):
"""Initializes the plugin with the given command line args"""
def cleanup_from_tests(self):
"""Performs any necessary cleanup from running plugin tests.
This is guarenteed to be called before the program exits.
"""
def has_more_configs(self):
"""Returns True if there are more configs to test"""
def load_config(self):
"""Loads the next configuration for the plugin to test"""
class IConfiguratorBaseTester(IPluginTester):
"""Common functionality for authenticator/installer tests"""
http_port = zope.interface.Attribute(
'The port to connect to on localhost for HTTP traffic')
https_port = zope.interface.Attribute(
'The port to connect to on localhost for HTTPS traffic')
def get_test_domain_names(self):
"""Returns a list of domain names to test against the plugin"""
class IAuthenticatorTester(
IConfiguratorBaseTester, letsencrypt.interfaces.IAuthenticator):
"""Wraps a Let's Encrypt authenticator"""
class IInstallerTester(
IConfiguratorBaseTester, letsencrypt.interfaces.IInstaller):
"""Wraps a Let's Encrypt installer"""
class IConfiguratorTester(IAuthenticatorTester, IInstallerTester):
"""Wraps a Let's Encrypt configurator"""

View File

@@ -1,49 +0,0 @@
"""Module for parsing command line arguments and config files."""
import argparse
DESCRIPTION = """
Tests Let's Encrypt plugins against different web servers and configurations
using Docker images. It is assumed that Docker is already installed.
"""
def parse_args():
"""Returns parsed command line arguments."""
parser = argparse.ArgumentParser(description=DESCRIPTION)
add_args(parser)
args = parser.parse_args()
if args.redirect:
args.names = True
args.install = True
elif args.install:
args.names = True
return args
def add_args(parser):
"""Adds general/program wide arguments to the group."""
group = parser.add_argument_group("general")
group.add_argument(
"-t", "--tar", default="configs.tar.gz",
help="a gzipped tarball containing server configurations")
group.add_argument(
"-p", "--plugin", default="apache",
help="the plugin to be tested")
group.add_argument(
"-n", "--names", action="store_true", help="tests installer's domain "
"name identification")
group.add_argument(
"-a", "--auth", action="store_true", help="tests authenticators")
group.add_argument(
"-i", "--install", action="store_true", help="tests installer's "
"certificate installation (implicitly includes -d)")
group.add_argument(
"-r", "--redirect", action="store_true", help="tests installer's "
"redirecting HTTP to HTTPS (implicitly includes -di)")
group.add_argument(
"--no-simple-http-tls", action="store_true", help="do not use TLS "
"when solving SimpleHTTP challenges")

View File

@@ -1,13 +1,78 @@
"""Tests Let's Encrypt plugins against different server configurations."""
import parser
import util
import argparse
import logging
import os
from tests.compatibility.configurators import common
DESCRIPTION = """
Tests Let's Encrypt plugins against different server configuratons. It is
assumed that Docker is already installed.
"""
PLUGINS = {'common' : common.ConfiguratorTester}
logger = logging.getLogger(__name__)
def get_args():
"""Returns parsed command line arguments."""
parser = argparse.ArgumentParser(description=DESCRIPTION)
group = parser.add_argument_group('general')
group.add_argument(
'-c', '--configs', default='configs.tar.gz',
help='a directory or tarball containing server configurations')
group.add_argument(
'-p', '--plugin', default='apache', help='the plugin to be tested')
group.add_argument(
'-a', '--auth', action='store_true',
help='tests the plugin as an authenticator')
group.add_argument(
'-i', '--install', action='store_true',
help='tests the plugin as an installer')
group.add_argument(
'-r', '--redirect', action='store_true', help='tests the plugin\'s '
'ability to redirect HTTP to HTTPS (implicitly includes installer '
'tests)')
for plugin in PLUGINS.itervalues():
plugin.add_parser_arguments(parser)
args = parser.parse_args()
if args.redirect:
args.install = True
elif not (args.auth or args.install):
args.auth = args.install = args.redirect = True
return args
def setup_logging():
"""Prepares logging for the program"""
fmt = "%(asctime)s:%(levelname)s:%(name)s:%(message)s"
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(fmt))
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)
root_logger.addHandler(handler)
def main():
"""Main test script execution."""
args = parser.parse_args()
setup_logging()
args = get_args()
plugin = PLUGINS[args.plugin](args)
plugin.start_docker('bradmw/apache2.4')
config = os.path.join(plugin.config_dir, 'apache2')
config_file = os.path.join(config, 'apache2.conf')
plugin.execute_in_docker('apachectl -d {0} -f {1} -k restart'.format(config, config_file))
#plugin.cleanup_from_tests()
print util.setup_tmp_dir(args.tar)
if __name__ == "__main__":
main()
if __name__ == '__main__':
main()

View File

@@ -1,21 +1,41 @@
"""Utility functions for Let's Encrypt plugin tests."""
import contextlib
import os
import shutil
import socket
import tarfile
import tempfile
TEMP_DIRECTORY = tempfile.mkdtemp()
# Location of decompressed server root configurations
CONFIGS = os.path.join(TEMP_DIRECTORY, "configs")
SCRIPTS = os.path.join(TEMP_DIRECTORY, "scripts")
from tests.compatibility import errors
def setup_tmp_dir(tar_path):
"""Sets up a temporary directory for this run and returns its path."""
tar = tarfile.open(tar_path, "r:gz")
tar.extractall(os.path.join(tmp_dir, SERVER_ROOTS))
# Paths used in the program relative to the temp directory
CONFIG_DIR = "configs"
LE_CONFIG = os.path.join("letsencrypt", "config")
LE_LOGS = os.path.join("letsencrypt", "logs")
os.makedirs(os.path.join(tmp_dir, "mnt"))
os.makedirs(os.path.join(tmp_dir, "scripts"))
return tmp_dir
def setup_temp_dir(configs):
"""Sets up a temporary directory and extracts server configs"""
temp_dir = tempfile.mkdtemp()
config_dir = os.path.join(temp_dir, CONFIG_DIR)
if os.path.isdir(configs):
shutil.copytree(configs, config_dir, symlinks=True)
elif tarfile.is_tarfile(configs):
with tarfile.open(configs, 'r') as tar:
tar.extractall(config_dir)
else:
raise errors.Error('Unknown configurations file type')
return temp_dir
def get_two_free_ports():
"""Returns two free ports to use for the tests"""
with contextlib.closing(socket.socket()) as sock1:
with contextlib.closing(socket.socket()) as sock2:
sock1.bind(('', 0))
sock2.bind(('', 0))
return sock1.getsockname()[1], sock2.getsockname()[1]

23
tests/setup.py Normal file
View File

@@ -0,0 +1,23 @@
from setuptools import setup
from setuptools import find_packages
install_requires = [
'letsencrypt',
'letsencrypt-apache',
'letsencrypt-nginx',
'docker-py',
'mock<1.1.0', # py26
'zope.interface',
]
setup(
name='compatibility-test',
packages=find_packages(),
install_requires=install_requires,
entry_points={
'console_scripts': [
'compatibility-test = compatibility.plugin_test:main',
],
},
)