Plugins may now register their own user preferences

To support our plugin system, I've added a stevedore hook that may be
used to load plugin values and their defaults from third parties.
This system initializes all plugins at startup, and decorates any
response with the discovered results on request. Loading is done via
stevedore, and I've started a new plugin package into which all of
our other plugins should probably be moved.

Change-Id: Idb7271dc37d01f36c512bbfd2be7f7b3c7b1a0c9
This commit is contained in:
Michael Krotscheck 2014-10-16 14:07:10 -07:00
parent 13fb13b379
commit 2629f857a5
10 changed files with 300 additions and 1 deletions

View File

@ -38,6 +38,8 @@ console_scripts =
storyboard-migrate = storyboard.migrate.cli:main
storyboard.worker.task =
subscription = storyboard.worker.task.subscription:Subscription
storyboard.plugin.user_preferences =
[build_sphinx]
source-dir = doc/source

View File

@ -30,6 +30,7 @@ from storyboard.api.v1.search import search_engine
from storyboard.notifications.notification_hook import NotificationHook
from storyboard.openstack.common.gettextutils import _ # noqa
from storyboard.openstack.common import log
from storyboard.plugin.user_preferences import initialize_user_preferences
CONF = cfg.CONF
@ -91,6 +92,9 @@ def setup_app(pecan_config=None):
search_engine_cls = search_engine_impls.ENGINE_IMPLS[search_engine_name]
search_engine.set_engine(search_engine_cls())
# Load user preference plugins
initialize_user_preferences()
# Setup notifier
if CONF.enable_notifications:
hooks.append(NotificationHook())

View File

