From 9b0c511ca6bdba61676559c5ac76596cdce4d2fa Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Wed, 30 Nov 2016 12:15:36 +0100 Subject: [PATCH] Use oslo.config for Horizon configuration This patch adds the infrastructure needed to move the configuration of Horizon into oslo.config-compatible configuration file, instead of the Django's Python-based configuration. It doesn't actually define any configuration options, just the mechanism for loading them and the additional types necessary to handle Horizon's complex configuration, and the integration with oslo-config-generator. Subsequent patches will add groups of options, making it possible to use them in the local_settings.conf file instead of the local_settings.py file. Note, that the options specified in the local_settings.py file will continue to work. Partially-Implements: blueprint ini-based-configuration Change-Id: I2ed79ef0c6ac6d3816bba13346b7d001c46a7e80 --- openstack_dashboard/settings.py | 18 ++ openstack_dashboard/test/tests/utils.py | 34 +++- openstack_dashboard/utils/config.py | 68 +++++++ openstack_dashboard/utils/config_types.py | 213 ++++++++++++++++++++++ setup.cfg | 4 + 5 files changed, 335 insertions(+), 2 deletions(-) create mode 100644 openstack_dashboard/utils/config.py create mode 100644 openstack_dashboard/utils/config_types.py diff --git a/openstack_dashboard/settings.py b/openstack_dashboard/settings.py index ba82951045..06f8aed3d4 100644 --- a/openstack_dashboard/settings.py +++ b/openstack_dashboard/settings.py @@ -16,6 +16,7 @@ # License for the specific language governing permissions and limitations # under the License. +import glob import logging import os import sys @@ -26,6 +27,7 @@ from django.utils.translation import ugettext_lazy as _ from openstack_dashboard import exceptions from openstack_dashboard import theme_settings +from openstack_dashboard.utils import config from openstack_dashboard.utils import settings as settings_utils from horizon.utils.escape import monkeypatch_escape @@ -339,11 +341,27 @@ OPENSTACK_PROFILER = { 'enabled': False } +if not LOCAL_PATH: + LOCAL_PATH = os.path.join(ROOT_PATH, 'local') +LOCAL_SETTINGS_DIR_PATH = os.path.join(LOCAL_PATH, "local_settings.d") + +_files = glob.glob(os.path.join(LOCAL_PATH, 'local_settings.conf')) +_files.extend( + sorted(glob.glob(os.path.join(LOCAL_SETTINGS_DIR_PATH, '*.conf')))) +_config = config.load_config(_files, ROOT_PATH, LOCAL_PATH) + +# Apply the general configuration. +config.apply_config(_config, globals()) + try: from local.local_settings import * # noqa: F403,H303 except ImportError: _LOG.warning("No local_settings file found.") +# configure templates +if not TEMPLATES[0]['DIRS']: + TEMPLATES[0]['DIRS'] = [os.path.join(ROOT_PATH, 'templates')] + # configure template debugging TEMPLATES[0]['OPTIONS']['debug'] = DEBUG diff --git a/openstack_dashboard/test/tests/utils.py b/openstack_dashboard/test/tests/utils.py index 0c6a4a917a..9534019923 100644 --- a/openstack_dashboard/test/tests/utils.py +++ b/openstack_dashboard/test/tests/utils.py @@ -13,13 +13,15 @@ # License for the specific language governing permissions and limitations # under the License. +import unittest + from oslo_utils import uuidutils -from openstack_dashboard.test import helpers as test +from openstack_dashboard.utils import config_types from openstack_dashboard.utils import filters -class UtilsFilterTests(test.TestCase): +class UtilsFilterTests(unittest.TestCase): def test_accept_valid_integer(self): val = 100 ret = filters.get_int_or_uuid(val) @@ -38,3 +40,31 @@ class UtilsFilterTests(test.TestCase): def test_reject_random_string(self): val = '55WbJTpJDf' self.assertRaises(ValueError, filters.get_int_or_uuid, val) + + +class ConfigTypesTest(unittest.TestCase): + def test_literal(self): + literal = config_types.Literal([0]) + self.assertEqual([1, 2, 3], literal("[1, 2, 3]")) + self.assertRaises(ValueError, literal, "[1, '2', 3]") + + literal = config_types.Literal({0: ""}) + self.assertEqual({1: 'a', 2: u'b'}, literal("{1: 'a', 2: u'b'}")) + self.assertRaises(ValueError, literal, "[1, '2', 3]") + self.assertRaises(ValueError, literal, "{1: 1, '2': 2}") + + literal = config_types.Literal((True, 1, "")) + self.assertEqual((True, 13, 'x'), literal("(True, 13, 'x')")) + self.assertRaises(ValueError, literal, "(True, True)") + self.assertRaises(ValueError, literal, "(True, True, False, False)") + self.assertRaises(ValueError, literal, "(2, True, 'a')") + self.assertRaises(ValueError, literal, "(") + + literal = config_types.Literal([(True, 1, lambda s: s.upper())]) + self.assertEqual([(True, 13, 'X')], literal("[(True, 13, 'x')]")) + + def test_url(self): + url = config_types.URL() + self.assertEqual('/webroot/static/', url('/webroot//static')) + self.assertEqual('http://webroot/static/', + url('http://webroot//static')) diff --git a/openstack_dashboard/utils/config.py b/openstack_dashboard/utils/config.py new file mode 100644 index 0000000000..32d4566179 --- /dev/null +++ b/openstack_dashboard/utils/config.py @@ -0,0 +1,68 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +This module contains utility functions for loading Horizon's +configuration from .ini files using the oslo.config library. +""" + +import six + +from oslo_config import cfg + +# XXX import the actual config groups here +# from openstack_dashboard.config import config_compress + + +def load_config(files=None, root_path=None, local_path=None): + """Load the configuration from specified files.""" + + config = cfg.ConfigOpts() + config.register_opts([ + cfg.Opt('root_path', default=root_path), + cfg.Opt('local_path', default=local_path), + ]) + # XXX register actual config groups here + # theme_group = config_theme.register_config(config) + if files is not None: + config(args=[], default_config_files=files) + return config + + +def apply_config(config, target): + """Apply the configuration on the specified settings module.""" + # TODO(rdopiera) fill with actual config groups + # apply_config_group(config.email, target, 'email') + + +def apply_config_group(config_group, target, prefix=None): + for key, value in six.iteritems(config_group): + name = key.upper() + if prefix: + name = '_'.join([prefix.upper(), name]) + target[name] = value + + +def list_options(): + # This is a really nasty hack to make the translatable strings + # work without having to initialize Django and read all the settings. + from django.apps import registry + from django.conf import settings + + settings.configure() + registry.apps.check_apps_ready = lambda: True + + config = load_config() + return [ + (name, [d['opt'] for d in group._opts.values()]) + for (name, group) in config._groups.items() + ] diff --git a/openstack_dashboard/utils/config_types.py b/openstack_dashboard/utils/config_types.py new file mode 100644 index 0000000000..10a50d0459 --- /dev/null +++ b/openstack_dashboard/utils/config_types.py @@ -0,0 +1,213 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +A set of custom types for oslo.config. +""" + +import ast +import os +import re + +import six + +from django.utils import encoding +from django.utils import functional +from django.utils.module_loading import import_string +from django.utils.translation import pgettext_lazy +from oslo_config import types + + +class Maybe(types.ConfigType): + """A custom option type for a value that may be None.""" + + def __init__(self, type_): + self.type_ = type_ + type_name = getattr(type_, 'type_name', 'unknown value') + super(Maybe, self).__init__('optional %s' % type_name) + + def __call__(self, value): + if value is None: + return None + return self.type_(value) + + def _formatter(self, value): + if value is None: + return '' + return self.type_._formatter(value) + + +class URL(types.ConfigType): + """A custom option type for a URL or part of URL.""" + + CLEAN_SLASH_RE = re.compile(r'(?