summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'src/pybind/mgr/dashboard/plugins')
-rw-r--r--src/pybind/mgr/dashboard/plugins/__init__.py74
-rw-r--r--src/pybind/mgr/dashboard/plugins/debug.py91
-rw-r--r--src/pybind/mgr/dashboard/plugins/feature_toggles.py140
-rw-r--r--src/pybind/mgr/dashboard/plugins/interfaces.py82
-rw-r--r--src/pybind/mgr/dashboard/plugins/lru_cache.py47
-rw-r--r--src/pybind/mgr/dashboard/plugins/pluggy.py111
-rw-r--r--src/pybind/mgr/dashboard/plugins/plugin.py33
-rw-r--r--src/pybind/mgr/dashboard/plugins/ttl_cache.py55
8 files changed, 633 insertions, 0 deletions
diff --git a/src/pybind/mgr/dashboard/plugins/__init__.py b/src/pybind/mgr/dashboard/plugins/__init__.py
new file mode 100644
index 00000000..43d71fde
--- /dev/null
+++ b/src/pybind/mgr/dashboard/plugins/__init__.py
@@ -0,0 +1,74 @@
+# -*- 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 Mixin(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
+
+ @staticmethod
+ def final(func):
+ setattr(func, '__final__', True)
+ return func
+
+ 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 hasattr(getattr(interface, method_name), '__final__'):
+ continue
+
+ 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/debug.py b/src/pybind/mgr/dashboard/plugins/debug.py
new file mode 100644
index 00000000..5f4cdbea
--- /dev/null
+++ b/src/pybind/mgr/dashboard/plugins/debug.py
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from enum import Enum
+import json
+
+from . import PLUGIN_MANAGER as PM
+from . import interfaces as I # noqa: E741,N812
+from .plugin import SimplePlugin as SP
+
+
+class Actions(Enum):
+ ENABLE = 'enable'
+ DISABLE = 'disable'
+ STATUS = 'status'
+
+
+@PM.add_plugin # pylint: disable=too-many-ancestors
+class Debug(SP, I.CanCherrypy, I.ConfiguresCherryPy, # pylint: disable=too-many-ancestors
+ I.Setupable, I.ConfigNotify):
+ NAME = 'debug'
+
+ OPTIONS = [
+ SP.Option(
+ name=NAME,
+ default=False,
+ type='bool',
+ desc="Enable/disable debug options"
+ )
+ ]
+
+ def _refresh_health_checks(self):
+ debug = self.get_option(self.NAME)
+ if debug:
+ self.mgr.health_checks.update({'DASHBOARD_DEBUG': {
+ 'severity': 'warning',
+ 'summary': 'Dashboard debug mode is enabled',
+ 'detail': [
+ 'Please disable debug mode in production environments using '
+ '"ceph dashboard {} {}"'.format(self.NAME, Actions.DISABLE.value)
+ ]
+ }})
+ else:
+ self.mgr.health_checks.pop('DASHBOARD_DEBUG', None)
+ self.mgr.refresh_health_checks()
+
+ @PM.add_hook
+ def setup(self):
+ self._refresh_health_checks()
+
+ def handler(self, action):
+ ret = 0
+ msg = ''
+ if action in [Actions.ENABLE.value, Actions.DISABLE.value]:
+ self.set_option(self.NAME, action == Actions.ENABLE.value)
+ self.mgr.update_cherrypy_config({})
+ self._refresh_health_checks()
+ else:
+ debug = self.get_option(self.NAME)
+ msg = "Debug: '{}'".format('enabled' if debug else 'disabled')
+ return ret, msg, None
+
+ COMMANDS = [
+ SP.Command(
+ prefix="dashboard {name}".format(name=NAME),
+ args="name=action,type=CephChoices,strings={states}".format(
+ states="|".join(a.value for a in Actions)),
+ desc="Control and report debug status in Ceph-Dashboard",
+ handler=handler
+ )
+ ]
+
+ def custom_error_response(self, status, message, traceback, version):
+ self.response.headers['Content-Type'] = 'application/json'
+ error_response = dict(status=status, detail=message, request_id=str(self.request.unique_id))
+
+ if self.get_option(self.NAME):
+ error_response.update(dict(traceback=traceback, version=version))
+
+ return json.dumps(error_response)
+
+ @PM.add_hook
+ def configure_cherrypy(self, config):
+ config.update({
+ 'environment': 'test_suite' if self.get_option(self.NAME) else 'production',
+ 'error_page.default': self.custom_error_response,
+ })
+
+ @PM.add_hook
+ def config_notify(self):
+ self._refresh_health_checks()
diff --git a/src/pybind/mgr/dashboard/plugins/feature_toggles.py b/src/pybind/mgr/dashboard/plugins/feature_toggles.py
new file mode 100644
index 00000000..7edbfa8f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/plugins/feature_toggles.py
@@ -0,0 +1,140 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from enum import Enum
+import cherrypy
+from mgr_module import CLICommand, Option
+
+from . import PLUGIN_MANAGER as PM
+from . import interfaces as I
+from .ttl_cache import ttl_cache
+
+from ..controllers.rbd import Rbd, RbdSnapshot, RbdTrash
+from ..controllers.rbd_mirroring import (
+ RbdMirroringSummary, RbdMirroringPoolMode, RbdMirroringPoolPeer)
+from ..controllers.iscsi import Iscsi, IscsiTarget
+from ..controllers.cephfs import CephFS
+from ..controllers.rgw import Rgw, RgwDaemon, RgwBucket, RgwUser
+
+
+class Features(Enum):
+ RBD = 'rbd'
+ MIRRORING = 'mirroring'
+ ISCSI = 'iscsi'
+ CEPHFS = 'cephfs'
+ RGW = 'rgw'
+
+
+PREDISABLED_FEATURES = set()
+
+
+Feature2Controller = {
+ Features.RBD: [Rbd, RbdSnapshot, RbdTrash],
+ Features.MIRRORING: [
+ RbdMirroringSummary, RbdMirroringPoolMode, RbdMirroringPoolPeer],
+ Features.ISCSI: [Iscsi, IscsiTarget],
+ Features.CEPHFS: [CephFS],
+ Features.RGW: [Rgw, RgwDaemon, RgwBucket, RgwUser],
+}
+
+
+class Actions(Enum):
+ ENABLE = 'enable'
+ DISABLE = 'disable'
+ STATUS = 'status'
+
+
+@PM.add_plugin
+class FeatureToggles(I.CanMgr, I.CanLog, I.Setupable, I.HasOptions,
+ I.HasCommands, I.FilterRequest.BeforeHandler,
+ I.HasControllers):
+ OPTION_FMT = 'FEATURE_TOGGLE_{}'
+ CACHE_MAX_SIZE = 128 # Optimum performance with 2^N sizes
+ CACHE_TTL = 10 # seconds
+
+ @PM.add_hook
+ def setup(self):
+ self.Controller2Feature = {
+ controller: feature
+ for feature, controllers in Feature2Controller.items()
+ for controller in controllers}
+
+ @PM.add_hook
+ def get_options(self):
+ return [Option(
+ name=self.OPTION_FMT.format(feature.value),
+ default=(feature not in PREDISABLED_FEATURES),
+ type='bool',) for feature in Features]
+
+ @PM.add_hook
+ def register_commands(self):
+ @CLICommand(
+ "dashboard feature",
+ "name=action,type=CephChoices,strings={} ".format(
+ "|".join(a.value for a in Actions))
+ + "name=features,type=CephChoices,strings={},req=false,n=N".format(
+ "|".join(f.value for f in Features)),
+ "Enable or disable features in Ceph-Mgr Dashboard")
+ def cmd(mgr, action, features=None):
+ ret = 0
+ msg = []
+ if action in [Actions.ENABLE.value, Actions.DISABLE.value]:
+ if features is None:
+ ret = 1
+ msg = ["At least one feature must be specified"]
+ else:
+ for feature in features:
+ mgr.set_module_option(
+ self.OPTION_FMT.format(feature),
+ action == Actions.ENABLE.value)
+ msg += ["Feature '{}': {}".format(
+ feature,
+ 'enabled' if action == Actions.ENABLE.value else
+ 'disabled')]
+ else:
+ for feature in features or [f.value for f in Features]:
+ enabled = mgr.get_module_option(self.OPTION_FMT.format(feature))
+ msg += ["Feature '{}': '{}'".format(
+ feature,
+ 'enabled' if enabled else 'disabled')]
+ return ret, '\n'.join(msg), ''
+ return {'handle_command': cmd}
+
+ def _get_feature_from_request(self, request):
+ try:
+ return self.Controller2Feature[
+ request.handler.callable.__self__]
+ except (AttributeError, KeyError):
+ return None
+
+ @ttl_cache(ttl=CACHE_TTL, maxsize=CACHE_MAX_SIZE)
+ def _is_feature_enabled(self, feature):
+ return self.mgr.get_module_option(self.OPTION_FMT.format(feature.value))
+
+ @PM.add_hook
+ def filter_request_before_handler(self, request):
+ feature = self._get_feature_from_request(request)
+ if feature is None:
+ return
+
+ if not self._is_feature_enabled(feature):
+ raise cherrypy.HTTPError(
+ 404, "Feature='{}' disabled by option '{}'".format(
+ feature.value,
+ self.OPTION_FMT.format(feature.value),
+ )
+ )
+
+ @PM.add_hook
+ def get_controllers(self):
+ from ..controllers import ApiController, RESTController
+
+ @ApiController('/feature_toggles')
+ class FeatureTogglesEndpoint(RESTController):
+
+ def list(_):
+ return {
+ feature.value: self._is_feature_enabled(feature)
+ for feature in Features
+ }
+ return [FeatureTogglesEndpoint]
diff --git a/src/pybind/mgr/dashboard/plugins/interfaces.py b/src/pybind/mgr/dashboard/plugins/interfaces.py
new file mode 100644
index 00000000..dbcd60a0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/plugins/interfaces.py
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from . import PLUGIN_MANAGER as PM, Interface, Mixin # pylint: disable=cyclic-import
+
+
+class CanMgr(Mixin):
+ from .. import mgr
+ mgr = mgr
+
+
+class CanLog(Mixin):
+ from .. import logger
+ log = logger
+
+
+class CanCherrypy(Mixin):
+ import cherrypy
+ request = cherrypy.request
+ response = cherrypy.response
+
+
+@PM.add_interface
+class Initializable(Interface):
+ @PM.add_abcspec
+ def init(self):
+ """
+ Placeholder for module scope initialization
+ """
+
+
+@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 HasControllers(Interface):
+ @PM.add_abcspec
+ def get_controllers(self): pass
+
+
+@PM.add_interface
+class ConfiguresCherryPy(Interface):
+ @PM.add_abcspec
+ def configure_cherrypy(self, config):
+ pass
+
+
+class FilterRequest(object):
+ @PM.add_interface
+ class BeforeHandler(Interface):
+ @PM.add_abcspec
+ def filter_request_before_handler(self, request): pass
+
+
+@PM.add_interface
+class ConfigNotify(Interface):
+ @PM.add_abcspec
+ def config_notify(self):
+ """
+ This method is called whenever a option of this mgr module has
+ been modified.
+ """
diff --git a/src/pybind/mgr/dashboard/plugins/lru_cache.py b/src/pybind/mgr/dashboard/plugins/lru_cache.py
new file mode 100644
index 00000000..19ad1b85
--- /dev/null
+++ b/src/pybind/mgr/dashboard/plugins/lru_cache.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+"""
+This is a minimal implementation of lru_cache function.
+
+Based on Python 3 functools and backports.functools_lru_cache.
+"""
+from __future__ import absolute_import
+
+from functools import wraps
+from collections import OrderedDict
+from threading import RLock
+
+
+def lru_cache(maxsize=128, typed=False):
+ if typed is not False:
+ raise NotImplementedError("typed caching not supported")
+
+ def decorating_function(function):
+ cache = OrderedDict()
+ stats = [0, 0]
+ rlock = RLock()
+ setattr(
+ function,
+ 'cache_info',
+ lambda:
+ "hits={}, misses={}, maxsize={}, currsize={}".format(
+ stats[0], stats[1], maxsize, len(cache)))
+
+ @wraps(function)
+ def wrapper(*args, **kwargs):
+ key = args + tuple(kwargs.items())
+ with rlock:
+ if key in cache:
+ ret = cache[key]
+ del cache[key]
+ cache[key] = ret
+ stats[0] += 1
+ else:
+ ret = function(*args, **kwargs)
+ if len(cache) == maxsize:
+ cache.popitem(last=False)
+ cache[key] = ret
+ stats[1] += 1
+ return ret
+
+ return wrapper
+ return decorating_function
diff --git a/src/pybind/mgr/dashboard/plugins/pluggy.py b/src/pybind/mgr/dashboard/plugins/pluggy.py
new file mode 100644
index 00000000..7517e6d6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/plugins/pluggy.py
@@ -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))
diff --git a/src/pybind/mgr/dashboard/plugins/plugin.py b/src/pybind/mgr/dashboard/plugins/plugin.py
new file mode 100644
index 00000000..0f52aefb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/plugins/plugin.py
@@ -0,0 +1,33 @@
+from mgr_module import Option, Command
+
+from . import PLUGIN_MANAGER as PM
+from . import interfaces as I # noqa: E741,N812
+
+
+class SimplePlugin(I.CanMgr, I.CanLog, I.HasOptions, I.HasCommands):
+ """
+ Helper class that provides simplified creation of plugins:
+ - Default Mixins/Interfaces: CanMgr, CanLog, HasOptions & HasCommands
+ - Options are defined by OPTIONS class variable, instead from get_options hook
+ - Commands are created with by COMMANDS list of Commands() and handlers
+ (less compact than CLICommand, but allows using method instances)
+ """
+ Option = Option
+ Command = Command
+
+ @PM.add_hook
+ def get_options(self):
+ return self.OPTIONS
+
+ @PM.final
+ def get_option(self, option):
+ return self.mgr.get_module_option(option)
+
+ @PM.final
+ def set_option(self, option, value):
+ self.mgr.set_module_option(option, value)
+
+ @PM.add_hook
+ def register_commands(self):
+ for cmd in self.COMMANDS:
+ cmd.register(instance=self)
diff --git a/src/pybind/mgr/dashboard/plugins/ttl_cache.py b/src/pybind/mgr/dashboard/plugins/ttl_cache.py
new file mode 100644
index 00000000..a0698542
--- /dev/null
+++ b/src/pybind/mgr/dashboard/plugins/ttl_cache.py
@@ -0,0 +1,55 @@
+"""
+This is a minimal implementation of TTL-ed lru_cache function.
+
+Based on Python 3 functools and backports.functools_lru_cache.
+"""
+from __future__ import absolute_import
+
+from functools import wraps
+from collections import OrderedDict
+from threading import RLock
+from time import time
+
+
+def ttl_cache(ttl, maxsize=128, typed=False):
+ if typed is not False:
+ raise NotImplementedError("typed caching not supported")
+
+ def decorating_function(function):
+ cache = OrderedDict()
+ stats = [0, 0, 0]
+ rlock = RLock()
+ setattr(
+ function,
+ 'cache_info',
+ lambda:
+ "hits={}, misses={}, expired={}, maxsize={}, currsize={}".format(
+ stats[0], stats[1], stats[2], maxsize, len(cache)))
+
+ @wraps(function)
+ def wrapper(*args, **kwargs):
+ key = args + tuple(kwargs.items())
+ with rlock:
+ refresh = True
+ if key in cache:
+ (ret, ts) = cache[key]
+ del cache[key]
+ if time() - ts < ttl:
+ refresh = False
+ stats[0] += 1
+ else:
+ stats[2] += 1
+
+ if refresh:
+ ret = function(*args, **kwargs)
+ ts = time()
+ if len(cache) == maxsize:
+ cache.popitem(last=False)
+ stats[1] += 1
+
+ cache[key] = (ret, ts)
+
+ return ret
+
+ return wrapper
+ return decorating_function