@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo.config import cfg
from pecan import abort
from pecan import request
from pecan import rest
@ -22,6 +23,11 @@ import wsmeext.pecan as wsme_pecan
from storyboard.api.auth import authorization_checks as checks
import storyboard.db.api.users as user_api
from storyboard.openstack.common import log
CONF = cfg.CONF
LOG = log.getLogger(__name__)
class UserPreferencesController(rest.RestController):
@ -40,7 +46,11 @@ class UserPreferencesController(rest.RestController):
@wsme_pecan.wsexpose(types.DictType(unicode, unicode), int,
body=types.DictType(unicode, unicode))
def post(self, user_id, body):
"""Allow a user to update their preferences.
"""Allow a user to update their preferences. Note that a user must
explicitly set a preference value to Null/None to have it deleted.
:param user_id The ID of the user whose preferences we're updating.
:param body A dictionary of preference values.
"""
if request.current_user_id != user_id:
abort(403)

View File

@ -18,6 +18,7 @@ from oslo.db import exception as db_exc
from storyboard.common import exception as exc
from storyboard.db.api import base as api_base
from storyboard.db import models
from storyboard.plugin.user_preferences import PREFERENCE_DEFAULTS
def user_get(user_id, filter_non_public=False):
@ -74,6 +75,11 @@ def user_get_preferences(user_id):
for pref in preferences:
pref_dict[pref.key] = pref.cast_value
# Decorate with plugin defaults.
for key in PREFERENCE_DEFAULTS:
if key not in pref_dict:
pref_dict[key] = PREFERENCE_DEFAULTS[key]
return pref_dict

View File

67
storyboard/plugin/base.py Normal file
View File

@ -0,0 +1,67 @@
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied. See the License for the specific language governing permissions and
# limitations under the License.
import abc
import six
from oslo.config import cfg
from stevedore.enabled import EnabledExtensionManager
CONF = cfg.CONF
def is_enabled(ext):
"""Check to see whether a plugin should be enabled. Assumes that the
plugin extends PluginBase.
:param ext: The extension instance to check.
:return: True if it should be enabled. Otherwise false.
"""
return ext.obj.enabled()
@six.add_metaclass(abc.ABCMeta)
class PluginBase(object):
"""Base class for all storyboard plugins.
Every storyboard plugin will be provided an instance of the application
configuration, and will then be asked whether it should be enabled. Each
plugin should decide, given the configuration and the environment,
whether it has the necessary resources to operate properly.
"""
def __init__(self, config):
self.config = config
@abc.abstractmethod
def enabled(self):
"""A method which indicates whether this plugin is properly
configured and should be enabled. If it's ready to go, return True.
Otherwise, return False.
"""
class StoryboardPluginLoader(EnabledExtensionManager):
"""The storyboard plugin loader, a stevedore abstraction that formalizes
our plugin contract.
"""
def __init__(self, namespace, on_load_failure_callback=None):
super(StoryboardPluginLoader, self) \
.__init__(namespace=namespace,
check_func=is_enabled,
invoke_on_load=True,
invoke_args=(CONF,),
on_load_failure_callback=on_load_failure_callback)

View File

@ -0,0 +1,68 @@
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied. See the License for the specific language governing permissions and
# limitations under the License.
import abc
import six
from storyboard.openstack.common import log
from storyboard.plugin.base import PluginBase
from storyboard.plugin.base import StoryboardPluginLoader
LOG = log.getLogger(__name__)
PREFERENCE_DEFAULTS = dict()
def initialize_user_preferences():
"""Initialize any plugins that were installed via pip. This will parse
out all the default preference values into one dictionary for later
use in the API.
"""
manager = StoryboardPluginLoader(
namespace='storyboard.plugin.user_preferences')
if manager.extensions:
manager.map(load_preferences, PREFERENCE_DEFAULTS)
def load_preferences(ext, defaults):
"""Load all plugin default preferences into our cache.
:param ext: The extension that's handling this event.
:param defaults: The current dict of default preferences.
"""
plugin_defaults = ext.obj.get_default_preferences()
for key in plugin_defaults:
if key in defaults:
# Let's not error out here.
LOG.error("Duplicate preference key %s found." % (key,))
else:
defaults[key] = plugin_defaults[key]
@six.add_metaclass(abc.ABCMeta)
class UserPreferencesPluginBase(PluginBase):
"""Base class for a plugin that provides a set of expected user
preferences and their default values. By extending this plugin, you can
add preferences for your own storyboard plugins and workers, and have
them be manageable via your web client (Your client may need to be
customized).
"""
@abc.abstractmethod
def get_default_preferences(self):
"""Return a dictionary of preferences and their default values."""

View File

View File

@ -0,0 +1,58 @@
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied. See the License for the specific language governing permissions and
# limitations under the License.
from stevedore.extension import Extension
import storyboard.plugin.base as plugin_base
import storyboard.tests.base as base
class TestPluginBase(base.TestCase):
def setUp(self):
super(TestPluginBase, self).setUp()
self.extensions = []
self.extensions.append(Extension(
'test_one', None, None,
TestBasePlugin(dict())
))
def test_extensibility(self):
"""Assert that we can actually instantiate a plugin."""
plugin = TestBasePlugin(dict())
self.assertIsNotNone(plugin)
self.assertTrue(plugin.enabled())
def test_plugin_loader(self):
manager = plugin_base.StoryboardPluginLoader.make_test_instance(
self.extensions,
namespace='storyboard.plugin.testing'
)
results = manager.map(self._count_invocations)
# One must exist.
self.assertEqual(1, len(manager.extensions))
# One should be invoked.
self.assertEqual(1, len(results))
def _count_invocations(self, ext):
return 1
class TestBasePlugin(plugin_base.PluginBase):
def enabled(self):
return True

View File

@ -0,0 +1,84 @@
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied. See the License for the specific language governing permissions and
# limitations under the License.
from stevedore.extension import Extension
import storyboard.plugin.base as plugin_base
import storyboard.plugin.user_preferences as prefs_base
import storyboard.tests.base as base
class TestUserPreferencesPluginBase(base.TestCase):
def setUp(self):
super(TestUserPreferencesPluginBase, self).setUp()
self.extensions = []
self.extensions.append(Extension(
'test_one', None, None,
TestPreferencesPlugin(dict())
))
self.extensions.append(Extension(
'test_two', None, None,
TestOtherPreferencesPlugin(dict())
))
def test_extensibility(self):
"""Assert that we can actually instantiate a plugin."""
plugin = TestPreferencesPlugin(dict())
self.assertIsNotNone(plugin)
self.assertTrue(plugin.enabled())
def test_plugin_loader(self):
"""Perform a single plugin loading run, including two plugins and a
couple of overlapping preferences.
"""
manager = plugin_base.StoryboardPluginLoader.make_test_instance(
self.extensions,
namespace='storyboard.plugin.user_preferences')
loaded_prefs = dict()
self.assertEqual(2, len(manager.extensions))
manager.map(prefs_base.load_preferences, loaded_prefs)
self.assertTrue("foo" in loaded_prefs)
self.assertTrue("omg" in loaded_prefs)
self.assertTrue("lol" in loaded_prefs)
self.assertEqual(loaded_prefs["foo"], "baz")
self.assertEqual(loaded_prefs["omg"], "wat")
self.assertEqual(loaded_prefs["lol"], "cat")
class TestPreferencesPlugin(prefs_base.UserPreferencesPluginBase):
def get_default_preferences(self):
return {
"foo": "baz",
"omg": "wat"
}
def enabled(self):
return True
class TestOtherPreferencesPlugin(prefs_base.UserPreferencesPluginBase):
def get_default_preferences(self):
return {
"foo": "bar",
"lol": "cat"
}
def enabled(self):
return True