From f169ee58ab17d1b0dde9afd5b27c8bd276de0c50 Mon Sep 17 00:00:00 2001 From: lin-hua-cheng Date: Wed, 26 Feb 2014 21:23:12 -0800 Subject: [PATCH] Plugin-based panel configuration This is an extension to the plugin-based dashboard configuration. It adds support processing panel configuration configuration file in the openstack_dashboard/enabled/ directory. Panels can be added, removed to/from the panel group of a dashboard. It also provide the ability to update the default panel of the dashboard. Change-Id: I2d7adfb8045c244ec063a6741e3b9fe21c188525 Implements: blueprint plugin-panel-config --- doc/source/topics/settings.rst | 90 ++++++++++++++++++- horizon/base.py | 58 ++++++++++++ .../enabled/_50_admin_add_panel.py.example | 10 +++ .../enabled/_60_admin_remove_panel.py.example | 9 ++ .../_70_admin_default_panel.py.example | 9 ++ .../test/test_panels/__init__.py | 0 .../test/test_panels/plugin_panel/__init__.py | 0 .../test/test_panels/plugin_panel/panel.py | 22 +++++ .../templates/plugin_panel/index.html | 15 ++++ .../test/test_panels/plugin_panel/urls.py | 22 +++++ .../test/test_panels/plugin_panel/views.py | 19 ++++ .../test/test_plugins/__init__.py | 0 .../panel_config/_10_admin_add_panel.py | 10 +++ .../panel_config/_20_admin_remove_panel.py | 9 ++ .../panel_config/_30_admin_default_panel.py | 9 ++ .../test_plugins/panel_config/__init__.py | 0 .../test/test_plugins/panel_tests.py | 86 ++++++++++++++++++ openstack_dashboard/utils/settings.py | 38 +++++--- 18 files changed, 393 insertions(+), 13 deletions(-) create mode 100644 openstack_dashboard/enabled/_50_admin_add_panel.py.example create mode 100644 openstack_dashboard/enabled/_60_admin_remove_panel.py.example create mode 100644 openstack_dashboard/enabled/_70_admin_default_panel.py.example create mode 100644 openstack_dashboard/test/test_panels/__init__.py create mode 100644 openstack_dashboard/test/test_panels/plugin_panel/__init__.py create mode 100644 openstack_dashboard/test/test_panels/plugin_panel/panel.py create mode 100644 openstack_dashboard/test/test_panels/plugin_panel/templates/plugin_panel/index.html create mode 100644 openstack_dashboard/test/test_panels/plugin_panel/urls.py create mode 100644 openstack_dashboard/test/test_panels/plugin_panel/views.py create mode 100644 openstack_dashboard/test/test_plugins/__init__.py create mode 100644 openstack_dashboard/test/test_plugins/panel_config/_10_admin_add_panel.py create mode 100644 openstack_dashboard/test/test_plugins/panel_config/_20_admin_remove_panel.py create mode 100644 openstack_dashboard/test/test_plugins/panel_config/_30_admin_default_panel.py create mode 100644 openstack_dashboard/test/test_plugins/panel_config/__init__.py create mode 100644 openstack_dashboard/test/test_plugins/panel_tests.py diff --git a/doc/source/topics/settings.rst b/doc/source/topics/settings.rst index 0cb59eb8ca..e2b302190f 100644 --- a/doc/source/topics/settings.rst +++ b/doc/source/topics/settings.rst @@ -434,7 +434,7 @@ 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 +``openstack_dashboard/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 @@ -493,3 +493,91 @@ create a file ``openstack_dashboard/local/enabled/_50_tuskar.py`` with:: 'not_found': exceptions.NOT_FOUND, 'unauthorized': exceptions.UNAUTHORIZED, } + +Pluggable Settings for Panels +================================= + +Panels customization can be made by providing a custom python module that +contains python code to add or remove panel to/from the dashboard. This +requires altering the settings file. For panels provided by third-party, +making this changes to add the panel is challenging. Panel configuration +files can now be dropped to a specified location and it will be read at startup +to alter the dashboard configuration. + +The default location for the panel configuration files is +``openstack_dashboard/enabled``, with another directory, +``openstack_dashboard/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 panel 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: + +``PANEL`` +------------- + +The name of the panel to be added to ``HORIZON_CONFIG``. Required. + +``PANEL_DASHBOARD`` +------------- + +The name of the dashboard the ``PANEL`` associated with. Required. + + +``PANEL_GROUP`` +------------- + +The name of the panel group the ``PANEL`` is associated with. + +``DEFAULT_PANEL`` +----------- + +If set, it will update the default panel of the ``PANEL_DASHBOARD``. + +``ADD_PANEL`` +---------------------- + +Python panel class of the ``PANEL`` to be added. + +``REMOVE_PANEL`` +------------ + +If set to ``True``, the PANEL will be removed from PANEL_DASHBOARD/PANEL_GROUP. + +``DISABLED`` +------------ + +If set to ``True``, this panel configuration will be skipped. + +Examples +-------- + +To add a new panel to the Admin panel group in Admin dashboard, create a file +``openstack_dashboard/local/enabled/_60_admin_add_panel.py`` with the follwing +content:: + + PANEL = 'plugin_panel' + PANEL_DASHBOARD = 'admin' + PANEL_GROUP = 'admin' + ADD_PANEL = 'test_panels.plugin_panel.panel.PluginPanel' + +To remove Info panel from Admin panel group in Admin dashboard locally, create +a file ``openstack_dashboard/local/enabled/_70_admin_remove_panel.py`` with +the following content:: + + PANEL = 'info' + PANEL_DASHBOARD = 'admin' + PANEL_GROUP = 'admin' + REMOVE_PANEL = True + +To change the default panel of Admin dashboard to Defaults panel, create a file +``openstack_dashboard/local/enabled/_80_admin_default_panel.py`` with the +following content:: + + PANEL = 'defaults' + PANEL_DASHBOARD = 'admin' + PANEL_GROUP = 'admin' + DEFAULT_PANEL = 'defaults' diff --git a/horizon/base.py b/horizon/base.py index 090e912480..f9488d2a1b 100644 --- a/horizon/base.py +++ b/horizon/base.py @@ -740,6 +740,9 @@ class Site(Registry, HorizonComponent): for dash in self._registry.values(): dash._autodiscover() + # Load the plugin-based panel configuration + self._load_panel_customization() + # Allow for override modules if self._conf.get("customization_module", None): customization_module = self._conf["customization_module"] @@ -785,6 +788,61 @@ class Site(Registry, HorizonComponent): if module_has_submodule(mod, mod_name): raise + def _load_panel_customization(self): + """Applies the plugin-based panel configurations. + + This method parses the panel customization from the ``HORIZON_CONFIG`` + and make changes to the dashboard accordingly. + + It supports adding, removing and setting default panels on the + dashboard. + """ + panel_customization = self._conf.get("panel_customization", []) + + for config in panel_customization: + dashboard = config.get('PANEL_DASHBOARD') + if not dashboard: + LOG.warning("Skipping %s because it doesn't have " + "PANEL_DASHBOARD defined.", config.__name__) + continue + try: + panel_slug = config.get('PANEL') + dashboard_cls = self.get_dashboard(dashboard) + panel_group = config.get('PANEL_GROUP') + default_panel = config.get('DEFAULT_PANEL') + + # Set the default panel + if default_panel: + dashboard_cls.default_panel = default_panel + + # Remove the panel + if config.get('REMOVE_PANEL', False): + for panel in dashboard_cls.get_panels(): + if panel_slug == panel.slug: + dashboard_cls.unregister(panel.__class__) + elif config.get('ADD_PANEL', None): + # Add the panel to the dashboard + panel_path = config['ADD_PANEL'] + mod_path, panel_cls = panel_path.rsplit(".", 1) + try: + mod = import_module(mod_path) + except ImportError: + LOG.warning("Could not load panel: %s", mod_path) + continue + + panel = getattr(mod, panel_cls) + dashboard_cls.register(panel) + if panel_group: + dashboard_cls.get_panel_group(panel_group).\ + panels.append(panel.slug) + else: + panels = list(dashboard_cls.panels) + panels.append(panel) + dashboard_cls.panels = tuple(panels) + except Exception as e: + LOG.warning('Could not process panel %(panel)s: %(exc)s', + {'panel': panel_slug, 'exc': e}) + class HorizonSite(Site): """A singleton implementation of Site such that all dealings with horizon diff --git a/openstack_dashboard/enabled/_50_admin_add_panel.py.example b/openstack_dashboard/enabled/_50_admin_add_panel.py.example new file mode 100644 index 0000000000..537aac75f4 --- /dev/null +++ b/openstack_dashboard/enabled/_50_admin_add_panel.py.example @@ -0,0 +1,10 @@ +# The name of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'plugin_panel' +# The name of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'admin' +# The name of the panel group the PANEL is associated with. +PANEL_GROUP = 'admin' + +# Python panel class of the PANEL to be added. +ADD_PANEL = \ + 'openstack_dashboard.test.test_panels.plugin_panel.panel.PluginPanel' diff --git a/openstack_dashboard/enabled/_60_admin_remove_panel.py.example b/openstack_dashboard/enabled/_60_admin_remove_panel.py.example new file mode 100644 index 0000000000..94d2856f83 --- /dev/null +++ b/openstack_dashboard/enabled/_60_admin_remove_panel.py.example @@ -0,0 +1,9 @@ +# The name of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'info' +# The name of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'admin' +# The name of the panel group the PANEL is associated with. +PANEL_GROUP = 'admin' + +# If set to True, the panel will be removed from PANEL_DASHBOARD/PANEL_GROUP. +REMOVE_PANEL = True diff --git a/openstack_dashboard/enabled/_70_admin_default_panel.py.example b/openstack_dashboard/enabled/_70_admin_default_panel.py.example new file mode 100644 index 0000000000..b38ebdaf89 --- /dev/null +++ b/openstack_dashboard/enabled/_70_admin_default_panel.py.example @@ -0,0 +1,9 @@ +# The name of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'defaults' +# The name of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'admin' +# The name of the panel group the PANEL is associated with. +PANEL_GROUP = 'admin' + +# If set, it will update the default panel of the PANEL_DASHBOARD. +DEFAULT_PANEL = 'defaults' diff --git a/openstack_dashboard/test/test_panels/__init__.py b/openstack_dashboard/test/test_panels/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/test/test_panels/plugin_panel/__init__.py b/openstack_dashboard/test/test_panels/plugin_panel/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/test/test_panels/plugin_panel/panel.py b/openstack_dashboard/test/test_panels/plugin_panel/panel.py new file mode 100644 index 0000000000..3a480e81d2 --- /dev/null +++ b/openstack_dashboard/test/test_panels/plugin_panel/panel.py @@ -0,0 +1,22 @@ +# 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. + +from django.utils.translation import ugettext_lazy as _ + +import horizon + + +class PluginPanel(horizon.Panel): + name = _("Plugin Panel") + slug = 'plugin_panel' diff --git a/openstack_dashboard/test/test_panels/plugin_panel/templates/plugin_panel/index.html b/openstack_dashboard/test/test_panels/plugin_panel/templates/plugin_panel/index.html new file mode 100644 index 0000000000..3f7affaefa --- /dev/null +++ b/openstack_dashboard/test/test_panels/plugin_panel/templates/plugin_panel/index.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Plugin-based Panel" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Plugin-based Panel")%} +{% endblock page_header %} + +{% block main %} +
+
+ Plugin-based Panel +
+
+{% endblock %} \ No newline at end of file diff --git a/openstack_dashboard/test/test_panels/plugin_panel/urls.py b/openstack_dashboard/test/test_panels/plugin_panel/urls.py new file mode 100644 index 0000000000..22c1be9eff --- /dev/null +++ b/openstack_dashboard/test/test_panels/plugin_panel/urls.py @@ -0,0 +1,22 @@ +# 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. + +from django.conf.urls import patterns # noqa +from django.conf.urls import url # noqa + +from openstack_dashboard.test.test_panels.plugin_panel import views + +urlpatterns = patterns('', + url(r'^$', views.IndexView.as_view(), name='index'), +) diff --git a/openstack_dashboard/test/test_panels/plugin_panel/views.py b/openstack_dashboard/test/test_panels/plugin_panel/views.py new file mode 100644 index 0000000000..acab1a0ded --- /dev/null +++ b/openstack_dashboard/test/test_panels/plugin_panel/views.py @@ -0,0 +1,19 @@ +# 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. + +from django.views.generic import TemplateView # noqa + + +class IndexView(TemplateView): + template_name = 'admin/plugin_panel/index.html' diff --git a/openstack_dashboard/test/test_plugins/__init__.py b/openstack_dashboard/test/test_plugins/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/test/test_plugins/panel_config/_10_admin_add_panel.py b/openstack_dashboard/test/test_plugins/panel_config/_10_admin_add_panel.py new file mode 100644 index 0000000000..537aac75f4 --- /dev/null +++ b/openstack_dashboard/test/test_plugins/panel_config/_10_admin_add_panel.py @@ -0,0 +1,10 @@ +# The name of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'plugin_panel' +# The name of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'admin' +# The name of the panel group the PANEL is associated with. +PANEL_GROUP = 'admin' + +# Python panel class of the PANEL to be added. +ADD_PANEL = \ + 'openstack_dashboard.test.test_panels.plugin_panel.panel.PluginPanel' diff --git a/openstack_dashboard/test/test_plugins/panel_config/_20_admin_remove_panel.py b/openstack_dashboard/test/test_plugins/panel_config/_20_admin_remove_panel.py new file mode 100644 index 0000000000..94d2856f83 --- /dev/null +++ b/openstack_dashboard/test/test_plugins/panel_config/_20_admin_remove_panel.py @@ -0,0 +1,9 @@ +# The name of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'info' +# The name of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'admin' +# The name of the panel group the PANEL is associated with. +PANEL_GROUP = 'admin' + +# If set to True, the panel will be removed from PANEL_DASHBOARD/PANEL_GROUP. +REMOVE_PANEL = True diff --git a/openstack_dashboard/test/test_plugins/panel_config/_30_admin_default_panel.py b/openstack_dashboard/test/test_plugins/panel_config/_30_admin_default_panel.py new file mode 100644 index 0000000000..b38ebdaf89 --- /dev/null +++ b/openstack_dashboard/test/test_plugins/panel_config/_30_admin_default_panel.py @@ -0,0 +1,9 @@ +# The name of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'defaults' +# The name of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'admin' +# The name of the panel group the PANEL is associated with. +PANEL_GROUP = 'admin' + +# If set, it will update the default panel of the PANEL_DASHBOARD. +DEFAULT_PANEL = 'defaults' diff --git a/openstack_dashboard/test/test_plugins/panel_config/__init__.py b/openstack_dashboard/test/test_plugins/panel_config/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/test/test_plugins/panel_tests.py b/openstack_dashboard/test/test_plugins/panel_tests.py new file mode 100644 index 0000000000..3d27db2001 --- /dev/null +++ b/openstack_dashboard/test/test_plugins/panel_tests.py @@ -0,0 +1,86 @@ +# 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 copy + +from django.conf import settings +from django.core import urlresolvers +from django.test.utils import override_settings +from django.utils.importlib import import_module # noqa + +import horizon +from horizon import base +from horizon import conf + +from openstack_dashboard.dashboards.admin.info import panel as info_panel +from openstack_dashboard.test import helpers as test +from openstack_dashboard.test.test_panels.plugin_panel \ + import panel as plugin_panel +import openstack_dashboard.test.test_plugins.panel_config +from openstack_dashboard.utils import settings as util_settings + + +HORIZON_CONFIG = copy.copy(settings.HORIZON_CONFIG) +INSTALLED_APPS = list(settings.INSTALLED_APPS) + +util_settings.update_dashboards([ + openstack_dashboard.test.test_plugins.panel_config, +], HORIZON_CONFIG, INSTALLED_APPS) + + +@override_settings(HORIZON_CONFIG=HORIZON_CONFIG, + INSTALLED_APPS=INSTALLED_APPS) +class PanelPluginTests(test.TestCase): + + def setUp(self): + super(PanelPluginTests, self).setUp() + self.old_horizon_config = conf.HORIZON_CONFIG + conf.HORIZON_CONFIG = conf.LazySettings() + base.Horizon._urls() + # Trigger discovery, registration, and URLconf generation if it + # hasn't happened yet. + self.client.get(settings.LOGIN_URL) + + def tearDown(self): + super(PanelPluginTests, self).tearDown() + conf.HORIZON_CONFIG = self.old_horizon_config + # Destroy our singleton and re-create it. + base.HorizonSite._instance = None + del base.Horizon + base.Horizon = base.HorizonSite() + self._reload_urls() + + def _reload_urls(self): + """Clears out the URL caches, reloads the root urls module, and + re-triggers the autodiscovery mechanism for Horizon. Allows URLs + to be re-calculated after registering new dashboards. Useful + only for testing and should never be used on a live site. + """ + urlresolvers.clear_url_caches() + reload(import_module(settings.ROOT_URLCONF)) + base.Horizon._urls() + + def test_add_panel(self): + dashboard = horizon.get_dashboard("admin") + self.assertIn(plugin_panel.PluginPanel, + [p.__class__ for p in dashboard.get_panels()]) + + def test_remove_panel(self): + dashboard = horizon.get_dashboard("admin") + self.assertNotIn(info_panel.Info, + [p.__class__ for p in dashboard.get_panels()]) + + def test_default_panel(self): + dashboard = horizon.get_dashboard("admin") + self.assertEqual(dashboard.default_panel, 'defaults') diff --git a/openstack_dashboard/utils/settings.py b/openstack_dashboard/utils/settings.py index ce3636a008..97b1a3ef68 100644 --- a/openstack_dashboard/utils/settings.py +++ b/openstack_dashboard/utils/settings.py @@ -41,19 +41,21 @@ def import_dashboard_config(modules): config = collections.defaultdict(dict) for module in modules: for key, submodule in import_submodules(module).iteritems(): - try: + if hasattr(submodule, 'DASHBOARD'): dashboard = submodule.DASHBOARD - except AttributeError: - logging.warning("Skipping %s because it doesn't " - "have DASHBOARD defined." % submodule.__name__) - else: config[dashboard].update(submodule.__dict__) + elif hasattr(submodule, 'PANEL'): + config[submodule.__name__] = submodule.__dict__ + #_update_panels(config, submodule) + else: + logging.warning("Skipping %s because it doesn't have DASHBOARD" + " or PANEL defined.", submodule.__name__) 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. + """Imports dashboard and panel 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 @@ -75,18 +77,30 @@ def update_dashboards(modules, horizon_config, installed_apps): 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``). + + Panel specific configurations are stored in horizon_config. Dashboards + from both plugin-based and openstack_dashboard must be registered before + the panel configuration can be applied. Making changes to the panel is + deferred until the horizon autodiscover is completed, configurations are + applied in alphabetical order of files where it was imported. """ dashboards = [] exceptions = {} apps = [] - for dashboard, config in import_dashboard_config(modules): + panel_customization = [] + for key, 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 + if config.get('DASHBOARD'): + dashboard = key + 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 + elif config.get('PANEL'): + panel_customization.append(config) + horizon_config['panel_customization'] = panel_customization horizon_config['dashboards'] = tuple(dashboards) horizon_config['exceptions'].update(exceptions) installed_apps.extend(apps)