]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: feature-toggles: add plugin support
authorErnesto Puerta <epuertat@redhat.com>
Fri, 25 Jan 2019 20:17:10 +0000 (21:17 +0100)
committerErnesto Puerta <epuertat@redhat.com>
Wed, 6 Feb 2019 17:08:00 +0000 (18:08 +0100)
Provide plugin infrastructure and a minimal set of hooks. As
python-pluggy library is not yet available for all the distros that Ceph
is targeted at, a minimal implementation has been provided.

Fixes: http://tracker.ceph.com/issues/37530
Signed-off-by: Ernesto Puerta <epuertat@redhat.com>
src/pybind/mgr/dashboard/module.py
src/pybind/mgr/dashboard/plugins/__init__.py [new file with mode: 0644]
src/pybind/mgr/dashboard/plugins/interfaces.py [new file with mode: 0644]
src/pybind/mgr/dashboard/plugins/pluggy.py [new file with mode: 0644]

index 78b7edeabbcbfd23cffd071e3896ca7184f19ae6..2cde09ff4ba8b9a79850928ca9469afdc7a0d3b9 100644 (file)
@@ -66,6 +66,8 @@ from .services.exception import dashboard_exception_handler
 from .settings import options_command_list, options_schema_list, \
                       handle_option_command
 
+from .plugins import PLUGIN_MANAGER
+
 
 # cherrypy likes to sys.exit on error.  don't let it take us down too!
 # pylint: disable=W0613
@@ -123,6 +125,10 @@ class CherryPyConfig(object):
 
         # Initialize custom handlers.
         cherrypy.tools.authenticate = AuthManagerTool()
+        cherrypy.tools.plugin_hooks = cherrypy.Tool(
+            'before_handler',
+            lambda: PLUGIN_MANAGER.hook.filter_request_before_handler(request=cherrypy.request),
+            priority=10)
         cherrypy.tools.request_logging = RequestLoggingTool()
         cherrypy.tools.dashboard_exception_handler = HandlerWrapperTool(dashboard_exception_handler,
                                                                         priority=31)
@@ -143,7 +149,8 @@ class CherryPyConfig(object):
                 'application/javascript',
             ],
             'tools.json_in.on': True,
-            'tools.json_in.force': False
+            'tools.json_in.force': False,
+            'tools.plugin_hooks.on': True,
         }
 
         if ssl:
@@ -237,6 +244,7 @@ class Module(MgrModule, CherryPyConfig):
     ]
     COMMANDS.extend(options_command_list())
     COMMANDS.extend(SSO_COMMANDS)
+    PLUGIN_MANAGER.hook.register_commands()
 
     MODULE_OPTIONS = [
         {'name': 'server_addr'},
@@ -250,6 +258,8 @@ class Module(MgrModule, CherryPyConfig):
         {'name': 'ssl'}
     ]
     MODULE_OPTIONS.extend(options_schema_list())
+    for options in PLUGIN_MANAGER.hook.get_options() or []:
+        MODULE_OPTIONS.extend(options)
 
     __pool_stats = collections.defaultdict(lambda: collections.defaultdict(
         lambda: collections.deque(maxlen=10)))
@@ -312,6 +322,8 @@ class Module(MgrModule, CherryPyConfig):
             }
         cherrypy.tree.mount(None, config=config)
 
+        PLUGIN_MANAGER.hook.setup()
+
         cherrypy.engine.start()
         NotificationQueue.start_queue()
         TaskManager.init()
