Plugin-based dashboard configuration
This is a proof of concept of how plugin-based configuration of OpenStack dashboards could be realized. Additional plugins add their configuration file in the opestack_dashboard/enabled/ directory (the exact location is up for discussion, we will probably want a whole hierarchy of locations, with the possibility of overriding settings). An example file _50_tuskar.py is added to show how an additional dashboard could plug into this system. If you have tuskar_ui installed, this would add Tuskar to Horizon. Change-Id: Iaa594deffbdb865116f6f62855c32c8633d2b208 Implements: blueprint plugin-architecture
This commit is contained in:
parent
efc88d4078
commit
303c354fd6
@ -338,3 +338,75 @@ generate a secret key for a single installation.
|
||||
These three settings should be configured if you are deploying Horizon with
|
||||
SSL. The values indicated in the default ``local_settings.py.example`` file
|
||||
are generally safe to use.
|
||||
|
||||
|
||||
Pluggable Settings for Dashboards
|
||||
=================================
|
||||
|
||||
Many dashboards may require their own modifications to the settings, and their
|
||||
installation would therefore require modifying the settings file. This is not
|
||||
optimal, so the dashboards can provide the settings that they require in a
|
||||
separate file. Those files are read at startup and used to modify the default
|
||||
settings.
|
||||
|
||||
The default location for the dashboard configuration files is
|
||||
``openstack_dashboard/enabled``, with another directory,
|
||||
``openstack_dashboarrd/local/enabled`` for local overrides. Both sets of files
|
||||
will be loaded, but the settings in ``openstack_dashboard/local/enabled`` will
|
||||
overwrite the default ones. The settings are applied in alphabetical order of
|
||||
the filenames. If the same dashboard has configuration files in ``enabled`` and
|
||||
``local/enabled``, the local name will be used. Note, that since names of
|
||||
python modules can't start with a digit, the files are usually named with a
|
||||
leading underscore and a number, so that you can control their order easily.
|
||||
|
||||
The files contain following keys:
|
||||
|
||||
``DASHBOARD``
|
||||
-------------
|
||||
|
||||
The name of the dashboard to be added to ``HORIZON['dashboards']``. Required.
|
||||
|
||||
``DEFAULT``
|
||||
-----------
|
||||
|
||||
If set to ``True``, this dashboard will be set as the default dashboard.
|
||||
|
||||
``ADD_EXCEPTIONS``
|
||||
------------------
|
||||
|
||||
A dictionary of exception classes to be added to ``HORIZON['exceptions']``.
|
||||
|
||||
``ADD_INSTALLED_APPS``
|
||||
----------------------
|
||||
|
||||
A list of applications to be added to ``INSTALLED_APPS``.
|
||||
|
||||
``DISABLED``
|
||||
------------
|
||||
|
||||
If set to ``True``, this dashboard will not be added to the settings.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
To disable the Router dashboard locally, create a file
|
||||
``openstack_dashboard/local/enabled/_40_router.py`` with the following
|
||||
content::
|
||||
|
||||
DASHBOARD = 'router'
|
||||
DISABLED = True
|
||||
|
||||
To add a Tuskar-UI (Infrastructure) dashboard, you have to install it, and then
|
||||
create a file ``openstack_dashboard/local/enabled/_50_tuskar.py`` with::
|
||||
|
||||
from tuskar_ui import exceptions
|
||||
|
||||
DASHBOARD = 'infrastructure'
|
||||
ADD_INSTALLED_APPS = [
|
||||
'tuskar_ui.infrastructure',
|
||||
]
|
||||
ADD_EXCEPTIONS = {
|
||||
'recoverable': exceptions.RECOVERABLE,
|
||||
'not_found': exceptions.NOT_FOUND,
|
||||
'unauthorized': exceptions.UNAUTHORIZED,
|
||||
}
|
||||
|
8
openstack_dashboard/enabled/_10_project.py
Normal file
8
openstack_dashboard/enabled/_10_project.py
Normal file
@ -0,0 +1,8 @@
|
||||
# The name of the dashboard to be added to HORIZON['dashboards']. Required.
|
||||
DASHBOARD = 'project'
|
||||
# If set to True, this dashboard will be set as the default dashboard.
|
||||
DEFAULT = True
|
||||
# A dictionary of exception classes to be added to HORIZON['exceptions'].
|
||||
ADD_EXCEPTIONS = {}
|
||||
# A list of applications to be added to INSTALLED_APPS.
|
||||
ADD_INSTALLED_APPS = ['openstack_dashboard.dashboards.project']
|
7
openstack_dashboard/enabled/_20_admin.py
Normal file
7
openstack_dashboard/enabled/_20_admin.py
Normal file
@ -0,0 +1,7 @@
|
||||
# The name of the dashboard to be added to HORIZON['dashboards']. Required.
|
||||
DASHBOARD = 'admin'
|
||||
|
||||
# A list of applications to be added to INSTALLED_APPS.
|
||||
ADD_INSTALLED_APPS = [
|
||||
'openstack_dashboard.dashboards.admin',
|
||||
]
|
7
openstack_dashboard/enabled/_30_settings.py
Normal file
7
openstack_dashboard/enabled/_30_settings.py
Normal file
@ -0,0 +1,7 @@
|
||||
# The name of the dashboard to be added to HORIZON['dashboards']. Required.
|
||||
DASHBOARD = 'settings'
|
||||
|
||||
# A list of applications to be added to INSTALLED_APPS.
|
||||
ADD_INSTALLED_APPS = [
|
||||
'openstack_dashboard.dashboards.settings',
|
||||
]
|
10
openstack_dashboard/enabled/_40_router.py
Normal file
10
openstack_dashboard/enabled/_40_router.py
Normal file
@ -0,0 +1,10 @@
|
||||
# The name of the dashboard to be added to HORIZON['dashboards']. Required.
|
||||
DASHBOARD = 'router'
|
||||
|
||||
# A list of applications to be added to INSTALLED_APPS.
|
||||
ADD_INSTALLED_APPS = [
|
||||
'openstack_dashboard.dashboards.router',
|
||||
]
|
||||
|
||||
# If set to True, this dashboard will not be added to the settings.
|
||||
DISABLED = True
|
0
openstack_dashboard/enabled/__init__.py
Normal file
0
openstack_dashboard/enabled/__init__.py
Normal file
5
openstack_dashboard/local/enabled/_40_router.py.example
Normal file
5
openstack_dashboard/local/enabled/_40_router.py.example
Normal file
@ -0,0 +1,5 @@
|
||||
# The name of the dashboard to be added to HORIZON['dashboards']. Required.
|
||||
DASHBOARD = 'router'
|
||||
|
||||
# If set to True, this dashboard will not be added to the settings.
|
||||
DISABLED = False
|
0
openstack_dashboard/local/enabled/__init__.py
Normal file
0
openstack_dashboard/local/enabled/__init__.py
Normal file
@ -145,7 +145,7 @@ COMPRESS_OUTPUT_DIR = 'dashboard'
|
||||
COMPRESS_CSS_HASHING_METHOD = 'hash'
|
||||
COMPRESS_PARSER = 'compressor.parser.HtmlParser'
|
||||
|
||||
INSTALLED_APPS = (
|
||||
INSTALLED_APPS = [
|
||||
'openstack_dashboard',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.auth',
|
||||
@ -160,7 +160,7 @@ INSTALLED_APPS = (
|
||||
'openstack_dashboard.dashboards.settings',
|
||||
'openstack_auth',
|
||||
'openstack_dashboard.dashboards.router',
|
||||
)
|
||||
]
|
||||
|
||||
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
||||
AUTHENTICATION_BACKENDS = ('openstack_auth.backend.KeystoneBackend',)
|
||||
@ -210,6 +210,17 @@ try:
|
||||
except ImportError:
|
||||
logging.warning("No local_settings file found.")
|
||||
|
||||
# Load the pluggable dashboard settings
|
||||
import openstack_dashboard.enabled
|
||||
import openstack_dashboard.local.enabled
|
||||
from openstack_dashboard.utils import settings
|
||||
|
||||
INSTALLED_APPS = list(INSTALLED_APPS) # Make sure it's mutable
|
||||
settings.update_dashboards([
|
||||
openstack_dashboard.enabled,
|
||||
openstack_dashboard.local.enabled,
|
||||
], HORIZON_CONFIG, INSTALLED_APPS)
|
||||
|
||||
# Ensure that we always have a SECRET_KEY set, even when no local_settings.py
|
||||
# file is present. See local_settings.py.example for full documentation on the
|
||||
# horizon.utils.secret_key module and its use.
|
||||
|
92
openstack_dashboard/utils/settings.py
Normal file
92
openstack_dashboard/utils/settings.py
Normal file
@ -0,0 +1,92 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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.
|
||||
|
||||
import collections
|
||||
import logging
|
||||
import pkgutil
|
||||
|
||||
from django.utils import importlib
|
||||
|
||||
|
||||
def import_submodules(module):
|
||||
"""Import all submodules and make them available in a dict."""
|
||||
submodules = {}
|
||||
for loader, name, ispkg in pkgutil.iter_modules(module.__path__,
|
||||
module.__name__ + '.'):
|
||||
try:
|
||||
submodule = importlib.import_module(name)
|
||||
except ImportError as e:
|
||||
# FIXME: Make the errors non-fatal (do we want that?).
|
||||
logging.warning("Error importing %s" % name)
|
||||
logging.exception(e)
|
||||
else:
|
||||
parent, child = name.rsplit('.', 1)
|
||||
submodules[child] = submodule
|
||||
return submodules
|
||||
|
||||
|
||||
def import_dashboard_config(modules):
|
||||
"""Imports configuration from all the modules and merges it."""
|
||||
config = collections.defaultdict(dict)
|
||||
for module in modules:
|
||||
for key, submodule in import_submodules(module).iteritems():
|
||||
try:
|
||||
dashboard = submodule.DASHBOARD
|
||||
except AttributeError:
|
||||
logging.warning("Skipping %s because it doesn't "
|
||||
"have DASHBOARD defined." % submodule.__name__)
|
||||
else:
|
||||
config[dashboard].update(submodule.__dict__)
|
||||
return sorted(config.iteritems(),
|
||||
key=lambda c: c[1]['__name__'].rsplit('.', 1))
|
||||
|
||||
|
||||
def update_dashboards(modules, horizon_config, installed_apps):
|
||||
"""Imports dashboard configuration from modules and applies it.
|
||||
|
||||
The submodules from specified modules are imported, and the configuration
|
||||
for the specific dashboards is merged, with the later modules overriding
|
||||
settings from the former. Then the configuration is applied to
|
||||
horizon_config and installed_apps, in alphabetical order of files from
|
||||
which the configurations were imported.
|
||||
|
||||
For example, given this setup:
|
||||
|
||||
foo/__init__.py
|
||||
foo/_10_baz.py
|
||||
foo/_20_qux.py
|
||||
|
||||
bar/__init__.py
|
||||
bar/_30_baz_.py
|
||||
|
||||
and being called with ``modules=[foo, bar]``, we will first have the
|
||||
configuration from ``_10_baz`` and ``_30_baz`` merged, then the
|
||||
configurations will be applied in order ``qux``, ``baz`` (``baz`` is
|
||||
second, because the most recent file which contributed to it, ``_30_baz``,
|
||||
comes after ``_20_qux``).
|
||||
"""
|
||||
dashboards = []
|
||||
exceptions = {}
|
||||
apps = []
|
||||
for dashboard, config in import_dashboard_config(modules):
|
||||
if config.get('DISABLED', False):
|
||||
continue
|
||||
dashboards.append(dashboard)
|
||||
exceptions.update(config.get('ADD_EXCEPTIONS', {}))
|
||||
apps.extend(config.get('ADD_INSTALLED_APPS', []))
|
||||
if config.get('DEFAULT', False):
|
||||
horizon_config['default_dashboard'] = dashboard
|
||||
horizon_config['dashboards'] = tuple(dashboards)
|
||||
horizon_config['exceptions'].update(exceptions)
|
||||
installed_apps.extend(apps)
|
Loading…
Reference in New Issue
Block a user