[Kimchi-devel] [PATCH] [WoK 1/2] /config/plugins API: backend changes

Daniel Henrique Barboza dhbarboza82 at gmail.com
Fri Feb 3 17:34:54 UTC 2017


Aline, I've found problems with this request:

" Please, set self.admin_methods = [POST] to restrict enable/disable 
operations to admin users.
Also update test_authorization.py to validate that. Use the sample 
plugin in the tests. "

First problem: there is no test_authorization.py in WoK. I would need to 
make
one similar to what Kimchi has. Not a big deal, just mentioning it here.


Second problem: setting admin_methods = ['POST'] is blocking the GET 
requests too.
This is the change I've made in the v4 of the patch:


diff --git a/src/wok/control/config.py b/src/wok/control/config.py
index 05383c7..a1fdd42 100644
--- a/src/wok/control/config.py
+++ b/src/wok/control/config.py
@@ -36,7 +36,7 @@ PLUGIN_REQUESTS = {
  }


- at UrlSubNode("config")
+ at UrlSubNode("config", True)
  class Config(Resource):
      def __init__(self, model, id=None):
          super(Config, self).__init__(model, id)
@@ -61,6 +61,7 @@ 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')



With this change, I am unable to get the contents of both /config and 
/config/plugins
without typing username and password.

And it kind of makes sense. I haven't looked into the internals perhaps 
'admin_methods' means
that the 'POST' method will require admin (sudo) privilege and the other 
http methods will
not require a sudo user, but *will require an authentication*. I can 
assert that this is the
behavior right now - using a non-sudo user I an able to retrieve the 
contents of the /config:

[danielhb at arthas wok_all_plugins]$  curl -k -u not_sudo -H 
"Content-Type: application/json" -H "Accept: application/json" -X GET  
'https://localhost:8001/config' -d'{}'
Enter host password for user 'not_sudo':
{
   "proxy_port":"8001",
   "websockets_port":"64667",
   "version":"2.3.0-72.gitf7effa8",
   "auth":"pam",
   "server_root":""
}[danielhb at arthas wok_all_plugins]$


If I do not supply credentials, a 401 html error is returned.

Both APIs are being retrieved in the UI without credentials to build WoK 
login. If I go
forward with this change as is, the plug-in icons aren't displayed in 
the bottom of
the login page.


Unless I am missing something trivial, I think we'll have to postpone 
this change
until we're certain we're not breaking anything that's currently working.

I'll add the 'self.admin_methods = [POST]' line alone, but I'll not turn 
on the authentication
of this controller. I'll postpone the test_authentication change too 
since it makes little sense
to add it with authentication off in the controller.


Daniel



On 02/03/2017 12:39 PM, Aline Manera wrote:
>
>
> On 02/03/2017 12:32 PM, Daniel Henrique Barboza wrote:
>>
>>
>> On 02/03/2017 12:21 PM, Aline Manera wrote:
>>> Hi Daniel,
>>>
>>> On 02/01/2017 10:03 AM, dhbarboza82 at gmail.com wrote:
>>>> From: Daniel Henrique Barboza <danielhb at 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 at 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 4ba455e..87619ac 100644
>>>> --- a/docs/API/config.md
>>>> +++ b/docs/API/config.md
>>>> @@ -26,3 +26,35 @@ 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.
>>>> +
>>>
>>> You forgot to add the description to depends and is_dependency_of 
>>> parameters
>>
>> v4
>>
>>>
>>>> +* **POST**: *See Plugin Actions*
>>>> +
>>>> +**Actions (POST):**
>>>> +
>>>> +* enable: Enable the plug-in in the configuration file.
>>>> +* disable: Disable the plug-in in the configuration file.
>>>
>>> As you are now doing the change on the fly, I'd say to add it to the 
>>> description action as well.
>>
>> v4
>>>
>>>> diff --git a/docs/API/plugins.md b/docs/API/plugins.md
>>>> deleted file mode 100644
>>>> index aaa37b5..0000000
>>>> --- a/docs/API/plugins.md
>>>> +++ /dev/null
>>>> @@ -1,13 +0,0 @@
>>>> -## 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 9573e66..0e46b17 100644
>>>> --- a/src/wok/config.py.in
>>>> +++ b/src/wok/config.py.in
>>>> @@ -1,7 +1,7 @@
>>>>   #
>>>>   # Project Wok
>>>>   #
>>>> -# Copyright IBM Corp, 2015-2016
>>>> +# Copyright IBM Corp, 2015-2017
>>>>   #
>>>>   # Code derived from Project Kimchi
>>>>   #
>>>> @@ -269,6 +269,7 @@ 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")
>>>
>>> The default value should be 'false' as by default Wok runs on 
>>> production mode.
>>
>> Yeah I've tested with both "true" and "false" there and it turned out 
>> that "true" allows
>> for less code changes. Reason is that when running in production mode 
>> WoK the
>> option does not exist and the value of this option is set to 'None', 
>> even when
>> setting this default to "false".
>
> Maybe set it to None so. 'true' is not a right value IMO
>
>>
>>>
>>>> config.add_section("authentication")
>>>>       config.set("authentication", "method", "pam")
>>>>       config.set("authentication", "ldap_server", "")
>>>> @@ -278,6 +279,8 @@ 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", "")
>>>
>>> Seems a rebase issue here. There was a patch to remove those 
>>> configuration (access_log and error_log)
>>
>> No it isn't, I've added the options because the command line has them. 
>
> That is not true. I did a patch that was applied 'recently' to remove 
> them from command line as they are not present in the config file.
>
> Check da528f461fdf0c82dbf864d5c1309cd9f159a1f0 for details.
>
>> Given
>> than the plug-ins use them in the load process I wanted to send the 
>> exact same
>> values in the "def get_plugin_config_options()" call.
>>
>> Why were those options removed? If no plug-in is using those values I 
>> think we
>> can safely remove them here too.
>
> The log parameters is only used by Wok to set them on cherrypy. The 
> plugins only use wok_log to get the log instance to use.
>
>>
>>>
>>>>
>>>>       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 419abc0..05383c7 100644
>>>> --- a/src/wok/control/config.py
>>>> +++ b/src/wok/control/config.py
>>>> @@ -17,7 +17,7 @@
>>>>   # 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
>>>>
>>>>
>>>> @@ -28,15 +28,44 @@ 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')
>>>> +
>>>
>>> Please, set self.admin_methods = [POST] to restrict enable/disable 
>>> operations to admin users.
>>> Also update test_authorization.py to validate that. Use the sample 
>>> plugin in the tests.
>>
>> v4
>>
>>>
>>>> +    @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 57dfa1b..0000000
>>>> --- a/src/wok/control/plugins.py
>>>> +++ /dev/null
>>>> @@ -1,29 +0,0 @@
>>>> -#
>>>> -# 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
>>>> -
>>>> -
>>>> - at 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 935c9c1..d44c2f6 100644
>>>> --- a/src/wok/i18n.py
>>>> +++ b/src/wok/i18n.py
>>>> @@ -57,6 +57,8 @@ 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'"),
>>>> @@ -65,4 +67,6 @@ 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 1b8ec5e..1b39e6c 100644
>>>> --- a/src/wok/model/plugins.py
>>>> +++ b/src/wok/model/plugins.py
>>>> @@ -1,7 +1,7 @@
>>>>   #
>>>>   # Project Wok
>>>>   #
>>>> -# Copyright IBM Corp, 2015-2016
>>>> +# Copyright IBM Corp, 2015-2017
>>>>   #
>>>>   # Code derived from Project Kimchi
>>>>   #
>>>> @@ -19,10 +19,11 @@
>>>>   # 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):
>>>> @@ -30,7 +31,30 @@ 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 48f455b..9b49c1a 100644
>>>> --- a/src/wok/server.py
>>>> +++ b/src/wok/server.py
>>>> @@ -1,7 +1,7 @@
>>>>   #
>>>>   # Project Wok
>>>>   #
>>>> -# Copyright IBM Corp, 2015-2016
>>>> +# Copyright IBM Corp, 2015-2017
>>>>   #
>>>>   # Code derived from Project Kimchi
>>>>   #
>>>> @@ -28,14 +28,14 @@ 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,
>>>> @@ -153,56 +153,12 @@ 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 9a08001..9e6bb8a 100644
>>>> --- a/src/wok/utils.py
>>>> +++ b/src/wok/utils.py
>>>> @@ -1,7 +1,7 @@
>>>>   #
>>>>   # Project Wok
>>>>   #
>>>> -# Copyright IBM Corp, 2015-2016
>>>> +# Copyright IBM Corp, 2015-2017
>>>>   #
>>>>   # Code derived from Project Kimchi
>>>>   #
>>>> @@ -37,9 +37,11 @@ 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
>>>>
>>>> @@ -57,13 +59,21 @@ 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 "
>>>> @@ -71,22 +81,223 @@ 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 1430bc1..6fbee75 100644
>>>> --- a/tests/test_api.py
>>>> +++ b/tests/test_api.py
>>>> @@ -26,6 +26,8 @@ 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
>>>> @@ -54,6 +56,63 @@ 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 e7fd264..e63e1a2 100644
>>>> --- a/tests/test_utils.py
>>>> +++ b/tests/test_utils.py
>>>> @@ -19,10 +19,14 @@
>>>>   # 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):
>>>> @@ -69,3 +73,72 @@ 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)
>>>> +                )
>>>
>>> _______________________________________________
>>> 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