diff --git a/src/pybind/mgr/dashboard/plugins/__init__.py b/src/pybind/mgr/dashboard/plugins/__init__.py
new file mode 100644 (file)
index 0000000..d3c54ce
--- /dev/null
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import abc
+import six
+
+from .pluggy import HookspecMarker, HookimplMarker, PluginManager
+
+
+@six.add_metaclass(abc.ABCMeta)
+class Interface(object):
+    pass
+
+
+class DashboardPluginManager(object):
+    def __init__(self, project_name):
+        self.__pm = PluginManager(project_name)
+        self.__add_spec = HookspecMarker(project_name)
+        self.__add_abcspec = lambda *args, **kwargs: abc.abstractmethod(
+            self.__add_spec(*args, **kwargs))
+        self.__add_hook = HookimplMarker(project_name)
+
+    pm = property(lambda self: self.__pm)
+    hook = property(lambda self: self.pm.hook)
+
+    add_spec = property(lambda self: self.__add_spec)
+    add_abcspec = property(lambda self: self.__add_abcspec)
+    add_hook = property(lambda self: self.__add_hook)
+
+    def add_interface(self, cls):
+        assert issubclass(cls, Interface)
+        self.pm.add_hookspecs(cls)
+        return cls
+
+    def add_plugin(self, plugin):
+        """ Provides decorator interface for PluginManager.register():
+            @PLUGIN_MANAGER.add_plugin
+            class Plugin(...):
+                ...
+        Additionally it checks whether the Plugin instance has all Interface
+        methods implemented and marked with add_hook decorator.
+        As a con of this approach, plugins cannot call super() from __init__()
+        """
+        assert issubclass(plugin, Interface)
+        from inspect import getmembers, ismethod
+        for interface in plugin.__bases__:
+            for method_name, _ in getmembers(interface, predicate=ismethod):
+                if self.pm.parse_hookimpl_opts(plugin, method_name) is None:
+                    raise NotImplementedError(
+                        "Plugin '{}' implements interface '{}' but existing"
+                        " method '{}' is not declared added as hook".format(
+                            plugin.__name__,
+                            interface.__name__,
+                            method_name))
+        self.pm.register(plugin())
+        return plugin
+
+
+PLUGIN_MANAGER = DashboardPluginManager("ceph-mgr.dashboard")
+
+# Load all interfaces and their hooks
+from . import interfaces
diff --git a/src/pybind/mgr/dashboard/plugins/interfaces.py b/src/pybind/mgr/dashboard/plugins/interfaces.py
new file mode 100644 (file)
index 0000000..56cb1d9
--- /dev/null
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from . import PLUGIN_MANAGER as PM, Interface
+
+
+class CanMgr(Interface):
+    from .. import mgr
+    mgr = mgr
+
+
+class CanLog(Interface):
+    from .. import logger
+    log = logger
+
+
+@PM.add_interface
+class Setupable(Interface):
+    @PM.add_abcspec
+    def setup(self):
+        """
+        Placeholder for plugin setup, right after server start.
+        CanMgr.mgr and CanLog.log are initialized by then.
+        """
+        pass
+
+
+@PM.add_interface
+class HasOptions(Interface):
+    @PM.add_abcspec
+    def get_options(self): pass
+
+
+@PM.add_interface
+class HasCommands(Interface):
+    @PM.add_abcspec
+    def register_commands(self): pass
+
+
+@PM.add_interface
+class HasEndpoints(Interface):
+    @PM.add_abcspec
+    def register_endpoints(self): pass
+
+
+class FilterRequest:
+    @PM.add_interface
+    class BeforeHandler(Interface):
+        @PM.add_abcspec
+        def filter_request_before_handler(self, request): pass
diff --git a/src/pybind/mgr/dashboard/plugins/pluggy.py b/src/pybind/mgr/dashboard/plugins/pluggy.py
new file mode 100644 (file)
index 0000000..7517e6d
--- /dev/null
@@ -0,0 +1,111 @@
+# -*- coding: utf-8 -*-
+"""
+The MIT License (MIT)
+
+Copyright (c) 2015 holger krekel (rather uses bitbucket/hpk42)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+"""
+
+"""
+CAVEAT:
+This is a minimal implementation of python-pluggy (based on 0.8.0 interface:
+https://github.com/pytest-dev/pluggy/releases/tag/0.8.0).
+
+
+Despite being a widely available Python library, it does not reach all the
+distros and releases currently targeted for Ceph Nautilus:
+- CentOS/RHEL 7.5 [ ]
+- CentOS/RHEL 8 [ ]
+- Debian 8.0 [ ]
+- Debian 9.0 [ ]
+- Ubuntu 14.05 [ ]
+- Ubuntu 16.04 [X]
+
+TODO: Once this becomes available in the above distros, this file should be
+REMOVED, and the fully featured python-pluggy should be used instead.
+"""
+
+
+class HookspecMarker(object):
+    """ Dummy implementation. No spec validation. """
+    def __init__(self, project_name):
+        self.project_name = project_name
+
+    def __call__(self, function, *args, **kwargs):
+        """ No options supported. """
+        if any(args) or any(kwargs):
+            raise NotImplementedError(
+                "This is a minimal implementation of pluggy")
+        return function
+
+
+class HookimplMarker(object):
+    def __init__(self, project_name):
+        self.project_name = project_name
+
+    def __call__(self, function, *args, **kwargs):
+        """ No options supported."""
+        if any(args) or any(kwargs):
+            raise NotImplementedError(
+                "This is a minimal implementation of pluggy")
+        setattr(function, self.project_name + "_impl", {})
+        return function
+
+
+class _HookRelay(object):
+    """
+    Provides the PluginManager.hook.<method_name>() syntax and
+    functionality.
+    """
+    def __init__(self):
+        from collections import defaultdict
+        self._registry = defaultdict(list)
+
+    def __getattr__(self, hook_name):
+        return lambda *args, **kwargs: [
+            hook(*args, **kwargs) for hook in self._registry[hook_name]]
+
+    def _add_hookimpl(self, hook_name, hook_method):
+        self._registry[hook_name].append(hook_method)
+
+
+class PluginManager(object):
+    def __init__(self, project_name):
+        self.project_name = project_name
+        self.__hook = _HookRelay()
+
+    @property
+    def hook(self):
+        return self.__hook
+
+    def parse_hookimpl_opts(self, plugin, name):
+        return getattr(
+            getattr(plugin, name),
+            self.project_name + "_impl",
+            None)
+
+    def add_hookspecs(self, module_or_class):
+        """ Dummy method"""
+        pass
+
+    def register(self, plugin, name=None):
+        for attr in dir(plugin):
+            if self.parse_hookimpl_opts(plugin, attr) is not None:
+                self.hook._add_hookimpl(attr, getattr(plugin, attr))