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:
Radomir Dopieralski 2013-11-14 12:43:05 +01:00
parent efc88d4078
commit 303c354fd6
10 changed files with 214 additions and 2 deletions

View File

@ -338,3 +338,75 @@ generate a secret key for a single installation.
These three settings should be configured if you are deploying Horizon with These three settings should be configured if you are deploying Horizon with
SSL. The values indicated in the default ``local_settings.py.example`` file SSL. The values indicated in the default ``local_settings.py.example`` file
are generally safe to use. 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,
}

View 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']

View 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',
]

View 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',
]

View 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

View File

View 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

View File

@ -145,7 +145,7 @@ COMPRESS_OUTPUT_DIR = 'dashboard'
COMPRESS_CSS_HASHING_METHOD = 'hash' COMPRESS_CSS_HASHING_METHOD = 'hash'
COMPRESS_PARSER = 'compressor.parser.HtmlParser' COMPRESS_PARSER = 'compressor.parser.HtmlParser'
INSTALLED_APPS = ( INSTALLED_APPS = [
'openstack_dashboard', 'openstack_dashboard',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.auth', 'django.contrib.auth',
@ -160,7 +160,7 @@ INSTALLED_APPS = (
'openstack_dashboard.dashboards.settings', 'openstack_dashboard.dashboards.settings',
'openstack_auth', 'openstack_auth',
'openstack_dashboard.dashboards.router', 'openstack_dashboard.dashboards.router',
) ]
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
AUTHENTICATION_BACKENDS = ('openstack_auth.backend.KeystoneBackend',) AUTHENTICATION_BACKENDS = ('openstack_auth.backend.KeystoneBackend',)
@ -210,6 +210,17 @@ try:
except ImportError: except ImportError:
logging.warning("No local_settings file found.") 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 # 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 # file is present. See local_settings.py.example for full documentation on the
# horizon.utils.secret_key module and its use. # horizon.utils.secret_key module and its use.

View 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)