mirror of
https://github.com/certbot/certbot.git
synced 2026-07-26 07:39:52 +02:00
This PR is the second part of #6497 to ease the integration, following the new plan propose by @bmw here: #6497 (comment) This PR creates the module certbot.compat.os, that delegates everything to os, and that will be the safeguard against problematic methods of the standard module. On top of that, a quality check wrapper is called in the lint tox environment. This wrapper calls pylint and ensures that standard os module is no used directly in the certbot codebase. Finally local oldest requirements are updated to ensure that tests will take the new logic when running. * Add executable permissions * Add the delegate certbot.compat.os module, add check coding style to enforce usage of certbot.compat.os instead of standard os * Load certbot.compat.os instead of os * Move existing compat test * Update local oldest requirements * Import sys * Update account_test.py * Update os.py * Update os.py * Update local oldest requirements * Implement the new linter_plugin * Fix local oldest for nginx * Remove check coding style * Update linter_plugin.py * Add several comments * Update the setup.py * Add documentation * Update acme dependencies * Update certbot/compat/os.py * Update docs/contributing.rst * Update linter_plugin.py * Handle os.path. Simplify checker. * Add a comment to a reference implementation * Update changelog * Fix module registering * Update docs/contributing.rst * Update config and changelog
340 lines
14 KiB
Python
340 lines
14 KiB
Python
"""Decide which plugins to use for authentication & installation"""
|
|
from __future__ import print_function
|
|
|
|
import logging
|
|
|
|
import six
|
|
import zope.component
|
|
|
|
from certbot import errors
|
|
from certbot import interfaces
|
|
from certbot.compat import os
|
|
from certbot.display import util as display_util
|
|
|
|
logger = logging.getLogger(__name__)
|
|
z_util = zope.component.getUtility
|
|
|
|
def pick_configurator(
|
|
config, default, plugins,
|
|
question="How would you like to authenticate and install "
|
|
"certificates?"):
|
|
"""Pick configurator plugin."""
|
|
return pick_plugin(
|
|
config, default, plugins, question,
|
|
(interfaces.IAuthenticator, interfaces.IInstaller))
|
|
|
|
|
|
def pick_installer(config, default, plugins,
|
|
question="How would you like to install certificates?"):
|
|
"""Pick installer plugin."""
|
|
return pick_plugin(
|
|
config, default, plugins, question, (interfaces.IInstaller,))
|
|
|
|
|
|
def pick_authenticator(
|
|
config, default, plugins, question="How would you "
|
|
"like to authenticate with the ACME CA?"):
|
|
"""Pick authentication plugin."""
|
|
return pick_plugin(
|
|
config, default, plugins, question, (interfaces.IAuthenticator,))
|
|
|
|
def get_unprepared_installer(config, plugins):
|
|
"""
|
|
Get an unprepared interfaces.IInstaller object.
|
|
|
|
:param certbot.interfaces.IConfig config: Configuration
|
|
:param certbot.plugins.disco.PluginsRegistry plugins:
|
|
All plugins registered as entry points.
|
|
|
|
:returns: Unprepared installer plugin or None
|
|
:rtype: IPlugin or None
|
|
"""
|
|
|
|
_, req_inst = cli_plugin_requests(config)
|
|
if not req_inst:
|
|
return None
|
|
installers = plugins.filter(lambda p_ep: p_ep.name == req_inst)
|
|
installers.init(config)
|
|
installers = installers.verify((interfaces.IInstaller,))
|
|
if len(installers) > 1:
|
|
raise errors.PluginSelectionError(
|
|
"Found multiple installers with the name %s, Certbot is unable to "
|
|
"determine which one to use. Skipping." % req_inst)
|
|
if installers:
|
|
inst = list(installers.values())[0]
|
|
logger.debug("Selecting plugin: %s", inst)
|
|
return inst.init(config)
|
|
else:
|
|
raise errors.PluginSelectionError(
|
|
"Could not select or initialize the requested installer %s." % req_inst)
|
|
|
|
def pick_plugin(config, default, plugins, question, ifaces):
|
|
"""Pick plugin.
|
|
|
|
:param certbot.interfaces.IConfig: Configuration
|
|
:param str default: Plugin name supplied by user or ``None``.
|
|
:param certbot.plugins.disco.PluginsRegistry plugins:
|
|
All plugins registered as entry points.
|
|
:param str question: Question to be presented to the user in case
|
|
multiple candidates are found.
|
|
:param list ifaces: Interfaces that plugins must provide.
|
|
|
|
:returns: Initialized plugin.
|
|
:rtype: IPlugin
|
|
|
|
"""
|
|
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)
|
|
else:
|
|
if config.noninteractive_mode:
|
|
# it's really bad to auto-select the single available plugin in
|
|
# non-interactive mode, because an update could later add a second
|
|
# available plugin
|
|
raise errors.MissingCommandlineFlag(
|
|
"Missing command line flags. For non-interactive execution, "
|
|
"you will need to specify a plugin on the command line. Run "
|
|
"with '--help plugins' to see a list of options, and see "
|
|
"https://eff.org/letsencrypt-plugins for more detail on what "
|
|
"the plugins do and how to use them.")
|
|
|
|
filtered = plugins.visible().ifaces(ifaces)
|
|
|
|
filtered.init(config)
|
|
verified = filtered.verify(ifaces)
|
|
verified.prepare()
|
|
prepared = verified.available()
|
|
|
|
if len(prepared) > 1:
|
|
logger.debug("Multiple candidate plugins: %s", prepared)
|
|
plugin_ep = choose_plugin(list(six.itervalues(prepared)), question)
|
|
if plugin_ep is None:
|
|
return None
|
|
return plugin_ep.init()
|
|
elif len(prepared) == 1:
|
|
plugin_ep = list(prepared.values())[0]
|
|
logger.debug("Single candidate plugin: %s", plugin_ep)
|
|
if plugin_ep.misconfigured:
|
|
return None
|
|
return plugin_ep.init()
|
|
else:
|
|
logger.debug("No candidate plugin")
|
|
return None
|
|
|
|
|
|
def choose_plugin(prepared, question):
|
|
"""Allow the user to choose their plugin.
|
|
|
|
:param list prepared: List of `~.PluginEntryPoint`.
|
|
:param str question: Question to be presented to the user.
|
|
|
|
:returns: Plugin entry point chosen by the user.
|
|
:rtype: `~.PluginEntryPoint`
|
|
|
|
"""
|
|
opts = [plugin_ep.description_with_name +
|
|
(" [Misconfigured]" if plugin_ep.misconfigured else "")
|
|
for plugin_ep in prepared]
|
|
names = set(plugin_ep.name for plugin_ep in prepared)
|
|
|
|
while True:
|
|
disp = z_util(interfaces.IDisplay)
|
|
if "CERTBOT_AUTO" in os.environ and names == set(("apache", "nginx")):
|
|
# The possibility of being offered exactly apache and nginx here
|
|
# is new interactivity brought by https://github.com/certbot/certbot/issues/4079,
|
|
# so set apache as a default for those kinds of non-interactive use
|
|
# (the user will get a warning to set --non-interactive or --force-interactive)
|
|
apache_idx = [n for n, p in enumerate(prepared) if p.name == "apache"][0]
|
|
code, index = disp.menu(question, opts, default=apache_idx)
|
|
else:
|
|
code, index = disp.menu(question, opts, force_interactive=True)
|
|
|
|
if code == display_util.OK:
|
|
plugin_ep = prepared[index]
|
|
if plugin_ep.misconfigured:
|
|
z_util(interfaces.IDisplay).notification(
|
|
"The selected plugin encountered an error while parsing "
|
|
"your server configuration and cannot be used. The error "
|
|
"was:\n\n{0}".format(plugin_ep.prepare()), pause=False)
|
|
else:
|
|
return plugin_ep
|
|
else:
|
|
return None
|
|
|
|
noninstaller_plugins = ["webroot", "manual", "standalone", "dns-cloudflare", "dns-cloudxns",
|
|
"dns-digitalocean", "dns-dnsimple", "dns-dnsmadeeasy", "dns-gehirn",
|
|
"dns-google", "dns-linode", "dns-luadns", "dns-nsone", "dns-ovh",
|
|
"dns-rfc2136", "dns-route53", "dns-sakuracloud"]
|
|
|
|
def record_chosen_plugins(config, plugins, auth, inst):
|
|
"Update the config entries to reflect the plugins we actually selected."
|
|
config.authenticator = plugins.find_init(auth).name if auth else None
|
|
config.installer = plugins.find_init(inst).name if inst else None
|
|
logger.info("Plugins selected: Authenticator %s, Installer %s",
|
|
config.authenticator, config.installer)
|
|
|
|
|
|
def choose_configurator_plugins(config, plugins, verb):
|
|
# pylint: disable=too-many-branches
|
|
"""
|
|
Figure out which configurator we're going to use, modifies
|
|
config.authenticator and config.installer strings to reflect that choice if
|
|
necessary.
|
|
|
|
:raises errors.PluginSelectionError if there was a problem
|
|
|
|
:returns: (an `IAuthenticator` or None, an `IInstaller` or None)
|
|
:rtype: tuple
|
|
"""
|
|
|
|
req_auth, req_inst = cli_plugin_requests(config)
|
|
installer_question = None
|
|
|
|
if verb == "enhance":
|
|
installer_question = ("Which installer would you like to use to "
|
|
"configure the selected enhancements?")
|
|
|
|
# Which plugins do we need?
|
|
if verb == "run":
|
|
need_inst = need_auth = True
|
|
from certbot.cli import cli_command
|
|
if req_auth in noninstaller_plugins and not req_inst:
|
|
msg = ('With the {0} plugin, you probably want to use the "certonly" command, eg:{1}'
|
|
'{1} {2} certonly --{0}{1}{1}'
|
|
'(Alternatively, add a --installer flag. See https://eff.org/letsencrypt-plugins'
|
|
'{1} and "--help plugins" for more information.)'.format(
|
|
req_auth, os.linesep, cli_command))
|
|
|
|
raise errors.MissingCommandlineFlag(msg)
|
|
else:
|
|
need_inst = need_auth = False
|
|
if verb == "certonly":
|
|
need_auth = True
|
|
if verb == "install" or verb == "enhance":
|
|
need_inst = True
|
|
if config.authenticator:
|
|
logger.warning("Specifying an authenticator doesn't make sense when "
|
|
"running Certbot with verb \"%s\"", verb)
|
|
# Try to meet the user's request and/or ask them to pick plugins
|
|
authenticator = installer = None
|
|
if verb == "run" and req_auth == req_inst:
|
|
# Unless the user has explicitly asked for different auth/install,
|
|
# only consider offering a single choice
|
|
authenticator = installer = pick_configurator(config, req_inst, plugins)
|
|
else:
|
|
if need_inst or req_inst:
|
|
installer = pick_installer(config, req_inst, plugins, installer_question)
|
|
if need_auth:
|
|
authenticator = pick_authenticator(config, req_auth, plugins)
|
|
logger.debug("Selected authenticator %s and installer %s", authenticator, installer)
|
|
|
|
# Report on any failures
|
|
if need_inst and not installer:
|
|
diagnose_configurator_problem("installer", req_inst, plugins)
|
|
if need_auth and not authenticator:
|
|
diagnose_configurator_problem("authenticator", req_auth, plugins)
|
|
|
|
record_chosen_plugins(config, plugins, authenticator, installer)
|
|
return installer, authenticator
|
|
|
|
|
|
def set_configurator(previously, now):
|
|
"""
|
|
Setting configurators multiple ways is okay, as long as they all agree
|
|
:param str previously: previously identified request for the installer/authenticator
|
|
:param str requested: the request currently being processed
|
|
"""
|
|
if not now:
|
|
# we're not actually setting anything
|
|
return previously
|
|
if previously:
|
|
if previously != now:
|
|
msg = "Too many flags setting configurators/installers/authenticators {0} -> {1}"
|
|
raise errors.PluginSelectionError(msg.format(repr(previously), repr(now)))
|
|
return now
|
|
|
|
|
|
def cli_plugin_requests(config): # pylint: disable=too-many-branches
|
|
"""
|
|
Figure out which plugins the user requested with CLI and config options
|
|
|
|
:returns: (requested authenticator string or None, requested installer string or None)
|
|
:rtype: tuple
|
|
"""
|
|
req_inst = req_auth = config.configurator
|
|
req_inst = set_configurator(req_inst, config.installer)
|
|
req_auth = set_configurator(req_auth, config.authenticator)
|
|
|
|
if config.nginx:
|
|
req_inst = set_configurator(req_inst, "nginx")
|
|
req_auth = set_configurator(req_auth, "nginx")
|
|
if config.apache:
|
|
req_inst = set_configurator(req_inst, "apache")
|
|
req_auth = set_configurator(req_auth, "apache")
|
|
if config.standalone:
|
|
req_auth = set_configurator(req_auth, "standalone")
|
|
if config.webroot:
|
|
req_auth = set_configurator(req_auth, "webroot")
|
|
if config.manual:
|
|
req_auth = set_configurator(req_auth, "manual")
|
|
if config.dns_cloudflare:
|
|
req_auth = set_configurator(req_auth, "dns-cloudflare")
|
|
if config.dns_cloudxns:
|
|
req_auth = set_configurator(req_auth, "dns-cloudxns")
|
|
if config.dns_digitalocean:
|
|
req_auth = set_configurator(req_auth, "dns-digitalocean")
|
|
if config.dns_dnsimple:
|
|
req_auth = set_configurator(req_auth, "dns-dnsimple")
|
|
if config.dns_dnsmadeeasy:
|
|
req_auth = set_configurator(req_auth, "dns-dnsmadeeasy")
|
|
if config.dns_gehirn:
|
|
req_auth = set_configurator(req_auth, "dns-gehirn")
|
|
if config.dns_google:
|
|
req_auth = set_configurator(req_auth, "dns-google")
|
|
if config.dns_linode:
|
|
req_auth = set_configurator(req_auth, "dns-linode")
|
|
if config.dns_luadns:
|
|
req_auth = set_configurator(req_auth, "dns-luadns")
|
|
if config.dns_nsone:
|
|
req_auth = set_configurator(req_auth, "dns-nsone")
|
|
if config.dns_ovh:
|
|
req_auth = set_configurator(req_auth, "dns-ovh")
|
|
if config.dns_rfc2136:
|
|
req_auth = set_configurator(req_auth, "dns-rfc2136")
|
|
if config.dns_route53:
|
|
req_auth = set_configurator(req_auth, "dns-route53")
|
|
if config.dns_sakuracloud:
|
|
req_auth = set_configurator(req_auth, "dns-sakuracloud")
|
|
logger.debug("Requested authenticator %s and installer %s", req_auth, req_inst)
|
|
return req_auth, req_inst
|
|
|
|
|
|
def diagnose_configurator_problem(cfg_type, requested, plugins):
|
|
"""
|
|
Raise the most helpful error message about a plugin being unavailable
|
|
|
|
:param str cfg_type: either "installer" or "authenticator"
|
|
:param str requested: the plugin that was requested
|
|
:param .PluginsRegistry plugins: available plugins
|
|
|
|
:raises error.PluginSelectionError: if there was a problem
|
|
"""
|
|
|
|
if requested:
|
|
if requested not in plugins:
|
|
msg = "The requested {0} plugin does not appear to be installed".format(requested)
|
|
else:
|
|
msg = ("The {0} plugin is not working; there may be problems with "
|
|
"your existing configuration.\nThe error was: {1!r}"
|
|
.format(requested, plugins[requested].problem))
|
|
elif cfg_type == "installer":
|
|
from certbot.cli import cli_command
|
|
msg = ('Certbot doesn\'t know how to automatically configure the web '
|
|
'server on this system. However, it can still get a certificate for '
|
|
'you. Please run "{0} certonly" to do so. You\'ll need to '
|
|
'manually configure your web server to use the resulting '
|
|
'certificate.').format(cli_command)
|
|
else:
|
|
msg = "{0} could not be determined or is not installed".format(cfg_type)
|
|
raise errors.PluginSelectionError(msg)
|