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
# 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)
'application/javascript',
],
'tools.json_in.on': True,
- 'tools.json_in.force': False
+ 'tools.json_in.force': False,
+ 'tools.plugin_hooks.on': True,
}
if ssl:
]
COMMANDS.extend(options_command_list())
COMMANDS.extend(SSO_COMMANDS)
+ PLUGIN_MANAGER.hook.register_commands()
MODULE_OPTIONS = [
{'name': 'server_addr'},
{'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)))
}
cherrypy.tree.mount(None, config=config)
+ PLUGIN_MANAGER.hook.setup()
+
cherrypy.engine.start()
NotificationQueue.start_queue()
TaskManager.init()
--- /dev/null
+# -*- 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
--- /dev/null
+# -*- 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
--- /dev/null
+# -*- 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))