Merge branch '919' into cli

This commit is contained in:
Peter Eckersley
2015-10-16 17:46:43 -07:00
28 changed files with 352 additions and 153 deletions
+9
View File
@@ -21,6 +21,15 @@ env:
- TOXENV=lint - TOXENV=lint
- TOXENV=cover - TOXENV=cover
# Only build pushes to the master branch, PRs, and branches beginning with
# `test-`. This reduces the number of simultaneous Travis runs, which speeds
# turnaround time on review since there is a cap of 5 simultaneous runs.
branches:
only:
- master
- /^test-.*$/
sudo: false # containers sudo: false # containers
addons: addons:
# make sure simplehttp simple verification works (custom /etc/hosts) # make sure simplehttp simple verification works (custom /etc/hosts)
+5 -2
View File
@@ -1,6 +1,8 @@
"""Tests for acme.jose.interfaces.""" """Tests for acme.jose.interfaces."""
import unittest import unittest
import six
class JSONDeSerializableTest(unittest.TestCase): class JSONDeSerializableTest(unittest.TestCase):
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
@@ -90,8 +92,9 @@ class JSONDeSerializableTest(unittest.TestCase):
self.assertEqual('["foo1", "foo2"]', self.seq.json_dumps()) self.assertEqual('["foo1", "foo2"]', self.seq.json_dumps())
def test_json_dumps_pretty(self): def test_json_dumps_pretty(self):
self.assertEqual( filler = ' ' if six.PY2 else ''
self.seq.json_dumps_pretty(), '[\n "foo1", \n "foo2"\n]') self.assertEqual(self.seq.json_dumps_pretty(),
'[\n "foo1",{0}\n "foo2"\n]'.format(filler))
def test_json_dump_default(self): def test_json_dump_default(self):
from acme.jose.interfaces import JSONDeSerializable from acme.jose.interfaces import JSONDeSerializable
+16
View File
@@ -267,6 +267,22 @@ Please:
.. _PEP 8 - Style Guide for Python Code: .. _PEP 8 - Style Guide for Python Code:
https://www.python.org/dev/peps/pep-0008 https://www.python.org/dev/peps/pep-0008
Submitting a pull request
=========================
Steps:
1. Write your code!
2. Make sure your environment is set up properly and that you're in your
virtualenv. You can do this by running ``./bootstrap/dev/venv.sh``.
(this is a **very important** step)
3. Run ``./pep8.travis.sh`` to do a cursory check of your code style.
Fix any errors.
4. Run ``tox -e lint`` to check for pylint errors. Fix any errors.
5. Run ``tox`` to run the entire test suite including coverage. Fix any errors.
6. If your code touches communication with an ACME server/Boulder, you
should run the integration tests, see `integration`_.
7. Submit the PR.
Updating the documentation Updating the documentation
========================== ==========================
@@ -1,5 +1,6 @@
"""Apache Configuration based off of Augeas Configurator.""" """Apache Configuration based off of Augeas Configurator."""
# pylint: disable=too-many-lines # pylint: disable=too-many-lines
import filecmp
import itertools import itertools
import logging import logging
import os import os
@@ -163,7 +164,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
temp_install(self.mod_ssl_conf) temp_install(self.mod_ssl_conf)
def deploy_cert(self, domain, cert_path, key_path, chain_path=None): def deploy_cert(self, domain, cert_path, key_path,
chain_path=None, fullchain_path=None): # pylint: disable=unused-argument
"""Deploys certificate to specified virtual host. """Deploys certificate to specified virtual host.
Currently tries to find the last directives to deploy the cert in Currently tries to find the last directives to deploy the cert in
@@ -945,9 +947,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
""" """
enabled_dir = os.path.join(self.parser.root, "sites-enabled") enabled_dir = os.path.join(self.parser.root, "sites-enabled")
for entry in os.listdir(enabled_dir): for entry in os.listdir(enabled_dir):
if os.path.realpath(os.path.join(enabled_dir, entry)) == avail_fp: try:
return True if filecmp.cmp(avail_fp, os.path.join(enabled_dir, entry)):
return True
except OSError:
pass
return False return False
def enable_site(self, vhost): def enable_site(self, vhost):
@@ -1,33 +1,18 @@
#!/bin/bash #!/bin/bash
# An extremely simplified version of `a2enmod` for enabling modules in the # An extremely simplified version of `a2enmod` for enabling modules in the
# httpd docker image. First argument is the server_root and the second is the # httpd docker image. First argument is the Apache ServerRoot which should be
# module to be enabled. # an absolute path. The second is the module to be enabled, such as `ssl`.
APACHE_CONFDIR=$1 confdir=$1
module=$2
enable () { echo "LoadModule ${module}_module " \
echo "LoadModule "$1"_module /usr/local/apache2/modules/mod_"$1".so" >> \ "/usr/local/apache2/modules/mod_${module}.so" >> "${confdir}/test.conf"
$APACHE_CONFDIR"/test.conf" availbase="/mods-available/${module}.conf"
available_base="/mods-available/"$1".conf" availconf=$confdir$availbase
available_conf=$APACHE_CONFDIR$available_base enabldir="$confdir/mods-enabled"
enabled_dir=$APACHE_CONFDIR"/mods-enabled" enablconf="$enabldir/${module}.conf"
enabled_conf=$enabled_dir"/"$1".conf" if [ -e $availconf -a -d $enabldir -a ! -e $enablconf ]
if [ -e "$available_conf" -a -d "$enabled_dir" -a ! -e "$enabled_conf" ]
then
ln -s "..$available_base" $enabled_conf
fi
}
if [ $2 == "ssl" ]
then then
# Enables ssl and all its dependencies ln -s "..$availbase" $enablconf
enable "setenvif"
enable "mime"
enable "socache_shmcb"
enable "ssl"
elif [ $2 == "rewrite" ]
then
enable "rewrite"
else
exit 1
fi fi
@@ -175,12 +175,13 @@ class Proxy(configurators_common.Proxy):
else: else:
return {"example.com"} return {"example.com"}
def deploy_cert(self, domain, cert_path, key_path, chain_path=None): def deploy_cert(self, domain, cert_path, key_path, chain_path=None,
fullchain_path=None):
"""Installs cert""" """Installs cert"""
cert_path, key_path, chain_path = self.copy_certs_and_keys( cert_path, key_path, chain_path = self.copy_certs_and_keys(
cert_path, key_path, chain_path) cert_path, key_path, chain_path)
self._apache_configurator.deploy_cert( self._apache_configurator.deploy_cert(
domain, cert_path, key_path, chain_path) domain, cert_path, key_path, chain_path, fullchain_path)
def _is_apache_command(command): def _is_apache_command(command):
@@ -39,7 +39,7 @@ def create_le_config(parent_dir):
def extract_configs(configs, parent_dir): def extract_configs(configs, parent_dir):
"""Extracts configs to a new dir under parent_dir and returns it""" """Extracts configs to a new dir under parent_dir and returns it"""
config_dir = os.path.join(parent_dir, "renewal") config_dir = os.path.join(parent_dir, "configs")
if os.path.isdir(configs): if os.path.isdir(configs):
shutil.copytree(configs, config_dir, symlinks=True) shutil.copytree(configs, config_dir, symlinks=True)
@@ -6,6 +6,7 @@ import shutil
import socket import socket
import subprocess import subprocess
import sys import sys
import time
import OpenSSL import OpenSSL
import zope.interface import zope.interface
@@ -117,30 +118,44 @@ class NginxConfigurator(common.Plugin):
temp_install(self.mod_ssl_conf) temp_install(self.mod_ssl_conf)
# Entry point in main.py for installing cert # Entry point in main.py for installing cert
def deploy_cert(self, domain, cert_path, key_path, chain_path=None): def deploy_cert(self, domain, cert_path, key_path,
chain_path, fullchain_path):
# pylint: disable=unused-argument # pylint: disable=unused-argument
"""Deploys certificate to specified virtual host. """Deploys certificate to specified virtual host.
.. note:: Aborts if the vhost is missing ssl_certificate or .. note:: Aborts if the vhost is missing ssl_certificate or
ssl_certificate_key. ssl_certificate_key.
.. note:: Nginx doesn't have a cert chain directive, so the last .. note:: Nginx doesn't have a cert chain directive.
parameter is always ignored. It expects the cert file to have It expects the cert file to have the concatenated chain.
the concatenated chain. However, we use the chain file as input to the
ssl_trusted_certificate directive, used for verify OCSP responses.
.. note:: This doesn't save the config files! .. note:: This doesn't save the config files!
""" """
vhost = self.choose_vhost(domain) vhost = self.choose_vhost(domain)
directives = [['ssl_certificate', cert_path], cert_directives = [['ssl_certificate', fullchain_path],
['ssl_certificate_key', key_path]] ['ssl_certificate_key', key_path]]
# OCSP stapling was introduced in Nginx 1.3.7. If we have that version
# or greater, add config settings for it.
stapling_directives = []
if self.version >= (1, 3, 7):
stapling_directives = [
['ssl_trusted_certificate', chain_path],
['ssl_stapling', 'on'],
['ssl_stapling_verify', 'on']]
try: try:
self.parser.add_server_directives(vhost.filep, vhost.names, self.parser.add_server_directives(vhost.filep, vhost.names,
directives, True) cert_directives, replace=True)
self.parser.add_server_directives(vhost.filep, vhost.names,
stapling_directives, replace=False)
logger.info("Deployed Certificate to VirtualHost %s for %s", logger.info("Deployed Certificate to VirtualHost %s for %s",
vhost.filep, vhost.names) vhost.filep, vhost.names)
except errors.MisconfigurationError: except errors.MisconfigurationError as error:
logger.debug(error)
logger.warn( logger.warn(
"Cannot find a cert or key directive in %s for %s. " "Cannot find a cert or key directive in %s for %s. "
"VirtualHost was not modified.", vhost.filep, vhost.names) "VirtualHost was not modified.", vhost.filep, vhost.names)
@@ -598,6 +613,10 @@ def nginx_restart(nginx_ctl, nginx_conf="/etc/nginx.conf"):
except (OSError, ValueError): except (OSError, ValueError):
logger.fatal("Nginx Restart Failed - Please Check the Configuration") logger.fatal("Nginx Restart Failed - Please Check the Configuration")
sys.exit(1) sys.exit(1)
# Nginx can take a moment to recognize a newly added TLS SNI servername, so sleep
# for a second. TODO: Check for expected servername and loop until it
# appears or return an error if looping too long.
time.sleep(1)
return True return True
+14 -6
View File
@@ -90,14 +90,22 @@ class NginxDvsni(common.Dvsni):
# Add the 'include' statement for the challenges if it doesn't exist # Add the 'include' statement for the challenges if it doesn't exist
# already in the main config # already in the main config
included = False included = False
directive = ['include', self.challenge_conf] include_directive = ['include', self.challenge_conf]
root = self.configurator.parser.loc["root"] root = self.configurator.parser.loc["root"]
bucket_directive = ['server_names_hash_bucket_size', '128']
main = self.configurator.parser.parsed[root] main = self.configurator.parser.parsed[root]
for entry in main: for key, body in main:
if entry[0] == ['http']: if key == ['http']:
body = entry[1] found_bucket = False
if directive not in body: for key, _ in body:
body.append(directive) if key == bucket_directive[0]:
found_bucket = True
if not found_bucket:
body.insert(0, bucket_directive)
if include_directive not in body:
body.insert(0, include_directive)
included = True included = True
break break
if not included: if not included:
@@ -476,8 +476,12 @@ def _add_directives(block, directives, replace=False):
:param list directives: The new directives. :param list directives: The new directives.
""" """
if replace: for directive in directives:
for directive in directives: if not replace:
# We insert new directives at the top of the block, mostly
# to work around https://trac.nginx.org/nginx/ticket/810
block.insert(0, directive)
else:
changed = False changed = False
if len(directive) == 0: if len(directive) == 0:
continue continue
@@ -489,5 +493,3 @@ def _add_directives(block, directives, replace=False):
raise errors.MisconfigurationError( raise errors.MisconfigurationError(
'LetsEncrypt expected directive for %s in the Nginx ' 'LetsEncrypt expected directive for %s in the Nginx '
'config but did not find it.' % directive[0]) 'config but did not find it.' % directive[0])
else:
block.extend(directives)
@@ -63,11 +63,11 @@ class NginxConfiguratorTest(util.NginxTest):
# pylint: disable=protected-access # pylint: disable=protected-access
parsed = self.config.parser._parse_files(filep, override=True) parsed = self.config.parser._parse_files(filep, override=True)
self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'], self.assertEqual([[['server'], [['listen', '5001 ssl'],
['listen', '69.50.225.155:9000'],
['listen', '127.0.0.1'], ['listen', '127.0.0.1'],
['server_name', '.example.com'], ['server_name', '.example.com'],
['server_name', 'example.*'], ['server_name', 'example.*']]]],
['listen', '5001 ssl']]]],
parsed[0]) parsed[0])
def test_choose_vhost(self): def test_choose_vhost(self):
@@ -96,18 +96,49 @@ class NginxConfiguratorTest(util.NginxTest):
def test_more_info(self): def test_more_info(self):
self.assertTrue('nginx.conf' in self.config.more_info()) self.assertTrue('nginx.conf' in self.config.more_info())
def test_deploy_cert_stapling(self):
# Choose a version of Nginx greater than 1.3.7 so stapling code gets
# invoked.
self.config.version = (1, 9, 6)
example_conf = self.config.parser.abs_path('sites-enabled/example.com')
self.config.deploy_cert(
"www.example.com",
"example/cert.pem",
"example/key.pem",
"example/chain.pem",
"example/fullchain.pem")
self.config.save()
self.config.parser.load()
generated_conf = self.config.parser.parsed[example_conf]
self.assertTrue(util.contains_at_depth(generated_conf,
['ssl_stapling', 'on'], 2))
self.assertTrue(util.contains_at_depth(generated_conf,
['ssl_stapling_verify', 'on'], 2))
self.assertTrue(util.contains_at_depth(generated_conf,
['ssl_trusted_certificate', 'example/chain.pem'], 2))
def test_deploy_cert(self): def test_deploy_cert(self):
server_conf = self.config.parser.abs_path('server.conf') server_conf = self.config.parser.abs_path('server.conf')
nginx_conf = self.config.parser.abs_path('nginx.conf') nginx_conf = self.config.parser.abs_path('nginx.conf')
example_conf = self.config.parser.abs_path('sites-enabled/example.com') example_conf = self.config.parser.abs_path('sites-enabled/example.com')
# Choose a version of Nginx less than 1.3.7 so stapling code doesn't get
# invoked.
self.config.version = (1, 3, 1)
# Get the default SSL vhost # Get the default SSL vhost
self.config.deploy_cert( self.config.deploy_cert(
"www.example.com", "www.example.com",
"example/cert.pem", "example/key.pem") "example/cert.pem",
"example/key.pem",
"example/chain.pem",
"example/fullchain.pem")
self.config.deploy_cert( self.config.deploy_cert(
"another.alias", "another.alias",
"/etc/nginx/cert.pem", "/etc/nginx/key.pem") "/etc/nginx/cert.pem",
"/etc/nginx/key.pem",
"/etc/nginx/chain.pem",
"/etc/nginx/fullchain.pem")
self.config.save() self.config.save()
self.config.parser.load() self.config.parser.load()
@@ -119,35 +150,34 @@ class NginxConfiguratorTest(util.NginxTest):
access_log = os.path.join(self.work_dir, "access.log") access_log = os.path.join(self.work_dir, "access.log")
error_log = os.path.join(self.work_dir, "error.log") error_log = os.path.join(self.work_dir, "error.log")
self.assertEqual([[['server'], self.assertEqual([[['server'],
[['listen', '69.50.225.155:9000'], [['include', self.config.parser.loc["ssl_options"]],
['ssl_certificate_key', 'example/key.pem'],
['ssl_certificate', 'example/fullchain.pem'],
['error_log', error_log],
['access_log', access_log],
['listen', '5001 ssl'],
['listen', '69.50.225.155:9000'],
['listen', '127.0.0.1'], ['listen', '127.0.0.1'],
['server_name', '.example.com'], ['server_name', '.example.com'],
['server_name', 'example.*'], ['server_name', 'example.*']]]],
['listen', '5001 ssl'],
['access_log', access_log],
['error_log', error_log],
['ssl_certificate', 'example/cert.pem'],
['ssl_certificate_key', 'example/key.pem'],
['include',
self.config.parser.loc["ssl_options"]]]]],
parsed_example_conf) parsed_example_conf)
self.assertEqual([['server_name', 'somename alias another.alias']], self.assertEqual([['server_name', 'somename alias another.alias']],
parsed_server_conf) parsed_server_conf)
self.assertEqual([['server'], self.assertTrue(util.contains_at_depth(parsed_nginx_conf,
[['listen', '8000'], [['server'],
['listen', 'somename:8080'], [['include', self.config.parser.loc["ssl_options"]],
['include', 'server.conf'], ['ssl_certificate_key', '/etc/nginx/key.pem'],
[['location', '/'], ['ssl_certificate', '/etc/nginx/fullchain.pem'],
[['root', 'html'], ['error_log', error_log],
['index', 'index.html index.htm']]], ['access_log', access_log],
['listen', '5001 ssl'], ['listen', '5001 ssl'],
['access_log', access_log], ['listen', '8000'],
['error_log', error_log], ['listen', 'somename:8080'],
['ssl_certificate', '/etc/nginx/cert.pem'], ['include', 'server.conf'],
['ssl_certificate_key', '/etc/nginx/key.pem'], [['location', '/'],
['include', [['root', 'html'], ['index', 'index.html index.htm']]]]],
self.config.parser.loc["ssl_options"]]]], 2))
parsed_nginx_conf[-1][-1][-1])
def test_get_all_certs_keys(self): def test_get_all_certs_keys(self):
nginx_conf = self.config.parser.abs_path('nginx.conf') nginx_conf = self.config.parser.abs_path('nginx.conf')
@@ -156,16 +186,22 @@ class NginxConfiguratorTest(util.NginxTest):
# Get the default SSL vhost # Get the default SSL vhost
self.config.deploy_cert( self.config.deploy_cert(
"www.example.com", "www.example.com",
"example/cert.pem", "example/key.pem") "example/cert.pem",
"example/key.pem",
"example/chain.pem",
"example/fullchain.pem")
self.config.deploy_cert( self.config.deploy_cert(
"another.alias", "another.alias",
"/etc/nginx/cert.pem", "/etc/nginx/key.pem") "/etc/nginx/cert.pem",
"/etc/nginx/key.pem",
"/etc/nginx/chain.pem",
"/etc/nginx/fullchain.pem")
self.config.save() self.config.save()
self.config.parser.load() self.config.parser.load()
self.assertEqual(set([ self.assertEqual(set([
('example/cert.pem', 'example/key.pem', example_conf), ('example/fullchain.pem', 'example/key.pem', example_conf),
('/etc/nginx/cert.pem', '/etc/nginx/key.pem', nginx_conf), ('/etc/nginx/fullchain.pem', '/etc/nginx/key.pem', nginx_conf),
]), self.config.get_all_certs_keys()) ]), self.config.get_all_certs_keys())
@mock.patch("letsencrypt_nginx.configurator.dvsni.NginxDvsni.perform") @mock.patch("letsencrypt_nginx.configurator.dvsni.NginxDvsni.perform")
@@ -85,7 +85,8 @@ class DvsniPerformTest(util.NginxTest):
# Make sure challenge config is included in main config # Make sure challenge config is included in main config
http = self.sni.configurator.parser.parsed[ http = self.sni.configurator.parser.parsed[
self.sni.configurator.parser.loc["root"]][-1] self.sni.configurator.parser.loc["root"]][-1]
self.assertTrue(['include', self.sni.challenge_conf] in http[1]) self.assertTrue(
util.contains_at_depth(http, ['include', self.sni.challenge_conf], 1))
def test_perform2(self): def test_perform2(self):
acme_responses = [] acme_responses = []
@@ -108,7 +109,8 @@ class DvsniPerformTest(util.NginxTest):
http = self.sni.configurator.parser.parsed[ http = self.sni.configurator.parser.parsed[
self.sni.configurator.parser.loc["root"]][-1] self.sni.configurator.parser.loc["root"]][-1]
self.assertTrue(['include', self.sni.challenge_conf] in http[1]) self.assertTrue(['include', self.sni.challenge_conf] in http[1])
self.assertTrue(['server_name', 'blah'] in http[1][-2][1]) self.assertTrue(
util.contains_at_depth(http, ['server_name', 'blah'], 3))
self.assertEqual(len(sni_responses), 3) self.assertEqual(len(sni_responses), 3)
for i in xrange(3): for i in xrange(3):
@@ -128,18 +128,20 @@ class NginxParserTest(util.NginxTest):
r'~^(www\.)?(example|bar)\.']), r'~^(www\.)?(example|bar)\.']),
[['foo', 'bar'], ['ssl_certificate', [['foo', 'bar'], ['ssl_certificate',
'/etc/ssl/cert.pem']]) '/etc/ssl/cert.pem']])
ssl_re = re.compile(r'foo bar;\n\s+ssl_certificate /etc/ssl/cert.pem') ssl_re = re.compile(r'\n\s+ssl_certificate /etc/ssl/cert.pem')
self.assertEqual(1, len(re.findall(ssl_re, nginxparser.dumps( dump = nginxparser.dumps(nparser.parsed[nparser.abs_path('nginx.conf')])
nparser.parsed[nparser.abs_path('nginx.conf')])))) self.assertEqual(1, len(re.findall(ssl_re, dump)))
nparser.add_server_directives(nparser.abs_path('server.conf'),
server_conf = nparser.abs_path('server.conf')
nparser.add_server_directives(server_conf,
set(['alias', 'another.alias', set(['alias', 'another.alias',
'somename']), 'somename']),
[['foo', 'bar'], ['ssl_certificate', [['foo', 'bar'], ['ssl_certificate',
'/etc/ssl/cert2.pem']]) '/etc/ssl/cert2.pem']])
self.assertEqual(nparser.parsed[nparser.abs_path('server.conf')], self.assertEqual(nparser.parsed[server_conf],
[['server_name', 'somename alias another.alias'], [['ssl_certificate', '/etc/ssl/cert2.pem'],
['foo', 'bar'], ['foo', 'bar'],
['ssl_certificate', '/etc/ssl/cert2.pem']]) ['server_name', 'somename alias another.alias']])
def test_add_http_directives(self): def test_add_http_directives(self):
nparser = parser.NginxParser(self.config_path, self.ssl_options) nparser = parser.NginxParser(self.config_path, self.ssl_options)
@@ -148,8 +150,15 @@ class NginxParserTest(util.NginxTest):
[['listen', '80'], [['listen', '80'],
['server_name', 'localhost']]] ['server_name', 'localhost']]]
nparser.add_http_directives(filep, block) nparser.add_http_directives(filep, block)
self.assertEqual(nparser.parsed[filep][-1][0], ['http']) root = nparser.parsed[filep]
self.assertEqual(nparser.parsed[filep][-1][1][-1], block) self.assertTrue(util.contains_at_depth(root, ['http'], 1))
self.assertTrue(util.contains_at_depth(root, block, 2))
# Check that our server block got inserted first among all server
# blocks.
http_block = [x for x in root if x[0] == ['http']][0][1]
server_blocks = [x for x in http_block if x[0] == ['server']]
self.assertEqual(server_blocks[0], block)
def test_replace_server_directives(self): def test_replace_server_directives(self):
nparser = parser.NginxParser(self.config_path, self.ssl_options) nparser = parser.NginxParser(self.config_path, self.ssl_options)
@@ -85,3 +85,22 @@ def filter_comments(tree):
yield [key, values] yield [key, values]
return list(traverse(tree)) return list(traverse(tree))
def contains_at_depth(haystack, needle, n):
"""Is the needle in haystack at depth n?
Return true if the needle is present in one of the sub-iterables in haystack
at depth n. Haystack must be an iterable.
"""
# Specifically use hasattr rather than isinstance(..., collections.Iterable)
# because we want to include lists but reject strings.
if not hasattr(haystack, '__iter__'):
return False
if n == 0:
return needle in haystack
else:
for item in haystack:
if contains_at_depth(item, needle, n - 1):
return True
return False
@@ -20,7 +20,6 @@ events {
} }
http { http {
server_names_hash_bucket_size 2048;
# Set an array of temp and cache file options that will otherwise default to # Set an array of temp and cache file options that will otherwise default to
# restricted locations accessible only to root. # restricted locations accessible only to root.
client_body_temp_path $root/client_body; client_body_temp_path $root/client_body;
+36 -23
View File
@@ -397,9 +397,9 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo
lineage = _auth_from_domains(le_client, config, domains, plugins) lineage = _auth_from_domains(le_client, config, domains, plugins)
# TODO: We also need to pass the fullchain (for Nginx)
le_client.deploy_certificate( le_client.deploy_certificate(
domains, lineage.privkey, lineage.cert, lineage.chain) domains, lineage.privkey, lineage.cert,
lineage.chain, lineage.fullchain)
le_client.enhance_config(domains, args.redirect) le_client.enhance_config(domains, args.redirect)
if len(lineage.available_versions("cert")) == 1: if len(lineage.available_versions("cert")) == 1:
@@ -452,7 +452,8 @@ def install(args, config, plugins):
args, config, authenticator=None, installer=installer) args, config, authenticator=None, installer=installer)
assert args.cert_path is not None # required=True in the subparser assert args.cert_path is not None # required=True in the subparser
le_client.deploy_certificate( le_client.deploy_certificate(
domains, args.key_path, args.cert_path, args.chain_path) domains, args.key_path, args.cert_path, args.chain_path,
args.fullchain_path)
le_client.enhance_config(domains, args.redirect) le_client.enhance_config(domains, args.redirect)
@@ -867,6 +868,8 @@ def _paths_parser(helpful):
default_cp = None default_cp = None
if verb == "auth": if verb == "auth":
default_cp = flag_default("auth_chain_path") default_cp = flag_default("auth_chain_path")
add("paths", "--fullchain-path", default=default_cp,
help="Accompanying path to a full certificate chain (cert plus chain).")
add("paths", "--chain-path", default=default_cp, add("paths", "--chain-path", default=default_cp,
help="Accompanying path to a certificate chain.") help="Accompanying path to a certificate chain.")
add("paths", "--config-dir", default=flag_default("config_dir"), add("paths", "--config-dir", default=flag_default("config_dir"),
@@ -903,9 +906,23 @@ def _plugins_parsing(helpful, plugins):
helpful.add_plugin_args(plugins) helpful.add_plugin_args(plugins)
def _setup_logging(args): def setup_log_file_handler(args, logfile, fmt):
level = -args.verbose_count * 10 """Setup file debug logging."""
fmt = "%(asctime)s:%(levelname)s:%(name)s:%(message)s" log_file_path = os.path.join(args.logs_dir, logfile)
handler = logging.handlers.RotatingFileHandler(
log_file_path, maxBytes=2 ** 20, backupCount=10)
# rotate on each invocation, rollover only possible when maxBytes
# is nonzero and backupCount is nonzero, so we set maxBytes as big
# as possible not to overrun in single CLI invocation (1MB).
handler.doRollover() # TODO: creates empty letsencrypt.log.1 file
handler.setLevel(logging.DEBUG)
handler_formatter = logging.Formatter(fmt=fmt)
handler_formatter.converter = time.gmtime # don't use localtime
handler.setFormatter(handler_formatter)
return handler, log_file_path
def _cli_log_handler(args, level, fmt):
if args.text_mode: if args.text_mode:
handler = colored_logging.StreamHandler() handler = colored_logging.StreamHandler()
handler.setFormatter(logging.Formatter(fmt)) handler.setFormatter(logging.Formatter(fmt))
@@ -914,30 +931,26 @@ def _setup_logging(args):
# dialog box is small, display as less as possible # dialog box is small, display as less as possible
handler.setFormatter(logging.Formatter("%(message)s")) handler.setFormatter(logging.Formatter("%(message)s"))
handler.setLevel(level) handler.setLevel(level)
return handler
def setup_logging(args, cli_handler_factory, logfile):
"""Setup logging."""
fmt = "%(asctime)s:%(levelname)s:%(name)s:%(message)s"
level = -args.verbose_count * 10
file_handler, log_file_path = setup_log_file_handler(
args, logfile=logfile, fmt=fmt)
cli_handler = cli_handler_factory(args, level, fmt)
# TODO: use fileConfig? # TODO: use fileConfig?
# unconditionally log to file for debugging purposes
# TODO: change before release?
log_file_name = os.path.join(args.logs_dir, 'letsencrypt.log')
file_handler = logging.handlers.RotatingFileHandler(
log_file_name, maxBytes=2 ** 20, backupCount=10)
# rotate on each invocation, rollover only possible when maxBytes
# is nonzero and backupCount is nonzero, so we set maxBytes as big
# as possible not to overrun in single CLI invocation (1MB).
file_handler.doRollover() # TODO: creates empty letsencrypt.log.1 file
file_handler.setLevel(logging.DEBUG)
file_handler_formatter = logging.Formatter(fmt=fmt)
file_handler_formatter.converter = time.gmtime # don't use localtime
file_handler.setFormatter(file_handler_formatter)
root_logger = logging.getLogger() root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG) # send all records to handlers root_logger.setLevel(logging.DEBUG) # send all records to handlers
root_logger.addHandler(handler) root_logger.addHandler(cli_handler)
root_logger.addHandler(file_handler) root_logger.addHandler(file_handler)
logger.debug("Root logging level set at %d", level) logger.debug("Root logging level set at %d", level)
logger.info("Saving debug log to %s", log_file_name) logger.info("Saving debug log to %s", log_file_path)
def _handle_exception(exc_type, exc_value, trace, args): def _handle_exception(exc_type, exc_value, trace, args):
@@ -1006,7 +1019,7 @@ def main(cli_args=sys.argv[1:]):
# private key! #525 # private key! #525
le_util.make_or_verify_dir( le_util.make_or_verify_dir(
args.logs_dir, 0o700, os.geteuid(), "--strict-permissions" in cli_args) args.logs_dir, 0o700, os.geteuid(), "--strict-permissions" in cli_args)
_setup_logging(args) setup_logging(args, _cli_log_handler, logfile='letsencrypt.log')
# do not log `args`, as it contains sensitive data (e.g. revoke --key)! # do not log `args`, as it contains sensitive data (e.g. revoke --key)!
logger.debug("Arguments: %r", cli_args) logger.debug("Arguments: %r", cli_args)
+6 -3
View File
@@ -336,7 +336,8 @@ class Client(object):
return os.path.abspath(act_cert_path), cert_chain_abspath return os.path.abspath(act_cert_path), cert_chain_abspath
def deploy_certificate(self, domains, privkey_path, cert_path, chain_path): def deploy_certificate(self, domains, privkey_path,
cert_path, chain_path, fullchain_path):
"""Install certificate """Install certificate
:param list domains: list of domains to install the certificate :param list domains: list of domains to install the certificate
@@ -357,8 +358,10 @@ class Client(object):
# TODO: Provide a fullchain reference for installers like # TODO: Provide a fullchain reference for installers like
# nginx that want it # nginx that want it
self.installer.deploy_cert( self.installer.deploy_cert(
dom, os.path.abspath(cert_path), domain=dom, cert_path=os.path.abspath(cert_path),
os.path.abspath(privkey_path), chain_path) key_path=os.path.abspath(privkey_path),
chain_path=chain_path,
fullchain_path=fullchain_path)
self.installer.save("Deployed Let's Encrypt Certificate") self.installer.save("Deployed Let's Encrypt Certificate")
# sites may have been enabled / final cleanup # sites may have been enabled / final cleanup
+1
View File
@@ -27,6 +27,7 @@ CLI_DEFAULTS = dict(
auth_cert_path="./cert.pem", auth_cert_path="./cert.pem",
auth_chain_path="./chain.pem", auth_chain_path="./chain.pem",
strict_permissions=False,
) )
"""Defaults for CLI flags and `.IConfig` attributes.""" """Defaults for CLI flags and `.IConfig` attributes."""
+3 -1
View File
@@ -241,13 +241,15 @@ class IInstaller(IPlugin):
""" """
def deploy_cert(domain, cert_path, key_path, chain_path=None): def deploy_cert(domain, cert_path, key_path, chain_path, fullchain_path):
"""Deploy certificate. """Deploy certificate.
:param str domain: domain to deploy certificate file :param str domain: domain to deploy certificate file
:param str cert_path: absolute path to the certificate file :param str cert_path: absolute path to the certificate file
:param str key_path: absolute path to the private key file :param str key_path: absolute path to the private key file
:param str chain_path: absolute path to the certificate chain file :param str chain_path: absolute path to the certificate chain file
:param str fullchain_path: absolute path to the certificate fullchain
file (cert plus chain)
:raises .PluginError: when cert cannot be deployed :raises .PluginError: when cert cannot be deployed
+6
View File
@@ -129,12 +129,18 @@ binary for temporary key/certificate generation.""".replace("\n", "")
ct=response.CONTENT_TYPE, port=port) ct=response.CONTENT_TYPE, port=port)
if self.conf("test-mode"): if self.conf("test-mode"):
logger.debug("Test mode. Executing the manual command: %s", command) logger.debug("Test mode. Executing the manual command: %s", command)
# sh shipped with OS X does't support echo -n
if sys.platform == "darwin":
executable = "/bin/bash"
else:
executable = None
try: try:
self._httpd = subprocess.Popen( self._httpd = subprocess.Popen(
command, command,
# don't care about setting stdout and stderr, # don't care about setting stdout and stderr,
# we're in test mode anyway # we're in test mode anyway
shell=True, shell=True,
executable=executable,
# "preexec_fn" is UNIX specific, but so is "command" # "preexec_fn" is UNIX specific, but so is "command"
preexec_fn=os.setsid) preexec_fn=os.setsid)
except OSError as error: # ValueError should not happen! except OSError as error: # ValueError should not happen!
+2 -1
View File
@@ -30,7 +30,8 @@ class Installer(common.Plugin):
def get_all_names(self): def get_all_names(self):
return [] return []
def deploy_cert(self, domain, cert_path, key_path, chain_path=None): def deploy_cert(self, domain, cert_path, key_path,
chain_path=None, fullchain_path=None):
pass # pragma: no cover pass # pragma: no cover
def enhance(self, domain, enhancement, options=None): def enhance(self, domain, enhancement, options=None):
@@ -1,4 +1,5 @@
"""Standalone authenticator.""" """Standalone authenticator."""
import logging
import os import os
import psutil import psutil
import signal import signal
@@ -19,6 +20,9 @@ from letsencrypt import interfaces
from letsencrypt.plugins import common from letsencrypt.plugins import common
logger = logging.getLogger(__name__)
class StandaloneAuthenticator(common.Plugin): class StandaloneAuthenticator(common.Plugin):
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
"""Standalone authenticator. """Standalone authenticator.
@@ -301,10 +305,21 @@ class StandaloneAuthenticator(common.Plugin):
:param int port: The TCP port in question. :param int port: The TCP port in question.
:returns: True or False.""" :returns: True or False."""
listeners = [conn.pid for conn in psutil.net_connections() try:
net_connections = psutil.net_connections()
except psutil.AccessDenied as error:
logger.info("Access denied when trying to list network "
"connections: %s. Are you root?", error)
# this function is just a pre-check that often causes false
# positives and problems in testing (c.f. #680 on Mac, #255
# generally); we will fail later in bind() anyway
return False
listeners = [conn.pid for conn in net_connections
if conn.status == 'LISTEN' and if conn.status == 'LISTEN' and
conn.type == socket.SOCK_STREAM and conn.type == socket.SOCK_STREAM and
conn.laddr[1] == port] conn.laddr[1] == port]
try: try:
if listeners and listeners[0] is not None: if listeners and listeners[0] is not None:
# conn.pid may be None if the current process doesn't have # conn.pid may be None if the current process doesn't have
+30 -4
View File
@@ -8,6 +8,7 @@ within lineages of successor certificates, according to configuration.
""" """
import argparse import argparse
import logging
import os import os
import sys import sys
@@ -17,10 +18,13 @@ import zope.component
from letsencrypt import account from letsencrypt import account
from letsencrypt import configuration from letsencrypt import configuration
from letsencrypt import constants
from letsencrypt import colored_logging
from letsencrypt import cli from letsencrypt import cli
from letsencrypt import client from letsencrypt import client
from letsencrypt import crypto_util from letsencrypt import crypto_util
from letsencrypt import errors from letsencrypt import errors
from letsencrypt import le_util
from letsencrypt import notify from letsencrypt import notify
from letsencrypt import storage from letsencrypt import storage
@@ -28,6 +32,9 @@ from letsencrypt.display import util as display_util
from letsencrypt.plugins import disco as plugins_disco from letsencrypt.plugins import disco as plugins_disco
logger = logging.getLogger(__name__)
class _AttrDict(dict): class _AttrDict(dict):
"""Attribute dictionary. """Attribute dictionary.
@@ -104,6 +111,12 @@ def renew(cert, old_version):
# (where fewer than all names were renewed) # (where fewer than all names were renewed)
def _cli_log_handler(args, level, fmt): # pylint: disable=unused-argument
handler = colored_logging.StreamHandler()
handler.setFormatter(logging.Formatter(fmt))
return handler
def _paths_parser(parser): def _paths_parser(parser):
add = parser.add_argument_group("paths").add_argument add = parser.add_argument_group("paths").add_argument
add("--config-dir", default=cli.flag_default("config_dir"), add("--config-dir", default=cli.flag_default("config_dir"),
@@ -119,11 +132,16 @@ def _paths_parser(parser):
def _create_parser(): def _create_parser():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
#parser.add_argument("--cron", action="store_true", help="Run as cronjob.") #parser.add_argument("--cron", action="store_true", help="Run as cronjob.")
# pylint: disable=protected-access parser.add_argument(
"-v", "--verbose", dest="verbose_count", action="count",
default=cli.flag_default("verbose_count"), help="This flag can be used "
"multiple times to incrementally increase the verbosity of output, "
"e.g. -vvv.")
return _paths_parser(parser) return _paths_parser(parser)
def main(config=None, args=sys.argv[1:]): def main(config=None, cli_args=sys.argv[1:]):
"""Main function for autorenewer script.""" """Main function for autorenewer script."""
# TODO: Distinguish automated invocation from manual invocation, # TODO: Distinguish automated invocation from manual invocation,
# perhaps by looking at sys.argv[0] and inhibiting automated # perhaps by looking at sys.argv[0] and inhibiting automated
@@ -133,8 +151,13 @@ def main(config=None, args=sys.argv[1:]):
zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
cli_config = configuration.RenewerConfiguration( args = _create_parser().parse_args(cli_args)
_create_parser().parse_args(args))
uid = os.geteuid()
le_util.make_or_verify_dir(args.logs_dir, 0o700, uid)
cli.setup_logging(args, _cli_log_handler, logfile='renewer.log')
cli_config = configuration.RenewerConfiguration(args)
config = storage.config_with_defaults(config) config = storage.config_with_defaults(config)
# Now attempt to read the renewer config file and augment or replace # Now attempt to read the renewer config file and augment or replace
@@ -145,6 +168,9 @@ def main(config=None, args=sys.argv[1:]):
# specify a config file on the command line, which, if provided, should # specify a config file on the command line, which, if provided, should
# take precedence over this one. # take precedence over this one.
config.merge(configobj.ConfigObj(cli_config.renewer_config_file)) config.merge(configobj.ConfigObj(cli_config.renewer_config_file))
# Ensure that all of the needed folders have been created before continuing
le_util.make_or_verify_dir(cli_config.work_dir,
constants.CONFIG_DIRS_MODE, uid)
for i in os.listdir(cli_config.renewal_configs_dir): for i in os.listdir(cli_config.renewal_configs_dir):
print "Processing", i print "Processing", i
+4 -1
View File
@@ -75,7 +75,10 @@ class CLITest(unittest.TestCase):
output.truncate(0) output.truncate(0)
self.assertRaises(SystemExit, self._call_stdout, ['-h', 'nginx']) self.assertRaises(SystemExit, self._call_stdout, ['-h', 'nginx'])
out = output.getvalue() out = output.getvalue()
self.assertTrue("--nginx-ctl" in out) from letsencrypt.plugins import disco
if "nginx" in disco.PluginsRegistry.find_all():
# may be false while building distributions without plugins
self.assertTrue("--nginx-ctl" in out)
self.assertTrue("--manual-test-mode" not in out) self.assertTrue("--manual-test-mode" not in out)
self.assertTrue("--checkpoints" not in out) self.assertTrue("--checkpoints" not in out)
output.truncate(0) output.truncate(0)
+8 -4
View File
@@ -177,15 +177,19 @@ class ClientTest(unittest.TestCase):
def test_deploy_certificate(self): def test_deploy_certificate(self):
self.assertRaises(errors.Error, self.client.deploy_certificate, self.assertRaises(errors.Error, self.client.deploy_certificate,
["foo.bar"], "key", "cert", "chain") ["foo.bar"], "key", "cert", "chain", "fullchain")
installer = mock.MagicMock() installer = mock.MagicMock()
self.client.installer = installer self.client.installer = installer
self.client.deploy_certificate(["foo.bar"], "key", "cert", "chain") self.client.deploy_certificate(["foo.bar"], "key", "cert", "chain",
"fullchain")
installer.deploy_cert.assert_called_once_with( installer.deploy_cert.assert_called_once_with(
"foo.bar", os.path.abspath("cert"), cert_path=os.path.abspath("cert"),
os.path.abspath("key"), os.path.abspath("chain")) chain_path=os.path.abspath("chain"),
domain='foo.bar',
fullchain_path='fullchain',
key_path=os.path.abspath("key"))
self.assertEqual(installer.save.call_count, 1) self.assertEqual(installer.save.call_count, 1)
installer.restart.assert_called_once_with() installer.restart.assert_called_once_with()
+21 -13
View File
@@ -44,8 +44,15 @@ class BaseRenewableCertTest(unittest.TestCase):
self.tempdir = tempfile.mkdtemp() self.tempdir = tempfile.mkdtemp()
self.cli_config = configuration.RenewerConfiguration( self.cli_config = configuration.RenewerConfiguration(
namespace=mock.MagicMock(config_dir=self.tempdir)) namespace=mock.MagicMock(
config_dir=self.tempdir,
work_dir=self.tempdir,
logs_dir=self.tempdir,
)
)
# TODO: maybe provide RenewerConfiguration.make_dirs? # TODO: maybe provide RenewerConfiguration.make_dirs?
# TODO: main() should create those dirs, c.f. #902
os.makedirs(os.path.join(self.tempdir, "live", "example.org")) os.makedirs(os.path.join(self.tempdir, "live", "example.org"))
os.makedirs(os.path.join(self.tempdir, "archive", "example.org")) os.makedirs(os.path.join(self.tempdir, "archive", "example.org"))
os.makedirs(os.path.join(self.tempdir, "renewal")) os.makedirs(os.path.join(self.tempdir, "renewal"))
@@ -62,6 +69,9 @@ class BaseRenewableCertTest(unittest.TestCase):
self.test_rc = storage.RenewableCert( self.test_rc = storage.RenewableCert(
self.config, self.defaults, self.cli_config) self.config, self.defaults, self.cli_config)
def tearDown(self):
shutil.rmtree(self.tempdir)
def _write_out_ex_kinds(self): def _write_out_ex_kinds(self):
for kind in ALL_FOUR: for kind in ALL_FOUR:
where = getattr(self.test_rc, kind) where = getattr(self.test_rc, kind)
@@ -79,11 +89,6 @@ class BaseRenewableCertTest(unittest.TestCase):
class RenewableCertTests(BaseRenewableCertTest): class RenewableCertTests(BaseRenewableCertTest):
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
"""Tests for letsencrypt.renewer.*.""" """Tests for letsencrypt.renewer.*."""
def setUp(self):
super(RenewableCertTests, self).setUp()
def tearDown(self):
shutil.rmtree(self.tempdir)
def test_initialization(self): def test_initialization(self):
self.assertEqual(self.test_rc.lineagename, "example.org") self.assertEqual(self.test_rc.lineagename, "example.org")
@@ -665,11 +670,17 @@ class RenewableCertTests(BaseRenewableCertTest):
# This should fail because the renewal itself appears to fail # This should fail because the renewal itself appears to fail
self.assertFalse(renewer.renew(self.test_rc, 1)) self.assertFalse(renewer.renew(self.test_rc, 1))
def _common_cli_args(self):
return [
"--config-dir", self.cli_config.config_dir,
"--work-dir", self.cli_config.work_dir,
"--logs-dir", self.cli_config.logs_dir,
]
@mock.patch("letsencrypt.renewer.notify") @mock.patch("letsencrypt.renewer.notify")
@mock.patch("letsencrypt.storage.RenewableCert") @mock.patch("letsencrypt.storage.RenewableCert")
@mock.patch("letsencrypt.renewer.renew") @mock.patch("letsencrypt.renewer.renew")
def test_main(self, mock_renew, mock_rc, mock_notify): def test_main(self, mock_renew, mock_rc, mock_notify):
"""Test for main() function."""
from letsencrypt import renewer from letsencrypt import renewer
mock_rc_instance = mock.MagicMock() mock_rc_instance = mock.MagicMock()
mock_rc_instance.should_autodeploy.return_value = True mock_rc_instance.should_autodeploy.return_value = True
@@ -691,8 +702,7 @@ class RenewableCertTests(BaseRenewableCertTest):
"example.com.conf"), "w") as f: "example.com.conf"), "w") as f:
f.write("cert = cert.pem\nprivkey = privkey.pem\n") f.write("cert = cert.pem\nprivkey = privkey.pem\n")
f.write("chain = chain.pem\nfullchain = fullchain.pem\n") f.write("chain = chain.pem\nfullchain = fullchain.pem\n")
renewer.main(self.defaults, args=[ renewer.main(self.defaults, cli_args=self._common_cli_args())
'--config-dir', self.cli_config.config_dir])
self.assertEqual(mock_rc.call_count, 2) self.assertEqual(mock_rc.call_count, 2)
self.assertEqual(mock_rc_instance.update_all_links_to.call_count, 2) self.assertEqual(mock_rc_instance.update_all_links_to.call_count, 2)
self.assertEqual(mock_notify.notify.call_count, 4) self.assertEqual(mock_notify.notify.call_count, 4)
@@ -705,8 +715,7 @@ class RenewableCertTests(BaseRenewableCertTest):
mock_happy_instance.should_autorenew.return_value = False mock_happy_instance.should_autorenew.return_value = False
mock_happy_instance.latest_common_version.return_value = 10 mock_happy_instance.latest_common_version.return_value = 10
mock_rc.return_value = mock_happy_instance mock_rc.return_value = mock_happy_instance
renewer.main(self.defaults, args=[ renewer.main(self.defaults, cli_args=self._common_cli_args())
'--config-dir', self.cli_config.config_dir])
self.assertEqual(mock_rc.call_count, 4) self.assertEqual(mock_rc.call_count, 4)
self.assertEqual(mock_happy_instance.update_all_links_to.call_count, 0) self.assertEqual(mock_happy_instance.update_all_links_to.call_count, 0)
self.assertEqual(mock_notify.notify.call_count, 4) self.assertEqual(mock_notify.notify.call_count, 4)
@@ -717,8 +726,7 @@ class RenewableCertTests(BaseRenewableCertTest):
with open(os.path.join(self.cli_config.renewal_configs_dir, with open(os.path.join(self.cli_config.renewal_configs_dir,
"bad.conf"), "w") as f: "bad.conf"), "w") as f:
f.write("incomplete = configfile\n") f.write("incomplete = configfile\n")
renewer.main(self.defaults, args=[ renewer.main(self.defaults, cli_args=self._common_cli_args())
'--config-dir', self.cli_config.config_dir])
# The errors.CertStorageError is caught inside and nothing happens. # The errors.CertStorageError is caught inside and nothing happens.
+7 -1
View File
@@ -14,6 +14,12 @@ export PATH="/usr/sbin:$PATH" # /usr/sbin/nginx
export GOPATH="${GOPATH:-/tmp/go}" export GOPATH="${GOPATH:-/tmp/go}"
export PATH="$GOPATH/bin:$PATH" export PATH="$GOPATH/bin:$PATH"
if [ `uname` == 'Darwin' ]; then
readlink="greadlink"
else
readlink="readlink"
fi
common() { common() {
letsencrypt_test \ letsencrypt_test \
--authenticator standalone \ --authenticator standalone \
@@ -49,7 +55,7 @@ dir="$root/conf/archive/le1.wtf"
for x in cert chain fullchain privkey; for x in cert chain fullchain privkey;
do do
latest="$(ls -1t $dir/ | grep -e "^${x}" | head -n1)" latest="$(ls -1t $dir/ | grep -e "^${x}" | head -n1)"
live="$(readlink -f "$root/conf/live/le1.wtf/${x}.pem")" live="$($readlink -f "$root/conf/live/le1.wtf/${x}.pem")"
[ "${dir}/${latest}" = "$live" ] # renewer fails this test [ "${dir}/${latest}" = "$live" ] # renewer fails this test
done done
+1 -2
View File
@@ -88,8 +88,7 @@ mkdir ../kgs
kgs="../kgs/$version" kgs="../kgs/$version"
pip freeze | tee $kgs pip freeze | tee $kgs
pip install nose pip install nose
# TODO: letsencrypt_apache fails due to symlink, c.f. #838 nosetests letsencrypt $SUBPKGS
nosetests letsencrypt $SUBPKGS || true
echo "New root: $root" echo "New root: $root"
echo "KGS is at $root/kgs" echo "KGS is at $root/kgs"