plugins: initial support

parent 6c68489b
......@@ -5,3 +5,4 @@
foris/static/css/*
foris/static/js/*.min.js
foris/templates/_layout.tpl
/plugins/
......@@ -315,9 +315,26 @@ config_page_map = ConfigPageMapItems((
('about', AboutConfigPage),
))
# config pages added by plugins
extra_config_pages = ConfigPageMapItems()
def add_config_page(page_name, page_class, top_level=False):
"""Register config page in /config/ URL namespace.
:param page_name: config page name (shown in url)
:param page_class: handler class
:param top_level: add to top-level navigation
"""
if top_level:
config_page_map[page_name] = page_class
else:
extra_config_pages[page_name] = page_class
def get_config_page(page_name):
ConfigPage = config_page_map.get(page_name)
ConfigPage = config_page_map.get(page_name,
extra_config_pages.get(page_name))
if ConfigPage is None:
raise bottle.HTTPError(404, "Unknown configuration page.")
return ConfigPage
......
......@@ -29,6 +29,7 @@ from ncclient.operations import TimeoutExpiredError, RPCError
from .nuci import client, filters
from .nuci.modules.uci_raw import Uci, Config, Section, Option
from .nuci.modules.user_notify import Severity
from .plugins import ForisPluginLoader
from .utils import redirect_unauthenticated, is_safe_redirect, is_user_authenticated
from .utils.bottle_csrf import get_csrf_token, update_csrf_token, CSRFValidationError, CSRFPlugin
from .utils import messages
......@@ -353,6 +354,10 @@ def prepare_main_app(args):
if args.nucipath:
client.StaticNetconfConnection.set_bin_path(args.nucipath)
# load Foris plugins before applying Bottle plugins to app
loader = ForisPluginLoader(app)
loader.autoload_plugins()
# read language saved in Uci
lang = read_uci_lang(DEFAULT_LANGUAGE)
# i18n middleware
......
# Foris - web administration interface for OpenWrt based on NETCONF
# Copyright (C) 2015 CZ.NIC, z. s. p. o. <https://www.nic.cz>
#
# Foris is distributed under the terms of GNU General Public License v3.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import gettext
from importlib import import_module
import inspect
import logging
import os
import sys
import bottle
logger = logging.getLogger(__name__)
class ForisPlugin(object):
"""Simple class that all Foris plugins should inherit from."""
DIRNAME = None
plugin_translations = None
def __init__(self, app):
self.app = app
if not self.DIRNAME:
raise NameError("DIRNAME attribute must be set by ForisPlugin subclass.")
# initialize templates
template_dir = os.path.join(self.DIRNAME, "templates")
bottle.TEMPLATE_PATH.append(template_dir)
# initialize translations
self.add_translations()
def add_translations(self):
"""Add translations in current plugin.
This approach has one design flaw - messages in the plugin apply to
the whole app. This is not an issue now, but it should be examined
later and replaced by a better solution.
"""
from foris.core import translations
for lang, default_translation in translations.iteritems():
local_translation = gettext.translation("messages", os.path.join(self.DIRNAME, "locale"),
languages=[lang], fallback=True)
default_translation.add_fallback(local_translation)
class ForisPluginLoader(object):
"""Class for loading plugins and holding references to them in runtime."""
PLUGIN_DIRECTORY = os.path.join(os.sep, "usr", "share", "foris", "plugins")
def __init__(self, app):
self.app = app
self.app.foris_plugin_loader = self
self.plugins = []
sys.path.append(self.PLUGIN_DIRECTORY)
def autoload_plugins(self):
"""Find and load plugins in ${PLUGIN_DIRECTORY}/plugin_name/*.py"""
for subdir_name in os.listdir(self.PLUGIN_DIRECTORY):
subdir_path = os.path.join(self.PLUGIN_DIRECTORY, subdir_name)
if os.path.isdir(subdir_path):
if "__init__.py" in os.listdir(subdir_path):
logger.debug("Found Python package in '%s'.", subdir_path)
self._load_plugins_in_package(subdir_name)
@staticmethod
def is_foris_plugin(klass):
"""Check that argument klass is a valid Foris plugin.
Check is True for all classes that have class named ForisPlugin
in their inheritance chain.
"""
if not inspect.isclass(klass):
return False
# first element is basically klass.__name__ - throw it away
mro_names = [c.__name__ for c in inspect.getmro(klass)][1:]
return ForisPlugin.__name__ in mro_names
def _load_plugins_in_package(self, package_name):
"""Import package and load all plugins in its __init__.py"""
try:
logger.info("Looking for plugins in package '%s'", package_name)
package = import_module("%s" % package_name)
classes = inspect.getmembers(package, self.is_foris_plugin)
for _, klass in classes:
self.load_plugin(klass)
except ImportError:
logger.exception("Unable to import package '%s'.", package_name)
except:
# catching all errors - plugins should not kill Foris
logger.exception("Error when loading plugin '%s': " % package_name)
def load_plugin(self, plugin_class):
"""Load a single plugin class."""
logger.info("Loading plugin: %s", plugin_class)
instance = plugin_class(self.app)
self.plugins.append(instance)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment