]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard/backend: API to manage dashboard settings 22457/head
authorPatrick Nawracay <pnawracay@suse.com>
Thu, 7 Jun 2018 11:49:39 +0000 (13:49 +0200)
committerPatrick Nawracay <pnawracay@suse.com>
Fri, 10 Aug 2018 09:32:25 +0000 (11:32 +0200)
Enables to change (set/unset) values of settings of the dashboard using
the REST API.

Fixes: https://tracker.ceph.com/issues/24273
Signed-off-by: Patrick Nawracay <pnawracay@suse.com>
doc/mgr/dashboard.rst
qa/tasks/mgr/dashboard/test_settings.py [new file with mode: 0644]
src/pybind/mgr/dashboard/controllers/settings.py [new file with mode: 0644]
src/pybind/mgr/dashboard/security.py
src/pybind/mgr/dashboard/services/access_control.py
src/pybind/mgr/dashboard/tests/helper.py
src/pybind/mgr/dashboard/tests/test_settings.py

index c0fb1b7cb0cdcdc21f48df58eb763622c60f1526..e058fdc230ac472cca0cdc44a5aee45e80cd5126 100644 (file)
@@ -395,6 +395,7 @@ scopes are:
   management.
 - **log**: include all features related to Ceph logs management.
 - **grafana**: include all features related to Grafana proxy.
+- **dashboard-settings**: allows to change dashboard settings.
 
 A *role* specifies a set of mappings between a *security scope* and a set of
 *permissions*. There are four types of permissions:
@@ -427,7 +428,8 @@ installation.
 The list of system roles are:
 
 - **administrator**: provides full permissions for all security scopes.
-- **read-only**: provides *read* permission for all security scopes.
+- **read-only**: provides *read* permission for all security scopes except
+  the dashboard settings.
 - **block-manager**: provides full permissions for *rbd-image*,
   *rbd-mirroring*, and *iscsi* scopes.
 - **rgw-manager**: provides full permissions for the *rgw* scope
diff --git a/qa/tasks/mgr/dashboard/test_settings.py b/qa/tasks/mgr/dashboard/test_settings.py
new file mode 100644 (file)
index 0000000..5badf6a
--- /dev/null
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, JList, JObj, JAny
+from pprint import pformat
+
+
+class SettingsTest(DashboardTestCase):
+    def setUp(self):
+        self.settings = self._get('/api/settings')
+
+    def tearDown(self):
+        self._put(
+            '/api/settings',
+            {setting['name']: setting['value']
+             for setting in self.settings})
+
+    def test_list_settings(self):
+        settings = self._get('/api/settings')
+        self.assertGreater(len(settings), 10)
+        self.assertSchema(
+            settings,
+            JList(
+                JObj({
+                    'default': JAny(none=False),
+                    'name': str,
+                    'type': str,
+                    'value': JAny(none=False)
+                })))
+        self.assertStatus(200)
+
+    def test_get_setting(self):
+        setting = self._get('/api/settings/rgw-api-access-key')
+        self.assertSchema(
+            setting,
+            JObj({
+                'default': JAny(none=False),
+                'name': str,
+                'type': str,
+                'value': JAny(none=False)
+            }))
+        self.assertStatus(200)
+
+    def test_set_setting(self):
+        self._put('/api/settings/rgw-api-access-key', {'value': 'foo'})
+        self.assertStatus(200)
+
+        value = self._get('/api/settings/rgw-api-access-key')['value']
+        self.assertEqual('foo', value)
+
+    def test_bulk_set(self):
+        self._put('/api/settings', {
+            'RGW_API_HOST': 'somehost',
+            'RGW_API_PORT': 7777,
+        })
+        self.assertStatus(200)
+
+        host = self._get('/api/settings/rgw-api-host')['value']
+        self.assertStatus(200)
+        self.assertEqual('somehost', host)
+
+        port = self._get('/api/settings/rgw-api-port')['value']
+        self.assertStatus(200)
+        self.assertEqual(7777, port)
diff --git a/src/pybind/mgr/dashboard/controllers/settings.py b/src/pybind/mgr/dashboard/controllers/settings.py
new file mode 100644 (file)
index 0000000..2f2ec3e
--- /dev/null
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+from contextlib import contextmanager
+
+import cherrypy
+
+
+from . import ApiController, RESTController
+from ..settings import Settings as SettingsModule, Options
+from ..security import Scope
+
+
+@ApiController('/settings', Scope.CONFIG_OPT)
+class Settings(RESTController):
+    """
+    Enables to manage the settings of the dashboard (not the Ceph cluster).
+    """
+    @contextmanager
+    def _attribute_handler(self, name):
+        """
+        :type name: str|dict[str, str]
+        :rtype: str|dict[str, str]
+        """
+        if isinstance(name, dict):
+            result = {self._to_native(key): value
+                      for key, value in name.items()}
+        else:
+            result = self._to_native(name)
+
+        try:
+            yield result
+        except AttributeError:
+            raise cherrypy.NotFound(result)
+
+    @staticmethod
+    def _to_native(setting):
+        return setting.upper().replace('-', '_')
+
+    def list(self):
+        return [
+            self._get(name) for name in Options.__dict__
+            if name.isupper() and not name.startswith('_')
+        ]
+
+    def _get(self, name):
+        with self._attribute_handler(name) as sname:
+            default, data_type = getattr(Options, sname)
+        return {
+            'name': sname,
+            'default': default,
+            'type': data_type.__name__,
+            'value': getattr(SettingsModule, sname)
+        }
+
+    def get(self, name):
+        return self._get(name)
+
+    def set(self, name, value):
+        with self._attribute_handler(name) as sname:
+            setattr(SettingsModule, self._to_native(sname), value)
+
+    def delete(self, name):
+        with self._attribute_handler(name) as sname:
+            delattr(SettingsModule, self._to_native(sname))
+
+    def bulk_set(self, **kwargs):
+        with self._attribute_handler(kwargs) as data:
+            for name, value in data.items():
+                setattr(SettingsModule, self._to_native(name), value)
index bd2b0471e92cc939e4022937f02b1d7b59ed7a1c..64209e7f445c2797f7299bb80c4e857e9237fc50 100644 (file)
@@ -24,6 +24,7 @@ class Scope(object):
     LOG = "log"
     GRAFANA = "grafana"
     USER = "user"
