Merge "Allow the python-fuelclient to register plugins with one POST call. Also added changes to avoid of possible circular dependency."

This commit is contained in:
Jenkins 2016-03-23 13:30:05 +00:00 committed by Gerrit Code Review
commit 67e8261e3d
5 changed files with 109 additions and 145 deletions

View File

@ -27,6 +27,7 @@ from nailgun.objects import DeploymentGraph
from nailgun.objects import NailgunCollection
from nailgun.objects import NailgunObject
from nailgun.objects.serializers.plugin import PluginSerializer
from nailgun.plugins.adapters import wrap_plugin
class Plugin(NailgunObject):
@ -44,13 +45,8 @@ class Plugin(NailgunObject):
accidental_deployment_tasks)
DeploymentGraph.attach_to_model(deployment_graph, new_plugin)
# FIXME (vmygal): This is very ugly hack and it must be fixed ASAP.
# Need to remove the syncing of plugin metadata from here.
# All plugin metadata must be sent via 'data' argument of this
# function and it must be fixed in 'python-fuelclient' repository.
from nailgun.plugins.adapters import wrap_plugin
plugin_adapter = wrap_plugin(new_plugin)
plugin_adapter.sync_metadata_to_db()
cls.update(new_plugin, plugin_adapter.get_metadata())
ClusterPlugins.add_compatible_clusters(new_plugin)
@ -131,13 +127,9 @@ class ClusterPlugins(NailgunObject):
model = models.ClusterPlugins
@classmethod
def validate_compatibility(cls, cluster, plugin):
def is_compatible(cls, cluster, plugin):
"""Validates if plugin is compatible with cluster.
- validates operating systems
- modes of clusters (simple or ha)
- release version
:param cluster: A cluster instance
:type cluster: nailgun.db.sqlalchemy.models.cluster.Cluster
:param plugin: A plugin instance
@ -145,39 +137,9 @@ class ClusterPlugins(NailgunObject):
:return: True if compatible, False if not
:rtype: bool
"""
cluster_os = cluster.release.operating_system.lower()
for release in plugin.releases:
if cluster_os != release['os'].lower():
continue
# plugin writer should be able to specify ha in release['mode']
# and know nothing about ha_compact
if not any(
cluster.mode.startswith(mode) for mode in release['mode']
):
continue
plugin_adapter = wrap_plugin(plugin)
if not cls.is_release_version_compatible(
cluster.release.version, release['version']
):
continue
return True
return False
@staticmethod
def is_release_version_compatible(rel_version, plugin_rel_version):
"""Checks if release version is compatible with plugin version.
:param rel_version: Release version
:type rel_version: str
:param plugin_rel_version: Plugin release version
:type plugin_rel_version: str
:return: True if compatible, False if not
:rtype: bool
"""
rel_os, rel_fuel = rel_version.split('-')
plugin_os, plugin_rel = plugin_rel_version.split('-')
return rel_os.startswith(plugin_os) and rel_fuel.startswith(plugin_rel)
return plugin_adapter.validate_compatibility(cluster)
@classmethod
def get_compatible_plugins(cls, cluster):
@ -189,7 +151,7 @@ class ClusterPlugins(NailgunObject):
:rtype: list
"""
return list(six.moves.filter(
lambda p: cls.validate_compatibility(cluster, p),
lambda p: cls.is_compatible(cluster, p),
PluginCollection.all()))
@classmethod
@ -219,7 +181,7 @@ class ClusterPlugins(NailgunObject):
:rtype: list
"""
return list(six.moves.filter(
lambda c: cls.validate_compatibility(c, plugin),
lambda c: cls.is_compatible(c, plugin),
db().query(models.Cluster)))
@classmethod

View File

