Merged in master

This commit is contained in:
Brad Warren
2015-08-03 18:32:32 -07:00
46 changed files with 2722 additions and 930 deletions
@@ -16,6 +16,7 @@ from letsencrypt_compatibility_test.configurators import common as configurators
APACHE_VERSION_REGEX = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE)
APACHE_COMMANDS = ["apachectl", "a2enmod"]
class Proxy(configurators_common.Proxy):
@@ -29,24 +30,58 @@ class Proxy(configurators_common.Proxy):
super(Proxy, self).__init__(args)
self.le_config.apache_le_vhost_ext = "-le-ssl.conf"
self._patches = list()
subprocess_patch = mock.patch(
"letsencrypt_apache.configurator.subprocess")
subprocess_mock = subprocess_patch.start()
subprocess_mock.check_call = self.check_call_in_docker
subprocess_mock.Popen = self.popen_in_docker
self._patches.append(subprocess_patch)
display_patch = mock.patch(
"letsencrypt_apache.configurator.display_ops.select_vhost")
display_mock = display_patch.start()
display_mock.side_effect = le_errors.PluginError(
"Unable to determine vhost")
self._patches.append(display_mock)
self._setup_mock()
self.modules = self.server_root = self.test_conf = self.version = None
self._apache_configurator = self._all_names = self._test_names = None
def _setup_mock(self):
"""Replaces specific modules with mock.MagicMock"""
mock_subprocess = mock.MagicMock()
mock_subprocess.check_call = self.check_call
mock_subprocess.Popen = self.popen
mock.patch(
"letsencrypt_apache.configurator.subprocess",
mock_subprocess).start()
mock.patch(
"letsencrypt_apache.parser.subprocess",
mock_subprocess).start()
mock.patch(
"letsencrypt.le_util.subprocess",
mock_subprocess).start()
mock.patch(
"letsencrypt_apache.configurator.le_util.exe_exists",
_is_apache_command).start()
patch = mock.patch(
"letsencrypt_apache.configurator.display_ops.select_vhost")
mock_display = patch.start()
mock_display.side_effect = le_errors.PluginError(
"Unable to determine vhost")
def check_call(self, command, *args, **kwargs):
"""If command is an Apache command, command is executed in the
running docker image. Otherwise, subprocess.check_call is used.
"""
if _is_apache_command(command):
command = _modify_command(command)
return super(Proxy, self).check_call(command, *args, **kwargs)
else:
return subprocess.check_call(command, *args, **kwargs)
def popen(self, command, *args, **kwargs):
"""If command is an Apache command, command is executed in the
running docker image. Otherwise, subprocess.Popen is used.
"""
if _is_apache_command(command):
command = _modify_command(command)
return super(Proxy, self).popen(command, *args, **kwargs)
else:
return subprocess.Popen(command, *args, **kwargs)
def __getattr__(self, name):
"""Wraps the Apache Configurator methods"""
method = getattr(self._apache_configurator, name, None)
@@ -59,8 +94,7 @@ class Proxy(configurators_common.Proxy):
"""Loads the next configuration for the plugin to test"""
if hasattr(self.le_config, "apache_init_script"):
try:
self.check_call_in_docker(
[self.le_config.apache_init_script, "stop"])
self.check_call([self.le_config.apache_init_script, "stop"])
except errors.Error:
raise errors.Error(
"Failed to stop previous apache config from running")
@@ -79,9 +113,8 @@ class Proxy(configurators_common.Proxy):
self._prepare_configurator(server_root, config_file)
try:
self.check_call_in_docker(
"apachectl -d {0} -f {1} -k start".format(
server_root, config_file))
self.check_call("apachectl -d {0} -f {1} -k start".format(
server_root, config_file))
except errors.Error:
raise errors.Error(
"Apache failed to load {0} before tests started".format(
@@ -115,6 +148,7 @@ class Proxy(configurators_common.Proxy):
self.le_config.apache_ctl = "apachectl -d {0} -f {1}".format(
server_root, config_file)
self.le_config.apache_enmod = "a2enmod.sh {0}".format(server_root)
self.le_config.apache_dismod = self.le_config.apache_enmod
self.le_config.apache_init_script = self.le_config.apache_ctl + " -k"
self._apache_configurator = configurator.ApacheConfigurator(
@@ -125,8 +159,7 @@ class Proxy(configurators_common.Proxy):
def cleanup_from_tests(self):
"""Performs any necessary cleanup from running plugin tests"""
super(Proxy, self).cleanup_from_tests()
for patch in self._patches:
patch.stop()
mock.patch.stopall()
def get_all_names_answer(self):
"""Returns the set of domain names that the plugin should find"""
@@ -150,6 +183,30 @@ class Proxy(configurators_common.Proxy):
domain, cert_path, key_path, chain_path)
def _is_apache_command(command):
"""Returns true if command is an Apache command"""
if isinstance(command, list):
command = command[0]
for apache_command in APACHE_COMMANDS:
if command.startswith(apache_command):
return True
return False
def _modify_command(command):
"""Modifies command so configtest works inside the docker image"""
if isinstance(command, list):
for i in xrange(len(command)):
if command[i] == "configtest":
command[i] = "-t"
else:
command = command.replace("configtest", "-t")
return command
def _create_test_conf(server_root, apache_config):
"""Creates a test config file and adds it to the Apache config"""
test_conf = os.path.join(server_root, "test.conf")
@@ -3,7 +3,6 @@ import logging
import os
import shutil
import tempfile
import threading
import docker
@@ -47,7 +46,7 @@ class Proxy(object):
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_thread = None
self._container_id = None
def has_more_configs(self):
"""Returns true if there are more configs to test"""
@@ -56,7 +55,6 @@ class Proxy(object):
def cleanup_from_tests(self):
"""Performs any necessary cleanup from running plugin tests"""
self._docker_client.stop(self._container_id, 0)
self._log_thread.join()
if not self.args.no_remove:
self._docker_client.remove_container(self._container_id)
@@ -87,26 +85,18 @@ class Proxy(object):
self._container_id = container["Id"]
self._docker_client.start(self._container_id)
self._log_thread = threading.Thread(target=self._start_log_thread)
self._log_thread.start()
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.rstrip())
def check_call_in_docker(
self, command, *args, **kwargs): # pylint: disable=unused-argument
def check_call(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:
if self.popen(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
def popen(self, command, *args, **kwargs):
# pylint: disable=unused-argument
"""Simulates a call to Popen but executes the command in the
running docker image
@@ -6,6 +6,7 @@ import logging
import os
import shutil
import tempfile
import time
import OpenSSL
@@ -60,11 +61,12 @@ def test_authenticator(plugin, config, temp_dir):
type(achalls[i]), achalls[i].domain, config)
success = False
elif isinstance(responses[i], challenges.DVSNIResponse):
if responses[i].simple_verify(achalls[i],
achalls[i].domain,
util.JWK.key.public_key(),
host="127.0.0.1",
port=plugin.https_port):
verify = functools.partial(responses[i].simple_verify, achalls[i],
achalls[i].domain,
util.JWK.key.public_key(),
host="127.0.0.1",
port=plugin.https_port)
if _try_until_true(verify):
logger.info(
"DVSNI verification for %s succeeded", achalls[i].domain)
else:
@@ -149,10 +151,11 @@ def test_deploy_cert(plugin, temp_dir, domains):
if not _save_and_restart(plugin, "deployed"):
return False
verify_cert = validator.Validator().certificate
success = True
for domain in domains:
if not verify_cert(cert, domain, "127.0.0.1", plugin.https_port):
verify = functools.partial(validator.Validator().certificate, cert,
domain, "127.0.0.1", plugin.https_port)
if not _try_until_true(verify):
logger.error("Could not verify certificate for domain %s", domain)
success = False
@@ -175,18 +178,18 @@ def test_enhancements(plugin, domains):
try:
plugin.enhance(domain, "redirect")
except le_errors.Error as error:
logger.error("Plugin failed to enable redirect for %s:", domain)
logger.exception(error)
return False
# Don't immediately fail because a redirect may already be enabled
logger.warning("Plugin failed to enable redirect for %s:", domain)
logger.warning("%s", error)
if not _save_and_restart(plugin, "enhanced"):
return False
verify_redirect = functools.partial(
validator.Validator().redirect, "localhost", plugin.http_port)
success = True
for domain in domains:
if not verify_redirect(headers={"Host" : domain}):
verify = functools.partial(validator.Validator().redirect, "localhost",
plugin.http_port, headers={"Host" : domain})
if not _try_until_true(verify):
logger.error("Improper redirect for domain %s", domain)
success = False
@@ -196,6 +199,17 @@ def test_enhancements(plugin, domains):
return success
def _try_until_true(func, max_tries=3):
"""Calls func up to max_tries times until it returns True"""
for _ in xrange(0, max_tries):
if func():
return True
else:
time.sleep(1)
return False
def _save_and_restart(plugin, title=None):
"""Saves and restart the plugin, returning True if no errors occurred"""
try: