summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/plugins/feature_toggles.py
blob: fc4619be3f9f8fd1ff3b99f0444a4a991bd0efa7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# -*- coding: utf-8 -*-
from __future__ import absolute_import

from enum import Enum
from typing import List, Optional

import cherrypy
from mgr_module import CLICommand, Option

from ..controllers.cephfs import CephFS
from ..controllers.iscsi import Iscsi, IscsiTarget
from ..controllers.nfs import NFSGaneshaExports, NFSGaneshaUi
from ..controllers.rbd import Rbd, RbdSnapshot, RbdTrash
from ..controllers.rbd_mirroring import RbdMirroringPoolMode, \
    RbdMirroringPoolPeer, RbdMirroringSummary
from ..controllers.rgw import Rgw, RgwBucket, RgwDaemon, RgwUser
from . import PLUGIN_MANAGER as PM
from . import interfaces as I  # noqa: E741,N812
from .ttl_cache import ttl_cache

try:
    from typing import Set, no_type_check
except ImportError:
    no_type_check = object()  # Just for type checking


class Features(Enum):
    RBD = 'rbd'
    MIRRORING = 'mirroring'
    ISCSI = 'iscsi'
    CEPHFS = 'cephfs'
    RGW = 'rgw'
    NFS = 'nfs'


PREDISABLED_FEATURES = set()  # type: Set[str]

Feature2Controller = {
    Features.RBD: [Rbd, RbdSnapshot, RbdTrash],
    Features.MIRRORING: [
        RbdMirroringSummary, RbdMirroringPoolMode, RbdMirroringPoolPeer],
    Features.ISCSI: [Iscsi, IscsiTarget],
    Features.CEPHFS: [CephFS],
    Features.RGW: [Rgw, RgwDaemon, RgwBucket, RgwUser],
    Features.NFS: [NFSGaneshaUi, NFSGaneshaExports],
}


class Actions(Enum):
    ENABLE = 'enable'
    DISABLE = 'disable'
    STATUS = 'status'


# pylint: disable=too-many-ancestors
@PM.add_plugin
class FeatureToggles(I.CanMgr, I.Setupable, I.HasOptions,
                     I.HasCommands, I.FilterRequest.BeforeHandler,
                     I.HasControllers):
    OPTION_FMT = 'FEATURE_TOGGLE_{.name}'
    CACHE_MAX_SIZE = 128  # Optimum performance with 2^N sizes
    CACHE_TTL = 10  # seconds

    @PM.add_hook
    def setup(self):
        # pylint: disable=attribute-defined-outside-init
        self.Controller2Feature = {
            controller: feature
            for feature, controllers in Feature2Controller.items()
            for controller in controllers}  # type: ignore

    @PM.add_hook
    def get_options(self):
        return [Option(
            name=self.OPTION_FMT.format(feature),
            default=(feature not in PREDISABLED_FEATURES),
            type='bool',) for feature in Features]

    @PM.add_hook
    def register_commands(self):
        @CLICommand("dashboard feature")
        def cmd(mgr,
                action: Actions = Actions.STATUS,
                features: Optional[List[Features]] = None):
            '''
            Enable or disable features in Ceph-Mgr Dashboard
            '''
            ret = 0
            msg = []
            if action in [Actions.ENABLE, Actions.DISABLE]:
                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)
                        msg += ["Feature '{.value}': {}".format(
                            feature,
                            'enabled' if action == Actions.ENABLE else
                            'disabled')]
            else:
                for feature in features or list(Features):
                    enabled = mgr.get_module_option(self.OPTION_FMT.format(feature))
                    msg += ["Feature '{.value}': {}".format(
                        feature,
                        'enabled' if enabled else 'disabled')]
            return ret, '\n'.join(msg), ''
        return {'handle_command': cmd}

    @no_type_check  # https://github.com/python/mypy/issues/7806
    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)
    @no_type_check  # https://github.com/python/mypy/issues/7806
    def _is_feature_enabled(self, feature):
        return self.mgr.get_module_option(self.OPTION_FMT.format(feature))

    @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),
                )
            )

    @PM.add_hook
    def get_controllers(self):
        from ..controllers import APIDoc, APIRouter, EndpointDoc, RESTController

        FEATURES_SCHEMA = {
            "rbd": (bool, ''),
            "mirroring": (bool, ''),
            "iscsi": (bool, ''),
            "cephfs": (bool, ''),
            "rgw": (bool, ''),
            "nfs": (bool, '')
        }

        @APIRouter('/feature_toggles')
        @APIDoc("Manage Features API", "FeatureTogglesEndpoint")
        class FeatureTogglesEndpoint(RESTController):
            @EndpointDoc("Get List Of Features",
                         responses={200: FEATURES_SCHEMA})
            def list(_):  # pylint: disable=no-self-argument  # noqa: N805
                return {
                    # pylint: disable=protected-access
                    feature.value: self._is_feature_enabled(feature)
                    for feature in Features
                }
        return [FeatureTogglesEndpoint]