diff --git a/README.rst b/README.rst index fe66f8af2..72188608b 100644 --- a/README.rst +++ b/README.rst @@ -39,4 +39,4 @@ Current Features .. Do not modify this comment unless you know what you're doing. tag:features-end -For extensive documentation on using and contributing to Certbot, go to https://certbot.eff.org/docs. If you would like to contribute to the project or run the latest code from git, you should read our `developer guide `. +For extensive documentation on using and contributing to Certbot, go to https://certbot.eff.org/docs. If you would like to contribute to the project or run the latest code from git, you should read our `developer guide `_. diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index c436cc631..6242c376c 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -14,7 +14,6 @@ from acme import crypto_util from acme import fields from acme import jose - logger = logging.getLogger(__name__) @@ -206,6 +205,74 @@ class KeyAuthorizationChallenge(_TokenChallenge): self.validation(account_key, *args, **kwargs)) +@ChallengeResponse.register +class DNS01Response(KeyAuthorizationChallengeResponse): + """ACME dns-01 challenge response.""" + typ = "dns-01" + + def simple_verify(self, chall, domain, account_public_key): + """Simple verify. + + :param challenges.DNS01 chall: Corresponding challenge. + :param unicode domain: Domain name being verified. + :param JWK account_public_key: Public key for the key pair + being authorized. + + :returns: ``True`` iff validation with the TXT records resolved from a + DNS server is successful. + :rtype: bool + + """ + if not self.verify(chall, account_public_key): + logger.debug("Verification of key authorization in response failed") + return False + + validation_domain_name = chall.validation_domain_name(domain) + validation = chall.validation(account_public_key) + logger.debug("Verifying %s at %s...", chall.typ, validation_domain_name) + + try: + from acme import dns_resolver + except ImportError: # pragma: no cover + raise errors.Error("Local validation for 'dns-01' challenges " + "requires 'dnspython'") + txt_records = dns_resolver.txt_records_for_name(validation_domain_name) + exists = validation in txt_records + if not exists: + logger.debug("Key authorization from response (%r) doesn't match " + "any DNS response in %r", self.key_authorization, + txt_records) + return exists + + +@Challenge.register # pylint: disable=too-many-ancestors +class DNS01(KeyAuthorizationChallenge): + """ACME dns-01 challenge.""" + response_cls = DNS01Response + typ = response_cls.typ + + LABEL = "_acme-challenge" + """Label clients prepend to the domain name being validated.""" + + def validation(self, account_key, **unused_kwargs): + """Generate validation. + + :param JWK account_key: + :rtype: unicode + + """ + return jose.b64encode(hashlib.sha256(self.key_authorization( + account_key).encode("utf-8")).digest()).decode() + + def validation_domain_name(self, name): + """Domain name for TXT validation record. + + :param unicode name: Domain name being validated. + + """ + return "{0}.{1}".format(self.LABEL, name) + + @ChallengeResponse.register class HTTP01Response(KeyAuthorizationChallengeResponse): """ACME http-01 challenge response.""" @@ -231,8 +298,8 @@ class HTTP01Response(KeyAuthorizationChallengeResponse): being authorized. :param int port: Port used in the validation. - :returns: ``True`` iff validation is successful, ``False`` - otherwise. + :returns: ``True`` iff validation with the files currently served by the + HTTP server is successful. :rtype: bool """ @@ -410,7 +477,7 @@ class TLSSNI01Response(KeyAuthorizationChallengeResponse): :returns: ``True`` iff client's control of the domain has been - verified, ``False`` otherwise. + verified. :rtype: bool """ diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 04b7442b0..27976931a 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -77,6 +77,93 @@ class KeyAuthorizationChallengeResponseTest(unittest.TestCase): self.assertFalse(response.verify(self.chall, KEY.public_key())) +class DNS01ResponseTest(unittest.TestCase): + # pylint: disable=too-many-instance-attributes + + def setUp(self): + from acme.challenges import DNS01Response + self.msg = DNS01Response(key_authorization=u'foo') + self.jmsg = { + 'resource': 'challenge', + 'type': 'dns-01', + 'keyAuthorization': u'foo', + } + + from acme.challenges import DNS01 + self.chall = DNS01(token=(b'x' * 16)) + self.response = self.chall.response(KEY) + + def test_to_partial_json(self): + self.assertEqual(self.jmsg, self.msg.to_partial_json()) + + def test_from_json(self): + from acme.challenges import DNS01Response + self.assertEqual(self.msg, DNS01Response.from_json(self.jmsg)) + + def test_from_json_hashable(self): + from acme.challenges import DNS01Response + hash(DNS01Response.from_json(self.jmsg)) + + def test_simple_verify_bad_key_authorization(self): + key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) + self.response.simple_verify(self.chall, "local", key2.public_key()) + + @mock.patch("acme.dns_resolver.txt_records_for_name") + def test_simple_verify_good_validation(self, mock_resolver): + mock_resolver.return_value = [self.chall.validation(KEY.public_key())] + self.assertTrue(self.response.simple_verify( + self.chall, "local", KEY.public_key())) + mock_resolver.assert_called_once_with( + self.chall.validation_domain_name("local")) + + @mock.patch("acme.dns_resolver.txt_records_for_name") + def test_simple_verify_good_validation_multiple_txts(self, mock_resolver): + mock_resolver.return_value = [ + "!", self.chall.validation(KEY.public_key())] + self.assertTrue(self.response.simple_verify( + self.chall, "local", KEY.public_key())) + mock_resolver.assert_called_once_with( + self.chall.validation_domain_name("local")) + + @mock.patch("acme.dns_resolver.txt_records_for_name") + def test_simple_verify_bad_validation(self, mock_dns): + mock_dns.return_value = ["!"] + self.assertFalse(self.response.simple_verify( + self.chall, "local", KEY.public_key())) + + +class DNS01Test(unittest.TestCase): + + def setUp(self): + from acme.challenges import DNS01 + self.msg = DNS01(token=jose.decode_b64jose( + 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA')) + self.jmsg = { + 'type': 'dns-01', + 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', + } + + def test_validation_domain_name(self): + self.assertEqual('_acme-challenge.www.example.com', + self.msg.validation_domain_name('www.example.com')) + + def test_validation(self): + self.assertEqual( + "rAa7iIg4K2y63fvUhCfy8dP1Xl7wEhmQq0oChTcE3Zk", + self.msg.validation(KEY)) + + def test_to_partial_json(self): + self.assertEqual(self.jmsg, self.msg.to_partial_json()) + + def test_from_json(self): + from acme.challenges import DNS01 + self.assertEqual(self.msg, DNS01.from_json(self.jmsg)) + + def test_from_json_hashable(self): + from acme.challenges import DNS01 + hash(DNS01.from_json(self.jmsg)) + + class HTTP01ResponseTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes diff --git a/acme/acme/dns_resolver.py b/acme/acme/dns_resolver.py new file mode 100644 index 000000000..f551c6095 --- /dev/null +++ b/acme/acme/dns_resolver.py @@ -0,0 +1,30 @@ +"""DNS Resolver for ACME client. +Required only for local validation of 'dns-01' challenges. +""" +import logging + +import dns.resolver +import dns.exception + +logger = logging.getLogger(__name__) + + +def txt_records_for_name(name): + """Resolve the name and return the TXT records. + + :param unicode name: Domain name being verified. + + :returns: A list of txt records, if empty the name could not be resolved + :rtype: list of unicode + + """ + try: + dns_response = dns.resolver.query(name, 'TXT') + except dns.resolver.NXDOMAIN as error: + return [] + except dns.exception.DNSException as error: + logger.error("Error resolving %s: %s", name, str(error)) + return [] + + return [txt_rec.decode("utf-8") for rdata in dns_response + for txt_rec in rdata.strings] diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py new file mode 100644 index 000000000..53fc0cc77 --- /dev/null +++ b/acme/acme/dns_resolver_test.py @@ -0,0 +1,53 @@ +"""Tests for acme.dns_resolver.""" +import unittest +import mock + +from acme import dns_resolver + +try: + import dns +except ImportError: # pragma: no cover + dns = None + + +def create_txt_response(name, txt_records): + """ + Returns an RRSet containing the 'txt_records' as the result of a DNS + query for 'name'. + + This takes advantage of the fact that an Answer object mostly behaves + like an RRset. + """ + return dns.rrset.from_text_list(name, 60, "IN", "TXT", txt_records) + + +class TxtRecordsForNameTest(unittest.TestCase): + + @mock.patch("acme.dns_resolver.dns.resolver.query") + def test_txt_records_for_name_with_single_response(self, mock_dns): + mock_dns.return_value = create_txt_response('name', ['response']) + self.assertEqual(['response'], + dns_resolver.txt_records_for_name('name')) + + @mock.patch("acme.dns_resolver.dns.resolver.query") + def test_txt_records_for_name_with_multiple_responses(self, mock_dns): + mock_dns.return_value = create_txt_response( + 'name', ['response1', 'response2']) + self.assertEqual(['response1', 'response2'], + dns_resolver.txt_records_for_name('name')) + + @mock.patch("acme.dns_resolver.dns.resolver.query") + def test_txt_records_for_name_domain_not_found(self, mock_dns): + mock_dns.side_effect = dns.resolver.NXDOMAIN + self.assertEquals([], dns_resolver.txt_records_for_name('name')) + + @mock.patch("acme.dns_resolver.dns.resolver.query") + def test_txt_records_for_name_domain_other_error(self, mock_dns): + mock_dns.side_effect = dns.exception.DNSException + self.assertEquals([], dns_resolver.txt_records_for_name('name')) + + def run(self, result=None): + if dns is None: # pragma: no cover + print(self, "... SKIPPING, no dnspython available") + return + super(TxtRecordsForNameTest, self).run(result) diff --git a/acme/setup.py b/acme/setup.py index ed133e128..94f78d4cd 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -35,6 +35,11 @@ if sys.version_info < (2, 7): else: install_requires.append('mock') +# dnspython 1.12 is required to support both Python 2 and Python 3. +dns_extras = [ + 'dnspython>=1.12', +] + dev_extras = [ 'nose', 'pep8', @@ -76,6 +81,7 @@ setup( include_package_data=True, install_requires=install_requires, extras_require={ + 'dns': dns_extras, 'dev': dev_extras, 'docs': docs_extras, }, diff --git a/certbot-apache/certbot_apache/constants.py b/certbot-apache/certbot_apache/constants.py index 9252814c4..ba545c613 100644 --- a/certbot-apache/certbot_apache/constants.py +++ b/certbot-apache/certbot_apache/constants.py @@ -2,7 +2,23 @@ import pkg_resources from certbot import util - +CLI_DEFAULTS_DEFAULT = dict( + server_root="/etc/apache2", + vhost_root="/etc/apache2/sites-available", + vhost_files="*", + version_cmd=['apache2ctl', '-v'], + define_cmd=['apache2ctl', '-t', '-D', 'DUMP_RUN_CFG'], + restart_cmd=['apache2ctl', 'graceful'], + conftest_cmd=['apache2ctl', 'configtest'], + enmod=None, + dismod=None, + le_vhost_ext="-le-ssl.conf", + handle_mods=False, + handle_sites=False, + challenge_location="/etc/apache2", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "certbot_apache", "options-ssl-apache.conf") +) CLI_DEFAULTS_DEBIAN = dict( server_root="/etc/apache2", vhost_root="/etc/apache2/sites-available", @@ -71,7 +87,25 @@ CLI_DEFAULTS_DARWIN = dict( MOD_SSL_CONF_SRC=pkg_resources.resource_filename( "certbot_apache", "options-ssl-apache.conf") ) +CLI_DEFAULTS_SUSE = dict( + server_root="/etc/apache2", + vhost_root="/etc/apache2/vhosts.d", + vhost_files="*.conf", + version_cmd=['apache2ctl', '-v'], + define_cmd=['apache2ctl', '-t', '-D', 'DUMP_RUN_CFG'], + restart_cmd=['apache2ctl', 'graceful'], + conftest_cmd=['apache2ctl', 'configtest'], + enmod="a2enmod", + dismod="a2dismod", + le_vhost_ext="-le-ssl.conf", + handle_mods=False, + handle_sites=False, + challenge_location="/etc/apache2/vhosts.d", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "certbot_apache", "options-ssl-apache.conf") +) CLI_DEFAULTS = { + "default": CLI_DEFAULTS_DEFAULT, "debian": CLI_DEFAULTS_DEBIAN, "ubuntu": CLI_DEFAULTS_DEBIAN, "centos": CLI_DEFAULTS_CENTOS, @@ -83,6 +117,8 @@ CLI_DEFAULTS = { "gentoo": CLI_DEFAULTS_GENTOO, "gentoo base system": CLI_DEFAULTS_GENTOO, "darwin": CLI_DEFAULTS_DARWIN, + "opensuse": CLI_DEFAULTS_SUSE, + "suse": CLI_DEFAULTS_SUSE, } """CLI defaults.""" @@ -115,13 +151,36 @@ HEADER_ARGS = {"Strict-Transport-Security": HSTS_ARGS, def os_constant(key): - """Get a constant value for operating system + """ + Get a constant value for operating system + :param key: name of cli constant :return: value of constant for active os """ + os_info = util.get_os_info() try: constants = CLI_DEFAULTS[os_info[0].lower()] except KeyError: - constants = CLI_DEFAULTS["debian"] + constants = os_like_constants() + if not constants: + constants = CLI_DEFAULTS["default"] return constants[key] + + +def os_like_constants(): + """ + Try to get constants for distribution with + similar layout and configuration, indicated by + /etc/os-release variable "LIKE" + + :returns: Constants dictionary + :rtype: `dict` + """ + + os_like = util.get_systemd_os_like() + if os_like: + for os_name in os_like: + if os_name in CLI_DEFAULTS.keys(): + return CLI_DEFAULTS[os_name] + return {} diff --git a/certbot-apache/certbot_apache/tests/constants_test.py b/certbot-apache/certbot_apache/tests/constants_test.py index c040030df..1c842aee9 100644 --- a/certbot-apache/certbot_apache/tests/constants_test.py +++ b/certbot-apache/certbot_apache/tests/constants_test.py @@ -25,3 +25,20 @@ class ConstantsTest(unittest.TestCase): os_info.return_value = ('Nonexistent Linux', '', '') self.assertEqual(constants.os_constant("vhost_root"), "/etc/apache2/sites-available") + + @mock.patch("certbot.util.get_os_info") + def test_get_default_constants(self, os_info): + os_info.return_value = ('Nonexistent Linux', '', '') + with mock.patch("certbot.util.get_systemd_os_like") as os_like: + # Get defaults + os_like.return_value = False + c_hm = constants.os_constant("handle_mods") + c_sr = constants.os_constant("server_root") + self.assertFalse(c_hm) + self.assertEqual(c_sr, "/etc/apache2") + # Use darwin as like test target + os_like.return_value = ["something", "nonexistent", "darwin"] + d_vr = constants.os_constant("vhost_root") + d_em = constants.os_constant("enmod") + self.assertFalse(d_em) + self.assertEqual(d_vr, "/etc/apache2/other") diff --git a/certbot/auth_handler.py b/certbot/auth_handler.py index f5557d604..a94734572 100644 --- a/certbot/auth_handler.py +++ b/certbot/auth_handler.py @@ -1,8 +1,8 @@ """ACME AuthHandler.""" -import itertools import logging import time +import six import zope.component from acme import challenges @@ -141,7 +141,7 @@ class AuthHandler(object): """ active_achalls = [] - for achall, resp in itertools.izip(achalls, resps): + for achall, resp in six.moves.zip(achalls, resps): # This line needs to be outside of the if block below to # ensure failed challenges are cleaned up correctly active_achalls.append(achall) @@ -472,7 +472,7 @@ def _report_failed_challs(failed_achalls): problems.setdefault(achall.error.typ, []).append(achall) reporter = zope.component.getUtility(interfaces.IReporter) - for achalls in problems.itervalues(): + for achalls in six.itervalues(problems): reporter.add_message( _generate_failed_chall_msg(achalls), reporter.MEDIUM_PRIORITY) diff --git a/certbot/cli.py b/certbot/cli.py index 5813af730..b01b0a7f1 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -343,8 +343,10 @@ class HelpfulArgumentParser(object): self.determine_verb() help1 = self.prescan_for_flag("-h", self.help_topics) help2 = self.prescan_for_flag("--help", self.help_topics) - assert max(True, "a") == "a", "Gravity changed direction" - self.help_arg = max(help1, help2) + if isinstance(help1, bool) and isinstance(help2, bool): + self.help_arg = help1 or help2 + else: + self.help_arg = help1 if isinstance(help1, str) else help2 if self.help_arg is True: # just --help with no topic; avoid argparse altogether print(usage) diff --git a/certbot/constants.py b/certbot/constants.py index fb278161d..1ddb9fedf 100644 --- a/certbot/constants.py +++ b/certbot/constants.py @@ -18,7 +18,7 @@ CLI_DEFAULTS = dict( os.path.join(os.environ.get("XDG_CONFIG_HOME", "~/.config"), "letsencrypt", "cli.ini"), ], - verbose_count=-(logging.INFO / 10), + verbose_count=-int(logging.INFO / 10), server="https://acme-v01.api.letsencrypt.org/directory", rsa_key_size=2048, rollback_checkpoints=1, diff --git a/certbot/plugins/disco.py b/certbot/plugins/disco.py index d88b871f6..59410757c 100644 --- a/certbot/plugins/disco.py +++ b/certbot/plugins/disco.py @@ -3,6 +3,7 @@ import collections import itertools import logging import pkg_resources +import six import zope.interface import zope.interface.verify @@ -194,12 +195,12 @@ class PluginsRegistry(collections.Mapping): def init(self, config): """Initialize all plugins in the registry.""" return [plugin_ep.init(config) for plugin_ep - in self._plugins.itervalues()] + in six.itervalues(self._plugins)] def filter(self, pred): """Filter plugins based on predicate.""" return type(self)(dict((name, plugin_ep) for name, plugin_ep - in self._plugins.iteritems() if pred(plugin_ep))) + in six.iteritems(self._plugins) if pred(plugin_ep))) def visible(self): """Filter plugins based on visibility.""" @@ -216,7 +217,7 @@ class PluginsRegistry(collections.Mapping): def prepare(self): """Prepare all plugins in the registry.""" - return [plugin_ep.prepare() for plugin_ep in self._plugins.itervalues()] + return [plugin_ep.prepare() for plugin_ep in six.itervalues(self._plugins)] def available(self): """Filter plugins based on availability.""" @@ -238,7 +239,7 @@ class PluginsRegistry(collections.Mapping): """ # use list instead of set because PluginEntryPoint is not hashable - candidates = [plugin_ep for plugin_ep in self._plugins.itervalues() + candidates = [plugin_ep for plugin_ep in six.itervalues(self._plugins) if plugin_ep.initialized and plugin_ep.init() is plugin] assert len(candidates) <= 1 if candidates: @@ -249,7 +250,7 @@ class PluginsRegistry(collections.Mapping): def __repr__(self): return "{0}({1})".format( self.__class__.__name__, ','.join( - repr(p_ep) for p_ep in self._plugins.itervalues())) + repr(p_ep) for p_ep in six.itervalues(self._plugins))) def __str__(self): if not self._plugins: diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py index 9b722aef4..6c7b822ab 100644 --- a/certbot/plugins/manual.py +++ b/certbot/plugins/manual.py @@ -10,6 +10,7 @@ import sys import tempfile import time +import six import zope.component import zope.interface @@ -187,7 +188,7 @@ s.serve_forever()" """ #answer = zope.component.getUtility(interfaces.IDisplay).notification( # message=message, height=25, pause=True) sys.stdout.write(message) - raw_input("Press ENTER to continue") + six.moves.input("Press ENTER to continue") def cleanup(self, achalls): # pylint: disable=missing-docstring,no-self-use,unused-argument diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index 21ebe9029..b16515d8f 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -84,7 +84,7 @@ def pick_plugin(config, default, plugins, question, ifaces): else: return plugin_ep.init() elif len(prepared) == 1: - plugin_ep = prepared.values()[0] + plugin_ep = list(prepared.values())[0] logger.debug("Single candidate plugin: %s", plugin_ep) if plugin_ep.misconfigured: return None diff --git a/certbot/tests/testdata/os-release b/certbot/tests/testdata/os-release index cd5297acf..15bc5fb3c 100644 --- a/certbot/tests/testdata/os-release +++ b/certbot/tests/testdata/os-release @@ -1,7 +1,7 @@ NAME="SystemdOS" VERSION="42.42.42 LTS, Unreal" ID=systemdos -ID_LIKE=debian +ID_LIKE="something nonexistent debian" VERSION_ID="42" HOME_URL="http://www.example.com/" SUPPORT_URL="http://help.example.com/" diff --git a/certbot/tests/util_test.py b/certbot/tests/util_test.py index 8e1b330ed..36676443a 100644 --- a/certbot/tests/util_test.py +++ b/certbot/tests/util_test.py @@ -359,6 +359,15 @@ class OsInfoTest(unittest.TestCase): with mock.patch('os.path.isfile', return_value=False): self.assertEqual(get_systemd_os_info(), ("", "")) + def test_systemd_os_release_like(self): + from certbot.util import get_systemd_os_like + + with mock.patch('os.path.isfile', return_value=True): + id_likes = get_systemd_os_like(test_util.vector_path( + "os-release")) + self.assertEqual(len(id_likes), 3) + self.assertTrue("debian" in id_likes) + @mock.patch("certbot.util.subprocess.Popen") def test_non_systemd_os_info(self, popen_mock): from certbot.util import (get_os_info, get_python_os_info, diff --git a/certbot/util.py b/certbot/util.py index 65aae59e2..998808be0 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -268,6 +268,19 @@ def get_systemd_os_info(filepath="/etc/os-release"): return (os_name, os_version) +def get_systemd_os_like(filepath="/etc/os-release"): + """ + Get a list of strings that indicate the distribution likeness to + other distributions. + + :param str filepath: File path of os-release file + :returns: List of distribution acronyms + :rtype: `list` of `str` + """ + + return _get_systemd_os_release_var("ID_LIKE", filepath).split(" ") + + def _get_systemd_os_release_var(varname, filepath="/etc/os-release"): """ Get single value from systemd /etc/os-release @@ -409,6 +422,9 @@ def enforce_domain_sanity(domain): else: raise errors.ConfigurationError(str(error_fmt).format(domain)) + if six.PY3: + domain = domain.decode('ascii') + # Remove trailing dot domain = domain[:-1] if domain.endswith('.') else domain diff --git a/docs/contributing.rst b/docs/contributing.rst index f1eec04df..a5b9b5688 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -6,9 +6,9 @@ Developer Guide :local: -.. _hacking: +.. _getting_started: -Hacking +Getting Started ======= Running a local copy of the client diff --git a/setup.py b/setup.py index 548314ad8..6d0909ea8 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,6 @@ install_requires = [ 'parsedatetime>=1.3', # Calendar.parseDT 'PyOpenSSL', 'pyrfc3339', - 'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280 'pytz', # For pkg_resources. >=1.0 so pip resolves it to a version cryptography # will tolerate; see #2599: @@ -52,6 +51,12 @@ install_requires = [ 'zope.interface', ] +# Debian squeeze support, cf. #280 +if sys.version_info[0] == 2: + install_requires.append('python2-pythondialog>=3.2.2rc1') +else: + install_requires.append('pythondialog>=3.2.2rc1') + # env markers in extras_require cause problems with older pip: #517 # Keep in sync with conditional_requirements.py. if sys.version_info < (2, 7): diff --git a/tox.ini b/tox.ini index 689bbf513..27979d9df 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,7 @@ envlist = py{26,33,34,35},cover,lint # packages installed separately to ensure that downstream deps problems # are detected, c.f. #1002 commands = - pip install -e acme[dev] + pip install -e acme[dns,dev] nosetests -v acme pip install -e .[dev] nosetests -v certbot @@ -38,23 +38,23 @@ deps = [testenv:py33] commands = - pip install -e acme[dev] + pip install -e acme[dns,dev] nosetests -v acme [testenv:py34] commands = - pip install -e acme[dev] + pip install -e acme[dns,dev] nosetests -v acme [testenv:py35] commands = - pip install -e acme[dev] + pip install -e acme[dns,dev] nosetests -v acme [testenv:cover] basepython = python2.7 commands = - pip install -e acme[dev] -e .[dev] -e certbot-apache -e certbot-nginx -e letshelp-certbot + pip install -e acme[dns,dev] -e .[dev] -e certbot-apache -e certbot-nginx -e letshelp-certbot ./tox.cover.sh [testenv:lint] @@ -64,7 +64,7 @@ basepython = python2.7 # duplicate code checking; if one of the commands fails, others will # continue, but tox return code will reflect previous error commands = - pip install -q -e acme[dev] -e .[dev] -e certbot-apache -e certbot-nginx -e certbot-compatibility-test -e letshelp-certbot + pip install -q -e acme[dns,dev] -e .[dev] -e certbot-apache -e certbot-nginx -e certbot-compatibility-test -e letshelp-certbot ./pep8.travis.sh pylint --reports=n --rcfile=.pylintrc certbot pylint --reports=n --rcfile=acme/.pylintrc acme/acme