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
|
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,
|
||||||
|
}
|
||||||
|
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_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.
|
||||||
|
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