@ -26,8 +26,6 @@ import yaml
from nailgun.errors import errors
from nailgun.logger import logger
from nailgun.objects.deployment_graph import DeploymentGraph
from nailgun.objects.plugin import ClusterPlugins
from nailgun.objects.plugin import Plugin
from nailgun.settings import settings
@ -40,31 +38,40 @@ class PluginAdapterBase(object):
3. Enabling/Disabling of plugin based on cluster attributes
4. Providing repositories/deployment scripts related info to clients
"""
environment_config_name = 'environment_config.yaml'
plugin_metadata = 'metadata.yaml'
task_config_name = 'tasks.yaml'
config_metadata = 'metadata.yaml'
config_tasks = 'tasks.yaml'
def __init__(self, plugin):
self.plugin = plugin
self.plugin_path = os.path.join(
settings.PLUGINS_PATH,
self.path_name)
self.plugin_path = os.path.join(settings.PLUGINS_PATH, self.path_name)
self.tasks = []
self.db_cfg_mapping = {}
@abc.abstractmethod
def path_name(self):
"""A name which is used to create path to plugin scripts and repos"""
def sync_metadata_to_db(self):
"""Sync metadata from config yaml files into DB"""
metadata_file_path = os.path.join(
self.plugin_path, self.plugin_metadata)
def get_metadata(self):
"""Get parsed plugin metadata from config yaml files.
metadata = self._load_config(metadata_file_path) or {}
Plugin.update(self.plugin, metadata)
:return: All plugin metadata
:rtype: dict
"""
metadata = self._load_config(self.config_metadata) or {}
def _load_config(self, config):
for attribute, config in six.iteritems(self.db_cfg_mapping):
attribute_data = self._load_config(config)
# Plugin columns have constraints for nullable data,
# so we need to check it
if attribute_data:
if attribute == 'attributes_metadata':
attribute_data = attribute_data['attributes']
metadata[attribute] = attribute_data
return metadata
def _load_config(self, file_name):
config = os.path.join(self.plugin_path, file_name)
if os.access(config, os.R_OK):
with open(config, "r") as conf:
try:
@ -76,8 +83,8 @@ class PluginAdapterBase(object):
else:
logger.warning("Config {0} is not readable.".format(config))
def _load_tasks(self, config):
data = self._load_config(config) or []
def _load_tasks(self, file_name):
data = self._load_config(file_name) or []
for item in data:
# backward compatibility for plugins added in version 6.0,
# and it is expected that task with role: [controller]
@ -95,17 +102,7 @@ class PluginAdapterBase(object):
Provided tasks are loaded from tasks config file.
"""
task_yaml = os.path.join(
self.plugin_path, self.task_config_name)
if os.path.exists(task_yaml):
self.tasks = self._load_tasks(task_yaml)
def filter_tasks(self, tasks, stage):
filtered = []
for task in tasks:
if stage and stage == task.get('stage'):
filtered.append(task)
return filtered
self.tasks = self._load_tasks(self.config_tasks)
@property
def plugin_release_versions(self):
@ -173,6 +170,52 @@ class PluginAdapterBase(object):
return result
@staticmethod
def _is_release_version_compatible(rel_version, plugin_rel_version):
"""Checks if release version is compatible with plugin version.
:param rel_version: Release version
:type rel_version: str
:param plugin_rel_version: Plugin release version
:type plugin_rel_version: str
:return: True if compatible, False if not
:rtype: bool
"""
rel_os, rel_fuel = rel_version.split('-')
plugin_os, plugin_rel = plugin_rel_version.split('-')
return rel_os.startswith(plugin_os) and rel_fuel.startswith(plugin_rel)
def validate_compatibility(self, cluster):
"""Validates if plugin is compatible with cluster.
- validates operating systems
- modes of clusters (simple or ha)
- release version
:param cluster: A cluster instance
:type cluster: nailgun.db.sqlalchemy.models.cluster.Cluster
:return: True if compatible, False if not
:rtype: bool
"""
cluster_os = cluster.release.operating_system.lower()
for release in self.plugin.releases:
if cluster_os != release['os'].lower():
continue
# plugin writer should be able to specify ha in release['mode']
# and know nothing about ha_compact
if not any(
cluster.mode.startswith(mode) for mode in release['mode']
):
continue
if not self._is_release_version_compatible(
cluster.release.version, release['version']
):
continue
return True
return False
def get_release_info(self, release):
"""Get plugin release information which corresponds to given release"""
rel_os = release.operating_system.lower()
@ -181,8 +224,7 @@ class PluginAdapterBase(object):
release_info = filter(
lambda r: (
r['os'] == rel_os and
ClusterPlugins.is_release_version_compatible(version,
r['version'])),
self._is_release_version_compatible(version, r['version'])),
self.plugin.releases)
return release_info[0]
@ -263,64 +305,28 @@ class PluginAdapterV2(PluginAdapterBase):
class PluginAdapterV3(PluginAdapterV2):
"""Plugin wrapper class for package version 3.0.0"""
node_roles_config_name = 'node_roles.yaml'
volumes_config_name = 'volumes.yaml'
deployment_tasks_config_name = 'deployment_tasks.yaml'
network_roles_config_name = 'network_roles.yaml'
def __init__(self, plugin):
super(PluginAdapterV3, self).__init__(plugin)
self.db_cfg_mapping['attributes_metadata'] = 'environment_config.yaml'
self.db_cfg_mapping['network_roles_metadata'] = 'network_roles.yaml'
self.db_cfg_mapping['roles_metadata'] = 'node_roles.yaml'
self.db_cfg_mapping['tasks'] = self.config_tasks
self.db_cfg_mapping['volumes_metadata'] = 'volumes.yaml'
def sync_metadata_to_db(self):
"""Sync metadata from all config yaml files to DB"""
super(PluginAdapterV3, self).sync_metadata_to_db()
def get_metadata(self):
# FIXME (ikutukov): rework to getters and setters to be able to
# change deployment graph type
self.deployment_tasks = self._load_config('deployment_tasks.yaml')
db_config_metadata_mapping = {
'attributes_metadata': self.environment_config_name,
'roles_metadata': self.node_roles_config_name,
'volumes_metadata': self.volumes_config_name,
'network_roles_metadata': self.network_roles_config_name,
'tasks': self.task_config_name
}
self._update_plugin(db_config_metadata_mapping)
def _update_plugin(self, mapping):
data_to_update = {}
for attribute, config in six.iteritems(mapping):
config_file_path = os.path.join(self.plugin_path, config)
attribute_data = self._load_config(config_file_path)
# Plugin columns have constraints for nullable data, so
# we need to check it
if attribute_data:
if attribute == 'attributes_metadata':
attribute_data = attribute_data['attributes']
data_to_update[attribute] = attribute_data
Plugin.update(self.plugin, data_to_update)
# update deployment tasks
deployment_tasks_file_path = os.path.join(
self.plugin_path,
self.deployment_tasks_config_name)
deployment_tasks_data = self._load_config(deployment_tasks_file_path)
if deployment_tasks_data:
# fixme(ikutukov) rework to getters and setters to be able to
# change deployment graph type
self.deployment_tasks = deployment_tasks_data
return super(PluginAdapterV3, self).get_metadata()
class PluginAdapterV4(PluginAdapterV3):
"""Plugin wrapper class for package version 4.0.0"""
components = 'components.yaml'
def sync_metadata_to_db(self):
super(PluginAdapterV4, self).sync_metadata_to_db()
db_config_metadata_mapping = {
'components_metadata': self.components
}
self._update_plugin(db_config_metadata_mapping)
def __init__(self, plugin):
super(PluginAdapterV4, self).__init__(plugin)
self.db_cfg_mapping['components_metadata'] = 'components.yaml'
class PluginAdapterV5(PluginAdapterV4):

