From: Ricardo Dias Date: Mon, 29 Jan 2018 16:10:50 +0000 (+0000) Subject: mgr/dashboard: added settings module X-Git-Tag: v13.0.2~54^2~1 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=f681b23280e45cfb845238757985b3042c215013;p=ceph.git mgr/dashboard: added settings module Signed-off-by: Ricardo Dias --- diff --git a/src/pybind/mgr/dashboard_v2/module.py b/src/pybind/mgr/dashboard_v2/module.py index 502e2c5d1002..6e2ea8aaf05f 100644 --- a/src/pybind/mgr/dashboard_v2/module.py +++ b/src/pybind/mgr/dashboard_v2/module.py @@ -24,6 +24,7 @@ from . import logger, mgr from .controllers.auth import Auth from .tools import load_controllers, json_error_page, SessionExpireAtBrowserCloseTool, \ NotificationQueue +from .settings import options_command_list, handle_option_command # cherrypy likes to sys.exit on error. don't let it take us down too! @@ -64,6 +65,7 @@ class Module(MgrModule): 'perm': 'w' } ] + COMMANDS.extend(options_command_list()) @property def url_prefix(self): @@ -144,6 +146,9 @@ class Module(MgrModule): logger.info('Stopped server') def handle_command(self, cmd): + res = handle_option_command(cmd) + if res[0] != -errno.ENOSYS: + return res if cmd['prefix'] == 'dashboard set-login-credentials': Auth.set_login_credentials(cmd['username'], cmd['password']) return 0, 'Username and password updated', '' diff --git a/src/pybind/mgr/dashboard_v2/settings.py b/src/pybind/mgr/dashboard_v2/settings.py new file mode 100644 index 000000000000..4f68fbb46ffd --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/settings.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +import errno +import inspect +from six import add_metaclass + +from . import mgr + + +class Options(object): + """ + If you need to store some configuration value please add the config option + name as a class attribute to this class. + + Example:: + + GRAFANA_API_HOST = ('localhost', str) + GRAFANA_API_PORT = (3000, int) + """ + pass + + +class SettingsMeta(type): + def __getattr__(cls, attr): + default, stype = getattr(Options, attr) + return stype(mgr.get_config(attr, default)) + + def __setattr__(cls, attr, value): + if not attr.startswith('_') and hasattr(Options, attr): + mgr.set_config(attr, str(value)) + else: + setattr(SettingsMeta, attr, value) + + +# pylint: disable=no-init +@add_metaclass(SettingsMeta) +class Settings(object): + pass + + +def _options_command_map(): + def filter_attr(member): + return not inspect.isroutine(member) + + cmd_map = {} + for option, value in inspect.getmembers(Options, filter_attr): + if option.startswith('_'): + continue + key_get = 'dashboard get-{}'.format(option.lower().replace('_', '-')) + key_set = 'dashboard set-{}'.format(option.lower().replace('_', '-')) + cmd_map[key_get] = {'name': option, 'type': None} + cmd_map[key_set] = {'name': option, 'type': value[1]} + return cmd_map + + +_OPTIONS_COMMAND_MAP = _options_command_map() + + +def options_command_list(): + """ + This function generates a list of ``get`` and ``set`` commands + for each declared configuration option in class ``Options``. + """ + def py2ceph(pytype): + if pytype == str: + return 'CephString' + elif pytype == int: + return 'CephInt' + return 'CephString' + + cmd_list = [] + for cmd, opt in _OPTIONS_COMMAND_MAP.items(): + if not opt['type']: + cmd_list.append({ + 'cmd': '{}'.format(cmd), + 'desc': 'Get the {} option value'.format(opt['name']), + 'perm': 'r' + }) + else: + cmd_list.append({ + 'cmd': '{} name=value,type={}' + .format(cmd, py2ceph(opt['type'])), + 'desc': 'Set the {} option value'.format(opt['name']), + 'perm': 'w' + }) + + return cmd_list + + +def handle_option_command(cmd): + if cmd['prefix'] not in _OPTIONS_COMMAND_MAP: + return (-errno.ENOSYS, '', "Command not found '{}'".format(cmd['prefix'])) + + opt = _OPTIONS_COMMAND_MAP[cmd['prefix']] + if not opt['type']: + # get option + return 0, str(getattr(Settings, opt['name'])), '' + + # set option + setattr(Settings, opt['name'], opt['type'](cmd['value'])) + return 0, 'Option {} updated'.format(opt['name']), '' diff --git a/src/pybind/mgr/dashboard_v2/tests/test_settings.py b/src/pybind/mgr/dashboard_v2/tests/test_settings.py new file mode 100644 index 000000000000..92fcf7ff349e --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/tests/test_settings.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +import errno +import unittest + +from .. import mgr +from .. import settings +from ..settings import Settings, handle_option_command + + +class SettingsTest(unittest.TestCase): + CONFIG_KEY_DICT = {} + + @classmethod + def setUpClass(cls): + # pylint: disable=protected-access + settings.Options.GRAFANA_API_HOST = ('localhost', str) + settings.Options.GRAFANA_API_PORT = (3000, int) + settings._OPTIONS_COMMAND_MAP = settings._options_command_map() + + @classmethod + def mock_set_config(cls, attr, val): + cls.CONFIG_KEY_DICT[attr] = val + + @classmethod + def mock_get_config(cls, attr, default): + return cls.CONFIG_KEY_DICT.get(attr, default) + + def setUp(self): + self.CONFIG_KEY_DICT.clear() + mgr.set_config.side_effect = self.mock_set_config + mgr.get_config.side_effect = self.mock_get_config + if Settings.GRAFANA_API_HOST != 'localhost': + Settings.GRAFANA_API_HOST = 'localhost' + if Settings.GRAFANA_API_PORT != 3000: + Settings.GRAFANA_API_PORT = 3000 + + def test_get_setting(self): + self.assertEqual(Settings.GRAFANA_API_HOST, 'localhost') + + def test_set_setting(self): + Settings.GRAFANA_API_HOST = 'grafanahost' + self.assertEqual(Settings.GRAFANA_API_HOST, 'grafanahost') + + def test_get_cmd(self): + r, out, err = handle_option_command( + {'prefix': 'dashboard get-grafana-api-port'}) + self.assertEqual(r, 0) + self.assertEqual(out, '3000') + self.assertEqual(err, '') + + def test_set_cmd(self): + r, out, err = handle_option_command( + {'prefix': 'dashboard set-grafana-api-port', + 'value': '4000'}) + self.assertEqual(r, 0) + self.assertEqual(out, 'Option GRAFANA_API_PORT updated') + self.assertEqual(err, '') + + def test_inv_cmd(self): + r, out, err = handle_option_command( + {'prefix': 'dashboard get-non-existent-option'}) + self.assertEqual(r, -errno.ENOSYS) + self.assertEqual(out, '') + self.assertEqual(err, "Command not found " + "'dashboard get-non-existent-option'") + + def test_sync(self): + Settings.GRAFANA_API_PORT = 5000 + r, out, err = handle_option_command( + {'prefix': 'dashboard get-grafana-api-port'}) + self.assertEqual(r, 0) + self.assertEqual(out, '5000') + self.assertEqual(err, '') + r, out, err = handle_option_command( + {'prefix': 'dashboard set-grafana-api-host', + 'value': 'new-local-host'}) + self.assertEqual(r, 0) + self.assertEqual(out, 'Option GRAFANA_API_HOST updated') + self.assertEqual(err, '') + self.assertEqual(Settings.GRAFANA_API_HOST, 'new-local-host') + + def test_attribute_error(self): + with self.assertRaises(AttributeError) as ctx: + _ = Settings.NON_EXISTENT_OPTION + + self.assertEqual(str(ctx.exception), + "type object 'Options' has no attribute 'NON_EXISTENT_OPTION'")