From 303c354fd6e3d0e0e1636f2f8cbd9f6df3ef0c25 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Thu, 14 Nov 2013 12:43:05 +0100 Subject: [PATCH] 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 --- doc/source/topics/settings.rst | 72 +++++++++++++++ openstack_dashboard/enabled/_10_project.py | 8 ++ openstack_dashboard/enabled/_20_admin.py | 7 ++ openstack_dashboard/enabled/_30_settings.py | 7 ++ openstack_dashboard/enabled/_40_router.py | 10 ++ openstack_dashboard/enabled/__init__.py | 0 .../local/enabled/_40_router.py.example | 5 + openstack_dashboard/local/enabled/__init__.py | 0 openstack_dashboard/settings.py | 15 ++- openstack_dashboard/utils/settings.py | 92 +++++++++++++++++++ 10 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 openstack_dashboard/enabled/_10_project.py create mode 100644 openstack_dashboard/enabled/_20_admin.py create mode 100644 openstack_dashboard/enabled/_30_settings.py create mode 100644 openstack_dashboard/enabled/_40_router.py create mode 100644 openstack_dashboard/enabled/__init__.py create mode 100644 openstack_dashboard/local/enabled/_40_router.py.example create mode 100644 openstack_dashboard/local/enabled/__init__.py create mode 100644 openstack_dashboard/utils/settings.py diff --git a/doc/source/topics/settings.rst b/doc/source/topics/settings.rst index 5f2558c644..893c312756 100644 --- a/doc/source/topics/settings.rst +++ b/doc/source/topics/settings.rst @@ -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, + } diff --git a/openstack_dashboard/enabled/_10_project.py b/openstack_dashboard/enabled/_10_project.py new file mode 100644 index 0000000000..ae34a869d4 --- /dev/null +++ b/openstack_dashboard/enabled/_10_project.py @@ -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'] diff --git a/openstack_dashboard/enabled/_20_admin.py b/openstack_dashboard/enabled/_20_admin.py new file mode 100644 index 0000000000..ed7b93c97b --- /dev/null +++ b/openstack_dashboard/enabled/_20_admin.py @@ -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', +] diff --git a/openstack_dashboard/enabled/_30_settings.py b/openstack_dashboard/enabled/_30_settings.py new file mode 100644 index 0000000000..984b268a73 --- /dev/null +++ b/openstack_dashboard/enabled/_30_settings.py @@ -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', +] diff --git a/openstack_dashboard/enabled/_40_router.py b/openstack_dashboard/enabled/_40_router.py new file mode 100644 index 0000000000..d81e326ef6 --- /dev/null +++ b/openstack_dashboard/enabled/_40_router.py @@ -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 diff --git a/openstack_dashboard/enabled/__init__.py b/openstack_dashboard/enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/local/enabled/_40_router.py.example b/openstack_dashboard/local/enabled/_40_router.py.example new file mode 100644 index 0000000000..36a888dd3b --- /dev/null +++ b/openstack_dashboard/local/enabled/_40_router.py.example @@ -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 diff --git a/openstack_dashboard/local/enabled/__init__.py b/openstack_dashboard/local/enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/settings.py b/openstack_dashboard/settings.py index 1216099eea..8f6951fe46 100644 --- a/openstack_dashboard/settings.py +++ b/openstack_dashboard/settings.py @@ -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. diff --git a/openstack_dashboard/utils/settings.py b/openstack_dashboard/utils/settings.py new file mode 100644 index 0000000000..01accb007a --- /dev/null +++ b/openstack_dashboard/utils/settings.py @@ -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)