View File

@ -96,8 +96,7 @@ class PluginManager(object):
actual_attrs = copy.deepcopy(plugin.attributes)
actual_attrs['metadata'] = default_attrs.get('metadata',
{})
cls.fill_plugin_metadata(
plugin, actual_attrs['metadata'], True)
cls.fill_plugin_metadata(plugin, actual_attrs['metadata'])
versions.append(actual_attrs)
container['metadata'].setdefault('chosen_id', plugin.id)
@ -146,15 +145,13 @@ class PluginManager(object):
metadata['enabled'] = enabled or metadata.get('enabled', False)
@classmethod
def fill_plugin_metadata(cls, plugin, metadata, all_versions=False):
def fill_plugin_metadata(cls, plugin, metadata):
"""Fill a plugin's metadata attribute.
:param plugin: A plugin instance
:type plugin: nailgun.db.sqlalchemy.models.plugins.Plugin
:param metadata: Plugin metadata
:type metadata: dict
:param all_versions: Create for all versions of plugin or not
:type all_versions: bool
"""
metadata['plugin_id'] = plugin.id
metadata['plugin_version'] = plugin.version
@ -352,7 +349,8 @@ class PluginManager(object):
for plugin in plugins:
plugin_adapter = wrap_plugin(plugin)
plugin_adapter.sync_metadata_to_db()
metadata = plugin_adapter.get_metadata()
Plugin.update(plugin, metadata)
@classmethod
def enable_plugins_by_components(cls, cluster):

