417 lines
16 KiB
Python
417 lines
16 KiB
Python
# Copyright 2014 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.
|
|
|
|
import copy
|
|
import six
|
|
from six.moves import map
|
|
|
|
from nailgun import errors
|
|
from nailgun.logger import logger
|
|
from nailgun.objects.plugin import ClusterPlugins
|
|
from nailgun.objects.plugin import Plugin
|
|
from nailgun.objects.plugin import PluginCollection
|
|
from nailgun.plugins.adapters import wrap_plugin
|
|
from nailgun.utils import dict_update
|
|
|
|
|
|
class PluginManager(object):
|
|
|
|
@classmethod
|
|
def process_cluster_attributes(cls, cluster, attributes):
|
|
"""Generate Cluster-Plugins relation based on attributes.
|
|
|
|
Iterates through plugins attributes, creates
|
|
or deletes Cluster <-> Plugins relation if plugin
|
|
is enabled or disabled.
|
|
|
|
:param cluster: A cluster instance
|
|
:type cluster: nailgun.db.sqlalchemy.models.cluster.Cluster
|
|
:param attributes: Cluster attributes
|
|
:type attributes: dict
|
|
"""
|
|
plugins = {}
|
|
|
|
# Detach plugins data
|
|
for k in list(attributes):
|
|
if cls.is_plugin_data(attributes[k]):
|
|
plugins[k] = attributes.pop(k)['metadata']
|
|
cluster.attributes.editable.pop(k, None)
|
|
|
|
for container in six.itervalues(plugins):
|
|
default = container.get('default', False)
|
|
for attrs in container.get('versions', []):
|
|
version_metadata = attrs.pop('metadata')
|
|
plugin_id = version_metadata['plugin_id']
|
|
plugin = Plugin.get_by_uid(plugin_id)
|
|
if not plugin:
|
|
logger.warning(
|
|
'Plugin with id "%s" is not found, skip it', plugin_id)
|
|
continue
|
|
enabled = container['enabled']\
|
|
and plugin_id == container['chosen_id']
|
|
ClusterPlugins.set_attributes(
|
|
cluster.id, plugin.id, enabled=enabled,
|
|
attrs=attrs if enabled or default else None
|
|
)
|
|
|
|
@classmethod
|
|
def get_plugins_attributes(
|
|
cls, cluster, all_versions=False, default=False):
|
|
"""Gets attributes of all plugins connected with given cluster.
|
|
|
|
:param cluster: A cluster instance
|
|
:type cluster: nailgun.db.sqlalchemy.models.cluster.Cluster
|
|
:param all_versions: True to get attributes of all versions of plugins
|
|
:type all_versions: bool
|
|
:param default: True to return a default plugins attributes (for UI)
|
|
:type default: bool
|
|
:return: Plugins attributes
|
|
:rtype: dict
|
|
"""
|
|
plugins_attributes = {}
|
|
for plugin in ClusterPlugins.get_connected_plugins_data(cluster.id):
|
|
db_plugin = Plugin.get_by_uid(plugin.id)
|
|
plugin_adapter = wrap_plugin(db_plugin)
|
|
default_attrs = plugin_adapter.attributes_metadata
|
|
|
|
if all_versions:
|
|
container = plugins_attributes.setdefault(plugin.name, {})
|
|
enabled = plugin.enabled and not (all_versions and default)
|
|
cls.create_common_metadata(plugin, container, enabled)
|
|
container['metadata']['default'] = default
|
|
|
|
versions = container['metadata'].setdefault('versions', [])
|
|
if default:
|
|
actual_attrs = copy.deepcopy(default_attrs)
|
|
actual_attrs.setdefault('metadata', {})
|
|
else:
|
|
actual_attrs = copy.deepcopy(plugin.attributes)
|
|
actual_attrs['metadata'] = default_attrs.get('metadata',
|
|
{})
|
|
cls.fill_plugin_metadata(plugin, actual_attrs['metadata'])
|
|
versions.append(actual_attrs)
|
|
|
|
container['metadata'].setdefault('chosen_id', plugin.id)
|
|
if enabled:
|
|
container['metadata']['chosen_id'] = plugin.id
|
|
|
|
elif plugin.enabled:
|
|
container = plugins_attributes.setdefault(plugin.name, {})
|
|
cls.create_common_metadata(plugin, container)
|
|
container['metadata'].update(default_attrs.get('metadata', {}))
|
|
cls.fill_plugin_metadata(plugin, container['metadata'])
|
|
container.update(plugin.attributes)
|
|
|
|
return plugins_attributes
|
|
|
|
@classmethod
|
|
def inject_plugin_attribute_values(cls, attributes):
|
|
"""Inject given attributes with plugin attributes values.
|
|
|
|
:param attributes: Cluster attributes
|
|
:type attributes: dict
|
|
"""
|
|
for k, attrs in six.iteritems(attributes):
|
|
if (not cls.is_plugin_data(attrs) or
|
|
not attrs['metadata']['enabled']):
|
|
continue
|
|
metadata = attrs['metadata']
|
|
selected_plugin_attrs = cls._get_specific_version(
|
|
metadata.get('versions', []),
|
|
metadata.get('chosen_id'))
|
|
selected_plugin_attrs.pop('metadata', None)
|
|
|
|
dict_update(attrs, selected_plugin_attrs, 1)
|
|
|
|
@classmethod
|
|
def is_plugin_data(cls, attributes):
|
|
"""Looking for a plugins hallmark.
|
|
|
|
:param attributes: Item of editable attributes of cluster
|
|
:type attributes: dict
|
|
:return: True if it's a plugins container
|
|
:rtype: bool
|
|
"""
|
|
return attributes.get('metadata', {}).get('class') == 'plugin'
|
|
|
|
@classmethod
|
|
def create_common_metadata(cls, plugin, attributes, enabled=None):
|
|
"""Create common metadata attribute for all versions of plugin.
|
|
|
|
:param plugin: A plugin instance
|
|
:type plugin: nailgun.db.sqlalchemy.models.plugins.Plugin
|
|
:param attributes: Common attributes of plugin versions
|
|
:type attributes: dict
|
|
:param enabled: Plugin status
|
|
:type enabled: bool
|
|
"""
|
|
metadata = attributes.setdefault('metadata', {
|
|
'class': 'plugin',
|
|
'toggleable': True,
|
|
'weight': 70
|
|
})
|
|
metadata['label'] = plugin.title
|
|
if enabled is None:
|
|
enabled = plugin.enabled
|
|
metadata['enabled'] = enabled or metadata.get('enabled', False)
|
|
|
|
@classmethod
|
|
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
|
|
"""
|
|
metadata['plugin_id'] = plugin.id
|
|
metadata['plugin_version'] = plugin.version
|
|
metadata['hot_pluggable'] = plugin.is_hotpluggable
|
|
|
|
@classmethod
|
|
def get_enabled_plugins(cls, cluster):
|
|
return [wrap_plugin(plugin)
|
|
for plugin in ClusterPlugins.get_enabled(cluster.id)]
|
|
|
|
@classmethod
|
|
def get_network_roles(cls, cluster, merge_policy):
|
|
"""Returns the network roles from plugins.
|
|
|
|
The roles cluster and plugins will be mixed
|
|
according to merge policy.
|
|
"""
|
|
|
|
instance_roles = cluster.release.network_roles_metadata
|
|
all_roles = dict((role['id'], role) for role in instance_roles)
|
|
conflict_roles = dict()
|
|
|
|
for plugin in ClusterPlugins.get_enabled(cluster.id):
|
|
for role in plugin.network_roles_metadata:
|
|
role_id = role['id']
|
|
if role_id in all_roles:
|
|
try:
|
|
merge_policy.apply_patch(
|
|
all_roles[role_id],
|
|
role
|
|
)
|
|
except errors.UnresolvableConflict as e:
|
|
logger.error("cannot merge plugin {0}: {1}"
|
|
.format(plugin.name, e))
|
|
conflict_roles[role_id] = plugin.name
|
|
else:
|
|
all_roles[role_id] = role
|
|
|
|
if conflict_roles:
|
|
raise errors.NetworkRoleConflict(
|
|
"Cannot override existing network roles: '{0}' in "
|
|
"plugins: '{1}'".format(
|
|
', '.join(conflict_roles),
|
|
', '.join(set(conflict_roles.values()))))
|
|
|
|
return list(all_roles.values())
|
|
|
|
@classmethod
|
|
def get_plugins_deployment_tasks(cls, cluster, graph_type=None):
|
|
deployment_tasks = []
|
|
processed_tasks = {}
|
|
|
|
enabled_plugins = ClusterPlugins.get_enabled(cluster.id)
|
|
for plugin_adapter in map(wrap_plugin, enabled_plugins):
|
|
depl_tasks = plugin_adapter.get_deployment_tasks(graph_type)
|
|
|
|
for t in depl_tasks:
|
|
t_id = t['id']
|
|
if t_id in processed_tasks:
|
|
raise errors.AlreadyExists(
|
|
'Plugin {0} is overlapping with plugin {1} '
|
|
'by introducing the same deployment task with '
|
|
'id {2}'
|
|
.format(plugin_adapter.full_name,
|
|
processed_tasks[t_id],
|
|
t_id)
|
|
)
|
|
processed_tasks[t_id] = plugin_adapter.full_name
|
|
|
|
deployment_tasks.extend(depl_tasks)
|
|
|
|
return deployment_tasks
|
|
|
|
@classmethod
|
|
def get_plugins_node_roles(cls, cluster):
|
|
result = {}
|
|
core_roles = set(cluster.release.roles_metadata)
|
|
|
|
for plugin_db in ClusterPlugins.get_enabled(cluster.id):
|
|
plugin_roles = wrap_plugin(plugin_db).normalized_roles_metadata
|
|
|
|
# we should check all possible cases of roles intersection
|
|
# with core ones and those from other plugins
|
|
# and afterwards show them in error message;
|
|
# thus role names for which following checks
|
|
# fails are accumulated in err_info variable
|
|
err_roles = set(
|
|
r for r in plugin_roles if r in core_roles or r in result
|
|
)
|
|
if err_roles:
|
|
raise errors.AlreadyExists(
|
|
"Plugin (ID={0}) is unable to register the following "
|
|
"node roles: {1}".format(plugin_db.id,
|
|
", ".join(sorted(err_roles)))
|
|
)
|
|
|
|
# update info on processed roles in case of
|
|
# success of all intersection checks
|
|
result.update(plugin_roles)
|
|
|
|
return result
|
|
|
|
@classmethod
|
|
def get_volumes_metadata(cls, cluster):
|
|
"""Get volumes metadata for cluster from all plugins which enabled it.
|
|
|
|
:param cluster: A cluster instance
|
|
:type cluster: Cluster model
|
|
:return: dict -- Object with merged volumes data from plugins
|
|
"""
|
|
volumes_metadata = {
|
|
'volumes': [],
|
|
'volumes_roles_mapping': {}
|
|
}
|
|
release_volumes = cluster.release.volumes_metadata.get('volumes', [])
|
|
release_volumes_ids = [v['id'] for v in release_volumes]
|
|
processed_volumes = {}
|
|
|
|
enabled_plugins = ClusterPlugins.get_enabled(cluster.id)
|
|
for plugin_adapter in map(wrap_plugin, enabled_plugins):
|
|
metadata = plugin_adapter.volumes_metadata
|
|
|
|
for volume in metadata.get('volumes', []):
|
|
volume_id = volume['id']
|
|
if volume_id in release_volumes_ids:
|
|
raise errors.AlreadyExists(
|
|
'Plugin {0} is overlapping with release '
|
|
'by introducing the same volume with id "{1}"'
|
|
.format(plugin_adapter.full_name, volume_id))
|
|
elif volume_id in processed_volumes:
|
|
raise errors.AlreadyExists(
|
|
'Plugin {0} is overlapping with plugin {1} '
|
|
'by introducing the same volume with id "{2}"'
|
|
.format(plugin_adapter.full_name,
|
|
processed_volumes[volume_id],
|
|
volume_id))
|
|
|
|
processed_volumes[volume_id] = plugin_adapter.full_name
|
|
|
|
volumes_metadata.get('volumes_roles_mapping', {}).update(
|
|
metadata.get('volumes_roles_mapping', {}))
|
|
volumes_metadata.get('volumes', []).extend(
|
|
metadata.get('volumes', []))
|
|
|
|
return volumes_metadata
|
|
|
|
@classmethod
|
|
def get_components_metadata(cls, release):
|
|
"""Get components metadata for all plugins which related to release.
|
|
|
|
:param release: A release instance
|
|
:type release: Release model
|
|
:return: list -- List of plugins components
|
|
"""
|
|
components = []
|
|
seen_components = \
|
|
dict((c['name'], 'release') for c in release.components_metadata)
|
|
|
|
for plugin_adapter in map(
|
|
wrap_plugin, PluginCollection.get_by_release(release)):
|
|
plugin_name = plugin_adapter.name
|
|
for component in plugin_adapter.components_metadata:
|
|
name = component['name']
|
|
if seen_components.get(name, plugin_name) != plugin_name:
|
|
raise errors.AlreadyExists(
|
|
'Plugin {0} is overlapping with {1} by introducing '
|
|
'the same component with name "{2}"'
|
|
.format(plugin_adapter.name,
|
|
seen_components[name],
|
|
name))
|
|
|
|
if name not in seen_components:
|
|
seen_components[name] = plugin_adapter.name
|
|
components.append(component)
|
|
|
|
return components
|
|
|
|
@classmethod
|
|
def sync_plugins_metadata(cls, plugin_ids=None):
|
|
"""Sync metadata for plugins by given IDs.
|
|
|
|
If there are no IDs, all newest plugins will be synced.
|
|
|
|
:param plugin_ids: list of plugin IDs
|
|
:type plugin_ids: list
|
|
"""
|
|
if plugin_ids:
|
|
plugins = PluginCollection.get_by_uids(plugin_ids)
|
|
else:
|
|
plugins = PluginCollection.all()
|
|
|
|
for plugin in plugins:
|
|
plugin_adapter = wrap_plugin(plugin)
|
|
metadata = plugin_adapter.get_metadata()
|
|
Plugin.update(plugin, metadata)
|
|
|
|
@classmethod
|
|
def enable_plugins_by_components(cls, cluster):
|
|
"""Enable plugin by components.
|
|
|
|
:param cluster: A cluster instance
|
|
:type cluster: Cluster model
|
|
"""
|
|
cluster_components = set(cluster.components)
|
|
plugin_ids = [p.id for p in PluginCollection.all_newest()]
|
|
|
|
for plugin in ClusterPlugins.get_connected_plugins(
|
|
cluster, plugin_ids):
|
|
plugin_adapter = wrap_plugin(plugin)
|
|
plugin_components = set(
|
|
component['name']
|
|
for component in plugin_adapter.components_metadata)
|
|
|
|
if cluster_components & plugin_components:
|
|
ClusterPlugins.set_attributes(
|
|
cluster.id, plugin.id, enabled=True)
|
|
|
|
@classmethod
|
|
def get_legacy_tasks_for_cluster(cls, cluster):
|
|
"""Gets the tasks from tasks.yaml for all plugins.
|
|
|
|
:param cluster: the cluster object
|
|
:return: all tasks from tasks.yaml
|
|
"""
|
|
tasks = []
|
|
for plugin in cls.get_enabled_plugins(cluster):
|
|
tasks.extend(plugin.tasks)
|
|
return tasks
|
|
|
|
@classmethod
|
|
def _get_specific_version(cls, versions, plugin_id):
|
|
"""Return plugin attributes for specific version.
|
|
|
|
:returns: dict -- plugin attributes
|
|
"""
|
|
for version in versions:
|
|
if version['metadata']['plugin_id'] == plugin_id:
|
|
return version
|
|
|
|
return {}
|