[Kimchi-devel] [PATCH v2][Wok 1/8] Creates pluginmanager.py module
Rodrigo Trujillo
rodrigo.trujillo at linux.vnet.ibm.com
Wed Jun 8 20:59:02 UTC 2016
On 06/08/2016 05:28 PM, Aline Manera wrote:
>
>
> On 06/08/2016 05:16 PM, Rodrigo Trujillo wrote:
>>
>>
>> On 06/08/2016 03:52 PM, Aline Manera wrote:
>>>
>>>
>>> On 06/06/2016 04:13 PM, Rodrigo Trujillo wrote:
>>>> Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo at linux.vnet.ibm.com>
>>>> ---
>>>> src/wok/pluginsmanager.py | 238
>>>> ++++++++++++++++++++++++++++++++++++++++++++++
>>>> 1 file changed, 238 insertions(+)
>>>> create mode 100644 src/wok/pluginsmanager.py
>>>>
>>>> diff --git a/src/wok/pluginsmanager.py b/src/wok/pluginsmanager.py
>>>> new file mode 100644
>>>> index 0000000..d799f11
>>>> --- /dev/null
>>>> +++ b/src/wok/pluginsmanager.py
>>>> @@ -0,0 +1,238 @@
>>>> +#
>>>> +# Project Wok
>>>> +#
>>>> +# Copyright IBM Corp, 2016
>>>> +#
>>>> +# 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
>>>> +#
>>>> +
>>>> +
>>>> +import cherrypy
>>>> +import os
>>>> +import xml.etree.ElementTree as ET
>>>> +from cherrypy.lib.reprconf import Parser
>>>> +from configobj import ConfigObj
>>>> +
>>>> +
>>>> +from basemodel import Singleton
>>>> +from wok.config import paths, PluginConfig, PluginPaths
>>>> +from wok.exception import OperationFailed
>>>> +from wok.utils import import_class, wok_log
>>>> +
>>>> +
>>>> +class Plugins():
>>>> + __metaclass__ = Singleton
>>>> +
>>>> + def __init__(self, options=None):
>>>> + # { '<PLUGIN_NAME>': {
>>>> + # config: <PLUGIN_CHERRYPY_CONFIG>,
>>>> + # enabled: <TRUE/FALSE>,
>>>> + # uri: <PLUGIN_URI_FROM_FILE_CONFIG>,
>>>> + # app: <PLUGIN_MAIN_MODULE>
>>>> + # conf_file: <PLUGIN_CONFIGURATION_FILE>
>>>> + self._plugins_dict = {}
>>>> + self.options = options
>>>> + self._init_all_plugins()
>>>> +
>>>> + def _load_plugin_app(self, name):
>>>> + """
>>>> + Loads the plugin main module
>>>> + """
>>>> + plugin_class = ('plugins.%s.%s' % (name, name[0].upper() +
>>>> name[1:]))
>>>> + try:
>>>> + plugin_app = import_class(plugin_class)(self.options)
>>>> + except ImportError, e:
>>>> + wok_log.error("Failed to import plugin %s, error: %s" %
>>>> + (plugin_class, e.message))
>>>> + self._plugins_dict[name]['enabled'] = False
>>>> + self._plugins_dict[name]['app'] = None
>>>> + return
>>>> + self._plugins_dict[name]['app'] = plugin_app
>>>> +
>>>> + def _load_plugin_app_config(self, name):
>>>> + """
>>>> + Sets the plugin's cherrypy configuration. That is a merge
>>>> between Wok
>>>> + standard configuration plus plugin's self configuration
>>>> + """
>>>> + app_conf = {}
>>>> + app_conf.update(PluginConfig(name))
>>>> + plugin_app = self._plugins_dict[name]['app']
>>>> + if plugin_app is None:
>>>> + 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:
>>>> + app_conf.update(get_custom_conf())
>>>> +
>>>> + # dynamically add tools.wokauth.on = True to extra plugin
>>>> APIs
>>>> + try:
>>>> + sub_nodes = import_class('plugins.%s.control.sub_nodes' %
>>>> + name)
>>>> + urlSubNodes = {}
>>>> + for ident, node in sub_nodes.items():
>>>> + if node.url_auth:
>>>> + ident = "/%s" % ident
>>>> + urlSubNodes[ident] = {'tools.wokauth.on': True}
>>>> + app_conf.update(urlSubNodes)
>>>> + except ImportError, e:
>>>> + wok_log.error("Failed to import subnodes for plugin
>>>> %s, error: %s"
>>>> + % (name, e.message))
>>>> + self._plugins_dict[name]['config'] = app_conf
>>>> +
>>>> + def _load_plugin_config_file(self, name):
>>>> + """
>>>> + Loads the information from Wok section in <plugin>.conf
>>>> file. Currently
>>>> + two tags/options are required to Wok and section looks like:
>>>> + [wok]
>>>> + enable = True
>>>> + uri = '/plugins/mypluginpath'
>>>> + """
>>>> + plugin_conf_file = PluginPaths(name).conf_file
>>>> + config = {}
>>
>> I have set "config" here, to avoid the problems you reported below
>>
>
> Oh. ok! I haven't noticed that before.
>
>>>> + if not os.path.exists(plugin_conf_file):
>>>> + wok_log.error("Plugin configuration file %s doesn't
>>>> exist." %
>>>> + plugin_conf_file)
>>>> + else:
>>>> + try:
>>>> + config = Parser().dict_from_file(plugin_conf_file)
>>>> + except ValueError as e:
>>>> + msg = "Failed to load plugin conf from %s: %s"
>>>> + wok_log.error(msg % (plugin_conf_file, e.message))
>>>> +
>>>
>>>> + plugin = self._plugins_dict[name]
>>>> + plugin['conf_file'] = plugin_conf_file
>>>> + plugin['enabled'] = config.get('wok', {}).get('enable',
>>>> False)
>>>> + plugin['uri'] = config.get('wok', {}).get('uri', 'Unknow')
>>>> +
>>>
>>> This block should be on 'else' statement (as part of the 'try'
>>> block') or add a 'return' in the 'if' statement as the config file
>>> was not found.
>>> Otherwise, it will raise an exception as 'config' may be not defined
>>> if it does not entered on 'else' block. The same will happen if it
>>> goes to the 'except' block. So move it to 'try' block.
>>>
>>> Tip: if you add 'return' in the 'if' statement you don't need the
>>> 'else' anymore and eliminate one indentation level.
>>>
>> See my comment above. I want to load the "possible" plugin as
>> disabled, even if the config file is missing
>>
>
> What about the uri? Setting it to 'unknown' is a good IMO.
> We should log that the plugin was not loaded and ignore it from a
> cherrypy perspective.
That is exactly what I do in "_set_cherrypy_app". If the plugin is
disabled it will not load into cherrypy
>
>>>> + def _init_all_plugins(self):
>>>> + """
>>>> + Initializes internal plugin dictionary, searching all
>>>> directories in
>>>> + <install_path>/wok/plugins, each directory should store a
>>>> plugin
>>>> + content. Then loads its configuration file in order to set
>>>> as enabled
>>>> + or disabled.
>>>> + """
>>>> + plugin_dir = paths.plugins_dir
>>>> + try:
>>>> + dir_contents = os.listdir(plugin_dir)
>>>> + except OSError as e:
>>>> + wok_log.error("Failed to fetch plugins from '%s': %s" %
>>>> + (plugin_dir, e.message))
>>>> + return
>>>> + for name in dir_contents:
>>>> + if os.path.isdir(os.path.join(plugin_dir, name)):
>>>> + # TODO:
>>>> + # Add command line option to disable plugin by its
>>>> name
>>>> + #
>>>> + self._plugins_dict[name] = {}
>>>> + self._plugins_dict[name]['config'] = {}
>>>> + self._plugins_dict[name]['app'] = None
>>>> + self._load_plugin_config_file(name)
>>>> +
>>>
>>>> + # Disable all plugins but 'sample' in test mode
>>>> + if (self.options is not None) and self.options.test:
>>>> + self._plugins_dict[name]['enabled'] = (name ==
>>>> 'sample')
>>>> +
>>>
>>> Why? I could be able to see all plugins running on test mode.
>>> For example, Kimchi provides an MockModel to allow user tests Kimchi
>>> without making any change in the host system.
>>
>> My idea here was only considering wok running its own tests.
>> I my opinion, when wok run its tests, there is no reason to load any
>> extra plugin but sample for testing.
>>
>> Thing is that I forgot to test if this behavior could impact other
>> plugins running their own tests, and indeed
>> (just tested) Kimchi is impacted.
>>
>> So, I would like to propose a differentiation I Wok behavior between
>> running its own tests and running plugins test.
>> For instance we can continue with 'test' option and create
>> 'plugintest' option which should be set by plugins when
>> they are in test mode.
>> Or (2), create a new option "woktest" that will be passed by wok
>> tests only, in order to disable all plugins and enable Sample.
>>
>
> No No! The test mode is not only for running tests. It is for the
> whole application to run on a mock environment.
>
> So a user running wok on test mode, will see exactly the same he/she
> would see when running on non-test mode. The difference is how the
> backend is implemented.
> In the first case (test mode) it will not do persistent changes on
> system and the last case, it will do.
>
> If you want to enable/disable the sample test for unit test proposals,
> you should do that during the test case. Before starting the test
> case, enable the plugin by editing its configuration file, do the
> tests, disable it again.
>
> There is also a --enable-sample to autogen command to enable the
> Sample plugin.
>
Today Wok does not know what options.test is (grep the code). So
options.test is only used to be passed to plugins "expecting" that they
run some mock model.
Also: "In the first case (test mode) it will not do persistent changes
on system" -- this is false for Wok, for instance, it will save logs in
the system (maybe a new feature here).
My real problem is disable all plugins before Wok run its tests. And I
do not want to touch their configuration files (too much intrusive).
Just thought a better solution that I can implement quickly to fix
this: add a new option "disableplugins" (I already had thoughts about
plugins command line options).
Then I can use it in the tests and does not touch "options.test"
>>>
>>>> + if not self._plugins_dict[name]['enabled']:
>>>> + wok_log.info("Initializing plugin: %s
>>>> [DISABLED]" % name)
>>>> + else:
>>>> + wok_log.info("Initializing plugin: %s
>>>> [ENABLED]" % name)
>>>> +
>>>> + def _set_cherrypy_app(self, name):
>>>> + """
>>>> + Load plugin module and cherrypy configuration, then set it
>>>> as cherrypy
>>>> + app.
>>>> + """
>>>> + self._load_plugin_app(name)
>>>> + self._load_plugin_app_config(name)
>>>> + if self._plugins_dict[name]['enabled']:
>>>> + cherrypy.tree.mount(
>>>> + self._plugins_dict[name]['app'],
>>>> + self._plugins_dict[name]['uri'],
>>>> + self._plugins_dict[name]['config'])
>>>> + wok_log.info("Plugin '%s' loaded" %
>>>> + self._plugins_dict[name]['app'])
>>>> +
>>>> + def load_plugins(self):
>>>> + """
>>>> + Set enabled plugins into Cherrypy
>>>> + """
>>>> + for plugin in self.get_enabled_plugins():
>>>> + self._set_cherrypy_app(plugin)
>>>> +
>>>> + def get_all_plugins_info(self):
>>>> + return self._plugins_dict
>>>> +
>>>> + def get_plugin_info(self, name):
>>>> + return self._plugins_dict.get(name, {})
>>>> +
>>>> + def get_all_plugins_names(self):
>>>> + ret = self._plugins_dict.keys()
>>>> + ret.sort()
>>>> + return ret
>>>> +
>>>> + def get_enabled_plugins(self):
>>>> + ret = [plugin for plugin in self._plugins_dict.keys() if
>>>> + self._plugins_dict[plugin]['enabled']]
>>>> + ret.sort()
>>>> + return ret
>>>> +
>>>> + def _enable_plugin_conf_file(self, f_conf, enable=True):
>>>> + try:
>>>> + # 'unrepr' makes ConfigObj read and write characters
>>>> like ("'?)
>>>> + conf = ConfigObj(infile=f_conf, unrepr=True)
>>>> + conf['wok']['enable'] = enable
>>>> + with open(f_conf, 'wb') as f:
>>>> + conf.write(f)
>>>> + except Exception as e:
>>>> + wok_log.error('Error updating plugin conf file. ' +
>>>> e.message)
>>>> + raise
>>>> +
>>>> + def _change_plugin_state(self, name, enable=True):
>>>> + plugin = self._plugins_dict[name]
>>>> + if plugin['enabled'] == enable:
>>>> + return
>>>> + try:
>>>> + self._enable_plugin_conf_file(plugin['conf_file'], enable)
>>>> + except:
>>>> + raise OperationFailed('WOKPLUG0002E', {'plugin': name})
>>>> + plugin['enabled'] = enable
>>>> +
>>>> + def enable_plugin(self, plugin):
>>>> + wok_log.info("PluginsManager: Enabling plugin '%s'" % plugin)
>>>> + self._change_plugin_state(plugin, True)
>>>> +
>>>> + def disable_plugin(self, plugin):
>>>> + wok_log.info("PluginsManager: Disabling plugin '%s'" %
>>>> plugin)
>>>> + self._change_plugin_state(plugin, False)
>>>> +
>>>> +
>>>> +def get_all_tabs():
>>>> + files = []
>>>> +
>>>> + for plugin in Plugins().get_enabled_plugins():
>>>> + files.append(os.path.join(PluginPaths(plugin).ui_dir,
>>>> + 'config/tab-ext.xml'))
>>>> +
>>>> + tabs = []
>>>> + for f in files:
>>>> + try:
>>>> + root = ET.parse(f)
>>>> + except (IOError):
>>>> + wok_log.debug("Unable to load %s", f)
>>>> + continue
>>>> + tabs.extend([t.text.lower() for t in
>>>> root.getiterator('title')])
>>>> +
>>>> + return tabs
>>>
>>> _______________________________________________
>>> Kimchi-devel mailing list
>>> Kimchi-devel at ovirt.org
>>> http://lists.ovirt.org/mailman/listinfo/kimchi-devel
>>>
>>
>
> _______________________________________________
> Kimchi-devel mailing list
> Kimchi-devel at ovirt.org
> http://lists.ovirt.org/mailman/listinfo/kimchi-devel
>
More information about the Kimchi-devel
mailing list