:p
atchew
Login
From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> v3: - atomic backend commit - using regex on parser - 'test_mode' is now being retrieved by wok config - plug-in dependencies are now being fetched - plug-ins are now being enabled/disabled the cherrypy tree v2: - added User Log capabilities on /config/plugins/enable|disable actions - added 'enable=' as a valid entry in the parsing of the conf file This patch set implements the '/config/plugins' API. The idea of this API is to replace the current '/plugins' API while adding new attributes in their return values: - enabled: true if the plug-in is enabled, false otherwise - depends: list of all the plug-ins that this plug-in depends on - is_dependency_of: list of all plug-in that depends on this plug-in This backend is capable of enabling/disabling the plugi-ns using the API /config/plugins/*name*/enable|disable. Please check the commit messages of each patch for further details. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Daniel Henrique Barboza (2): /config/plugins API: backend changes /config/plugins: changing existing UI calls docs/API/config.md | 32 +++++++ docs/API/plugins.md | 13 --- src/wok/config.py.in | 5 +- src/wok/control/config.py | 31 ++++++- src/wok/control/plugins.py | 29 ------ src/wok/i18n.py | 4 + src/wok/model/plugins.py | 40 ++++++-- src/wok/server.py | 56 ++--------- src/wok/utils.py | 227 +++++++++++++++++++++++++++++++++++++++++++-- tests/test_api.py | 59 ++++++++++++ tests/test_utils.py | 75 ++++++++++++++- ui/js/src/wok.api.js | 4 +- ui/js/src/wok.logos.js | 11 ++- ui/js/src/wok.main.js | 10 +- 14 files changed, 476 insertions(+), 120 deletions(-) delete mode 100644 docs/API/plugins.md delete mode 100644 src/wok/control/plugins.py -- 2.9.3 _______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel
From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> This patch adds a backend for a new API called /config/plugins. The idea is to be able to retrieve the 'enable' status of WoK plug-ins and also provide a way to enable/disable them. The enable|disable operation consists on two steps: - changing the 'enable=' attribute of the [WoK] section of the plugin .conf file; - the plug-in is removed/added in the cherrypy.tree on the fly. Several changes/enhancements in the backend were made to make this possible, such as: - added the 'test' parameter in the config.py.in file to make it available for reading in the backend. This parameter indicates whether WoK is running in test mode; - 'load_plugin' was moved from server.py to utils.py to make it available for utils functions to load plug-ins; - a new 'depends' attribute is now being considered in the root class of each plug-in. This is an array that indicates all the plug-ins it has a dependency on. For example, Kimchi would mark self.depends = ['gingerbase'] in its root file. The absence of this attribute means that the plug-in does not have any dependency aside from WoK. Previous /plugins API were removed because it was redundant with this work. Uni tests included. Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- docs/API/config.md | 32 +++++++ docs/API/plugins.md | 13 --- src/wok/config.py.in | 5 +- src/wok/control/config.py | 31 ++++++- src/wok/control/plugins.py | 29 ------ src/wok/i18n.py | 4 + src/wok/model/plugins.py | 40 ++++++-- src/wok/server.py | 56 ++--------- src/wok/utils.py | 227 +++++++++++++++++++++++++++++++++++++++++++-- tests/test_api.py | 59 ++++++++++++ tests/test_utils.py | 75 ++++++++++++++- 11 files changed, 460 insertions(+), 111 deletions(-) delete mode 100644 docs/API/plugins.md delete mode 100644 src/wok/control/plugins.py diff --git a/docs/API/config.md b/docs/API/config.md index XXXXXXX..XXXXXXX 100644 --- a/docs/API/config.md +++ b/docs/API/config.md @@ -XXX,XX +XXX,XX @@ GET /config websockets_port: 64667, version: 2.0 } + +### Collection: Plugins + +**URI:** /config/plugins + +**Methods:** + +* **GET**: Retrieve a summarized list of all UI Plugins. + +#### Examples +GET /plugins +[{'name': 'pluginA', 'enabled': True, "depends":['pluginB'], "is_dependency_of":[]}, + {'name': 'pluginB', 'enabled': False, "depends":[], "is_dependency_of":['pluginA']}] + +### Resource: Plugins + +**URI:** /config/plugins/*:name* + +Represents the current state of a given WoK plug-in. + +**Methods:** + +* **GET**: Retrieve the state of the plug-in. + * name: The name of the plug-in. + * enabled: True if the plug-in is currently enabled in WoK, False otherwise. + +* **POST**: *See Plugin Actions* + +**Actions (POST):** + +* enable: Enable the plug-in in the configuration file. +* disable: Disable the plug-in in the configuration file. diff --git a/docs/API/plugins.md b/docs/API/plugins.md deleted file mode 100644 index XXXXXXX..XXXXXXX --- a/docs/API/plugins.md +++ /dev/null @@ -XXX,XX +XXX,XX @@ -## REST API Specification for Plugins - -### Collection: Plugins - -**URI:** /plugins - -**Methods:** - -* **GET**: Retrieve a summarized list names of all UI Plugins - -#### Examples -GET /plugins -[pluginA, pluginB, pluginC] diff --git a/src/wok/config.py.in b/src/wok/config.py.in index XXXXXXX..XXXXXXX 100644 --- a/src/wok/config.py.in +++ b/src/wok/config.py.in @@ -XXX,XX +XXX,XX @@ # # Project Wok # -# Copyright IBM Corp, 2015-2016 +# Copyright IBM Corp, 2015-2017 # # Code derived from Project Kimchi # @@ -XXX,XX +XXX,XX @@ def _get_config(): config.set("server", "environment", "production") config.set('server', 'max_body_size', '4*1024*1024') config.set("server", "server_root", "") + config.set("server", "test", "true") config.add_section("authentication") config.set("authentication", "method", "pam") config.set("authentication", "ldap_server", "") @@ -XXX,XX +XXX,XX @@ def _get_config(): config.add_section("logging") config.set("logging", "log_dir", paths.log_dir) config.set("logging", "log_level", DEFAULT_LOG_LEVEL) + config.set("logging", "access_log", "") + config.set("logging", "error_log", "") config_file = os.path.join(paths.conf_dir, 'wok.conf') if os.path.exists(config_file): diff --git a/src/wok/control/config.py b/src/wok/control/config.py index XXXXXXX..XXXXXXX 100644 --- a/src/wok/control/config.py +++ b/src/wok/control/config.py @@ -XXX,XX +XXX,XX @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -from wok.control.base import Resource +from wok.control.base import Collection, Resource from wok.control.utils import UrlSubNode @@ -XXX,XX +XXX,XX @@ CONFIG_REQUESTS = { } +PLUGIN_REQUESTS = { + 'POST': { + 'enable': "WOKPLUGIN0001L", + 'disable': "WOKPLUGIN0002L", + }, +} + + @UrlSubNode("config") class Config(Resource): def __init__(self, model, id=None): super(Config, self).__init__(model, id) self.uri_fmt = '/config/%s' self.admin_methods = ['POST'] + self.plugins = Plugins(self.model) self.log_map = CONFIG_REQUESTS self.reload = self.generate_action_handler('reload') @property def data(self): return self.info + + +class Plugins(Collection): + def __init__(self, model): + super(Plugins, self).__init__(model) + self.resource = Plugin + + +class Plugin(Resource): + def __init__(self, model, ident=None): + super(Plugin, self).__init__(model, ident) + self.ident = ident + self.uri_fmt = "/config/plugins/%s" + self.log_map = PLUGIN_REQUESTS + self.enable = self.generate_action_handler('enable') + self.disable = self.generate_action_handler('disable') + + @property + def data(self): + return self.info diff --git a/src/wok/control/plugins.py b/src/wok/control/plugins.py deleted file mode 100644 index XXXXXXX..XXXXXXX --- a/src/wok/control/plugins.py +++ /dev/null @@ -XXX,XX +XXX,XX @@ -# -# Project Wok -# -# Copyright IBM Corp, 2015-2016 -# -# Code derived from Project Kimchi -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -from wok.control.base import SimpleCollection -from wok.control.utils import UrlSubNode - - -@UrlSubNode("plugins") -class Plugins(SimpleCollection): - def __init__(self, model): - super(Plugins, self).__init__(model) diff --git a/src/wok/i18n.py b/src/wok/i18n.py index XXXXXXX..XXXXXXX 100644 --- a/src/wok/i18n.py +++ b/src/wok/i18n.py @@ -XXX,XX +XXX,XX @@ messages = { "WOKCONFIG0001I": _("WoK is going to restart. Existing WoK connections will be closed."), + "WOKPLUGIN0001E": _("Unable to find plug-in %(name)s"), + # These messages (ending with L) are for user log purposes "WOKASYNC0001L": _("Successfully completed task '%(target_uri)s'"), "WOKASYNC0002L": _("Failed to complete task '%(target_uri)s'"), @@ -XXX,XX +XXX,XX @@ messages = { "WOKRES0001L": _("Request made on resource"), "WOKROOT0001L": _("User '%(username)s' login"), "WOKROOT0002L": _("User '%(username)s' logout"), + "WOKPLUGIN0001L": _("Enable plug-in %(ident)s."), + "WOKPLUGIN0002L": _("Disable plug-in %(ident)s."), } diff --git a/src/wok/model/plugins.py b/src/wok/model/plugins.py index XXXXXXX..XXXXXXX 100644 --- a/src/wok/model/plugins.py +++ b/src/wok/model/plugins.py @@ -XXX,XX +XXX,XX @@ # # Project Wok # -# Copyright IBM Corp, 2015-2016 +# Copyright IBM Corp, 2015-2017 # # Code derived from Project Kimchi # @@ -XXX,XX +XXX,XX @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import cherrypy -from wok.config import get_base_plugin_uri -from wok.utils import get_enabled_plugins +from wok.exception import NotFoundError +from wok.utils import get_all_affected_plugins_by_plugin +from wok.utils import get_plugin_dependencies, get_plugins, load_plugin_conf +from wok.utils import set_plugin_state class PluginsModel(object): @@ -XXX,XX +XXX,XX @@ class PluginsModel(object): pass def get_list(self): - # Will only return plugins that were loaded correctly by WOK and are - # properly configured in cherrypy - return [plugin for (plugin, config) in get_enabled_plugins() - if get_base_plugin_uri(plugin) in cherrypy.tree.apps.keys()] + return [plugin for (plugin, config) in get_plugins()] + + +class PluginModel(object): + def __init__(self, **kargs): + pass + + def lookup(self, name): + name = name.encode('utf-8') + + plugin_conf = load_plugin_conf(name) + if not plugin_conf: + raise NotFoundError("WOKPLUGIN0001E", {'name': name}) + + depends = get_plugin_dependencies(name) + is_dependency_of = get_all_affected_plugins_by_plugin(name) + + return {"name": name, "enabled": plugin_conf['wok']['enable'], + "depends": depends, "is_dependency_of": is_dependency_of} + + def enable(self, name): + name = name.encode('utf-8') + set_plugin_state(name, True) + + def disable(self, name): + name = name.encode('utf-8') + set_plugin_state(name, False) diff --git a/src/wok/server.py b/src/wok/server.py index XXXXXXX..XXXXXXX 100644 --- a/src/wok/server.py +++ b/src/wok/server.py @@ -XXX,XX +XXX,XX @@ # # Project Wok # -# Copyright IBM Corp, 2015-2016 +# Copyright IBM Corp, 2015-2017 # # Code derived from Project Kimchi # @@ -XXX,XX +XXX,XX @@ import os from wok import auth from wok import config from wok.config import config as configParser -from wok.config import PluginConfig, WokConfig +from wok.config import WokConfig from wok.control import sub_nodes from wok.model import model from wok.proxy import check_proxy_config from wok.reqlogger import RequestLogger from wok.root import WokRoot from wok.safewatchedfilehandler import SafeWatchedFileHandler -from wok.utils import get_enabled_plugins, import_class +from wok.utils import get_enabled_plugins, load_plugin LOGGING_LEVEL = {"debug": logging.DEBUG, @@ -XXX,XX +XXX,XX @@ class Server(object): self.app = cherrypy.tree.mount(WokRoot(model_instance, dev_env), options.server_root, self.configObj) - self._load_plugins(options) + self._load_plugins() cherrypy.lib.sessions.init() - def _load_plugins(self, options): + def _load_plugins(self): for plugin_name, plugin_config in get_enabled_plugins(): - try: - plugin_class = ('plugins.%s.%s' % - (plugin_name, - plugin_name[0].upper() + plugin_name[1:])) - del plugin_config['wok'] - plugin_config.update(PluginConfig(plugin_name)) - except KeyError: - continue - - try: - plugin_app = import_class(plugin_class)(options) - except (ImportError, Exception), e: - cherrypy.log.error_log.error( - "Failed to import plugin %s, " - "error: %s" % (plugin_class, e.message) - ) - continue - - # dynamically extend plugin config with custom data, if provided - get_custom_conf = getattr(plugin_app, "get_custom_conf", None) - if get_custom_conf is not None: - plugin_config.update(get_custom_conf()) - - # dynamically add tools.wokauth.on = True to extra plugin APIs - try: - sub_nodes = import_class('plugins.%s.control.sub_nodes' % - plugin_name) - - urlSubNodes = {} - for ident, node in sub_nodes.items(): - if node.url_auth: - ident = "/%s" % ident - urlSubNodes[ident] = {'tools.wokauth.on': True} - - plugin_config.update(urlSubNodes) - - except ImportError, e: - cherrypy.log.error_log.error( - "Failed to import subnodes for plugin %s, " - "error: %s" % (plugin_class, e.message) - ) - - cherrypy.tree.mount(plugin_app, - config.get_base_plugin_uri(plugin_name), - plugin_config) + load_plugin(plugin_name, plugin_config) def start(self): # Subscribe to SignalHandler plugin diff --git a/src/wok/utils.py b/src/wok/utils.py index XXXXXXX..XXXXXXX 100644 --- a/src/wok/utils.py +++ b/src/wok/utils.py @@ -XXX,XX +XXX,XX @@ # # Project Wok # -# Copyright IBM Corp, 2015-2016 +# Copyright IBM Corp, 2015-2017 # # Code derived from Project Kimchi # @@ -XXX,XX +XXX,XX @@ import xml.etree.ElementTree as ET from cherrypy.lib.reprconf import Parser from datetime import datetime, timedelta from multiprocessing import Process, Queue +from optparse import Values from threading import Timer -from wok.config import paths, PluginPaths +from wok import config +from wok.config import paths, PluginConfig, PluginPaths from wok.exception import InvalidParameter, TimeoutExpired from wok.stringutils import decode_value @@ -XXX,XX +XXX,XX @@ def is_digit(value): return False -def _load_plugin_conf(name): +def get_plugin_config_file(name): plugin_conf = PluginPaths(name).conf_file if not os.path.exists(plugin_conf): cherrypy.log.error_log.error("Plugin configuration file %s" " doesn't exist." % plugin_conf) - return + return None + return plugin_conf + + +def load_plugin_conf(name): try: + plugin_conf = get_plugin_config_file(name) + if not plugin_conf: + return None + return Parser().dict_from_file(plugin_conf) except ValueError as e: cherrypy.log.error_log.error("Failed to load plugin " @@ -XXX,XX +XXX,XX @@ def _load_plugin_conf(name): (plugin_conf, e.message)) -def get_enabled_plugins(): +def get_plugins(enabled_only=False): plugin_dir = paths.plugins_dir + try: dir_contents = os.listdir(plugin_dir) except OSError: return + + test_mode = config.config.get('server', 'test').lower() == 'true' + for name in dir_contents: if os.path.isdir(os.path.join(plugin_dir, name)): - plugin_config = _load_plugin_conf(name) + if name == 'sample' and not test_mode: + continue + + plugin_config = load_plugin_conf(name) + if not plugin_config: + continue try: - if plugin_config['wok']['enable']: - yield (name, plugin_config) + if plugin_config['wok']['enable'] is None: + continue + + plugin_enabled = plugin_config['wok']['enable'] + if enabled_only and not plugin_enabled: + continue + + yield (name, plugin_config) except (TypeError, KeyError): continue +def get_enabled_plugins(): + return get_plugins(enabled_only=True) + + +def get_plugin_app_mounted_in_cherrypy(name): + plugin_uri = '/plugins/' + name + return cherrypy.tree.apps.get(plugin_uri, None) + + +def get_plugin_dependencies(name): + app = get_plugin_app_mounted_in_cherrypy(name) + if app is None or not hasattr(app.root, 'depends'): + return [] + return app.root.depends + + +def get_all_plugins_dependent_on(name): + if not cherrypy.tree.apps: + return [] + + dependencies = [] + for plugin, app in cherrypy.tree.apps.iteritems(): + if hasattr(app.root, 'depends') and name in app.root.depends: + dependencies.append(plugin.replace('/plugins/', '')) + + return dependencies + + +def get_all_affected_plugins_by_plugin(name): + dependencies = get_all_plugins_dependent_on(name) + if len(dependencies) == 0: + return [] + + all_affected_plugins = dependencies + for dep in dependencies: + all_affected_plugins += get_all_affected_plugins_by_plugin(dep) + + return all_affected_plugins + + +def disable_plugin(name): + plugin_deps = get_all_affected_plugins_by_plugin(name) + + for dep in set(plugin_deps): + update_plugin_config_file(dep, False) + update_cherrypy_mounted_tree(dep, False) + + update_plugin_config_file(name, False) + update_cherrypy_mounted_tree(name, False) + + +def enable_plugin(name): + update_plugin_config_file(name, True) + update_cherrypy_mounted_tree(name, True) + + plugin_deps = get_plugin_dependencies(name) + + for dep in set(plugin_deps): + enable_plugin(dep) + + +def set_plugin_state(name, state): + if state is False: + disable_plugin(name) + else: + enable_plugin(name) + + +def update_plugin_config_file(name, state): + plugin_conf = get_plugin_config_file(name) + if not plugin_conf: + return + + config_contents = None + + with open(plugin_conf, 'r') as f: + config_contents = f.readlines() + + wok_section_found = False + + pattern = re.compile("^\s*enable\s*=\s*") + + for i in range(0, len(config_contents)): + if config_contents[i] == '[wok]\n': + wok_section_found = True + continue + + if pattern.match(config_contents[i]) and wok_section_found: + config_contents[i] = 'enable = %s\n' % str(state) + break + + with open(plugin_conf, 'w') as f: + f.writelines(config_contents) + + +def load_plugin(plugin_name, plugin_config): + try: + plugin_class = ('plugins.%s.%s' % + (plugin_name, + plugin_name[0].upper() + plugin_name[1:])) + del plugin_config['wok'] + plugin_config.update(PluginConfig(plugin_name)) + except KeyError: + return + + try: + options = get_plugin_config_options() + plugin_app = import_class(plugin_class)(options) + except (ImportError, Exception), e: + cherrypy.log.error_log.error( + "Failed to import plugin %s, " + "error: %s" % (plugin_class, e.message) + ) + return + + # dynamically extend plugin config with custom data, if provided + get_custom_conf = getattr(plugin_app, "get_custom_conf", None) + if get_custom_conf is not None: + plugin_config.update(get_custom_conf()) + + # dynamically add tools.wokauth.on = True to extra plugin APIs + try: + sub_nodes = import_class('plugins.%s.control.sub_nodes' % + plugin_name) + + urlSubNodes = {} + for ident, node in sub_nodes.items(): + if node.url_auth: + ident = "/%s" % ident + urlSubNodes[ident] = {'tools.wokauth.on': True} + + plugin_config.update(urlSubNodes) + + except ImportError, e: + cherrypy.log.error_log.error( + "Failed to import subnodes for plugin %s, " + "error: %s" % (plugin_class, e.message) + ) + + cherrypy.tree.mount(plugin_app, + config.get_base_plugin_uri(plugin_name), + plugin_config) + + +def is_plugin_mounted_in_cherrypy(plugin_uri): + return cherrypy.tree.apps.get(plugin_uri) is not None + + +def update_cherrypy_mounted_tree(plugin, state): + plugin_uri = '/plugin/' + plugin + + if state is False and is_plugin_mounted_in_cherrypy(plugin_uri): + del cherrypy.tree.apps[plugin_uri] + + if state is True and not is_plugin_mounted_in_cherrypy(plugin_uri): + plugin_config = load_plugin_conf(plugin) + load_plugin(plugin, plugin_config) + + +def get_plugin_config_options(): + options = Values() + + options.websockets_port = config.config.getint('server', + 'websockets_port') + options.cherrypy_port = config.config.getint('server', + 'cherrypy_port') + options.proxy_port = config.config.getint('server', 'proxy_port') + options.session_timeout = config.config.getint('server', + 'session_timeout') + + options.test = config.config.get('server', 'test') + if options.test == 'None': + options.test = None + + options.environment = config.config.get('server', 'environment') + options.server_root = config.config.get('server', 'server_root') + options.max_body_size = config.config.get('server', 'max_body_size') + + options.log_dir = config.config.get('logging', 'log_dir') + options.log_level = config.config.get('logging', 'log_level') + options.access_log = config.config.get('logging', 'access_log') + options.error_log = config.config.get('logging', 'error_log') + + return options + + def get_all_tabs(): files = [os.path.join(paths.ui_dir, 'config/tab-ext.xml')] diff --git a/tests/test_api.py b/tests/test_api.py index XXXXXXX..XXXXXXX 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -XXX,XX +XXX,XX @@ import utils from functools import partial from wok.asynctask import AsyncTask +from wok.utils import set_plugin_state +from wok.rollbackcontext import RollbackContext test_server = None model = None @@ -XXX,XX +XXX,XX @@ class APITests(unittest.TestCase): "server_root"] self.assertEquals(sorted(keys), sorted(conf.keys())) + def test_config_plugins(self): + resp = self.request('/config/plugins') + self.assertEquals(200, resp.status) + + plugins = json.loads(resp.read()) + if len(plugins) == 0: + return + + plugin_name = '' + plugin_state = '' + for p in plugins: + if p.get('name') == 'sample': + plugin_name = p.get('name').encode('utf-8') + plugin_state = p.get('enabled') + break + else: + return + + with RollbackContext() as rollback: + rollback.prependDefer(set_plugin_state, plugin_name, + plugin_state) + + resp = self.request('/config/plugins/sample') + self.assertEquals(200, resp.status) + + resp = self.request('/config/plugins/sample/enable', + '{}', 'POST') + self.assertEquals(200, resp.status) + + resp = self.request('/config/plugins') + self.assertEquals(200, resp.status) + plugins = json.loads(resp.read()) + + for p in plugins: + if p.get('name') == 'sample': + plugin_state = p.get('enabled') + break + self.assertTrue(plugin_state) + + resp = self.request('/config/plugins/sample/disable', + '{}', 'POST') + self.assertEquals(200, resp.status) + + resp = self.request('/config/plugins') + self.assertEquals(200, resp.status) + plugins = json.loads(resp.read()) + + for p in plugins: + if p.get('name') == 'sample': + plugin_state = p.get('enabled') + break + self.assertFalse(plugin_state) + + def test_plugins_api_404(self): + resp = self.request('/plugins') + self.assertEquals(404, resp.status) + def test_user_log(self): # Login and logout to make sure there there are entries in user log hdrs = {'AUTHORIZATION': '', diff --git a/tests/test_utils.py b/tests/test_utils.py index XXXXXXX..XXXXXXX 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -XXX,XX +XXX,XX @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +import mock +import os +import tempfile import unittest from wok.exception import InvalidParameter -from wok.utils import convert_data_size +from wok.rollbackcontext import RollbackContext +from wok.utils import convert_data_size, set_plugin_state class UtilsTests(unittest.TestCase): @@ -XXX,XX +XXX,XX @@ class UtilsTests(unittest.TestCase): for d in success_data: self.assertEquals(d['got'], d['want']) + + def _get_fake_config_file_content(self, enable=True): + return """\ +[a_random_section] +# a random section for testing purposes +enable = 1 + +[wok] +# Enable plugin on Wok server (values: True|False) +enable = %s + +[fakeplugin] +# Yet another comment on this config file +enable = 2 +very_interesting_option = True +""" % str(enable) + + def _get_config_file_template(self, enable=True): + return """\ +[a_random_section] +# a random section for testing purposes +enable = 1 + +[wok] +# Enable plugin on Wok server (values: True|False) +enable = %s + +[fakeplugin] +# Yet another comment on this config file +enable = 2 +very_interesting_option = True +""" % str(enable) + + def _create_fake_config_file(self): + _, tmp_file_name = tempfile.mkstemp(suffix='.conf') + + config_contents = self._get_fake_config_file_content() + with open(tmp_file_name, 'w') as f: + f.writelines(config_contents) + + return tmp_file_name + + @mock.patch('wok.utils.get_plugin_config_file') + @mock.patch('wok.utils.update_cherrypy_mounted_tree') + def test_set_plugin_state(self, mock_update_cherrypy, mock_config_file): + mock_update_cherrypy.return_value = True + + with RollbackContext() as rollback: + + config_file_name = self._create_fake_config_file() + rollback.prependDefer(os.remove, config_file_name) + + mock_config_file.return_value = config_file_name + + set_plugin_state('pluginA', False) + with open(config_file_name, 'r') as f: + updated_conf = f.read() + self.assertEqual( + updated_conf, + self._get_config_file_template(enable=False) + ) + + set_plugin_state('pluginA', True) + with open(config_file_name, 'r') as f: + updated_conf = f.read() + self.assertEqual( + updated_conf, + self._get_config_file_template(enable=True) + ) -- 2.9.3 _______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel
From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> This patch changes ui/js/src/wok.api.js 'listPlugins' method to use the URL /config/plugins instead of /plugins. With this change, ui/js/src/wok.logos.js and ui/js/src/wok.main.js were also changed to handle the different return value from the /config/plugins API. Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- ui/js/src/wok.api.js | 4 ++-- ui/js/src/wok.logos.js | 11 +++++++---- ui/js/src/wok.main.js | 10 +++++++--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/ui/js/src/wok.api.js b/ui/js/src/wok.api.js index XXXXXXX..XXXXXXX 100644 --- a/ui/js/src/wok.api.js +++ b/ui/js/src/wok.api.js @@ -XXX,XX +XXX,XX @@ /* * Project Wok * - * Copyright IBM Corp, 2015-2016 + * Copyright IBM Corp, 2015-2017 * * Code derived from Project Kimchi * @@ -XXX,XX +XXX,XX @@ var wok = { listPlugins : function(suc, err, sync) { wok.requestJSON({ - url : 'plugins', + url : '/config/plugins', type : 'GET', contentType : 'application/json', dataType : 'json', diff --git a/ui/js/src/wok.logos.js b/ui/js/src/wok.logos.js index XXXXXXX..XXXXXXX 100644 --- a/ui/js/src/wok.logos.js +++ b/ui/js/src/wok.logos.js @@ -XXX,XX +XXX,XX @@ /* * Project Wok * - * Copyright IBM Corp, 2016 + * Copyright IBM Corp, 2016-2017 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -XXX,XX +XXX,XX @@ wok.logos = function(element, powered) { wok.listPlugins(function(plugins) { if(plugins && plugins.length > 0) { $(plugins).each(function(i, p) { + if (p.enabled === false) { + return true; + } var url = wok.substitute(pluginUrl, { - plugin: p + plugin: p.name }); obj[i] = { - name : p + name : p.name } var pluginVersions; pluginVersions = retrieveVersion(url); if(pluginVersions && pluginVersions.length > 0){ obj[i].version = pluginVersions; } - var imagepath = url+'/images/'+p; + var imagepath = url+'/images/'+p.name; if(checkImage(imagepath+'.svg') == 200) { obj[i].image = imagepath+'.svg'; } diff --git a/ui/js/src/wok.main.js b/ui/js/src/wok.main.js index XXXXXXX..XXXXXXX 100644 --- a/ui/js/src/wok.main.js +++ b/ui/js/src/wok.main.js @@ -XXX,XX +XXX,XX @@ wok.main = function() { var tabs = retrieveTabs('wok', wokConfigUrl); wok.listPlugins(function(plugins) { $(plugins).each(function(i, p) { + if (p.enabled === false) { + return true; + } + var url = wok.substitute(pluginConfigUrl, { - plugin: p + plugin: p.name }); var i18nUrl = wok.substitute(pluginI18nUrl, { - plugin: p + plugin: p.name }); wok.getI18n(function(i18nObj){ $.extend(i18n, i18nObj)}, function(i18nObj){ //i18n is not define by plugin }, i18nUrl, true); - var pluginTabs = retrieveTabs(p, url); + var pluginTabs = retrieveTabs(p.name, url); if(pluginTabs.length > 0){ tabs.push.apply(tabs, pluginTabs); } -- 2.9.3 _______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel
From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> v4: - added more info in docs/API/config.md - removed 'access_log' and 'error_log' from wok config - changed the default value of "test" parameter from "true" to "" - added 'self.admin_methods = ['POST'] ' in the Plugin Resource of control/config.py v3: - atomic backend commit - using regex on parser - 'test_mode' is now being retrieved by wok config - plug-in dependencies are now being fetched - plug-ins are now being enabled/disabled the cherrypy tree v2: - added User Log capabilities on /config/plugins/enable|disable actions - added 'enable=' as a valid entry in the parsing of the conf file This patch set implements the '/config/plugins' API. The idea of this API is to replace the current '/plugins' API while adding new attributes in their return values: - enabled: true if the plug-in is enabled, false otherwise - depends: list of all the plug-ins that this plug-in depends on - is_dependency_of: list of all plug-in that depends on this plug-in This backend is capable of enabling/disabling the plugi-ns using the API /config/plugins/*name*/enable|disable. Please check the commit messages of each patch for further details. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Daniel Henrique Barboza (2): /config/plugins API: backend changes /config/plugins: changing existing UI calls docs/API/config.md | 40 ++++++++ docs/API/plugins.md | 13 --- src/wok/config.py.in | 3 +- src/wok/control/config.py | 32 ++++++- src/wok/control/plugins.py | 29 ------ src/wok/i18n.py | 4 + src/wok/model/plugins.py | 40 ++++++-- src/wok/server.py | 56 ++--------- src/wok/utils.py | 225 +++++++++++++++++++++++++++++++++++++++++++-- tests/test_api.py | 59 ++++++++++++ tests/test_utils.py | 75 ++++++++++++++- ui/js/src/wok.api.js | 4 +- ui/js/src/wok.logos.js | 11 ++- ui/js/src/wok.main.js | 10 +- 14 files changed, 481 insertions(+), 120 deletions(-) delete mode 100644 docs/API/plugins.md delete mode 100644 src/wok/control/plugins.py -- 2.9.3 _______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel
From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> This patch adds a backend for a new API called /config/plugins. The idea is to be able to retrieve the 'enable' status of WoK plug-ins and also provide a way to enable/disable them. The enable|disable operation consists on two steps: - changing the 'enable=' attribute of the [WoK] section of the plugin .conf file; - the plug-in is removed/added in the cherrypy.tree on the fly. Several changes/enhancements in the backend were made to make this possible, such as: - added the 'test' parameter in the config.py.in file to make it available for reading in the backend. This parameter indicates whether WoK is running in test mode; - 'load_plugin' was moved from server.py to utils.py to make it available for utils functions to load plug-ins; - a new 'depends' attribute is now being considered in the root class of each plug-in. This is an array that indicates all the plug-ins it has a dependency on. For example, Kimchi would mark self.depends = ['gingerbase'] in its root file. The absence of this attribute means that the plug-in does not have any dependency aside from WoK. Previous /plugins API were removed because it was redundant with this work. Uni tests included. Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- docs/API/config.md | 40 ++++++++ docs/API/plugins.md | 13 --- src/wok/config.py.in | 3 +- src/wok/control/config.py | 32 ++++++- src/wok/control/plugins.py | 29 ------ src/wok/i18n.py | 4 + src/wok/model/plugins.py | 40 ++++++-- src/wok/server.py | 56 ++--------- src/wok/utils.py | 225 +++++++++++++++++++++++++++++++++++++++++++-- tests/test_api.py | 59 ++++++++++++ tests/test_utils.py | 75 ++++++++++++++- 11 files changed, 465 insertions(+), 111 deletions(-) delete mode 100644 docs/API/plugins.md delete mode 100644 src/wok/control/plugins.py diff --git a/docs/API/config.md b/docs/API/config.md index XXXXXXX..XXXXXXX 100644 --- a/docs/API/config.md +++ b/docs/API/config.md @@ -XXX,XX +XXX,XX @@ GET /config websockets_port: 64667, version: 2.0 } + +### Collection: Plugins + +**URI:** /config/plugins + +**Methods:** + +* **GET**: Retrieve a summarized list of all UI Plugins. + +#### Examples +GET /plugins +[{'name': 'pluginA', 'enabled': True, "depends":['pluginB'], "is_dependency_of":[]}, + {'name': 'pluginB', 'enabled': False, "depends":[], "is_dependency_of":['pluginA']}] + +### Resource: Plugins + +**URI:** /config/plugins/*:name* + +Represents the current state of a given WoK plug-in. + +**Methods:** + +* **GET**: Retrieve the state of the plug-in. + * name: The name of the plug-in. + * enabled: True if the plug-in is currently enabled in WoK, False otherwise. + * depends: The plug-ins that are dependencies for this plug-in. + * is_dependency_of: The plug-ins that rely on this plug-in to work properly. + +* **POST**: *See Plugin Actions* + +**Actions (POST):** + +* enable: Enables the plug-in. +* disable: Disables the plug-in. + +'enable' and 'disable' changes the plug-in configuration file attribute 'enable' +to either 'True' or 'False' respectively. It also enables or disables the plug-in +on the fly by adding/removing it from the mounted cherrypy tree. The plug-in +dependencies are taken into account and are enabled/disabled in the process +when applicable. diff --git a/docs/API/plugins.md b/docs/API/plugins.md deleted file mode 100644 index XXXXXXX..XXXXXXX --- a/docs/API/plugins.md +++ /dev/null @@ -XXX,XX +XXX,XX @@ -## REST API Specification for Plugins - -### Collection: Plugins - -**URI:** /plugins - -**Methods:** - -* **GET**: Retrieve a summarized list names of all UI Plugins - -#### Examples -GET /plugins -[pluginA, pluginB, pluginC] diff --git a/src/wok/config.py.in b/src/wok/config.py.in index XXXXXXX..XXXXXXX 100644 --- a/src/wok/config.py.in +++ b/src/wok/config.py.in @@ -XXX,XX +XXX,XX @@ # # Project Wok # -# Copyright IBM Corp, 2015-2016 +# Copyright IBM Corp, 2015-2017 # # Code derived from Project Kimchi # @@ -XXX,XX +XXX,XX @@ def _get_config(): config.set("server", "environment", "production") config.set('server', 'max_body_size', '4*1024*1024') config.set("server", "server_root", "") + config.set("server", "test", "") config.add_section("authentication") config.set("authentication", "method", "pam") config.set("authentication", "ldap_server", "") diff --git a/src/wok/control/config.py b/src/wok/control/config.py index XXXXXXX..XXXXXXX 100644 --- a/src/wok/control/config.py +++ b/src/wok/control/config.py @@ -XXX,XX +XXX,XX @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -from wok.control.base import Resource +from wok.control.base import Collection, Resource from wok.control.utils import UrlSubNode @@ -XXX,XX +XXX,XX @@ CONFIG_REQUESTS = { } +PLUGIN_REQUESTS = { + 'POST': { + 'enable': "WOKPLUGIN0001L", + 'disable': "WOKPLUGIN0002L", + }, +} + + @UrlSubNode("config") class Config(Resource): def __init__(self, model, id=None): super(Config, self).__init__(model, id) self.uri_fmt = '/config/%s' self.admin_methods = ['POST'] + self.plugins = Plugins(self.model) self.log_map = CONFIG_REQUESTS self.reload = self.generate_action_handler('reload') @property def data(self): return self.info + + +class Plugins(Collection): + def __init__(self, model): + super(Plugins, self).__init__(model) + self.resource = Plugin + + +class Plugin(Resource): + def __init__(self, model, ident=None): + super(Plugin, self).__init__(model, ident) + self.ident = ident + self.admin_methods = ['POST'] + self.uri_fmt = "/config/plugins/%s" + self.log_map = PLUGIN_REQUESTS + self.enable = self.generate_action_handler('enable') + self.disable = self.generate_action_handler('disable') + + @property + def data(self): + return self.info diff --git a/src/wok/control/plugins.py b/src/wok/control/plugins.py deleted file mode 100644 index XXXXXXX..XXXXXXX --- a/src/wok/control/plugins.py +++ /dev/null @@ -XXX,XX +XXX,XX @@ -# -# Project Wok -# -# Copyright IBM Corp, 2015-2016 -# -# Code derived from Project Kimchi -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -from wok.control.base import SimpleCollection -from wok.control.utils import UrlSubNode - - -@UrlSubNode("plugins") -class Plugins(SimpleCollection): - def __init__(self, model): - super(Plugins, self).__init__(model) diff --git a/src/wok/i18n.py b/src/wok/i18n.py index XXXXXXX..XXXXXXX 100644 --- a/src/wok/i18n.py +++ b/src/wok/i18n.py @@ -XXX,XX +XXX,XX @@ messages = { "WOKCONFIG0001I": _("WoK is going to restart. Existing WoK connections will be closed."), + "WOKPLUGIN0001E": _("Unable to find plug-in %(name)s"), + # These messages (ending with L) are for user log purposes "WOKASYNC0001L": _("Successfully completed task '%(target_uri)s'"), "WOKASYNC0002L": _("Failed to complete task '%(target_uri)s'"), @@ -XXX,XX +XXX,XX @@ messages = { "WOKRES0001L": _("Request made on resource"), "WOKROOT0001L": _("User '%(username)s' login"), "WOKROOT0002L": _("User '%(username)s' logout"), + "WOKPLUGIN0001L": _("Enable plug-in %(ident)s."), + "WOKPLUGIN0002L": _("Disable plug-in %(ident)s."), } diff --git a/src/wok/model/plugins.py b/src/wok/model/plugins.py index XXXXXXX..XXXXXXX 100644 --- a/src/wok/model/plugins.py +++ b/src/wok/model/plugins.py @@ -XXX,XX +XXX,XX @@ # # Project Wok # -# Copyright IBM Corp, 2015-2016 +# Copyright IBM Corp, 2015-2017 # # Code derived from Project Kimchi # @@ -XXX,XX +XXX,XX @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import cherrypy -from wok.config import get_base_plugin_uri -from wok.utils import get_enabled_plugins +from wok.exception import NotFoundError +from wok.utils import get_all_affected_plugins_by_plugin +from wok.utils import get_plugin_dependencies, get_plugins, load_plugin_conf +from wok.utils import set_plugin_state class PluginsModel(object): @@ -XXX,XX +XXX,XX @@ class PluginsModel(object): pass def get_list(self): - # Will only return plugins that were loaded correctly by WOK and are - # properly configured in cherrypy - return [plugin for (plugin, config) in get_enabled_plugins() - if get_base_plugin_uri(plugin) in cherrypy.tree.apps.keys()] + return [plugin for (plugin, config) in get_plugins()] + + +class PluginModel(object): + def __init__(self, **kargs): + pass + + def lookup(self, name): + name = name.encode('utf-8') + + plugin_conf = load_plugin_conf(name) + if not plugin_conf: + raise NotFoundError("WOKPLUGIN0001E", {'name': name}) + + depends = get_plugin_dependencies(name) + is_dependency_of = get_all_affected_plugins_by_plugin(name) + + return {"name": name, "enabled": plugin_conf['wok']['enable'], + "depends": depends, "is_dependency_of": is_dependency_of} + + def enable(self, name): + name = name.encode('utf-8') + set_plugin_state(name, True) + + def disable(self, name): + name = name.encode('utf-8') + set_plugin_state(name, False) diff --git a/src/wok/server.py b/src/wok/server.py index XXXXXXX..XXXXXXX 100644 --- a/src/wok/server.py +++ b/src/wok/server.py @@ -XXX,XX +XXX,XX @@ # # Project Wok # -# Copyright IBM Corp, 2015-2016 +# Copyright IBM Corp, 2015-2017 # # Code derived from Project Kimchi # @@ -XXX,XX +XXX,XX @@ import os from wok import auth from wok import config from wok.config import config as configParser -from wok.config import PluginConfig, WokConfig +from wok.config import WokConfig from wok.control import sub_nodes from wok.model import model from wok.proxy import check_proxy_config from wok.reqlogger import RequestLogger from wok.root import WokRoot from wok.safewatchedfilehandler import SafeWatchedFileHandler -from wok.utils import get_enabled_plugins, import_class +from wok.utils import get_enabled_plugins, load_plugin LOGGING_LEVEL = {"debug": logging.DEBUG, @@ -XXX,XX +XXX,XX @@ class Server(object): self.app = cherrypy.tree.mount(WokRoot(model_instance, dev_env), options.server_root, self.configObj) - self._load_plugins(options) + self._load_plugins() cherrypy.lib.sessions.init() - def _load_plugins(self, options): + def _load_plugins(self): for plugin_name, plugin_config in get_enabled_plugins(): - try: - plugin_class = ('plugins.%s.%s' % - (plugin_name, - plugin_name[0].upper() + plugin_name[1:])) - del plugin_config['wok'] - plugin_config.update(PluginConfig(plugin_name)) - except KeyError: - continue - - try: - plugin_app = import_class(plugin_class)(options) - except (ImportError, Exception), e: - cherrypy.log.error_log.error( - "Failed to import plugin %s, " - "error: %s" % (plugin_class, e.message) - ) - continue - - # dynamically extend plugin config with custom data, if provided - get_custom_conf = getattr(plugin_app, "get_custom_conf", None) - if get_custom_conf is not None: - plugin_config.update(get_custom_conf()) - - # dynamically add tools.wokauth.on = True to extra plugin APIs - try: - sub_nodes = import_class('plugins.%s.control.sub_nodes' % - plugin_name) - - urlSubNodes = {} - for ident, node in sub_nodes.items(): - if node.url_auth: - ident = "/%s" % ident - urlSubNodes[ident] = {'tools.wokauth.on': True} - - plugin_config.update(urlSubNodes) - - except ImportError, e: - cherrypy.log.error_log.error( - "Failed to import subnodes for plugin %s, " - "error: %s" % (plugin_class, e.message) - ) - - cherrypy.tree.mount(plugin_app, - config.get_base_plugin_uri(plugin_name), - plugin_config) + load_plugin(plugin_name, plugin_config) def start(self): # Subscribe to SignalHandler plugin diff --git a/src/wok/utils.py b/src/wok/utils.py index XXXXXXX..XXXXXXX 100644 --- a/src/wok/utils.py +++ b/src/wok/utils.py @@ -XXX,XX +XXX,XX @@ # # Project Wok # -# Copyright IBM Corp, 2015-2016 +# Copyright IBM Corp, 2015-2017 # # Code derived from Project Kimchi # @@ -XXX,XX +XXX,XX @@ import xml.etree.ElementTree as ET from cherrypy.lib.reprconf import Parser from datetime import datetime, timedelta from multiprocessing import Process, Queue +from optparse import Values from threading import Timer -from wok.config import paths, PluginPaths +from wok import config +from wok.config import paths, PluginConfig, PluginPaths from wok.exception import InvalidParameter, TimeoutExpired from wok.stringutils import decode_value @@ -XXX,XX +XXX,XX @@ def is_digit(value): return False -def _load_plugin_conf(name): +def get_plugin_config_file(name): plugin_conf = PluginPaths(name).conf_file if not os.path.exists(plugin_conf): cherrypy.log.error_log.error("Plugin configuration file %s" " doesn't exist." % plugin_conf) - return + return None + return plugin_conf + + +def load_plugin_conf(name): try: + plugin_conf = get_plugin_config_file(name) + if not plugin_conf: + return None + return Parser().dict_from_file(plugin_conf) except ValueError as e: cherrypy.log.error_log.error("Failed to load plugin " @@ -XXX,XX +XXX,XX @@ def _load_plugin_conf(name): (plugin_conf, e.message)) -def get_enabled_plugins(): +def get_plugins(enabled_only=False): plugin_dir = paths.plugins_dir + try: dir_contents = os.listdir(plugin_dir) except OSError: return + + test_mode = config.config.get('server', 'test').lower() == 'true' + for name in dir_contents: if os.path.isdir(os.path.join(plugin_dir, name)): - plugin_config = _load_plugin_conf(name) + if name == 'sample' and not test_mode: + continue + + plugin_config = load_plugin_conf(name) + if not plugin_config: + continue try: - if plugin_config['wok']['enable']: - yield (name, plugin_config) + if plugin_config['wok']['enable'] is None: + continue + + plugin_enabled = plugin_config['wok']['enable'] + if enabled_only and not plugin_enabled: + continue + + yield (name, plugin_config) except (TypeError, KeyError): continue +def get_enabled_plugins(): + return get_plugins(enabled_only=True) + + +def get_plugin_app_mounted_in_cherrypy(name): + plugin_uri = '/plugins/' + name + return cherrypy.tree.apps.get(plugin_uri, None) + + +def get_plugin_dependencies(name): + app = get_plugin_app_mounted_in_cherrypy(name) + if app is None or not hasattr(app.root, 'depends'): + return [] + return app.root.depends + + +def get_all_plugins_dependent_on(name): + if not cherrypy.tree.apps: + return [] + + dependencies = [] + for plugin, app in cherrypy.tree.apps.iteritems(): + if hasattr(app.root, 'depends') and name in app.root.depends: + dependencies.append(plugin.replace('/plugins/', '')) + + return dependencies + + +def get_all_affected_plugins_by_plugin(name): + dependencies = get_all_plugins_dependent_on(name) + if len(dependencies) == 0: + return [] + + all_affected_plugins = dependencies + for dep in dependencies: + all_affected_plugins += get_all_affected_plugins_by_plugin(dep) + + return all_affected_plugins + + +def disable_plugin(name): + plugin_deps = get_all_affected_plugins_by_plugin(name) + + for dep in set(plugin_deps): + update_plugin_config_file(dep, False) + update_cherrypy_mounted_tree(dep, False) + + update_plugin_config_file(name, False) + update_cherrypy_mounted_tree(name, False) + + +def enable_plugin(name): + update_plugin_config_file(name, True) + update_cherrypy_mounted_tree(name, True) + + plugin_deps = get_plugin_dependencies(name) + + for dep in set(plugin_deps): + enable_plugin(dep) + + +def set_plugin_state(name, state): + if state is False: + disable_plugin(name) + else: + enable_plugin(name) + + +def update_plugin_config_file(name, state): + plugin_conf = get_plugin_config_file(name) + if not plugin_conf: + return + + config_contents = None + + with open(plugin_conf, 'r') as f: + config_contents = f.readlines() + + wok_section_found = False + + pattern = re.compile("^\s*enable\s*=\s*") + + for i in range(0, len(config_contents)): + if config_contents[i] == '[wok]\n': + wok_section_found = True + continue + + if pattern.match(config_contents[i]) and wok_section_found: + config_contents[i] = 'enable = %s\n' % str(state) + break + + with open(plugin_conf, 'w') as f: + f.writelines(config_contents) + + +def load_plugin(plugin_name, plugin_config): + try: + plugin_class = ('plugins.%s.%s' % + (plugin_name, + plugin_name[0].upper() + plugin_name[1:])) + del plugin_config['wok'] + plugin_config.update(PluginConfig(plugin_name)) + except KeyError: + return + + try: + options = get_plugin_config_options() + plugin_app = import_class(plugin_class)(options) + except (ImportError, Exception), e: + cherrypy.log.error_log.error( + "Failed to import plugin %s, " + "error: %s" % (plugin_class, e.message) + ) + return + + # dynamically extend plugin config with custom data, if provided + get_custom_conf = getattr(plugin_app, "get_custom_conf", None) + if get_custom_conf is not None: + plugin_config.update(get_custom_conf()) + + # dynamically add tools.wokauth.on = True to extra plugin APIs + try: + sub_nodes = import_class('plugins.%s.control.sub_nodes' % + plugin_name) + + urlSubNodes = {} + for ident, node in sub_nodes.items(): + if node.url_auth: + ident = "/%s" % ident + urlSubNodes[ident] = {'tools.wokauth.on': True} + + plugin_config.update(urlSubNodes) + + except ImportError, e: + cherrypy.log.error_log.error( + "Failed to import subnodes for plugin %s, " + "error: %s" % (plugin_class, e.message) + ) + + cherrypy.tree.mount(plugin_app, + config.get_base_plugin_uri(plugin_name), + plugin_config) + + +def is_plugin_mounted_in_cherrypy(plugin_uri): + return cherrypy.tree.apps.get(plugin_uri) is not None + + +def update_cherrypy_mounted_tree(plugin, state): + plugin_uri = '/plugin/' + plugin + + if state is False and is_plugin_mounted_in_cherrypy(plugin_uri): + del cherrypy.tree.apps[plugin_uri] + + if state is True and not is_plugin_mounted_in_cherrypy(plugin_uri): + plugin_config = load_plugin_conf(plugin) + load_plugin(plugin, plugin_config) + + +def get_plugin_config_options(): + options = Values() + + options.websockets_port = config.config.getint('server', + 'websockets_port') + options.cherrypy_port = config.config.getint('server', + 'cherrypy_port') + options.proxy_port = config.config.getint('server', 'proxy_port') + options.session_timeout = config.config.getint('server', + 'session_timeout') + + options.test = config.config.get('server', 'test') + if options.test == 'None': + options.test = None + + options.environment = config.config.get('server', 'environment') + options.server_root = config.config.get('server', 'server_root') + options.max_body_size = config.config.get('server', 'max_body_size') + + options.log_dir = config.config.get('logging', 'log_dir') + options.log_level = config.config.get('logging', 'log_level') + + return options + + def get_all_tabs(): files = [os.path.join(paths.ui_dir, 'config/tab-ext.xml')] diff --git a/tests/test_api.py b/tests/test_api.py index XXXXXXX..XXXXXXX 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -XXX,XX +XXX,XX @@ import utils from functools import partial from wok.asynctask import AsyncTask +from wok.utils import set_plugin_state +from wok.rollbackcontext import RollbackContext test_server = None model = None @@ -XXX,XX +XXX,XX @@ class APITests(unittest.TestCase): "server_root"] self.assertEquals(sorted(keys), sorted(conf.keys())) + def test_config_plugins(self): + resp = self.request('/config/plugins') + self.assertEquals(200, resp.status) + + plugins = json.loads(resp.read()) + if len(plugins) == 0: + return + + plugin_name = '' + plugin_state = '' + for p in plugins: + if p.get('name') == 'sample': + plugin_name = p.get('name').encode('utf-8') + plugin_state = p.get('enabled') + break + else: + return + + with RollbackContext() as rollback: + rollback.prependDefer(set_plugin_state, plugin_name, + plugin_state) + + resp = self.request('/config/plugins/sample') + self.assertEquals(200, resp.status) + + resp = self.request('/config/plugins/sample/enable', + '{}', 'POST') + self.assertEquals(200, resp.status) + + resp = self.request('/config/plugins') + self.assertEquals(200, resp.status) + plugins = json.loads(resp.read()) + + for p in plugins: + if p.get('name') == 'sample': + plugin_state = p.get('enabled') + break + self.assertTrue(plugin_state) + + resp = self.request('/config/plugins/sample/disable', + '{}', 'POST') + self.assertEquals(200, resp.status) + + resp = self.request('/config/plugins') + self.assertEquals(200, resp.status) + plugins = json.loads(resp.read()) + + for p in plugins: + if p.get('name') == 'sample': + plugin_state = p.get('enabled') + break + self.assertFalse(plugin_state) + + def test_plugins_api_404(self): + resp = self.request('/plugins') + self.assertEquals(404, resp.status) + def test_user_log(self): # Login and logout to make sure there there are entries in user log hdrs = {'AUTHORIZATION': '', diff --git a/tests/test_utils.py b/tests/test_utils.py index XXXXXXX..XXXXXXX 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -XXX,XX +XXX,XX @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +import mock +import os +import tempfile import unittest from wok.exception import InvalidParameter -from wok.utils import convert_data_size +from wok.rollbackcontext import RollbackContext +from wok.utils import convert_data_size, set_plugin_state class UtilsTests(unittest.TestCase): @@ -XXX,XX +XXX,XX @@ class UtilsTests(unittest.TestCase): for d in success_data: self.assertEquals(d['got'], d['want']) + + def _get_fake_config_file_content(self, enable=True): + return """\ +[a_random_section] +# a random section for testing purposes +enable = 1 + +[wok] +# Enable plugin on Wok server (values: True|False) +enable = %s + +[fakeplugin] +# Yet another comment on this config file +enable = 2 +very_interesting_option = True +""" % str(enable) + + def _get_config_file_template(self, enable=True): + return """\ +[a_random_section] +# a random section for testing purposes +enable = 1 + +[wok] +# Enable plugin on Wok server (values: True|False) +enable = %s + +[fakeplugin] +# Yet another comment on this config file +enable = 2 +very_interesting_option = True +""" % str(enable) + + def _create_fake_config_file(self): + _, tmp_file_name = tempfile.mkstemp(suffix='.conf') + + config_contents = self._get_fake_config_file_content() + with open(tmp_file_name, 'w') as f: + f.writelines(config_contents) + + return tmp_file_name + + @mock.patch('wok.utils.get_plugin_config_file') + @mock.patch('wok.utils.update_cherrypy_mounted_tree') + def test_set_plugin_state(self, mock_update_cherrypy, mock_config_file): + mock_update_cherrypy.return_value = True + + with RollbackContext() as rollback: + + config_file_name = self._create_fake_config_file() + rollback.prependDefer(os.remove, config_file_name) + + mock_config_file.return_value = config_file_name + + set_plugin_state('pluginA', False) + with open(config_file_name, 'r') as f: + updated_conf = f.read() + self.assertEqual( + updated_conf, + self._get_config_file_template(enable=False) + ) + + set_plugin_state('pluginA', True) + with open(config_file_name, 'r') as f: + updated_conf = f.read() + self.assertEqual( + updated_conf, + self._get_config_file_template(enable=True) + ) -- 2.9.3 _______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel
From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> This patch changes ui/js/src/wok.api.js 'listPlugins' method to use the URL /config/plugins instead of /plugins. With this change, ui/js/src/wok.logos.js and ui/js/src/wok.main.js were also changed to handle the different return value from the /config/plugins API. Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- ui/js/src/wok.api.js | 4 ++-- ui/js/src/wok.logos.js | 11 +++++++---- ui/js/src/wok.main.js | 10 +++++++--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/ui/js/src/wok.api.js b/ui/js/src/wok.api.js index XXXXXXX..XXXXXXX 100644 --- a/ui/js/src/wok.api.js +++ b/ui/js/src/wok.api.js @@ -XXX,XX +XXX,XX @@ /* * Project Wok * - * Copyright IBM Corp, 2015-2016 + * Copyright IBM Corp, 2015-2017 * * Code derived from Project Kimchi * @@ -XXX,XX +XXX,XX @@ var wok = { listPlugins : function(suc, err, sync) { wok.requestJSON({ - url : 'plugins', + url : '/config/plugins', type : 'GET', contentType : 'application/json', dataType : 'json', diff --git a/ui/js/src/wok.logos.js b/ui/js/src/wok.logos.js index XXXXXXX..XXXXXXX 100644 --- a/ui/js/src/wok.logos.js +++ b/ui/js/src/wok.logos.js @@ -XXX,XX +XXX,XX @@ /* * Project Wok * - * Copyright IBM Corp, 2016 + * Copyright IBM Corp, 2016-2017 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -XXX,XX +XXX,XX @@ wok.logos = function(element, powered) { wok.listPlugins(function(plugins) { if(plugins && plugins.length > 0) { $(plugins).each(function(i, p) { + if (p.enabled === false) { + return true; + } var url = wok.substitute(pluginUrl, { - plugin: p + plugin: p.name }); obj[i] = { - name : p + name : p.name } var pluginVersions; pluginVersions = retrieveVersion(url); if(pluginVersions && pluginVersions.length > 0){ obj[i].version = pluginVersions; } - var imagepath = url+'/images/'+p; + var imagepath = url+'/images/'+p.name; if(checkImage(imagepath+'.svg') == 200) { obj[i].image = imagepath+'.svg'; } diff --git a/ui/js/src/wok.main.js b/ui/js/src/wok.main.js index XXXXXXX..XXXXXXX 100644 --- a/ui/js/src/wok.main.js +++ b/ui/js/src/wok.main.js @@ -XXX,XX +XXX,XX @@ wok.main = function() { var tabs = retrieveTabs('wok', wokConfigUrl); wok.listPlugins(function(plugins) { $(plugins).each(function(i, p) { + if (p.enabled === false) { + return true; + } + var url = wok.substitute(pluginConfigUrl, { - plugin: p + plugin: p.name }); var i18nUrl = wok.substitute(pluginI18nUrl, { - plugin: p + plugin: p.name }); wok.getI18n(function(i18nObj){ $.extend(i18n, i18nObj)}, function(i18nObj){ //i18n is not define by plugin }, i18nUrl, true); - var pluginTabs = retrieveTabs(p, url); + var pluginTabs = retrieveTabs(p.name, url); if(pluginTabs.length > 0){ tabs.push.apply(tabs, pluginTabs); } -- 2.9.3 _______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel