diff --git a/AUTHORS.md b/AUTHORS.md index 6b6b5d118..0cedcbd19 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -237,6 +237,7 @@ Authors * [Spencer Bliven](https://github.com/sbliven) * [Stacey Sheldon](https://github.com/solidgoldbomb) * [Stavros Korokithakis](https://github.com/skorokithakis) +* [Ștefan Talpalaru](https://github.com/stefantalpalaru) * [Stefan Weil](https://github.com/stweil) * [Steve Desmond](https://github.com/stevedesmond-ca) * [sydneyli](https://github.com/sydneyli) diff --git a/certbot-ci/certbot_integration_tests/utils/misc.py b/certbot-ci/certbot_integration_tests/utils/misc.py index 9d1676c60..38c2e60a8 100644 --- a/certbot-ci/certbot_integration_tests/utils/misc.py +++ b/certbot-ci/certbot_integration_tests/utils/misc.py @@ -279,16 +279,17 @@ def load_sample_data_path(workspace): shutil.copytree(original, copied, symlinks=True) if os.name == 'nt': - # Fix the symlinks on Windows since GIT is not creating them upon checkout + # Fix the symlinks on Windows if GIT is not configured to create them upon checkout for lineage in ['a.encryption-example.com', 'b.encryption-example.com']: current_live = os.path.join(copied, 'live', lineage) for name in os.listdir(current_live): if name != 'README': current_file = os.path.join(current_live, name) - with open(current_file) as file_h: - src = file_h.read() - os.unlink(current_file) - os.symlink(os.path.join(current_live, src), current_file) + if not os.path.islink(current_file): + with open(current_file) as file_h: + src = file_h.read() + os.unlink(current_file) + os.symlink(os.path.join(current_live, src), current_file) return copied diff --git a/certbot-dns-linode/certbot_dns_linode/__init__.py b/certbot-dns-linode/certbot_dns_linode/__init__.py index 107781a13..4bfd95573 100644 --- a/certbot-dns-linode/certbot_dns_linode/__init__.py +++ b/certbot-dns-linode/certbot_dns_linode/__init__.py @@ -14,10 +14,10 @@ Named Arguments DNS to propagate before asking the ACME server to verify the DNS record. - (Default: 1200 because Linode - updates its first DNS every 15 - minutes and we allow 5 more minutes - for the update to reach the other 5 + (Default: 120 because Linode + updates its first DNS every 60 + seconds and we allow 60 more seconds + for the update to reach other 5 servers) ========================================== =================================== @@ -80,15 +80,15 @@ Examples -d www.example.com .. code-block:: bash - :caption: To acquire a certificate for ``example.com``, waiting 1000 seconds - for DNS propagation (Linode updates its first DNS every 15 minutes - and we allow some extra time for the update to reach the other 5 + :caption: To acquire a certificate for ``example.com``, waiting 120 seconds + for DNS propagation (Linode updates its first DNS every minute + and we allow some extra time for the update to reach other 5 servers) certbot certonly \\ --dns-linode \\ --dns-linode-credentials ~/.secrets/certbot/linode.ini \\ - --dns-linode-propagation-seconds 1000 \\ + --dns-linode-propagation-seconds 120 \\ -d example.com """ diff --git a/certbot-dns-linode/certbot_dns_linode/_internal/dns_linode.py b/certbot-dns-linode/certbot_dns_linode/_internal/dns_linode.py index f7b3ec3d4..f9450c02c 100644 --- a/certbot-dns-linode/certbot_dns_linode/_internal/dns_linode.py +++ b/certbot-dns-linode/certbot_dns_linode/_internal/dns_linode.py @@ -32,7 +32,7 @@ class Authenticator(dns_common.DNSAuthenticator): @classmethod def add_parser_arguments(cls, add): # pylint: disable=arguments-differ - super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=1200) + super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=120) add('credentials', help='Linode credentials INI file.') def more_info(self): # pylint: disable=missing-function-docstring diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index c127d83a6..58eb03984 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -6,11 +6,14 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). ### Added -* +* Third-party plugins can be used without prefix (`plugin_name` instead of `dist_name:plugin_name`): + this concerns the plugin name, CLI flags, and keys in credential files. + The prefixed form is still supported but is deprecated, and will be removed in a future release. ### Changed -* +* The Linode DNS plugin now waits 120 seconds for DNS propagation, instead of 1200, + due to https://www.linode.com/blog/linode/linode-turns-17/ ### Fixed diff --git a/certbot/certbot/_internal/plugins/disco.py b/certbot/certbot/_internal/plugins/disco.py index 4cce895d8..4538e8123 100644 --- a/certbot/certbot/_internal/plugins/disco.py +++ b/certbot/certbot/_internal/plugins/disco.py @@ -23,47 +23,58 @@ except ImportError: # pragma: no cover logger = logging.getLogger(__name__) +PREFIX_FREE_DISTRIBUTIONS = [ + "certbot", + "certbot-apache", + "certbot-dns-cloudflare", + "certbot-dns-cloudxns", + "certbot-dns-digitalocean", + "certbot-dns-dnsimple", + "certbot-dns-dnsmadeeasy", + "certbot-dns-gehirn", + "certbot-dns-google", + "certbot-dns-linode", + "certbot-dns-luadns", + "certbot-dns-nsone", + "certbot-dns-ovh", + "certbot-dns-rfc2136", + "certbot-dns-route53", + "certbot-dns-sakuracloud", + "certbot-nginx", +] +"""Distributions for which prefix will be omitted.""" + class PluginEntryPoint(object): """Plugin entry point.""" - PREFIX_FREE_DISTRIBUTIONS = [ - "certbot", - "certbot-apache", - "certbot-dns-cloudflare", - "certbot-dns-cloudxns", - "certbot-dns-digitalocean", - "certbot-dns-dnsimple", - "certbot-dns-dnsmadeeasy", - "certbot-dns-gehirn", - "certbot-dns-google", - "certbot-dns-linode", - "certbot-dns-luadns", - "certbot-dns-nsone", - "certbot-dns-ovh", - "certbot-dns-rfc2136", - "certbot-dns-route53", - "certbot-dns-sakuracloud", - "certbot-nginx", - ] - """Distributions for which prefix will be omitted.""" - # this object is mutable, don't allow it to be hashed! __hash__ = None # type: ignore - def __init__(self, entry_point): - self.name = self.entry_point_to_plugin_name(entry_point) + def __init__(self, entry_point, with_prefix=False): + self.name = self.entry_point_to_plugin_name(entry_point, with_prefix) self.plugin_cls = entry_point.load() self.entry_point = entry_point + self.warning_message = None self._initialized = None self._prepared = None + self._hidden = False + self._long_description = None + + def check_name(self, name): + """Check if the name refers to this plugin.""" + if name == self.name: + if self.warning_message: + logger.warning(self.warning_message) + return True + return False @classmethod - def entry_point_to_plugin_name(cls, entry_point): + def entry_point_to_plugin_name(cls, entry_point, with_prefix): """Unique plugin name for an ``entry_point``""" - if entry_point.dist.key in cls.PREFIX_FREE_DISTRIBUTIONS: - return entry_point.name - return entry_point.dist.key + ":" + entry_point.name + if with_prefix: + return entry_point.dist.key + ":" + entry_point.name + return entry_point.name @property def description(self): @@ -78,15 +89,25 @@ class PluginEntryPoint(object): @property def long_description(self): """Long description of the plugin.""" + if self._long_description: + return self._long_description try: return self.plugin_cls.long_description except AttributeError: return self.description + @long_description.setter + def long_description(self, description): + self._long_description = description + @property def hidden(self): """Should this plugin be hidden from UI?""" - return getattr(self.plugin_cls, "hidden", False) + return self._hidden or getattr(self.plugin_cls, "hidden", False) + + @hidden.setter + def hidden(self, hide): + self._hidden = hide def ifaces(self, *ifaces_groups): """Does plugin implements specified interface groups?""" @@ -212,16 +233,34 @@ class PluginsRegistry(Mapping): pkg_resources.iter_entry_points( constants.OLD_SETUPTOOLS_PLUGINS_ENTRY_POINT),) for entry_point in entry_points: - plugin_ep = PluginEntryPoint(entry_point) - assert plugin_ep.name not in plugins, ( - "PREFIX_FREE_DISTRIBUTIONS messed up") - if interfaces.IPluginFactory.providedBy(plugin_ep.plugin_cls): - plugins[plugin_ep.name] = plugin_ep - else: # pragma: no cover - logger.warning( - "%r does not provide IPluginFactory, skipping", plugin_ep) + plugin_ep = cls._load_entry_point(entry_point, plugins, with_prefix=False) + if entry_point.dist.key not in PREFIX_FREE_DISTRIBUTIONS: + prefixed_plugin_ep = cls._load_entry_point(entry_point, plugins, with_prefix=True) + prefixed_plugin_ep.hidden = True + message = ( + "Plugin legacy name {0} may be removed in a future version. " + "Please use {1} instead.").format(prefixed_plugin_ep.name, plugin_ep.name) + prefixed_plugin_ep.warning_message = message + prefixed_plugin_ep.long_description = "(WARNING: {0}) {1}".format( + message, prefixed_plugin_ep.long_description) + return cls(plugins) + @classmethod + def _load_entry_point(cls, entry_point, plugins, with_prefix): + plugin_ep = PluginEntryPoint(entry_point, with_prefix) + if plugin_ep.name in plugins: + other_ep = plugins[plugin_ep.name] + raise Exception("Duplicate plugin name {0} from {1} and {2}.".format( + plugin_ep.name, plugin_ep.entry_point.dist.key, other_ep.entry_point.dist.key)) + if interfaces.IPluginFactory.providedBy(plugin_ep.plugin_cls): + plugins[plugin_ep.name] = plugin_ep + else: # pragma: no cover + logger.warning( + "%r does not provide IPluginFactory, skipping", plugin_ep) + + return plugin_ep + def __getitem__(self, name): return self._plugins[name] diff --git a/certbot/certbot/_internal/plugins/selection.py b/certbot/certbot/_internal/plugins/selection.py index 53cef3969..0b04791c6 100644 --- a/certbot/certbot/_internal/plugins/selection.py +++ b/certbot/certbot/_internal/plugins/selection.py @@ -38,6 +38,7 @@ def pick_authenticator( return pick_plugin( config, default, plugins, question, (interfaces.IAuthenticator,)) + def get_unprepared_installer(config, plugins): """ Get an unprepared interfaces.IInstaller object. @@ -53,7 +54,7 @@ def get_unprepared_installer(config, plugins): _, req_inst = cli_plugin_requests(config) if not req_inst: return None - installers = plugins.filter(lambda p_ep: p_ep.name == req_inst) + installers = plugins.filter(lambda p_ep: p_ep.check_name(req_inst)) installers.init(config) installers = installers.verify((interfaces.IInstaller,)) if len(installers) > 1: @@ -67,6 +68,7 @@ def get_unprepared_installer(config, plugins): raise errors.PluginSelectionError( "Could not select or initialize the requested installer %s." % req_inst) + def pick_plugin(config, default, plugins, question, ifaces): """Pick plugin. @@ -84,7 +86,7 @@ def pick_plugin(config, default, plugins, question, ifaces): """ if default is not None: # throw more UX-friendly error if default not in plugins - filtered = plugins.filter(lambda p_ep: p_ep.name == default) + filtered = plugins.filter(lambda p_ep: p_ep.check_name(default)) else: if config.noninteractive_mode: # it's really bad to auto-select the single available plugin in diff --git a/certbot/docs/using.rst b/certbot/docs/using.rst index 0a0b6d1a2..0423b1fec 100644 --- a/certbot/docs/using.rst +++ b/certbot/docs/using.rst @@ -281,6 +281,7 @@ proxmox_ N Y Install certificates in Proxmox Virtualization serv dns-standalone_ Y N Obtain certificates via an integrated DNS server dns-ispconfig_ Y N DNS Authentication using ISPConfig as DNS server dns-clouddns_ Y N DNS Authentication using CloudDNS API +dns-inwx Y Y DNS Authentication for INWX through the XML API ================== ==== ==== =============================================================== .. _haproxy: https://github.com/greenhost/certbot-haproxy @@ -293,6 +294,7 @@ dns-clouddns_ Y N DNS Authentication using CloudDNS API .. _dns-standalone: https://github.com/siilike/certbot-dns-standalone .. _dns-ispconfig: https://github.com/m42e/certbot-dns-ispconfig .. _dns-clouddns: https://github.com/vshosting/certbot-dns-clouddns +.. _dns-inwx: https://github.com/oGGy990/certbot-dns-inwx/ If you're interested, you can also :ref:`write your own plugin `. diff --git a/certbot/setup.py b/certbot/setup.py index b2e0837d3..efa7e3c28 100644 --- a/certbot/setup.py +++ b/certbot/setup.py @@ -83,7 +83,6 @@ elif sys.version_info < (3,3): dev_extras = [ 'coverage', - 'ipdb', 'pytest', 'pytest-cov', 'pytest-xdist', @@ -94,6 +93,7 @@ dev_extras = [ dev3_extras = [ 'astroid', + 'ipdb', 'mypy', 'pylint', ] diff --git a/certbot/tests/plugins/disco_test.py b/certbot/tests/plugins/disco_test.py index 5a0a392b0..ed13544de 100644 --- a/certbot/tests/plugins/disco_test.py +++ b/certbot/tests/plugins/disco_test.py @@ -5,7 +5,7 @@ import unittest try: import mock -except ImportError: # pragma: no cover +except ImportError: # pragma: no cover from unittest import mock import pkg_resources import six @@ -13,6 +13,7 @@ import zope.interface from certbot import errors from certbot import interfaces +from certbot._internal.plugins import null from certbot._internal.plugins import standalone from certbot._internal.plugins import webroot @@ -44,7 +45,22 @@ class PluginEntryPointTest(unittest.TestCase): from certbot._internal.plugins.disco import PluginEntryPoint self.plugin_ep = PluginEntryPoint(EP_SA) - def test_entry_point_to_plugin_name(self): + def test_entry_point_to_plugin_name_not_prefixed(self): + from certbot._internal.plugins.disco import PluginEntryPoint + + names = { + self.ep1: "ep1", + self.ep1prim: "ep1", + self.ep2: "ep2", + self.ep3: "ep3", + EP_SA: "sa", + } + + for entry_point, name in six.iteritems(names): + self.assertEqual( + name, PluginEntryPoint.entry_point_to_plugin_name(entry_point, with_prefix=False)) + + def test_entry_point_to_plugin_name_prefixed(self): from certbot._internal.plugins.disco import PluginEntryPoint names = { @@ -52,12 +68,11 @@ class PluginEntryPointTest(unittest.TestCase): self.ep1prim: "p2:ep1", self.ep2: "p2:ep2", self.ep3: "p3:ep3", - EP_SA: "sa", } for entry_point, name in six.iteritems(names): self.assertEqual( - name, PluginEntryPoint.entry_point_to_plugin_name(entry_point)) + name, PluginEntryPoint.entry_point_to_plugin_name(entry_point, with_prefix=True)) def test_description(self): self.assertTrue("temporary webserver" in self.plugin_ep.description) @@ -197,17 +212,28 @@ class PluginsRegistryTest(unittest.TestCase): self.plugin_ep.__hash__.side_effect = TypeError self.plugins = {self.plugin_ep.name: self.plugin_ep} self.reg = self._create_new_registry(self.plugins) + self.ep1 = pkg_resources.EntryPoint( + "ep1", "p1.ep1", dist=mock.MagicMock(key="p1")) def test_find_all(self): from certbot._internal.plugins.disco import PluginsRegistry with mock.patch("certbot._internal.plugins.disco.pkg_resources") as mock_pkg: - mock_pkg.iter_entry_points.side_effect = [iter([EP_SA]), - iter([EP_WR])] - plugins = PluginsRegistry.find_all() + mock_pkg.iter_entry_points.side_effect = [ + iter([EP_SA]), iter([EP_WR, self.ep1]) + ] + with mock.patch.object(pkg_resources.EntryPoint, 'load') as mock_load: + mock_load.side_effect = [ + standalone.Authenticator, webroot.Authenticator, + null.Installer, null.Installer] + plugins = PluginsRegistry.find_all() self.assertTrue(plugins["sa"].plugin_cls is standalone.Authenticator) self.assertTrue(plugins["sa"].entry_point is EP_SA) self.assertTrue(plugins["wr"].plugin_cls is webroot.Authenticator) self.assertTrue(plugins["wr"].entry_point is EP_WR) + self.assertTrue(plugins["ep1"].plugin_cls is null.Installer) + self.assertTrue(plugins["ep1"].entry_point is self.ep1) + self.assertTrue(plugins["p1:ep1"].plugin_cls is null.Installer) + self.assertTrue(plugins["p1:ep1"].entry_point is self.ep1) def test_getitem(self): self.assertEqual(self.plugin_ep, self.reg["mock"]) diff --git a/certbot/tests/plugins/selection_test.py b/certbot/tests/plugins/selection_test.py index e5e6db031..a5de99e60 100644 --- a/certbot/tests/plugins/selection_test.py +++ b/certbot/tests/plugins/selection_test.py @@ -174,6 +174,7 @@ class ChoosePluginTest(unittest.TestCase): self.assertTrue("default" in mock_util().menu.call_args[1]) + class GetUnpreparedInstallerTest(test_util.ConfigTestCase): """Tests for certbot._internal.plugins.selection.get_unprepared_installer.""" @@ -181,10 +182,10 @@ class GetUnpreparedInstallerTest(test_util.ConfigTestCase): super(GetUnpreparedInstallerTest, self).setUp() self.mock_apache_fail_ep = mock.Mock( description_with_name="afail") - self.mock_apache_fail_ep.name = "afail" + self.mock_apache_fail_ep.check_name = lambda name: name == "afail" self.mock_apache_ep = mock.Mock( description_with_name="apache") - self.mock_apache_ep.name = "apache" + self.mock_apache_ep.check_name = lambda name: name == "apache" self.mock_apache_plugin = mock.MagicMock() self.mock_apache_ep.init.return_value = self.mock_apache_plugin self.plugins = PluginsRegistry({ @@ -213,7 +214,7 @@ class GetUnpreparedInstallerTest(test_util.ConfigTestCase): def test_multiple_installers_returned(self): self.config.configurator = "apache" # Two plugins with the same name - self.mock_apache_fail_ep.name = "apache" + self.mock_apache_fail_ep.check_name = lambda name: name == "apache" self.assertRaises(errors.PluginSelectionError, self._call) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 5637aa968..edd8c8963 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -15,6 +15,7 @@ description: | - Help you revoke the certificate if that ever becomes necessary. confinement: classic base: core20 +grade: stable adopt-info: certbot apps: diff --git a/tools/dev_constraints.txt b/tools/dev_constraints.txt index 6e692841b..31ca577d5 100644 --- a/tools/dev_constraints.txt +++ b/tools/dev_constraints.txt @@ -10,6 +10,7 @@ asn1crypto==0.22.0 astroid==2.3.3 attrs==17.3.0 Babel==2.5.1 +backcall==0.2.0 backports.functools-lru-cache==1.5 backports.shutil-get-terminal-size==1.0.0 backports.ssl-match-hostname==3.7.0.1 @@ -40,9 +41,10 @@ httplib2==0.10.3 imagesize==0.7.1 importlib-metadata==0.23 ipdb==0.12.3 -ipython==5.8.0 +ipython==7.9.0 ipython-genutils==0.2.0 isort==4.3.21 +jedi==0.17.1 Jinja2==2.9.6 jmespath==0.9.4 josepy==1.1.0 @@ -59,13 +61,14 @@ ndg-httpsclient==0.3.2 oauth2client==4.0.0 packaging==19.2 paramiko==2.4.2 +parso==0.7.0 pathlib2==2.3.5 pexpect==4.7.0 pickleshare==0.7.5 pkginfo==1.4.2 pluggy==0.13.0 ply==3.4 -prompt-toolkit==1.0.18 +prompt-toolkit==2.0.10 ptyprocess==0.6.0 py==1.8.0 pyasn1==0.1.9 diff --git a/tools/oldest_constraints.txt b/tools/oldest_constraints.txt index ff4b8361a..5145e3ddf 100644 --- a/tools/oldest_constraints.txt +++ b/tools/oldest_constraints.txt @@ -63,3 +63,10 @@ dns-lexicon==2.2.1 # Tracking at https://github.com/certbot/certbot/issues/6473 boto3==1.4.7 botocore==1.7.41 + +# Old certbot[dev] constraints +# Old versions of certbot[dev] required ipdb and our normally pinned version of +# ipython which ipdb depends on doesn't support Python 2 so we pin an older +# version here to keep tests working while we have Python 2 support. +ipython==5.8.0 +prompt-toolkit==1.0.18