/ letsencrypt / plugins / disco.py
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())