+    DASHBOARD_SETTINGS = "dashboard-settings"
 
     @classmethod
     def all_scopes(cls):
index f980d6e932a3879865dd8cbf7d520ba9acd553cf..abe5a422320247e1313822779a0676374499788c 100644 (file)
@@ -94,6 +94,7 @@ ADMIN_ROLE = Role('administrator', 'Administrator', dict([
 # read-only role provides read-only permission for all scopes
 READ_ONLY_ROLE = Role('read-only', 'Read-Only', dict([
     (scope_name, [_P.READ]) for scope_name in Scope.all_scopes()
+    if scope_name != Scope.DASHBOARD_SETTINGS
 ]))
 
 
index 8a0064b90f60a574f44f7b8d7f2c53f64c914463..23c0def78f14c725ba6524363057c805bdba91fd 100644 (file)
@@ -155,3 +155,10 @@ class ControllerTestCase(helper.CPWebCase):
                 msg = 'expected body:\n%r\n\nactual body:\n%r' % (
                     data, json_body)
             self._handlewebError(msg)
+
+    def assertInJsonBody(self, data, msg=None):
+        json_body = self.jsonBody()
+        if data not in json_body:
+            if msg is None:
+                msg = 'expected %r to be in %r' % (data, json_body)
+            self._handlewebError(msg)
index 0087f7e6d59e9b96f16c619b92a5ce55b87dd85a..a29931cc956acac22b11de43048b213b240205d5 100644 (file)
@@ -3,10 +3,11 @@ from __future__ import absolute_import
 
 import errno
 import unittest
-
 from .. import mgr
 from .. import settings
+from ..controllers.settings import Settings as SettingsController
 from ..settings import Settings, handle_option_command
+from .helper import ControllerTestCase
 
 
 class SettingsTest(unittest.TestCase):
@@ -105,3 +106,79 @@ class SettingsTest(unittest.TestCase):
 
         self.assertEqual(str(ctx.exception),
                          "type object 'Options' has no attribute 'NON_EXISTENT_OPTION'")
+
+
+class SettingsControllerTest(ControllerTestCase):
+    config_values = {}
+
+    @classmethod
+    def setup_server(cls):
+        # pylint: disable=protected-access
+
+        SettingsController._cp_config['tools.authenticate.on'] = False
+        cls.setup_controllers([SettingsController])
+
+    @classmethod
+    def mock_set_config(cls, attr, val):
+        cls.config_values[attr] = val
+
+    @classmethod
+    def mock_get_config(cls, attr, default):
+        return cls.config_values.get(attr, default)
+
+    def setUp(self):
+        self.config_values.clear()
+        mgr.set_config.side_effect = self.mock_set_config
+        mgr.get_config.side_effect = self.mock_get_config
+
+    def test_settings_list(self):
+        self._get('/api/settings')
+        data = self.jsonBody()
+        self.assertTrue(len(data) > 0)
+        self.assertStatus(200)
+        self.assertIn('default', data[0].keys())
+        self.assertIn('type', data[0].keys())
+        self.assertIn('name', data[0].keys())
+        self.assertIn('value', data[0].keys())
+
+    def test_rgw_daemon_get(self):
+        self._get('/api/settings/grafana-api-username')
+        self.assertStatus(200)
+        self.assertJsonBody({
+            u'default': u'admin',
+            u'type': u'str',
+            u'name': u'GRAFANA_API_USERNAME',
+            u'value': u'admin',
+        })
+
+    def test_set(self):
+        self._put('/api/settings/GRAFANA_API_USERNAME', {'value': 'foo'},)
+        self.assertStatus(200)
+
+        self._get('/api/settings/GRAFANA_API_USERNAME')
+        self.assertStatus(200)
+        self.assertInJsonBody('default')
+        self.assertInJsonBody('type')
+        self.assertInJsonBody('name')
+        self.assertInJsonBody('value')
+        self.assertEqual(self.jsonBody()['value'], 'foo')
+
+    def test_bulk_set(self):
+        self._put('/api/settings', {
+            'GRAFANA_API_USERNAME': 'foo',
+            'GRAFANA_API_HOST': 'somehost',
+        })
+        self.assertStatus(200)
+
+        self._get('/api/settings/grafana-api-username')
+        self.assertStatus(200)
+        body = self.jsonBody()
+        self.assertEqual(body['value'], 'foo')
+
+        self._get('/api/settings/grafana-api-username')
+        self.assertStatus(200)
+        self.assertEqual(self.jsonBody()['value'], 'foo')
+
+        self._get('/api/settings/grafana-api-host')
+        self.assertStatus(200)
+        self.assertEqual(self.jsonBody()['value'], 'somehost')