Merge branch 'master' into snap-remotebuild

This commit is contained in:
Adrien Ferrand 2020-07-21 01:02:41 +02:00
commit 6b7dd05126
14 changed files with 153 additions and 67 deletions

View file

@ -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)

View file

@ -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

View file

@ -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
"""

View file

@ -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

View file

@ -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

View file

@ -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]

View file

@ -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

View file

@ -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 <dev-plugin>`.

View file

@ -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',
]

View file

@ -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"])

View file

@ -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)

View file

@ -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:

View file

@ -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

View file

@ -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