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
This commit is contained in:
parent
d7f29a54b2
commit
9b0c511ca6
@ -16,6 +16,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import glob
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@ -26,6 +27,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
|
|
||||||
from openstack_dashboard import exceptions
|
from openstack_dashboard import exceptions
|
||||||
from openstack_dashboard import theme_settings
|
from openstack_dashboard import theme_settings
|
||||||
|
from openstack_dashboard.utils import config
|
||||||
from openstack_dashboard.utils import settings as settings_utils
|
from openstack_dashboard.utils import settings as settings_utils
|
||||||
|
|
||||||
from horizon.utils.escape import monkeypatch_escape
|
from horizon.utils.escape import monkeypatch_escape
|
||||||
@ -339,11 +341,27 @@ OPENSTACK_PROFILER = {
|
|||||||
'enabled': False
|
'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:
|
try:
|
||||||
from local.local_settings import * # noqa: F403,H303
|
from local.local_settings import * # noqa: F403,H303
|
||||||
except ImportError:
|
except ImportError:
|
||||||
_LOG.warning("No local_settings file found.")
|
_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
|
# configure template debugging
|
||||||
TEMPLATES[0]['OPTIONS']['debug'] = DEBUG
|
TEMPLATES[0]['OPTIONS']['debug'] = DEBUG
|
||||||
|
|
||||||
|
@ -13,13 +13,15 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
from oslo_utils import uuidutils
|
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
|
from openstack_dashboard.utils import filters
|
||||||
|
|
||||||
|
|
||||||
class UtilsFilterTests(test.TestCase):
|
class UtilsFilterTests(unittest.TestCase):
|
||||||
def test_accept_valid_integer(self):
|
def test_accept_valid_integer(self):
|
||||||
val = 100
|
val = 100
|
||||||
ret = filters.get_int_or_uuid(val)
|
ret = filters.get_int_or_uuid(val)
|
||||||
@ -38,3 +40,31 @@ class UtilsFilterTests(test.TestCase):
|
|||||||
def test_reject_random_string(self):
|
def test_reject_random_string(self):
|
||||||
val = '55WbJTpJDf'
|
val = '55WbJTpJDf'
|
||||||
self.assertRaises(ValueError, filters.get_int_or_uuid, val)
|
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'))
|
||||||
|
68
openstack_dashboard/utils/config.py
Normal file
68
openstack_dashboard/utils/config.py
Normal file
@ -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()
|
||||||
|
]
|
213
openstack_dashboard/utils/config_types.py
Normal file
213
openstack_dashboard/utils/config_types.py
Normal file
@ -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'(?<!:)//')
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(URL, self).__init__('web URL')
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
if not isinstance(value, six.string_types):
|
||||||
|
raise ValueError("Expected URL.")
|
||||||
|
value = re.sub(self.CLEAN_SLASH_RE, '/', value)
|
||||||
|
if not value.endswith('/'):
|
||||||
|
value += '/'
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _formatter(self, value):
|
||||||
|
return self.quote_trailing_and_leading_space(value)
|
||||||
|
|
||||||
|
|
||||||
|
class Path(types.ConfigType):
|
||||||
|
"""A custom option type for a path to file."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(Path, self).__init__('filesystem path')
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
if not isinstance(value, six.string_types):
|
||||||
|
raise ValueError("Expected file path.")
|
||||||
|
return os.path.normpath(value)
|
||||||
|
|
||||||
|
def _formatter(self, value):
|
||||||
|
return self.quote_trailing_and_leading_space(value)
|
||||||
|
|
||||||
|
|
||||||
|
class Translate(types.ConfigType):
|
||||||
|
"""A custom option type for translatable strings."""
|
||||||
|
|
||||||
|
def __init__(self, hint=None):
|
||||||
|
self.hint = hint
|
||||||
|
super(Translate, self).__init__('translatable string')
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
if not isinstance(value, six.string_types):
|
||||||
|
return value
|
||||||
|
return pgettext_lazy(value, self.hint)
|
||||||
|
|
||||||
|
def _formatter(self, value):
|
||||||
|
return self.quote_trailing_and_leading_space(
|
||||||
|
encoding.force_text(value))
|
||||||
|
|
||||||
|
|
||||||
|
class Literal(types.ConfigType):
|
||||||
|
"""A custom option type for a Python literal."""
|
||||||
|
|
||||||
|
def __init__(self, spec=None):
|
||||||
|
self.spec = spec
|
||||||
|
super(Literal, self).__init__('python literal')
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
if isinstance(value, six.string_types):
|
||||||
|
try:
|
||||||
|
value = ast.literal_eval(value)
|
||||||
|
except SyntaxError as e:
|
||||||
|
six.raise_from(ValueError(e), e)
|
||||||
|
self.validate(value, self.spec)
|
||||||
|
return self.update(value, self.spec)
|
||||||
|
|
||||||
|
def validate(self, result, spec): # noqa Yes, it's too complex.
|
||||||
|
"""Validate that the result has the correct structure."""
|
||||||
|
if spec is None:
|
||||||
|
# None matches anything.
|
||||||
|
return
|
||||||
|
if isinstance(spec, dict):
|
||||||
|
if not isinstance(result, dict):
|
||||||
|
raise ValueError('Dictionary expected, but %r found.' % result)
|
||||||
|
if spec:
|
||||||
|
spec_value = next(iter(spec.values())) # Yay Python 3!
|
||||||
|
for value in result.values():
|
||||||
|
self.validate(value, spec_value)
|
||||||
|
spec_key = next(iter(spec.keys()))
|
||||||
|
for key in result.keys():
|
||||||
|
self.validate(key, spec_key)
|
||||||
|
if isinstance(spec, list):
|
||||||
|
if not isinstance(result, list):
|
||||||
|
raise ValueError('List expected, but %r found.' % result)
|
||||||
|
if spec:
|
||||||
|
for value in result:
|
||||||
|
self.validate(value, spec[0])
|
||||||
|
if isinstance(spec, tuple):
|
||||||
|
if not isinstance(result, tuple):
|
||||||
|
raise ValueError('Tuple expected, but %r found.' % result)
|
||||||
|
if len(result) != len(spec):
|
||||||
|
raise ValueError('Expected %d elements in tuple %r.' %
|
||||||
|
(len(spec), result))
|
||||||
|
for s, value in zip(spec, result):
|
||||||
|
self.validate(value, s)
|
||||||
|
if isinstance(spec, six.string_types):
|
||||||
|
if not isinstance(result, six.string_types):
|
||||||
|
raise ValueError('String expected, but %r found.' % result)
|
||||||
|
if isinstance(spec, int):
|
||||||
|
if not isinstance(result, int):
|
||||||
|
raise ValueError('Integer expected, but %r found.' % result)
|
||||||
|
if isinstance(spec, bool):
|
||||||
|
if not isinstance(result, bool):
|
||||||
|
raise ValueError('Boolean expected, but %r found.' % result)
|
||||||
|
|
||||||
|
def update(self, result, spec):
|
||||||
|
"""Replace elements with results of calling callables."""
|
||||||
|
if isinstance(spec, dict):
|
||||||
|
if spec:
|
||||||
|
spec_value = next(iter(spec.values()))
|
||||||
|
for key, value in result.items():
|
||||||
|
result[key] = self.update(value, spec_value)
|
||||||
|
if isinstance(spec, list):
|
||||||
|
if spec:
|
||||||
|
for i, value in enumerate(result):
|
||||||
|
result[i] = self.update(value, spec[0])
|
||||||
|
if isinstance(spec, tuple):
|
||||||
|
return tuple(self.update(value, s)
|
||||||
|
for s, value in zip(spec, result))
|
||||||
|
if callable(spec):
|
||||||
|
return spec(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _format(self, result):
|
||||||
|
if isinstance(result, dict):
|
||||||
|
return '{%s}' % ', '.join(
|
||||||
|
'%s: %s' % (key, self._format(value))
|
||||||
|
for key, value in result.items()
|
||||||
|
)
|
||||||
|
if isinstance(result, list):
|
||||||
|
return '[%s]' % ', '.join(self._format(value) for value in result)
|
||||||
|
if isinstance(result, tuple):
|
||||||
|
return '(%s)' % ', '.join(self._format(value) for value in result)
|
||||||
|
if isinstance(result, functional.Promise):
|
||||||
|
# Lazy translatable string.
|
||||||
|
return repr(encoding.force_text(result))
|
||||||
|
return repr(result)
|
||||||
|
|
||||||
|
def _formatter(self, value):
|
||||||
|
# We need to walk the lists and dicts to handle the Django lazy
|
||||||
|
# translatable strings inside.
|
||||||
|
return self._format(value)
|
||||||
|
|
||||||
|
|
||||||
|
class Importable(types.ConfigType):
|
||||||
|
"""A custom option type for an importable python object."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(Importable, self).__init__('importable python object')
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
if not isinstance(value, six.string_types):
|
||||||
|
# Already imported.
|
||||||
|
return value
|
||||||
|
try:
|
||||||
|
return import_string(value)
|
||||||
|
except ImportError as e:
|
||||||
|
six.raise_from(ValueError(e), e)
|
||||||
|
|
||||||
|
def _formatter(self, value):
|
||||||
|
module = value.__module__
|
||||||
|
name = value.__name__
|
||||||
|
return self.quote_trailing_and_leading_space('%s.%s' % (module, name))
|
@ -64,3 +64,7 @@ directory = reports
|
|||||||
[extract_messages]
|
[extract_messages]
|
||||||
keywords = gettext_noop gettext_lazy ngettext_lazy:1,2 ugettext_noop ugettext_lazy ungettext_lazy:1,2 npgettext:1c,2,3 pgettext_lazy:1c,2 npgettext_lazy:1c,2,3
|
keywords = gettext_noop gettext_lazy ngettext_lazy:1,2 ugettext_noop ugettext_lazy ungettext_lazy:1,2 npgettext:1c,2,3 pgettext_lazy:1c,2 npgettext_lazy:1c,2,3
|
||||||
add_comments = Translators:
|
add_comments = Translators:
|
||||||
|
|
||||||
|
[entry_points]
|
||||||
|
oslo.config.opts =
|
||||||
|
openstack_dashboard = openstack_dashboard.utils.config:list_options
|
||||||
|
Loading…
x
Reference in New Issue
Block a user