From 595230fd8ecc004ff56433b7ec5902e39018ceb6 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 3 May 2015 07:56:49 +0000 Subject: [PATCH] PluginsEntryPoint.prepare, move pick_* to display_ops --- .../plugins/letsencrypt_example_plugins.py | 8 +- letsencrypt/client/cli.py | 19 +-- letsencrypt/client/client.py | 6 +- letsencrypt/client/display/ops.py | 54 +++++- letsencrypt/client/interfaces.py | 6 +- letsencrypt/client/plugins/common.py | 3 +- letsencrypt/client/plugins/disco.py | 155 ++++++++---------- .../plugins/standalone/authenticator.py | 2 + letsencrypt/client/tests/client_test.py | 4 +- 9 files changed, 146 insertions(+), 111 deletions(-) diff --git a/examples/plugins/letsencrypt_example_plugins.py b/examples/plugins/letsencrypt_example_plugins.py index 11baf35a7..7c6d1311c 100644 --- a/examples/plugins/letsencrypt_example_plugins.py +++ b/examples/plugins/letsencrypt_example_plugins.py @@ -10,18 +10,22 @@ from letsencrypt.client.plugins import common class Authenticator(common.Plugin): + """Example Authenticator.""" zope.interface.implements(interfaces.IAuthenticator) + zope.interface.classProvides(interfaces.IPluginFactory) - description = 'Example Authenticator plugin' + description = "Example Authenticator plugin" # Implement all methods from IAuthenticator, remembering to add # "self" as first argument, e.g. def prepare(self)... class Installer(common.Plugins): + """Example Installer.""" zope.interface.implements(interfaces.IInstaller) + zope.interface.classProvides(interfaces.IPluginFactory) - description = 'Example Installer plugin' + description = "Example Installer plugin" # Implement all methods from IInstaller, remembering to add # "self" as first argument, e.g. def get_all_names(self)... diff --git a/letsencrypt/client/cli.py b/letsencrypt/client/cli.py index b10868b3e..e4150df7c 100644 --- a/letsencrypt/client/cli.py +++ b/letsencrypt/client/cli.py @@ -89,14 +89,14 @@ def run(args, config, plugins): "pair, but not both, is allowed") if args.authenticator is not None or args.installer is not None: - installer = plugins_disco.pick_installer( + installer = display_ops.pick_installer( config, args.installer, plugins) - authenticator = plugins_disco.pick_authenticator( + authenticator = display_ops.pick_authenticator( config, args.authenticator, plugins) else: # TODO: this assume that user doesn't want to pick authenticator # and installer separately... - authenticator = installer = plugins_disco.pick_configurator( + authenticator = installer = display_ops.pick_configurator( config, args.configurator, plugins) if installer is None or authenticator is None: @@ -114,13 +114,13 @@ def auth(args, config, plugins): if acc is None: return None - authenticator = plugins_disco.pick_authenticator( + authenticator = display_ops.pick_authenticator( config, args.authenticator, plugins) if authenticator is None: return "Authenticator could not be determined" if args.installer is not None: - installer = plugins_disco.pick_installer(config, args.installer, plugins) + installer = display_ops.pick_installer(config, args.installer, plugins) else: installer = None @@ -135,7 +135,7 @@ def install(args, config, plugins): if acc is None: return None - installer = plugins_disco.pick_installer(config, args.installer, plugins) + installer = display_ops.pick_installer(config, args.installer, plugins) if installer is None: return "Installer could not be determined" acme, doms = _common_run( @@ -214,11 +214,10 @@ def plugins_cmd(args, config, plugins): if not args.prepare: return _print_plugins(filtered) - prepared = plugins_disco.prepare_plugins(initialized) - logging.debug("Prepared plugins: %s", plugins) + available = plugins_disco.available_plugins(filtered) + logging.debug("Prepared plugins: %s", available) - _print_plugins(prepared, plugins, names) - plugins_disco + _print_plugins(available) def read_file(filename): diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index a98de272d..3f1c627e8 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -20,8 +20,6 @@ from letsencrypt.client import network2 from letsencrypt.client import reverter from letsencrypt.client import revoker -from letsencrypt.client.plugins import disco as plugins_disco - from letsencrypt.client.plugins.apache import configurator from letsencrypt.client.display import ops as display_ops @@ -349,7 +347,7 @@ def rollback(default_installer, checkpoints, config, plugins): """ # Misconfigurations are only a slight problems... allow the user to rollback - installer = plugins_disco.pick_installer( + installer = display_ops.pick_installer( config, default_installer, plugins, question="Which installer " "should be used for rollback?") @@ -368,7 +366,7 @@ def revoke(default_installer, config, plugins, no_confirm, cert, authkey): :type config: :class:`letsencrypt.client.interfaces.IConfig` """ - installer = plugins_disco.pick_installer( + installer = display_ops.pick_installer( config, default_installer, plugins, question="Which installer " "should be used for certificate revocation?") diff --git a/letsencrypt/client/display/ops.py b/letsencrypt/client/display/ops.py index d59c1ceb1..7b035ffd0 100644 --- a/letsencrypt/client/display/ops.py +++ b/letsencrypt/client/display/ops.py @@ -6,6 +6,7 @@ import zope.component from letsencrypt.client import interfaces from letsencrypt.client.display import util as display_util +from letsencrypt.client.plugins import disco as plugins_disco # Define a helper function to avoid verbose code util = zope.component.getUtility # pylint: disable=invalid-name @@ -17,9 +18,9 @@ def choose_plugin(prepared, question): :param list prepared: """ - opts = [plugin_ep.name_with_description if error is None + opts = [plugin_ep.name_with_description if not plugin_ep.misconfigured else "%s (Misconfigured)" % plugin_ep.name_with_description - for (plugin_ep, error) in prepared] + for plugin_ep in prepared.itervalues()] while True: code, index = util(interfaces.IDisplay).menu( @@ -29,7 +30,7 @@ def choose_plugin(prepared, question): return prepared[index][0] elif code == display_util.HELP: if prepared[index][1] is not None: - msg = "Reported Error: %s" % prepared[index][1] + msg = "Reported Error: %s" % prepared[index].prepare() else: msg = prepared[index][0].init().more_info() util(interfaces.IDisplay).notification( @@ -37,6 +38,53 @@ def choose_plugin(prepared, question): else: return None +def _pick_plugin(config, default, plugins, question, ifaces): + if default is not None: + filtered = {default: plugins[default]} + else: + filtered = plugins.filter(ifaces) + + for plugin_ep in plugins.itervalues(): + plugin_ep.init(config) + verified = plugins_disco.verify_plugins(filtered, ifaces) + prepared = plugins_disco.available_plugins(verified) + + if len(prepared) > 1: + logging.debug("Multiple candidate plugins: %s", prepared) + return choose_plugin(prepared.values(), question).init() + elif len(prepared) == 1: + plugin_ep = prepared.values()[0] + logging.debug("Single candidate plugin: %s", plugin_ep) + return plugin_ep.init() + else: + logging.debug("No candidate plugin") + return None + + +def pick_authenticator( + config, default, plugins, question="How would you " + "like to authenticate with Let's Encrypt CA?"): + """Pick authentication plugin.""" + return _pick_plugin( + config, default, plugins, question, (interfaces.IAuthenticator,)) + + +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_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 choose_account(accounts): """Choose an account. diff --git a/letsencrypt/client/interfaces.py b/letsencrypt/client/interfaces.py index df8b616a6..018462b3c 100644 --- a/letsencrypt/client/interfaces.py +++ b/letsencrypt/client/interfaces.py @@ -69,9 +69,11 @@ class IPlugin(zope.interface.Interface): Finish up any additional initialization. :raises letsencrypt.client.errors.LetsEncryptMisconfigurationError: - when full initialization cannot be completed. + when full initialization cannot be completed. Plugin will be + displayed on a list of available plugins. :raises letsencrypt.client.errors.LetsEncryptNoInstallationError: - when the necessary programs/files cannot be located. + when the necessary programs/files cannot be located. Plugin + will NOT be displayed on a list of available plugins. """ diff --git a/letsencrypt/client/plugins/common.py b/letsencrypt/client/plugins/common.py index e9ee4d573..60b868c37 100644 --- a/letsencrypt/client/plugins/common.py +++ b/letsencrypt/client/plugins/common.py @@ -18,7 +18,8 @@ def dest_namespace(name): class Plugin(object): """Generic plugin.""" zope.interface.implements(interfaces.IPlugin) - zope.interface.classProvides(interfaces.IPluginFactory) + # classProvides is not inherited, subclasses must define it on their own + #zope.interface.classProvides(interfaces.IPluginFactory) def __init__(self, config, name): self.config = config diff --git a/letsencrypt/client/plugins/disco.py b/letsencrypt/client/plugins/disco.py index 3262a315b..dec80afa9 100644 --- a/letsencrypt/client/plugins/disco.py +++ b/letsencrypt/client/plugins/disco.py @@ -9,8 +9,6 @@ from letsencrypt.client import constants from letsencrypt.client import errors from letsencrypt.client import interfaces -from letsencrypt.client.display import ops as display_ops - class PluginEntryPoint(object): """Plugin entry point.""" @@ -23,11 +21,7 @@ class PluginEntryPoint(object): self.plugin_cls = entry_point.load() self.entry_point = entry_point self._initialized = None - - @property - def initialized(self): - """Has the plugin been initialized already?""" - return self._initialized is not None + self._prepared = None @classmethod def entry_point_to_plugin_name(cls, entry_point): @@ -36,6 +30,11 @@ class PluginEntryPoint(object): return entry_point.name return entry_point.dist.key + ":" + entry_point.name + @property + def initialized(self): + """Has the plugin been initialized already?""" + return self._initialized is not None + def init(self, config=None): """Memoized plugin inititialization.""" if not self.initialized: @@ -43,6 +42,39 @@ class PluginEntryPoint(object): self._initialized = self.plugin_cls(config, self.name) return self._initialized + @property + def prepared(self): + """Has the plugin been prepared already?""" + if not self.initialized: + logging.debug(".prepared called on uninitialized %s", self) + return self._prepared is not None + + def prepare(self): + """Memoized plugin preparation.""" + assert self.initialized + if self._prepared is None: + try: + self._initialized.prepare() + except errors.LetsEncryptMisconfigurationError as error: + logging.debug("Misconfigured %s: %s", self, error) + self._prepared = error + except errors.LetsEncryptNoInstallationError as error: + logging.debug("No installation (%s): %s", self, error) + self._prepared = error + else: + self._prepared = True + return self._prepared + + @property + def misconfigured(self): + """Is plugin misconfigured?""" + return isinstance(self._prepared, errors.LetsEncryptMisconfigurationError) + + @property + def available(self): + """Is plugin available, i.e. prepared or misconfigured?""" + return self._prepared is True or self.misconfigured + def __repr__(self): return "PluginEntryPoint#{0}".format(self.name) @@ -51,6 +83,20 @@ class PluginEntryPoint(object): """Name with description. Handy for UI.""" return "{0} ({1})".format(self.name, self.plugin_cls.description) + def verify(self, ifaces): + assert self.initialized + for iface in ifaces: # zope.interface.providedBy(plugin) + try: + zope.interface.verify.verifyObject(iface, self.init()) + except zope.interface.exceptions.BrokenImplementation: + if iface.implementedBy(self.plugin_cls): + logging.debug( + "%s implements %s but object does " + "not verify", self.plugin_cls, iface.__name__) + return False + return True + + class PluginsRegistry(collections.Mapping): """Plugins registry.""" @@ -66,8 +112,12 @@ class PluginsRegistry(collections.Mapping): constants.SETUPTOOLS_PLUGINS_ENTRY_POINT): plugin_ep = PluginEntryPoint(entry_point) assert plugin_ep.name not in plugins, ( - "PREFIX_FREE_DISTRIBTIONS messed up") - plugins[plugin_ep.name] = plugin_ep + "PREFIX_FREE_DISTRIBUTIONS messed up") + if interfaces.IPluginFactory.providedBy(plugin_ep.plugin_cls): + plugins[plugin_ep.name] = plugin_ep + else: + logging.warning("Plugin entry point %s does not provide " + "IPluginFactory, skipping", plugin_ep) return cls(plugins) def filter(self, *ifaces_groups): @@ -81,7 +131,8 @@ class PluginsRegistry(collections.Mapping): for ifaces in ifaces_groups))) def __repr__(self): - return "{0}({1!r})".format(self.__class__.__name__, self.plugins) + return "{0}({1!r})".format( + self.__class__.__name__, set(self.plugins.itervalues())) def __getitem__(self, name): return self.plugins[name] @@ -95,85 +146,15 @@ class PluginsRegistry(collections.Mapping): def verify_plugins(initialized, ifaces): """Verify plugin objects.""" - verified = {} - for name, plugin_ep in initialized.iteritems(): - verifies = True - for iface in ifaces: # zope.interface.providedBy(plugin) - try: - zope.interface.verify.verifyObject(iface, plugin_ep.init()) - except zope.interface.exceptions.BrokenImplementation: - if iface.implementedBy(plugin_ep.plugin_cls): - logging.debug( - "%s implements %s but object does " - "not verify", plugin_ep.plugin_cls, iface.__name__) - verifies = False - break - if verifies: - verified[name] = plugin_ep - return verified + return dict((name, plugin_ep) for name, plugin_ep in initialized.iteritems() + if plugin_ep.verify(ifaces)) -def prepare_plugins(initialized): - """Prepare plugins.""" +def available_plugins(initialized): + """Prepare plugins and filter available.""" prepared = {} - for name, plugin_ep in initialized.iteritems(): - error = None - try: - plugin_ep.init().prepare() - except errors.LetsEncryptMisconfigurationError as error: - logging.debug("Misconfigured %s: %s", plugin_ep, error) - except errors.LetsEncryptNoInstallationError as error: - logging.debug("No installation (%s): %s", plugin_ep, error) - continue - prepared[name] = (plugin_ep, error) - + plugin_ep.prepare() + if plugin_ep.available: + prepared[name] = plugin_ep return prepared # succefully prepared + misconfigured - - -def _pick_plugin(config, default, plugins, question, ifaces): - if default is not None: - filtered = {default: plugins[default]} - else: - filtered = plugins.filter(ifaces) - - for plugin_ep in plugins.itervalues(): - plugin_ep.init(config) - verified = verify_plugins(filtered, ifaces) - prepared = prepare_plugins(verified) - - if len(prepared) > 1: - logging.debug("Multiple candidate plugins: %s", prepared) - return display_ops.choose_plugin(prepared.values(), question).init() - elif len(prepared) == 1: - plugin_ep = prepared.values()[0][0] - logging.debug("Single candidate plugin: %s", plugin_ep) - return plugin_ep.init() - else: - logging.debug("No candidate plugin") - return None - - -def pick_authenticator( - config, default, plugins, question="How would you " - "like to authenticate with Let's Encrypt CA?"): - """Pick authentication plugin.""" - return _pick_plugin( - config, default, plugins, question, (interfaces.IAuthenticator,)) - - -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_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)) diff --git a/letsencrypt/client/plugins/standalone/authenticator.py b/letsencrypt/client/plugins/standalone/authenticator.py index 668b3f716..a10ffd32d 100644 --- a/letsencrypt/client/plugins/standalone/authenticator.py +++ b/letsencrypt/client/plugins/standalone/authenticator.py @@ -31,6 +31,8 @@ class StandaloneAuthenticator(common.Plugin): """ zope.interface.implements(interfaces.IAuthenticator) + zope.interface.classProvides(interfaces.IPluginFactory) + description = "Standalone Authenticator" def __init__(self, *args, **kwargs): diff --git a/letsencrypt/client/tests/client_test.py b/letsencrypt/client/tests/client_test.py index b4595f257..aff9b5c84 100644 --- a/letsencrypt/client/tests/client_test.py +++ b/letsencrypt/client/tests/client_test.py @@ -66,7 +66,7 @@ class RollbackTest(unittest.TestCase): from letsencrypt.client.client import rollback rollback(None, checkpoints, {}, mock.MagicMock()) - @mock.patch("letsencrypt.client.client.plugins_disco.pick_installer") + @mock.patch("letsencrypt.client.client.display_ops.pick_installer") def test_no_problems(self, mock_pick_installer): mock_pick_installer.side_effect = self.m_install @@ -75,7 +75,7 @@ class RollbackTest(unittest.TestCase): self.assertEqual(self.m_install().rollback_checkpoints.call_count, 1) self.assertEqual(self.m_install().restart.call_count, 1) - @mock.patch("letsencrypt.client.client.plugins_disco.pick_installer") + @mock.patch("letsencrypt.client.client.display_ops.pick_installer") def test_no_installer(self, mock_pick_installer): mock_pick_installer.return_value = None self._call(1)