View File

@ -156,12 +156,12 @@ class TestPluginManager(base.BaseIntegrationTest):
expected_message):
PluginManager.get_volumes_metadata(self.cluster)
@mock.patch.object(PluginAdapterV3, 'sync_metadata_to_db')
@mock.patch.object(PluginAdapterV3, 'get_metadata')
def test_sync_metadata_for_all_plugins(self, sync_mock):
PluginManager.sync_plugins_metadata()
self.assertEqual(sync_mock.call_count, 2)
@mock.patch.object(PluginAdapterV3, 'sync_metadata_to_db')
@mock.patch.object(PluginAdapterV3, 'get_metadata')
def test_sync_metadata_for_specific_plugin(self, sync_mock):
PluginManager.sync_plugins_metadata([self.env.plugins[0].id])
self.assertEqual(sync_mock.call_count, 1)

View File

@ -142,13 +142,13 @@ class TestPluginBase(base.BaseTestCase):
self.assertEqual(
expected, self.plugin_adapter.master_scripts_path(self.cluster))
def test_sync_metadata_to_db(self):
def test_get_metadata(self):
plugin_metadata = self.env.get_default_plugin_metadata()
with mock.patch.object(
self.plugin_adapter, '_load_config') as load_conf:
load_conf.return_value = plugin_metadata
self.plugin_adapter.sync_metadata_to_db()
Plugin.update(self.plugin, self.plugin_adapter.get_metadata())
for key, val in six.iteritems(plugin_metadata):
self.assertEqual(
@ -172,9 +172,7 @@ class TestPluginBase(base.BaseTestCase):
self.assertEqual(depl_task['parameters'].get('cwd'), expected)
def _find_path(self, config_name):
return os.path.join(
self.plugin_adapter.plugin_path,
'{0}.yaml'.format(config_name))
return '{0}.yaml'.format(config_name)
class TestPluginV1(TestPluginBase):
@ -224,7 +222,7 @@ class TestPluginV3(TestPluginBase):
__test__ = True
package_version = '3.0.0'
def test_sync_metadata_to_db(self):
def test_get_metadata(self):
self.maxDiff = None
plugin_metadata = self.env.get_default_plugin_metadata()
attributes_metadata = self.env.get_default_plugin_env_config()
@ -247,7 +245,7 @@ class TestPluginV3(TestPluginBase):
with mock.patch.object(
self.plugin_adapter, '_load_config') as load_conf:
load_conf.side_effect = lambda key: mocked_metadata[key]
self.plugin_adapter.sync_metadata_to_db()
Plugin.update(self.plugin, self.plugin_adapter.get_metadata())
for key, val in six.iteritems(plugin_metadata):
self.assertEqual(
@ -278,7 +276,7 @@ class TestPluginV4(TestPluginBase):
__test__ = True
package_version = '4.0.0'
def test_sync_metadata_to_db(self):
def test_get_metadata(self):
plugin_metadata = self.env.get_default_plugin_metadata()
attributes_metadata = self.env.get_default_plugin_env_config()
roles_metadata = self.env.get_default_plugin_node_roles_config()
@ -302,7 +300,7 @@ class TestPluginV4(TestPluginBase):
with mock.patch.object(
self.plugin_adapter, '_load_config') as load_conf:
load_conf.side_effect = lambda key: mocked_metadata[key]
self.plugin_adapter.sync_metadata_to_db()
Plugin.update(self.plugin, self.plugin_adapter.get_metadata())
for key, val in six.iteritems(plugin_metadata):
self.assertEqual(
@ -357,7 +355,7 @@ class TestClusterCompatibilityValidation(base.BaseTestCase):
def validate_with_cluster(self, **kwargs):
cluster = self.cluster_mock(**kwargs)
return ClusterPlugins.validate_compatibility(cluster, self.plugin)
return ClusterPlugins.is_compatible(cluster, self.plugin)
def test_validation_ubuntu_ha(self):
self.assertTrue(self.validate_with_cluster(