]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: feature-toggles: Add plugin
authorErnesto Puerta <epuertat@redhat.com>
Fri, 25 Jan 2019 21:43:23 +0000 (22:43 +0100)
committerErnesto Puerta <epuertat@redhat.com>
Wed, 6 Feb 2019 17:08:01 +0000 (18:08 +0100)
Add feature-toggles plugin. It allows to enable, disable and check
status of a feature. Features are disabled by making their
corresponding enpoints return HTTP 501 error (Not Implemented).

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/feature_toggles.py [new file with mode: 0644]

index 2cde09ff4ba8b9a79850928ca9469afdc7a0d3b9..268216b024db373443c5db764ea96bcdf297b3ea 100644 (file)
@@ -67,6 +67,7 @@ from .settings import options_command_list, options_schema_list, \
                       handle_option_command
 
 from .plugins import PLUGIN_MANAGER
+from .plugins import feature_toggles  # noqa # pylint: disable=unused-import
 
 
 # cherrypy likes to sys.exit on error.  don't let it take us down too!
diff --git a/src/pybind/mgr/dashboard/plugins/feature_toggles.py b/src/pybind/mgr/dashboard/plugins/feature_toggles.py
new file mode 100644 (file)
index 0000000..a8863f6
--- /dev/null
@@ -0,0 +1,168 @@
+# -*- 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
+
+
+try:
+    from functools import lru_cache
+except ImportError:
+    try:
+        from backports.functools_lru_cache import lru_cache
+    except ImportError:
+        """
+        This is a minimal implementation of lru_cache function.
+
+        Based on Python 3 functools and backports.functools_lru_cache.
+        """
+
+        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
+
+
+class Features(Enum):
+    RBD_IMAGES = 'rbd_images'
+    RBD_MIRRORING = 'rbd_mirroring'
+    RBD_ISCSI = 'rbd_iscsi'
+    CEPHFS = 'cephfs'
+    RGW = 'rgw'
+
+
+class Actions(Enum):
+    ENABLE = 'enable'
+    DISABLE = 'disable'
+    STATUS = 'status'
+
+
+PREDISABLED_FEATURES = set()
+
+Feature2Endpoint = {
+    Features.RBD_IMAGES: ["/api/block/image"],
+    Features.RBD_MIRRORING: ["/api/block/mirroring"],
+    Features.RBD_ISCSI: ["/api/tcmuiscsi"],
+    Features.CEPHFS: ["/api/cephfs"],
+    Features.RGW: ["/api/rgw"],
+}
+
+
+@PM.add_plugin
+class FeatureToggles(I.CanMgr, I.CanLog, I.Setupable, I.HasOptions,
+                     I.HasCommands, I.FilterRequest.BeforeHandler,
+                     I.HasEndpoints):
+    OPTION_FMT = 'FEATURE_TOGGLE_{}'
+    CACHE_MAX_SIZE = 128  # Optimum performance with 2^N sizes
+
+    @PM.add_hook
+    def setup(self):
+        url_prefix = self.mgr.get_module_option('url_prefix')
+        self.Endpoint2Feature = {
+            '{}{}'.format(url_prefix, endpoint): feature
+            for feature, endpoints in Feature2Endpoint.items()
+            for endpoint in endpoints}
+
+    @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 _(mgr, action, features=None):
+            ret = 0
+            msg = []
+            if action in [Actions.ENABLE.value, Actions.DISABLE.value]:
+                if features is None:
+                    ret = 1
+                    msg = ["Feature '{}' requires at least a feature specified".format(
+                        action)]
+                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), ''
+
+    @lru_cache(maxsize=CACHE_MAX_SIZE)
+    def __get_feature_from_path(self, path):
+        for endpoint in self.Endpoint2Feature:
+            if path.startswith(endpoint):
+                return self.Endpoint2Feature[endpoint]
+        return None
+
+    @PM.add_hook
+    def filter_request_before_handler(self, request):
+        feature = self.__get_feature_from_path(request.path_info)
+
+        if feature is None:
+            return
+
+        if self.mgr.get_module_option(self.OPTION_FMT.format(feature.value)) is False:
+            raise cherrypy.HTTPError(
+                501, "Feature='{}' (path='{}') disabled by option '{}'".format(
+                    feature.value,
+                    request.path_info,
+                    self.OPTION_FMT.format(feature.value)
+                    ))
+
+    @PM.add_hook
+    def register_endpoints(self):
+        pass