disco.py
1 """Utilities for plugins discovery and selection.""" 2 import collections 3 import logging 4 import pkg_resources 5 6 import zope.interface 7 8 from letsencrypt import constants 9 from letsencrypt import errors 10 from letsencrypt import interfaces 11 12 13 logger = logging.getLogger(__name__) 14 15 16 class PluginEntryPoint(object): 17 """Plugin entry point.""" 18 19 PREFIX_FREE_DISTRIBUTIONS = [ 20 "letsencrypt", 21 "letsencrypt-apache", 22 "letsencrypt-nginx", 23 ] 24 """Distributions for which prefix will be omitted.""" 25 26 # this object is mutable, don't allow it to be hashed! 27 __hash__ = None 28 29 def __init__(self, entry_point): 30 self.name = self.entry_point_to_plugin_name(entry_point) 31 self.plugin_cls = entry_point.load() 32 self.entry_point = entry_point 33 self._initialized = None 34 self._prepared = None 35 36 @classmethod 37 def entry_point_to_plugin_name(cls, entry_point): 38 """Unique plugin name for an ``entry_point``""" 39 if entry_point.dist.key in cls.PREFIX_FREE_DISTRIBUTIONS: 40 return entry_point.name 41 return entry_point.dist.key + ":" + entry_point.name 42 43 @property 44 def description(self): 45 """Description of the plugin.""" 46 return self.plugin_cls.description 47 48 @property 49 def description_with_name(self): 50 """Description with name. Handy for UI.""" 51 return "{0} ({1})".format(self.description, self.name) 52 53 @property 54 def hidden(self): 55 """Should this plugin be hidden from UI?""" 56 return getattr(self.plugin_cls, "hidden", False) 57 58 def ifaces(self, *ifaces_groups): 59 """Does plugin implements specified interface groups?""" 60 return not ifaces_groups or any( 61 all(iface.implementedBy(self.plugin_cls) 62 for iface in ifaces) 63 for ifaces in ifaces_groups) 64 65 @property 66 def initialized(self): 67 """Has the plugin been initialized already?""" 68 return self._initialized is not None 69 70 def init(self, config=None): 71 """Memoized plugin inititialization.""" 72 if not self.initialized: 73 self.entry_point.require() # fetch extras! 74 self._initialized = self.plugin_cls(config, self.name) 75 return self._initialized 76 77 def verify(self, ifaces): 78 """Verify that the plugin conforms to the specified interfaces.""" 79 assert self.initialized 80 for iface in ifaces: # zope.interface.providedBy(plugin) 81 try: 82 zope.interface.verify.verifyObject(iface, self.init()) 83 except zope.interface.exceptions.BrokenImplementation as error: 84 if iface.implementedBy(self.plugin_cls): 85 logger.debug( 86 "%s implements %s but object does not verify: %s", 87 self.plugin_cls, iface.__name__, error, exc_info=True) 88 return False 89 return True 90 91 @property 92 def prepared(self): 93 """Has the plugin been prepared already?""" 94 if not self.initialized: 95 logger.debug(".prepared called on uninitialized %r", self) 96 return self._prepared is not None 97 98 def prepare(self): 99 """Memoized plugin preparation.""" 100 assert self.initialized 101 if self._prepared is None: 102 try: 103 self._initialized.prepare() 104 except errors.MisconfigurationError as error: 105 logger.debug("Misconfigured %r: %s", self, error, exc_info=True) 106 self._prepared = error 107 except errors.NoInstallationError as error: 108 logger.debug( 109 "No installation (%r): %s", self, error, exc_info=True) 110 self._prepared = error 111 except errors.PluginError as error: 112 logger.debug("Other error:(%r): %s", self, error, exc_info=True) 113 self._prepared = error 114 else: 115 self._prepared = True 116 return self._prepared 117 118 @property 119 def misconfigured(self): 120 """Is plugin misconfigured?""" 121 return isinstance(self._prepared, errors.MisconfigurationError) 122 123 @property 124 def problem(self): 125 """Return the Exception raised during plugin setup, or None if all is well""" 126 if isinstance(self._prepared, Exception): 127 return self._prepared 128 return None 129 130 @property 131 def available(self): 132 """Is plugin available, i.e. prepared or misconfigured?""" 133 return self._prepared is True or self.misconfigured 134 135 def __repr__(self): 136 return "PluginEntryPoint#{0}".format(self.name) 137 138 def __str__(self): 139 lines = [ 140 "* {0}".format(self.name), 141 "Description: {0}".format(self.plugin_cls.description), 142 "Interfaces: {0}".format(", ".join( 143 iface.__name__ for iface in zope.interface.implementedBy( 144 self.plugin_cls))), 145 "Entry point: {0}".format(self.entry_point), 146 ] 147 148 if self.initialized: 149 lines.append("Initialized: {0}".format(self.init())) 150 if self.prepared: 151 lines.append("Prep: {0}".format(self.prepare())) 152 153 return "\n".join(lines) 154 155 156 class PluginsRegistry(collections.Mapping): 157 """Plugins registry.""" 158 159 def __init__(self, plugins): 160 self._plugins = plugins 161 162 @classmethod 163 def find_all(cls): 164 """Find plugins using setuptools entry points.""" 165 plugins = {} 166 for entry_point in pkg_resources.iter_entry_points( 167 constants.SETUPTOOLS_PLUGINS_ENTRY_POINT): 168 plugin_ep = PluginEntryPoint(entry_point) 169 assert plugin_ep.name not in plugins, ( 170 "PREFIX_FREE_DISTRIBUTIONS messed up") 171 # providedBy | pylint: disable=no-member 172 if interfaces.IPluginFactory.providedBy(plugin_ep.plugin_cls): 173 plugins[plugin_ep.name] = plugin_ep 174 else: # pragma: no cover 175 logger.warning( 176 "%r does not provide IPluginFactory, skipping", plugin_ep) 177 return cls(plugins) 178 179 def __getitem__(self, name): 180 return self._plugins[name] 181 182 def __iter__(self): 183 return iter(self._plugins) 184 185 def __len__(self): 186 return len(self._plugins) 187 188 def init(self, config): 189 """Initialize all plugins in the registry.""" 190 return [plugin_ep.init(config) for plugin_ep 191 in self._plugins.itervalues()] 192 193 def filter(self, pred): 194 """Filter plugins based on predicate.""" 195 return type(self)(dict((name, plugin_ep) for name, plugin_ep 196 in self._plugins.iteritems() if pred(plugin_ep))) 197 198 def visible(self): 199 """Filter plugins based on visibility.""" 200 return self.filter(lambda plugin_ep: not plugin_ep.hidden) 201 202 def ifaces(self, *ifaces_groups): 203 """Filter plugins based on interfaces.""" 204 # pylint: disable=star-args 205 return self.filter(lambda p_ep: p_ep.ifaces(*ifaces_groups)) 206 207 def verify(self, ifaces): 208 """Filter plugins based on verification.""" 209 return self.filter(lambda p_ep: p_ep.verify(ifaces)) 210 211 def prepare(self): 212 """Prepare all plugins in the registry.""" 213 return [plugin_ep.prepare() for plugin_ep in self._plugins.itervalues()] 214 215 def available(self): 216 """Filter plugins based on availability.""" 217 return self.filter(lambda p_ep: p_ep.available) 218 # succefully prepared + misconfigured 219 220 def find_init(self, plugin): 221 """Find an initialized plugin. 222 223 This is particularly useful for finding a name for the plugin 224 (although `.IPluginFactory.__call__` takes ``name`` as one of 225 the arguments, ``IPlugin.name`` is not part of the interface):: 226 227 # plugin is an instance providing IPlugin, initialized 228 # somewhere else in the code 229 plugin_registry.find_init(plugin).name 230 231 Returns ``None`` if ``plugin`` is not found in the registry. 232 233 """ 234 # use list instead of set because PluginEntryPoint is not hashable 235 candidates = [plugin_ep for plugin_ep in self._plugins.itervalues() 236 if plugin_ep.initialized and plugin_ep.init() is plugin] 237 assert len(candidates) <= 1 238 if candidates: 239 return candidates[0] 240 else: 241 return None 242 243 def __repr__(self): 244 return "{0}({1})".format( 245 self.__class__.__name__, ','.join( 246 repr(p_ep) for p_ep in self._plugins.itervalues())) 247 248 def __str__(self): 249 if not self._plugins: 250 return "No plugins" 251 return "\n\n".join(str(p_ep) for p_ep in self._plugins.itervalues())