Merge remote-tracking branch 'upstream/master' into 2478

This commit is contained in:
Joona Hoikkala
2016-03-25 16:35:39 +02:00
18 changed files with 940 additions and 785 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ class Plugin(object):
NOTE: if you add argpase arguments such that users setting them can
create a config entry that python's bool() would consider false (ie,
the use might set the variable to "", [], 0, etc), please ensure that
cli._set_by_cli() works for your variable.
cli.set_by_cli() works for your variable.
"""
+1
View File
@@ -4,6 +4,7 @@ import logging
import pkg_resources
import zope.interface
import zope.interface.verify
from letsencrypt import constants
from letsencrypt import errors
+273
View File
@@ -0,0 +1,273 @@
"""Decide which plugins to use for authentication & installation"""
from __future__ import print_function
import os
import logging
import zope.component
from letsencrypt import errors
from letsencrypt import interfaces
from letsencrypt.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 Let's Encrypt CA?"):
"""Pick authentication plugin."""
return pick_plugin(
config, default, plugins, question, (interfaces.IAuthenticator,))
def pick_plugin(config, default, plugins, question, ifaces):
"""Pick plugin.
:param letsencrypt.interfaces.IConfig: Configuration
:param str default: Plugin name supplied by user or ``None``.
:param letsencrypt.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(prepared.values(), question)
if plugin_ep is None:
return None
else:
return plugin_ep.init()
elif len(prepared) == 1:
plugin_ep = 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]
while True:
disp = z_util(interfaces.IDisplay)
code, index = disp.menu(question, opts, help_label="More Info")
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()),
height=display_util.HEIGHT, pause=False)
else:
return plugin_ep
elif code == display_util.HELP:
if prepared[index].misconfigured:
msg = "Reported Error: %s" % prepared[index].prepare()
else:
msg = prepared[index].init().more_info()
z_util(interfaces.IDisplay).notification(
msg, height=display_util.HEIGHT)
else:
return None
noninstaller_plugins = ["webroot", "manual", "standalone"]
def record_chosen_plugins(config, plugins, auth, inst):
"Update the config entries to reflect the plugins we actually selected."
cn = config.namespace
cn.authenticator = plugins.find_init(auth).name if auth else "None"
cn.installer = plugins.find_init(inst).name if inst else "None"
def choose_configurator_plugins(config, plugins, verb):
"""
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)
# Which plugins do we need?
if verb == "run":
need_inst = need_auth = True
from letsencrypt.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":
need_inst = True
if config.authenticator:
logger.warn("Specifying an authenticator doesn't make sense in install mode")
# 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)
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):
"""
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")
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":
if os.path.exists("/etc/debian_version"):
# Debian... installers are at least possible
msg = ('No installers seem to be present and working on your system; '
'fix that or try running letsencrypt with the "certonly" command')
else:
# XXX update this logic as we make progress on #788 and nginx support
msg = ('No installers are available on your OS yet; try running '
'"letsencrypt-auto certonly" to get a cert you can install manually')
else:
msg = "{0} could not be determined or is not installed".format(cfg_type)
raise errors.PluginSelectionError(msg)
+149
View File
@@ -0,0 +1,149 @@
"""Tests for letsenecrypt.plugins.selection"""
import sys
import unittest
import mock
import zope.component
from letsencrypt.display import util as display_util
from letsencrypt import interfaces
class ConveniencePickPluginTest(unittest.TestCase):
"""Tests for letsencrypt.plugins.selection.pick_*."""
def _test(self, fun, ifaces):
config = mock.Mock()
default = mock.Mock()
plugins = mock.Mock()
with mock.patch("letsencrypt.plugins.selection.pick_plugin") as mock_p:
mock_p.return_value = "foo"
self.assertEqual("foo", fun(config, default, plugins, "Question?"))
mock_p.assert_called_once_with(
config, default, plugins, "Question?", ifaces)
def test_authenticator(self):
from letsencrypt.plugins.selection import pick_authenticator
self._test(pick_authenticator, (interfaces.IAuthenticator,))
def test_installer(self):
from letsencrypt.plugins.selection import pick_installer
self._test(pick_installer, (interfaces.IInstaller,))
def test_configurator(self):
from letsencrypt.plugins.selection import pick_configurator
self._test(pick_configurator,
(interfaces.IAuthenticator, interfaces.IInstaller))
class PickPluginTest(unittest.TestCase):
"""Tests for letsencrypt.plugins.selection.pick_plugin."""
def setUp(self):
self.config = mock.Mock(noninteractive_mode=False)
self.default = None
self.reg = mock.MagicMock()
self.question = "Question?"
self.ifaces = []
def _call(self):
from letsencrypt.plugins.selection import pick_plugin
return pick_plugin(self.config, self.default, self.reg,
self.question, self.ifaces)
def test_default_provided(self):
self.default = "foo"
self._call()
self.assertEqual(1, self.reg.filter.call_count)
def test_no_default(self):
self._call()
self.assertEqual(1, self.reg.visible().ifaces.call_count)
def test_no_candidate(self):
self.assertTrue(self._call() is None)
def test_single(self):
plugin_ep = mock.MagicMock()
plugin_ep.init.return_value = "foo"
plugin_ep.misconfigured = False
self.reg.visible().ifaces().verify().available.return_value = {
"bar": plugin_ep}
self.assertEqual("foo", self._call())
def test_single_misconfigured(self):
plugin_ep = mock.MagicMock()
plugin_ep.init.return_value = "foo"
plugin_ep.misconfigured = True
self.reg.visible().ifaces().verify().available.return_value = {
"bar": plugin_ep}
self.assertTrue(self._call() is None)
def test_multiple(self):
plugin_ep = mock.MagicMock()
plugin_ep.init.return_value = "foo"
self.reg.visible().ifaces().verify().available.return_value = {
"bar": plugin_ep,
"baz": plugin_ep,
}
with mock.patch("letsencrypt.plugins.selection.choose_plugin") as mock_choose:
mock_choose.return_value = plugin_ep
self.assertEqual("foo", self._call())
mock_choose.assert_called_once_with(
[plugin_ep, plugin_ep], self.question)
def test_choose_plugin_none(self):
self.reg.visible().ifaces().verify().available.return_value = {
"bar": None,
"baz": None,
}
with mock.patch("letsencrypt.plugins.selection.choose_plugin") as mock_choose:
mock_choose.return_value = None
self.assertTrue(self._call() is None)
class ChoosePluginTest(unittest.TestCase):
"""Tests for letsencrypt.plugins.selection.choose_plugin."""
def setUp(self):
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
self.mock_apache = mock.Mock(
description_with_name="a", misconfigured=True)
self.mock_stand = mock.Mock(
description_with_name="s", misconfigured=False)
self.mock_stand.init().more_info.return_value = "standalone"
self.plugins = [
self.mock_apache,
self.mock_stand,
]
def _call(self):
from letsencrypt.plugins.selection import choose_plugin
return choose_plugin(self.plugins, "Question?")
@mock.patch("letsencrypt.plugins.selection.z_util")
def test_selection(self, mock_util):
mock_util().menu.side_effect = [(display_util.OK, 0),
(display_util.OK, 1)]
self.assertEqual(self.mock_stand, self._call())
self.assertEqual(mock_util().notification.call_count, 1)
@mock.patch("letsencrypt.plugins.selection.z_util")
def test_more_info(self, mock_util):
mock_util().menu.side_effect = [
(display_util.HELP, 0),
(display_util.HELP, 1),
(display_util.OK, 1),
]
self.assertEqual(self.mock_stand, self._call())
self.assertEqual(mock_util().notification.call_count, 2)
@mock.patch("letsencrypt.plugins.selection.z_util")
def test_no_choice(self, mock_util):
mock_util().menu.return_value = (display_util.CANCEL, 0)
self.assertTrue(self._call() is None)