Merge "Nailgun extensions in stevedore"

This commit is contained in:
Jenkins 2016-02-04 08:22:48 +00:00 committed by Gerrit Code Review
commit d8ae4b399a
8 changed files with 201 additions and 156 deletions

View File

@ -15,12 +15,12 @@
# under the License.
from nailgun.extensions.base import BaseExtension
from nailgun.extensions.base import get_extension
from nailgun.extensions.base import get_all_extensions
from nailgun.extensions.base import node_extension_call
from nailgun.extensions.base import fire_callback_on_node_delete
from nailgun.extensions.base import fire_callback_on_node_collection_delete
from nailgun.extensions.base import fire_callback_on_node_create
from nailgun.extensions.base import fire_callback_on_node_update
from nailgun.extensions.base import fire_callback_on_node_reset
from nailgun.extensions.base import fire_callback_on_cluster_delete
from nailgun.extensions.manager import get_extension
from nailgun.extensions.manager import get_all_extensions
from nailgun.extensions.manager import node_extension_call
from nailgun.extensions.manager import fire_callback_on_node_delete
from nailgun.extensions.manager import fire_callback_on_node_collection_delete
from nailgun.extensions.manager import fire_callback_on_node_create
from nailgun.extensions.manager import fire_callback_on_node_update
from nailgun.extensions.manager import fire_callback_on_node_reset
from nailgun.extensions.manager import fire_callback_on_cluster_delete

View File

@ -14,118 +14,37 @@
# License for the specific language governing permissions and limitations
# under the License.
"Contains base class for Nailgun extensions"
import abc
import six
from nailgun.errors import errors
def get_all_extensions():
# TODO(eli): implement extensions autodiscovery
# should be done as a part of blueprint
# https://blueprints.launchpad.net/fuel/+spec/volume-manager-refactoring
from nailgun.extensions.cluster_upgrade.extension \
import ClusterUpgradeExtension
from nailgun.extensions.volume_manager.extension \
import VolumeManagerExtension
extensions = [
VolumeManagerExtension,
ClusterUpgradeExtension,
]
return extensions
def get_extension(name):
"""Retrieves extension by name
:param str name: name of the extension
:returns: extension class
"""
extensions = filter(lambda e: e.name == name, get_all_extensions())
if not extensions:
raise errors.CannotFindExtension(
"Cannot find extension with name '{0}'".format(name))
return extensions[0]
def _get_extension_by_node_or_env(call_name, node):
found_extension = None
# Try to find extension in node
if node:
for extension in node.extensions:
if call_name in get_extension(extension).provides:
found_extension = extension
# Try to find extension by environment
if not found_extension and node.cluster:
for extension in node.cluster.extensions:
if call_name in get_extension(extension).provides:
found_extension = extension
if not found_extension:
raise errors.CannotFindExtension(
"Cannot find extension which provides "
"'{0}' call".format(call_name))
return get_extension(found_extension)
def node_extension_call(call_name, node, *args, **kwargs):
extension = _get_extension_by_node_or_env(call_name, node)
return getattr(extension, call_name)(node, *args, **kwargs)
def fire_callback_on_node_create(node):
for extension in get_all_extensions():
extension.on_node_create(node)
def fire_callback_on_node_update(node):
for extension in get_all_extensions():
extension.on_node_update(node)
def fire_callback_on_node_reset(node):
for extension in get_all_extensions():
extension.on_node_reset(node)
def fire_callback_on_node_delete(node):
for extension in get_all_extensions():
extension.on_node_delete(node)
def fire_callback_on_node_collection_delete(node_ids):
for extension in get_all_extensions():
extension.on_node_collection_delete(node_ids)
def fire_callback_on_cluster_delete(cluster):
for extension in get_all_extensions():
extension.on_cluster_delete(cluster)
@six.add_metaclass(abc.ABCMeta)
class BaseExtension(object):
"""Base class for Nailgun extension
# If extension provides API, define here urls in then
# next format:
# [
# {
# "uri": r'/new/url',
# "handler": HandlerClass
# }
# ]
If extension provides API, define here urls in then following format:
urls = [
{
"uri": r'/new/url',
"handler": HandlerClass
}
]
urls = []
# Specify a list of calls which extension provides.
# This list is required for core and other extensions
# to find extension with specific functionality.
Specify a list of calls which extension provides.
This list is required for core and other extensions
to find extension with specific functionality.
provides = [
'method_1',
'method_2',
]
"""
urls = []
provides = []
@classmethod
@ -137,6 +56,10 @@ class BaseExtension(object):
def name(self):
"""Uniq name of the extension."""
@abc.abstractproperty
def description(self):
"""Brief description of extension"""
@abc.abstractproperty
def version(self):
"""Version of the extension

View File

@ -24,6 +24,7 @@ from . import handlers
class ClusterUpgradeExtension(extensions.BaseExtension):
name = 'cluster_upgrade'
version = '0.0.1'
description = "Cluster Upgrade Extension"
urls = [
{'uri': r'/clusters/(?P<cluster_id>\d+)/upgrade/clone/?$',

View File

@ -18,3 +18,5 @@
# not hardcode information about tables which are specific
# for extension or core into extension's and core's migrations.
extensions_migration_buffer_table_name = 'extensions_migration_buffer'
EXTENSIONS_NAMESPACE = 'nailgun.extensions'

View File

@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Mirantis, Inc.
#
# 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 itertools import chain
from stevedore.extension import ExtensionManager
from nailgun.errors import errors
from nailgun.extensions import consts
_EXTENSION_MANAGER = None
def get_all_extensions():
"""Retrieves all available extensions for Nailgun
:returns: generator of extensions objects
"""
# NOTE(sbrzeczkowski): in order to not cause circular imports from
# extensions and not creating instances ExtensionManager on every time
# this function is called, we use 'global' variable.
global _EXTENSION_MANAGER
if _EXTENSION_MANAGER is None:
_EXTENSION_MANAGER = ExtensionManager(
namespace=consts.EXTENSIONS_NAMESPACE)
return (ext.plugin for ext in _EXTENSION_MANAGER.extensions)
def get_extension(name):
"""Retrieves extension by name
:param str name: name of the extension
:returns: extension class
:raises errors.CannotFindExtension: on non existing extension
"""
extension = next(
(ext for ext in get_all_extensions() if ext.name == name), None)
if extension is not None:
return extension
raise errors.CannotFindExtension(
"Cannot find extension with name '{0}'".format(name))
def _get_extension_by_node(call_name, node):
all_extensions = {ext.name: ext for ext in get_all_extensions()}
for extension in chain(node.extensions,
node.cluster.extensions if node.cluster else []):
if (extension in all_extensions and
call_name in all_extensions[extension].provides):
return all_extensions[extension]
raise errors.CannotFindExtension("Cannot find extension which provides "
"'{0}' call".format(call_name))
def node_extension_call(call_name, node, *args, **kwargs):
# NOTE(sbrzeczkowski): should be removed once data-pipeline blueprint is
# done: https://blueprints.launchpad.net/fuel/+spec/data-pipeline
extension = _get_extension_by_node(call_name, node)
return getattr(extension, call_name)(node, *args, **kwargs)
def fire_callback_on_node_create(node):
for extension in get_all_extensions():
extension.on_node_create(node)
def fire_callback_on_node_update(node):
for extension in get_all_extensions():
extension.on_node_update(node)
def fire_callback_on_node_reset(node):
for extension in get_all_extensions():
extension.on_node_reset(node)
def fire_callback_on_node_delete(node):
for extension in get_all_extensions():
extension.on_node_delete(node)
def fire_callback_on_node_collection_delete(node_ids):
for extension in get_all_extensions():
extension.on_node_collection_delete(node_ids)
def fire_callback_on_cluster_delete(cluster):
for extension in get_all_extensions():
extension.on_cluster_delete(cluster)

View File

@ -37,6 +37,8 @@ class VolumeManagerExtension(BaseExtension):
'set_node_volumes',
'set_default_node_volumes']
description = "Volume Manager Extension"
@classmethod
def alembic_migrations_path(cls):
return os.path.join(os.path.dirname(__file__),

View File

@ -34,11 +34,12 @@ class TestBaseExtension(BaseTestCase):
def setUp(self):
super(TestBaseExtension, self).setUp()
class Extexnsion(BaseExtension):
class Extension(BaseExtension):
name = 'ext_name'
version = '1.0.0'
description = 'ext description'
self.extension = Extexnsion()
self.extension = Extension()
def test_alembic_table_version(self):
self.assertEqual(
@ -82,12 +83,13 @@ class TestExtensionUtils(BaseTestCase):
return node
@mock.patch('nailgun.extensions.base.get_all_extensions',
@mock.patch('nailgun.extensions.manager.get_all_extensions',
return_value=make_mock_extensions())
def test_get_extension(self, get_m):
get_extension('ex1')
extension = get_extension('ex1')
self.assertEqual(extension.name, 'ex1')
@mock.patch('nailgun.extensions.base.get_all_extensions',
@mock.patch('nailgun.extensions.manager.get_all_extensions',
return_value=make_mock_extensions())
def test_get_extension_raises_errors(self, get_m):
self.assertRaisesRegexp(
@ -96,7 +98,7 @@ class TestExtensionUtils(BaseTestCase):
get_extension,
'unknown_ex')
@mock.patch('nailgun.extensions.base.get_all_extensions',
@mock.patch('nailgun.extensions.manager.get_all_extensions',
return_value=make_mock_extensions())
def test_node_extension_call_raises_error(self, _):
self.assertRaisesRegexp(
@ -106,7 +108,7 @@ class TestExtensionUtils(BaseTestCase):
'method_call',
self.make_node())
@mock.patch('nailgun.extensions.base.get_all_extensions',
@mock.patch('nailgun.extensions.manager.get_all_extensions',
return_value=make_mock_extensions())
def test_node_extension_call_extension_from_node(self, get_m):
node = self.make_node(
@ -122,7 +124,7 @@ class TestExtensionUtils(BaseTestCase):
ex1.method_call.assert_called_once_with(node)
self.assertFalse(ex2.method_call.called)
@mock.patch('nailgun.extensions.base.get_all_extensions',
@mock.patch('nailgun.extensions.manager.get_all_extensions',
return_value=make_mock_extensions())
def test_node_extension_call_default_extension_from_cluster(self, get_m):
node = self.make_node(
@ -138,7 +140,7 @@ class TestExtensionUtils(BaseTestCase):
self.assertFalse(ex1.method_call.called)
ex2.method_call.assert_called_once_with(node)
@mock.patch('nailgun.extensions.base.get_all_extensions',
@mock.patch('nailgun.extensions.manager.get_all_extensions',
return_value=make_mock_extensions())
def test_fire_callback_on_node_create(self, get_m):
node = mock.MagicMock()
@ -147,7 +149,7 @@ class TestExtensionUtils(BaseTestCase):
for ext in get_m.return_value:
ext.on_node_create.assert_called_once_with(node)
@mock.patch('nailgun.extensions.base.get_all_extensions',
@mock.patch('nailgun.extensions.manager.get_all_extensions',
return_value=make_mock_extensions())
def test_fire_callback_on_node_update(self, get_m):
node = mock.MagicMock()
@ -156,7 +158,7 @@ class TestExtensionUtils(BaseTestCase):
for ext in get_m.return_value:
ext.on_node_update.assert_called_once_with(node)
@mock.patch('nailgun.extensions.base.get_all_extensions',
@mock.patch('nailgun.extensions.manager.get_all_extensions',
return_value=make_mock_extensions())
def test_fire_callback_on_node_reset(self, get_m):
node = mock.MagicMock()
@ -165,7 +167,7 @@ class TestExtensionUtils(BaseTestCase):
for ext in get_m.return_value:
ext.on_node_reset.assert_called_once_with(node)
@mock.patch('nailgun.extensions.base.get_all_extensions',
@mock.patch('nailgun.extensions.manager.get_all_extensions',
return_value=make_mock_extensions())
def test_fire_callback_on_node_delete(self, get_m):
node = mock.MagicMock()
@ -174,7 +176,7 @@ class TestExtensionUtils(BaseTestCase):
for ext in get_m.return_value:
ext.on_node_delete.assert_called_once_with(node)
@mock.patch('nailgun.extensions.base.get_all_extensions',
@mock.patch('nailgun.extensions.manager.get_all_extensions',
return_value=make_mock_extensions())
def test_fire_callback_on_node_collection_delete(self, get_m):
node_ids = [1, 2, 3, 4]
@ -183,7 +185,7 @@ class TestExtensionUtils(BaseTestCase):
for ext in get_m.return_value:
ext.on_node_collection_delete.assert_called_once_with(node_ids)
@mock.patch('nailgun.extensions.base.get_all_extensions',
@mock.patch('nailgun.extensions.manager.get_all_extensions',
return_value=make_mock_extensions())
def test_fire_callback_on_cluster_deletion(self, get_m):
cluster = mock.MagicMock()

View File

@ -45,38 +45,45 @@ def recursive_data_files(spec_data_files):
if __name__ == "__main__":
setup(name=name,
version=version,
description='Nailgun package',
long_description="""Nailgun package""",
classifiers=[
setup(
name=name,
version=version,
description='Nailgun package',
long_description="""Nailgun package""",
classifiers=[
"Development Status :: 4 - Beta",
"Programming Language :: Python",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
],
author='Mirantis Inc.',
author_email='product@mirantis.com',
url='http://mirantis.com',
keywords='web wsgi nailgun mirantis',
packages=find_packages(),
zip_safe=False,
install_requires=find_requires(),
include_package_data=True,
scripts=['manage.py'],
entry_points={
'console_scripts': [
'nailgun_syncdb = nailgun.db:syncdb',
'nailgun_fixtures = \
nailgun.db.sqlalchemy.fixman:upload_fixtures',
'nailgund = nailgun.app:appstart',
'assassind = nailgun.assassin.assassind:run',
'receiverd = nailgun.rpc.receiverd:run',
'statsenderd = nailgun.statistics.statsenderd:run',
'oswl_collectord = nailgun.statistics.oswl.collector:run',
('oswl_cleaner = nailgun.statistics.oswl.helpers:'
'delete_expired_oswl_entries'),
],
},
data_files=recursive_data_files([('share/nailgun', 'static')])
)
author='Mirantis Inc.',
author_email='product@mirantis.com',
url='http://mirantis.com',
keywords='web wsgi nailgun mirantis',
packages=find_packages(),
zip_safe=False,
install_requires=find_requires(),
include_package_data=True,
scripts=['manage.py'],
entry_points={
'console_scripts': [
'nailgun_syncdb = nailgun.db:syncdb',
('nailgun_fixtures = '
'nailgun.db.sqlalchemy.fixman:upload_fixtures'),
'nailgund = nailgun.app:appstart',
'assassind = nailgun.assassin.assassind:run',
'receiverd = nailgun.rpc.receiverd:run',
'statsenderd = nailgun.statistics.statsenderd:run',
'oswl_collectord = nailgun.statistics.oswl.collector:run',
('oswl_cleaner = nailgun.statistics.oswl.helpers:'
'delete_expired_oswl_entries'),
],
'nailgun.extensions': [
('cluster_upgrade = nailgun.extensions.cluster_upgrade'
'.extension:ClusterUpgradeExtension'),
('volume_manager = nailgun.extensions.volume_manager'
'.extension:VolumeManagerExtension')
],
},
data_files=recursive_data_files([('share/nailgun', 'static')]),
)