fuel-web/nailgun/nailgun/objects/plugin.py

722 lines
24 KiB
Python

# -*- coding: utf-8 -*-
# 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.
from distutils.version import LooseVersion
from itertools import groupby
import operator
import six
from nailgun import consts
from nailgun.db import db
from nailgun.db.sqlalchemy import models
from nailgun import errors
from nailgun.objects import DeploymentGraph
from nailgun.objects import NailgunCollection
from nailgun.objects import NailgunObject
from nailgun.objects.serializers.plugin import PluginSerializer
from nailgun import plugins
from nailgun import utils as nailgun_utils
class Plugin(NailgunObject):
model = models.Plugin
serializer = PluginSerializer
@classmethod
def create(cls, data):
"""Create plugin.
WARNING: don't pass keys with none to non nullable fields.
:param data: data
:type data: dict
:return: plugin instance
:rtype: models.Plugin
"""
graphs = {}
for graph in data.pop("graphs", []):
graphs[graph.pop('type')] = graph
deployment_tasks = data.pop("deployment_tasks", [])
data['releases'] = [
r for r in data.pop("releases", [])
if not r.get('is_release', False)
]
plugin_obj = super(Plugin, cls).create(data)
if not graphs.get(consts.DEFAULT_DEPLOYMENT_GRAPH_TYPE):
graphs[consts.DEFAULT_DEPLOYMENT_GRAPH_TYPE] = \
{'tasks': deployment_tasks}
for graph_type, graph_data in six.iteritems(graphs):
DeploymentGraph.create_for_model(
graph_data, plugin_obj, graph_type)
plugin_adapter = plugins.wrap_plugin(plugin_obj)
# todo(ikutukov): this update is a smell from the current plugins
# installation schema. Remove it.
plugin_meta = cls._process_tags(plugin_adapter.get_metadata())
cls.update(plugin_obj, plugin_meta)
ClusterPlugin.add_compatible_clusters(plugin_obj)
return plugin_obj
@staticmethod
def _process_tags(data):
roles_meta = data.get('roles_metadata')
# nothing to check if no roles is introduced
if not roles_meta:
return data
tags_meta = data.get('tags_metadata')
if tags_meta is not None:
return data
# add creation of so-called tags for roles if tags are not
# present in role's metadata. it's necessary for compatibility
# with plugins without tags feature
tags_meta = {}
for role, meta in six.iteritems(roles_meta):
# if developer specified 'tags' field then he is in charge
# of tags management
if 'tags' in meta:
continue
# it's necessary for auto adding tag when we are
# assigning the role
roles_meta[role]['tags'] = [role]
# so-called tags should be added to plugin tags_metadata as well
tags_meta[role] = {
'has_primary': meta.get('has_primary', False)
}
# update changed data
data['roles_metadata'] = roles_meta
data['tags_metadata'] = tags_meta
return data
@classmethod
def update(cls, instance, data):
# Plugin name can't be changed. Plugins sync operation uses
# name for searching plugin data on the file system.
new_name = data.get('name')
if new_name is not None and instance.name != new_name:
raise errors.InvalidData(
"Plugin can't be renamed. Trying to change name "
"of the plugin {0} to {1}".format(instance.name, new_name))
graphs = {}
data_graphs = data.pop("graphs", [])
for graph in data_graphs:
graphs[graph.pop('type')] = graph
data.pop("deployment_tasks", []) # could not be updated
# We must save tags info in the roles_metadata on the update
data = cls._process_tags(data)
super(Plugin, cls).update(instance, data)
for graph_type, graph_data in six.iteritems(graphs):
existing_graph = DeploymentGraph.get_for_model(
instance, graph_type=graph_type)
if existing_graph:
DeploymentGraph.update(existing_graph, graph_data)
else:
DeploymentGraph.create_for_model(
graph_data, instance, graph_type)
@classmethod
def get_by_name_version(cls, name, version):
return db()\
.query(cls.model)\
.filter_by(name=name, version=version)\
.first()
@classmethod
def delete(cls, instance):
"""Delete plugin.
:param instance: Plugin model instance
:type instance: models.Plugin
"""
DeploymentGraph.delete_for_parent(instance)
super(Plugin, cls).delete(instance)
class PluginCollection(NailgunCollection):
single = Plugin
@classmethod
def all_newest(cls):
"""Returns plugins in most recent versions
Example:
There are 4 plugins:
- name: plugin_name, version: 1.0.0
- name: plugin_name, version: 2.0.0
- name: plugin_name, version: 0.1.0
- name: plugin_another_name, version: 1.0.0
In this case the method returns 2 plugins:
- name: plugin_name, version: 2.0.0
- name: plugin_another_name, version: 1.0.0
:returns: list of Plugin models
"""
newest_plugins = []
get_name = operator.attrgetter('name')
grouped_by_name = groupby(sorted(cls.all(), key=get_name), get_name)
for name, plugins_group in grouped_by_name:
newest_plugin = max(
plugins_group,
key=lambda p: LooseVersion(p.version)
)
newest_plugins.append(newest_plugin)
return newest_plugins
@classmethod
def get_by_uids(cls, plugin_ids):
"""Returns plugins by given IDs.
:param plugin_ids: list of plugin IDs
:type plugin_ids: list
:returns: iterable (SQLAlchemy query)
"""
return cls.filter_by_id_list(cls.all(), plugin_ids)
@classmethod
def get_by_release(cls, release):
"""Returns plugins by given release
:param release: Release instance
:type release: Release DB model
:returns: list -- list of sorted plugins
"""
release_plugins = set()
release_os = release.operating_system.lower()
release_version = release.version
for plugin in PluginCollection.all():
for plugin_release in plugin.releases:
if (release_os == plugin_release.get('os') and
release_version == plugin_release.get('version')):
release_plugins.add(plugin)
return sorted(release_plugins, key=lambda plugin: plugin.name)
class ClusterPlugin(NailgunObject):
model = models.ClusterPlugin
@classmethod
def is_compatible(cls, cluster, plugin):
"""Validates if plugin is compatible with cluster.
:param cluster: A cluster instance
:type cluster: nailgun.db.sqlalchemy.models.cluster.Cluster
:param plugin: A plugin instance
:type plugin: nailgun.db.sqlalchemy.models.plugins.Plugin
:return: True if compatible, False if not
:rtype: bool
"""
plugin_adapter = plugins.wrap_plugin(plugin)
return plugin_adapter.validate_compatibility(cluster)
@classmethod
def get_compatible_plugins(cls, cluster):
"""Returns a list of plugins that are compatible with a given cluster.
:param cluster: A cluster instance
:type cluster: nailgun.db.sqlalchemy.models.cluster.Cluster
:return: A list of plugin instances
:rtype: list
"""
return list(six.moves.filter(
lambda p: cls.is_compatible(cluster, p),
PluginCollection.all()))
@classmethod
def add_compatible_plugins(cls, cluster):
"""Populates 'cluster_plugins' table with compatible plugins.
:param cluster: A cluster instance
:type cluster: nailgun.db.sqlalchemy.models.cluster.Cluster
"""
for plugin in cls.get_compatible_plugins(cluster):
plugin_attributes = dict(plugin.attributes_metadata)
plugin_attributes.pop('metadata', None)
cls.create({
'cluster_id': cluster.id,
'plugin_id': plugin.id,
'enabled': False,
'attributes': plugin_attributes
})
@classmethod
def get_compatible_clusters(cls, plugin):
"""Returns a list of clusters that are compatible with a given plugin.
:param plugin: A plugin instance
:type plugin: nailgun.db.sqlalchemy.models.plugins.Plugin
:return: A list of cluster instances
:rtype: list
"""
return list(six.moves.filter(
lambda c: cls.is_compatible(c, plugin),
db().query(models.Cluster)))
@classmethod
def add_compatible_clusters(cls, plugin):
"""Populates 'cluster_plugins' table with compatible cluster.
:param plugin: A plugin instance
:type plugin: nailgun.db.sqlalchemy.models.plugins.Plugin
"""
plugin_attributes = dict(plugin.attributes_metadata)
plugin_attributes.pop('metadata', None)
for cluster in cls.get_compatible_clusters(plugin):
cluster_plugin = cls.create({
'cluster_id': cluster.id,
'plugin_id': plugin.id,
'enabled': False,
'attributes': plugin_attributes
})
# Set default attributes for Nodes, NICs and Bonds during
# plugin installation
NodeClusterPlugin.add_nodes_for_cluster_plugin(
cluster_plugin)
NodeNICInterfaceClusterPlugin.add_node_nics_for_cluster_plugin(
cluster_plugin)
NodeBondInterfaceClusterPlugin.add_node_bonds_for_cluster_plugin(
cluster_plugin)
db().flush()
@classmethod
def set_attributes(cls, cluster_id, plugin_id, enabled=None, attrs=None):
"""Sets plugin's attributes in cluster_plugins table.
:param cluster_id: Cluster ID
:type cluster_id: int
:param plugin_id: Plugin ID
:type plugin_id: int
:param enabled: Enabled or disabled plugin for given cluster
:type enabled: bool
:param attrs: Plugin metadata
:type attrs: dict
"""
params = {}
if enabled is not None:
params['enabled'] = enabled
if attrs is not None:
params['attributes'] = attrs
db().query(cls.model)\
.filter_by(plugin_id=plugin_id, cluster_id=cluster_id)\
.update(params, synchronize_session='fetch')
db().flush()
@classmethod
def get_connected_plugins_data(cls, cluster_id):
"""Returns plugins and cluster_plugins data connected with cluster.
:param cluster_id: Cluster ID
:type cluster_id: int
:returns: List of mixed data from plugins and cluster_plugins
:rtype: iterable (SQLAlchemy query)
"""
return db().query(
models.Plugin.id,
models.Plugin.name,
models.Plugin.title,
models.Plugin.version,
models.Plugin.is_hotpluggable,
models.Plugin.attributes_metadata,
cls.model.enabled,
cls.model.attributes
).join(cls.model)\
.filter(cls.model.cluster_id == cluster_id)\
.order_by(models.Plugin.name, models.Plugin.version)
@classmethod
def get_connected_plugins(cls, cluster, plugin_ids=None):
"""Returns plugins connected with given cluster.
:param cluster: Cluster instance
:type cluster: Cluster SQLAlchemy model
:param plugin_ids: List of specific plugins ids to chose from
:type plugin_ids: list
:returns: List of plugins
:rtype: iterable (SQLAlchemy query)
"""
plugins = db().query(
models.Plugin
).join(cls.model)\
.filter(cls.model.cluster_id == cluster.id)\
.order_by(models.Plugin.name, models.Plugin.version)
if plugin_ids:
plugins = plugins.filter(cls.model.plugin_id.in_(plugin_ids))
return plugins
@classmethod
def get_connected_clusters(cls, plugin_id):
"""Returns clusters connected with given plugin.
:param plugin_id: Plugin ID
:type plugin_id: int
:returns: List of clusters
:rtype: iterable (SQLAlchemy query)
"""
return db()\
.query(models.Cluster)\
.join(cls.model)\
.filter(cls.model.plugin_id == plugin_id)\
.order_by(models.Cluster.name)
@classmethod
def get_enabled(cls, cluster_id):
"""Returns a list of plugins enabled for a given cluster.
:param cluster_id: Cluster ID
:type cluster_id: int
:returns: List of plugin instances
:rtype: iterable (SQLAlchemy query)
"""
return db().query(models.Plugin)\
.join(cls.model)\
.filter(cls.model.cluster_id == cluster_id)\
.filter(cls.model.enabled.is_(True))\
.order_by(models.Plugin.id)
@classmethod
def is_plugin_used(cls, plugin_id):
"""Check if plugin is used for any cluster or not.
:param plugin_id: Plugin ID
:type plugin_id: int
:return: True if some cluster uses this plugin
:rtype: bool
"""
q = db().query(cls.model)\
.filter(cls.model.plugin_id == plugin_id)\
.filter(cls.model.enabled.is_(True))
return db().query(q.exists()).scalar()
class BasicNodeClusterPlugin(NailgunObject):
@classmethod
def set_attributes(cls, instance_id, attrs=None):
"""Update plugin NIC|Bond|Node attributes
:param instance_id: NIC|Bond|Node instance id
:type instance: int
:returns: None
"""
if attrs:
db().query(cls.model) \
.filter_by(
id=instance_id) \
.update({'attributes': attrs}, synchronize_session='fetch')
db().flush()
class NodeClusterPlugin(BasicNodeClusterPlugin):
model = models.NodeClusterPlugin
@classmethod
def get_all_enabled_attributes_by_node(cls, node):
"""Returns node attributes from enabled plugins
:param node: target node instance
:type node: models.Node
:returns: object with plugin Node attributes
:rtype: dict
"""
node_attributes = {}
node_plugin_attributes_query = db().query(
cls.model.id,
cls.model.attributes
).join(
models.ClusterPlugin,
models.Plugin
).filter(
cls.model.node_id == node.id,
models.ClusterPlugin.enabled.is_(True)
)
for node_plugin_id, attributes in node_plugin_attributes_query:
for section_name, section_attributes in six.iteritems(attributes):
# TODO(apopovych): resolve conflicts of same attribute names
# for different plugins
section_attributes.setdefault('metadata', {}).update({
'node_plugin_id': node_plugin_id,
'class': 'plugin'
})
node_attributes[section_name] = section_attributes
return node_attributes
@classmethod
def add_nodes_for_cluster_plugin(cls, cluster_plugin):
"""Populates 'node_cluster_plugins' table with nodes.
:param cluster_plugin: ClusterPlugin instance
:type cluster_plugin: models.ClusterPlugin
:returns: None
"""
node_attributes = dict(
cluster_plugin.plugin.node_attributes_metadata)
for node in cluster_plugin.cluster.nodes:
if node_attributes:
cls.create({
'cluster_plugin_id': cluster_plugin.id,
'node_id': node.id,
'attributes': node_attributes
})
db().flush()
@classmethod
def add_cluster_plugins_for_node(cls, node):
"""Populates 'node_cluster_plugins' table.
:param node: target node instance
:type node: models.Node
"""
node_cluster_plugin_ids = set(
item.id for item in node.node_cluster_plugins)
# TODO(ekosareva): rethink, move it in another place
# remove old relations for nodes
cls.bulk_delete(node_cluster_plugin_ids)
for cluster_plugin in node.cluster.cluster_plugins:
node_attributes = dict(
cluster_plugin.plugin.node_attributes_metadata)
if node_attributes:
cls.create({
'cluster_plugin_id': cluster_plugin.id,
'node_id': node.id,
'attributes': node_attributes
})
db().flush()
class NodeNICInterfaceClusterPlugin(BasicNodeClusterPlugin):
model = models.NodeNICInterfaceClusterPlugin
@classmethod
def get_all_enabled_attributes_by_interface(cls, interface):
"""Returns plugin enabled attributes for specific NIC.
:param interface: Interface instance
:type interface: models.node.NodeNICInterface
:returns: dict -- Dict object with plugin NIC attributes
"""
nic_attributes = {}
nic_plugin_attributes_query = db().query(
cls.model.id,
models.Plugin.name,
models.Plugin.title,
cls.model.attributes
).join(
models.ClusterPlugin,
models.Plugin
).filter(
cls.model.interface_id == interface.id
).filter(
models.ClusterPlugin.enabled.is_(True)).all()
for nic_plugin_id, name, title, attributes \
in nic_plugin_attributes_query:
nic_attributes[name] = {
'metadata': {
'label': title,
'nic_plugin_id': nic_plugin_id,
'class': 'plugin'
}
}
# TODO(apopovych): resolve conflicts of same attribute names
# for different plugins
nic_attributes[name] = nailgun_utils.dict_merge(
nic_attributes[name],
attributes
)
return nic_attributes
@classmethod
def add_node_nics_for_cluster_plugin(cls, cluster_plugin):
"""Populates 'node_nic_interface_cluster_plugins' table.
:param cluster_plugin: ClusterPlugin instance
:type cluster_plugin: models.cluster.ClusterPlugin
:returns: None
"""
nic_attributes = dict(
cluster_plugin.plugin.nic_attributes_metadata)
for node in cluster_plugin.cluster.nodes:
for interface in node.nic_interfaces:
if nic_attributes:
cls.create({
'cluster_plugin_id': cluster_plugin.id,
'interface_id': interface.id,
'node_id': node.id,
'attributes': nic_attributes
})
db().flush()
@classmethod
def add_cluster_plugins_for_node_nic(cls, interface):
"""Populates 'node_nic_interface_cluster_plugins' table.
:param interface: Interface instance
:type interface: models.node.NodeNICInterface
:returns: None
"""
node = interface.node
# remove old relations for interfaces
nic_cluster_plugin_ids = set(
item.id for item in node.node_nic_interface_cluster_plugins
if item.interface_id == interface.id)
cls.bulk_delete(nic_cluster_plugin_ids)
for cluster_plugin in node.cluster.cluster_plugins:
nic_attributes = dict(
cluster_plugin.plugin.nic_attributes_metadata)
if nic_attributes:
cls.create({
'cluster_plugin_id': cluster_plugin.id,
'interface_id': interface.id,
'node_id': node.id,
'attributes': nic_attributes
})
db().flush()
class NodeBondInterfaceClusterPlugin(BasicNodeClusterPlugin):
model = models.NodeBondInterfaceClusterPlugin
@classmethod
def get_all_enabled_attributes_by_bond(cls, bond):
"""Returns plugin enabled attributes for specific Bond.
:param interface: Bond instance
:type interface: models.node.NodeBondInterface
:returns: dict -- Dict object with plugin Bond attributes
"""
bond_attributes = {}
bond_plugin_attributes_query = db().query(
cls.model.id,
models.Plugin.name,
models.Plugin.title,
cls.model.attributes
).join(
models.ClusterPlugin,
models.Plugin
).filter(
cls.model.bond_id == bond.id
).filter(
models.ClusterPlugin.enabled.is_(True))
for bond_plugin_id, name, title, attributes \
in bond_plugin_attributes_query:
bond_attributes[name] = {
'metadata': {
'label': title,
'bond_plugin_id': bond_plugin_id,
'class': 'plugin'
}
}
# TODO(apopovych): resolve conflicts of same attribute names
# for different plugins
bond_attributes[name] = nailgun_utils.dict_merge(
bond_attributes[name],
attributes
)
return bond_attributes
@classmethod
def add_node_bonds_for_cluster_plugin(cls, cluster_plugin):
"""Populates 'node_bond_interface_cluster_plugins' table with bonds.
:param cluster_plugin: ClusterPlugin instance
:type cluster_plugin: models.cluster.ClusterPlugin
:returns: None
"""
bond_attributes = dict(
cluster_plugin.plugin.bond_attributes_metadata)
for node in cluster_plugin.cluster.nodes:
for bond in node.bond_interfaces:
if bond_attributes:
cls.create({
'cluster_plugin_id': cluster_plugin.id,
'bond_id': bond.id,
'node_id': node.id,
'attributes': bond_attributes
})
db().flush()
@classmethod
def add_cluster_plugins_for_node_bond(cls, bond):
"""Populates 'node_bond_interface_cluster_plugins' table.
:param interface: Bond instance
:type interface: models.node.NodeBondInterface
:returns: None
"""
node = bond.node
# remove old relations for bonds
bond_cluster_plugin_ids = set(
item.id for item in node.node_bond_interface_cluster_plugins
if item.bond_id == bond.id)
cls.bulk_delete(bond_cluster_plugin_ids)
for cluster_plugin in node.cluster.cluster_plugins:
bond_attributes = dict(
cluster_plugin.plugin.bond_attributes_metadata)
if bond_attributes:
cls.create({
'cluster_plugin_id': cluster_plugin.id,
'bond_id': bond.id,
'node_id': node.id,
'attributes': bond_attributes
})
db().flush()