diff --git a/nailgun/nailgun/api/v1/handlers/component.py b/nailgun/nailgun/api/v1/handlers/component.py index 1c5462937a..9fbcc19bcc 100644 --- a/nailgun/nailgun/api/v1/handlers/component.py +++ b/nailgun/nailgun/api/v1/handlers/component.py @@ -15,7 +15,7 @@ # under the License. from nailgun.api.v1.handlers import base -from nailgun.objects import ComponentCollection +from nailgun.objects import Release class ComponentCollectionHandler(base.CollectionHandler): @@ -26,9 +26,10 @@ class ComponentCollectionHandler(base.CollectionHandler): """:returns: JSONized component data for release and releated plugins. :http: * 200 (OK) + * 404 (release not found in db) """ - components = ComponentCollection.get_all_by_release(release_id) - return ComponentCollection.to_json(components) + release = self.get_object_or_404(Release, release_id) + return Release.get_all_components(release) def POST(self, release_id): """Creating of components is disallowed diff --git a/nailgun/nailgun/consts.py b/nailgun/nailgun/consts.py index ba1dd884db..7981576f27 100644 --- a/nailgun/nailgun/consts.py +++ b/nailgun/nailgun/consts.py @@ -423,13 +423,6 @@ CLOUD_INIT_TEMPLATES = Enum( 'meta_data', ) -COMPONENT_TYPES = Enum( - 'hypervisor', - 'network', - 'storage', - 'additional_service', -) - # NOTE(kozhukalov): This constant is used to collect # the information about installed fuel packages (rpm -q). # This information is necessary for fuel-stats. diff --git a/nailgun/nailgun/db/migration/alembic_migrations/versions/fuel_8_0.py b/nailgun/nailgun/db/migration/alembic_migrations/versions/fuel_8_0.py index 74e3d15fd1..6c5629764f 100644 --- a/nailgun/nailgun/db/migration/alembic_migrations/versions/fuel_8_0.py +++ b/nailgun/nailgun/db/migration/alembic_migrations/versions/fuel_8_0.py @@ -28,10 +28,7 @@ from alembic import op from nailgun.db.sqlalchemy.models import fields from oslo_serialization import jsonutils import sqlalchemy as sa -from sqlalchemy.dialects import postgresql as psql -from nailgun.db.sqlalchemy.models.fields import JSON -from nailgun.utils.migration import drop_enum from nailgun.utils.migration import upgrade_enum @@ -100,8 +97,6 @@ node_errors_new = ( def upgrade(): - create_components_table() - create_release_components_table() upgrade_nodegroups_name_cluster_constraint() upgrade_release_state() task_statuses_upgrade() @@ -110,9 +105,11 @@ def upgrade(): upgrade_neutron_parameters() upgrade_cluster_plugins() upgrade_add_baremetal_net() + upgrade_with_components() def downgrade(): + downgrade_with_components() downgrade_add_baremetal_net() downgrade_cluster_plugins() downgrade_neutron_parameters() @@ -120,11 +117,7 @@ def downgrade(): task_names_downgrade() task_statuses_downgrade() downgrade_release_state() - - op.drop_constraint('_name_cluster_uc', 'nodegroups',) - op.drop_table('release_components') - op.drop_table('components') - drop_enum('component_types') + downgrade_nodegroups_name_cluster_constraint() def upgrade_release_state(): @@ -167,42 +160,27 @@ def upgrade_nodegroups_name_cluster_constraint(): ) -def create_components_table(): - op.create_table('components', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('type', sa.Enum('hypervisor', 'network', - 'storage', 'additional_service', - name='component_types'), - nullable=False), - sa.Column('hypervisors', psql.ARRAY(sa.String()), - server_default='{}', nullable=False), - sa.Column('networks', psql.ARRAY(sa.String()), - server_default='{}', nullable=False), - sa.Column('storages', psql.ARRAY(sa.String()), - server_default='{}', nullable=False), - sa.Column('additional_services', psql.ARRAY(sa.String()), - server_default='{}', nullable=False), - sa.Column('plugin_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint( - ['plugin_id'], ['plugins.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('name', 'type', - name='_component_name_type_uc') - ) +def downgrade_nodegroups_name_cluster_constraint(): + op.drop_constraint('_name_cluster_uc', 'nodegroups',) -def create_release_components_table(): - op.create_table('release_components', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('release_id', sa.Integer(), nullable=False), - sa.Column('component_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint( - ['component_id'], ['components.id'], ), - sa.ForeignKeyConstraint( - ['release_id'], ['releases.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) +def upgrade_with_components(): + op.add_column( + 'plugins', + sa.Column( + 'components_metadata', + fields.JSON(), + server_default='[]' + ) + ) + op.add_column( + 'releases', + sa.Column( + 'components_metadata', + fields.JSON(), + server_default='[]' + ) + ) def downgrade_release_state(): @@ -484,10 +462,15 @@ def upgrade_add_baremetal_net(): sa.Column('baremetal_gateway', sa.String(length=25), nullable=True)) op.add_column('neutron_config', - sa.Column('baremetal_range', JSON(), nullable=True, + sa.Column('baremetal_range', fields.JSON(), nullable=True, server_default='[]')) def downgrade_add_baremetal_net(): op.drop_column('neutron_config', 'baremetal_gateway') op.drop_column('neutron_config', 'baremetal_range') + + +def downgrade_with_components(): + op.drop_column('plugins', 'components_metadata') + op.drop_column('releases', 'components_metadata') diff --git a/nailgun/nailgun/db/sqlalchemy/models/__init__.py b/nailgun/nailgun/db/sqlalchemy/models/__init__.py index fac9f07e9a..b143790e8e 100644 --- a/nailgun/nailgun/db/sqlalchemy/models/__init__.py +++ b/nailgun/nailgun/db/sqlalchemy/models/__init__.py @@ -51,6 +51,3 @@ from nailgun.db.sqlalchemy.models.master_node_settings \ from nailgun.db.sqlalchemy.models.plugins import ClusterPlugins from nailgun.db.sqlalchemy.models.plugins import Plugin - -from nailgun.db.sqlalchemy.models.component import Component -from nailgun.db.sqlalchemy.models.component import ReleaseComponent diff --git a/nailgun/nailgun/db/sqlalchemy/models/component.py b/nailgun/nailgun/db/sqlalchemy/models/component.py deleted file mode 100644 index 2ed1aeec26..0000000000 --- a/nailgun/nailgun/db/sqlalchemy/models/component.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2015 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 sqlalchemy import Column -from sqlalchemy import Enum -from sqlalchemy import ForeignKey -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy import UniqueConstraint - -from sqlalchemy.dialects import postgresql as psql -from sqlalchemy.orm import relationship - -from nailgun import consts -from nailgun.db.sqlalchemy.models.base import Base - - -class Component(Base): - __tablename__ = 'components' - __table_args__ = ( - UniqueConstraint('name', 'type', name='_component_name_type_uc'), - ) - id = Column(Integer, primary_key=True) - name = Column(String, nullable=False) - type = Column( - Enum(*consts.COMPONENT_TYPES, name='component_types'), - nullable=False - ) - hypervisors = Column(psql.ARRAY(String), nullable=False, default=[], - server_default='{}') - networks = Column(psql.ARRAY(String), nullable=False, default=[], - server_default='{}') - storages = Column(psql.ARRAY(String), nullable=False, default=[], - server_default='{}') - additional_services = Column(psql.ARRAY(String), nullable=False, - default=[], server_default='{}') - plugin_id = Column(Integer, ForeignKey('plugins.id', ondelete='CASCADE')) - plugin = relationship("Plugin", backref="components") - releases = relationship('Release', - secondary='release_components', - backref='components') - - -class ReleaseComponent(Base): - - __tablename__ = 'release_components' - id = Column(Integer, primary_key=True) - release_id = Column(Integer, ForeignKey('releases.id', ondelete='CASCADE'), - nullable=False) - component_id = Column(Integer, ForeignKey('components.id'), nullable=False) diff --git a/nailgun/nailgun/db/sqlalchemy/models/plugins.py b/nailgun/nailgun/db/sqlalchemy/models/plugins.py index 79fc83863a..f2ef94e51f 100644 --- a/nailgun/nailgun/db/sqlalchemy/models/plugins.py +++ b/nailgun/nailgun/db/sqlalchemy/models/plugins.py @@ -25,6 +25,7 @@ from sqlalchemy.orm import relationship from nailgun.db.sqlalchemy.models.base import Base from nailgun.db.sqlalchemy.models.fields import JSON +from nailgun.db.sqlalchemy.models.mutable import MutableList class ClusterPlugins(Base): @@ -74,6 +75,8 @@ class Plugin(Base): volumes_metadata = Column(JSON, server_default='{}', nullable=False) roles_metadata = Column(JSON, server_default='{}', nullable=False) network_roles_metadata = Column(JSON, server_default='[]', nullable=False) + components_metadata = Column( + MutableList.as_mutable(JSON), server_default='[]') deployment_tasks = Column(JSON, server_default='[]', nullable=False) # TODO(apopovych): To support old plugins versions we need separate # tasks which runs directly during deployment(stored in `deployment_tasks` diff --git a/nailgun/nailgun/db/sqlalchemy/models/release.py b/nailgun/nailgun/db/sqlalchemy/models/release.py index 6cefb973aa..96cf2edafc 100644 --- a/nailgun/nailgun/db/sqlalchemy/models/release.py +++ b/nailgun/nailgun/db/sqlalchemy/models/release.py @@ -27,6 +27,7 @@ from sqlalchemy.orm import relationship from nailgun import consts from nailgun.db.sqlalchemy.models.base import Base from nailgun.db.sqlalchemy.models.fields import JSON +from nailgun.db.sqlalchemy.models.mutable import MutableList class Release(Base): @@ -58,6 +59,8 @@ class Release(Base): wizard_metadata = Column(JSON, default={}) deployment_tasks = Column(JSON, default=[]) vmware_attributes_metadata = Column(JSON, default=[]) + components_metadata = Column( + MutableList.as_mutable(JSON), default=[], server_default='[]') modes = Column(JSON, default=[]) clusters = relationship( "Cluster", diff --git a/nailgun/nailgun/objects/__init__.py b/nailgun/nailgun/objects/__init__.py index aa12d26045..1e4b238123 100644 --- a/nailgun/nailgun/objects/__init__.py +++ b/nailgun/nailgun/objects/__init__.py @@ -54,6 +54,3 @@ from nailgun.objects.plugin import ClusterPlugins from nailgun.objects.network_group import NetworkGroup from nailgun.objects.network_group import NetworkGroupCollection - -from nailgun.objects.component import Component -from nailgun.objects.component import ComponentCollection diff --git a/nailgun/nailgun/objects/component.py b/nailgun/nailgun/objects/component.py deleted file mode 100644 index 0844e75977..0000000000 --- a/nailgun/nailgun/objects/component.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2015 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 sqlalchemy.orm import joinedload - -from nailgun.db import db -from nailgun.db.sqlalchemy import models -from nailgun.objects import NailgunCollection -from nailgun.objects import NailgunObject -from nailgun.objects import Release -from nailgun.objects.serializers import component - - -class Component(NailgunObject): - - model = models.Component - serializer = component.ComponentSerializer - - @classmethod - def get_by_name_and_type(cls, component_name, component_type): - return db().query(cls.model).filter_by( - name=component_name, type=component_type).first() - - -class ComponentCollection(NailgunCollection): - - single = Component - - @classmethod - def get_all_by_release(cls, release_id): - """Get all components for specific release. - - :param release_id: release ID - :type release_id: int - - :returns: list -- list of components - """ - components = [] - release = Release.get_by_uid(release_id) - release_os = release.operating_system.lower() - release_version = release.version - - db_components = db().query(cls.single.model).options( - joinedload(cls.single.model.releases), - joinedload(cls.single.model.plugin)).all() - - for db_component in db_components: - if db_component.releases: - for db_release in db_component.releases: - if db_release.id == release.id: - components.append(db_component) - elif db_component.plugin: - for plugin_release in db_component.plugin.releases: - if (release_os == plugin_release.get('os') and - release_version == plugin_release.get('version')): - components.append(db_component) - - return components diff --git a/nailgun/nailgun/objects/plugin.py b/nailgun/nailgun/objects/plugin.py index e2389aafec..3d9448195e 100644 --- a/nailgun/nailgun/objects/plugin.py +++ b/nailgun/nailgun/objects/plugin.py @@ -97,6 +97,26 @@ class PluginCollection(NailgunCollection): """ 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 ClusterPlugins(NailgunObject): diff --git a/nailgun/nailgun/objects/release.py b/nailgun/nailgun/objects/release.py index 47790c3454..cf0ca8a7bf 100644 --- a/nailgun/nailgun/objects/release.py +++ b/nailgun/nailgun/objects/release.py @@ -29,6 +29,7 @@ from nailgun.objects import NailgunCollection from nailgun.objects import NailgunObject from nailgun.objects.serializers import release as release_serializer from nailgun.orchestrator import graph_configuration +from nailgun.plugins.manager import PluginManager from nailgun.settings import settings @@ -162,6 +163,17 @@ class Release(NailgunObject): def get_min_controller_count(cls, instance): return instance.roles_metadata['controller']['limits']['min'] + @classmethod + def get_all_components(cls, instance): + """Get all components related to release + + :param instance: Release instance + :type instance: Release DB instance + :returns: list -- list of all components + """ + plugin_components = PluginManager.get_components_metadata(instance) + return instance.components_metadata + plugin_components + class ReleaseCollection(NailgunCollection): """Release collection""" diff --git a/nailgun/nailgun/objects/serializers/component.py b/nailgun/nailgun/objects/serializers/component.py deleted file mode 100644 index 4cd9899e27..0000000000 --- a/nailgun/nailgun/objects/serializers/component.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2015 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 nailgun.objects.serializers.base import BasicSerializer - - -class ComponentSerializer(BasicSerializer): - - fields = ( - "name", - "type", - "plugin_id" - ) - - @classmethod - def serialize(cls, instance, fields=None): - data_dict = BasicSerializer.serialize( - instance, fields=fields if fields else cls.fields) - prepare = lambda array: '*' if array == ['*'] else array - data_dict['compatible'] = { - 'hypervisors': prepare(instance.hypervisors), - 'networks': prepare(instance.networks), - 'storages': prepare(instance.storages), - 'additional_services': prepare(instance.additional_services) - } - data_dict['releases_ids'] = [r.id for r in instance.releases] - - return data_dict diff --git a/nailgun/nailgun/plugins/adapters.py b/nailgun/nailgun/plugins/adapters.py index 9fd2e26673..372c3395bc 100644 --- a/nailgun/nailgun/plugins/adapters.py +++ b/nailgun/nailgun/plugins/adapters.py @@ -25,7 +25,6 @@ import yaml from nailgun.errors import errors from nailgun.logger import logger -from nailgun.objects.component import Component from nailgun.objects.plugin import ClusterPlugins from nailgun.objects.plugin import Plugin from nailgun.settings import settings @@ -135,6 +134,14 @@ class PluginAdapterBase(object): def volumes_metadata(self): return self.plugin.volumes_metadata + @property + def components_metadata(self): + return self.plugin.components_metadata + + @property + def releases(self): + return self.plugin.releases + @property def normalized_roles_metadata(self): """Block plugin disabling if nodes with plugin-provided roles exist""" @@ -247,7 +254,6 @@ class PluginAdapterV3(PluginAdapterV2): """Sync metadata from all config yaml files to DB""" super(PluginAdapterV3, self).sync_metadata_to_db() - data_to_update = {} db_config_metadata_mapping = { 'attributes_metadata': self.environment_config_name, 'roles_metadata': self.node_roles_config_name, @@ -257,7 +263,12 @@ class PluginAdapterV3(PluginAdapterV2): 'tasks': self.task_config_name } - for attribute, config in six.iteritems(db_config_metadata_mapping): + 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 @@ -277,23 +288,12 @@ class PluginAdapterV4(PluginAdapterV3): def sync_metadata_to_db(self): super(PluginAdapterV4, self).sync_metadata_to_db() - components_file_path = os.path.join( - self.plugin_path, self.components) - components = self._load_config(components_file_path) or [] - for component in components: - component_name = component.get('name') - component_type = component.get('type') - db_component = Component.get_by_name_and_type( - component_name, component_type) - if not db_component: - components_data = component.get('compatible', {}) - components_data.update({ - 'name': component_name, - 'type': component_type, - 'plugin_id': self.plugin.id - }) - Component.create(components_data) + db_config_metadata_mapping = { + 'components_metadata': self.components + } + + self._update_plugin(db_config_metadata_mapping) __version_mapping = { diff --git a/nailgun/nailgun/plugins/manager.py b/nailgun/nailgun/plugins/manager.py index ba4469efe0..819d92c4ad 100644 --- a/nailgun/nailgun/plugins/manager.py +++ b/nailgun/nailgun/plugins/manager.py @@ -261,9 +261,8 @@ class PluginManager(object): """Get volumes metadata for cluster from all plugins which enabled it. :param cluster: A cluster instance - :type cluster: nailgun.db.sqlalchemy.models.cluster.Cluster - :return: Object with merged volumes data from plugins - :rtype: dict + :type cluster: Cluster model + :return: dict -- Object with merged volumes data from plugins """ volumes_metadata = { 'volumes': [], @@ -301,6 +300,35 @@ class PluginManager(object): 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)): + for component in plugin_adapter.components_metadata: + name = component['name'] + if name in seen_components: + raise errors.AlreadyExists( + 'Plugin {0} is overlapping with {1} by introducing ' + 'the same component with name "{2}"' + .format(plugin_adapter.full_name, + seen_components[name], + name)) + + seen_components[name] = plugin_adapter.full_name + components.append(component) + + return components + @classmethod def sync_plugins_metadata(cls, plugin_ids=None): """Sync metadata for plugins by given IDs. diff --git a/nailgun/nailgun/test/base.py b/nailgun/nailgun/test/base.py index 81e454e4e9..054b9f7c24 100644 --- a/nailgun/nailgun/test/base.py +++ b/nailgun/nailgun/test/base.py @@ -61,7 +61,6 @@ from nailgun.db.sqlalchemy.models import Task # here come objects from nailgun.objects import Cluster from nailgun.objects import ClusterPlugins -from nailgun.objects import Component from nailgun.objects import MasterNodeSettings from nailgun.objects import Node from nailgun.objects import NodeGroup @@ -446,23 +445,6 @@ class EnvironmentManager(object): ClusterPlugins.set_attributes(cluster.id, plugin.id, enabled=True) return plugin - def create_component(self, release=None, plugin=None, **kwargs): - component = self.get_default_components(**kwargs)[0] - component_data = component.get('compatible', {}) - component_data.update({ - 'name': component.get('name'), - 'type': component.get('type') - }) - component = Component.create(component_data) - if release: - component.releases.append(release) - - if plugin: - component.plugin = plugin - self.db.flush() - - return component - def default_metadata(self): item = self.find_item_by_pk_model( self.read_fixtures(("sample_environment",)), @@ -719,14 +701,15 @@ class EnvironmentManager(object): def get_default_components(self, **kwargs): default_components = [ { - 'name': 'test_hypervisor', - 'type': 'hypervisor', - 'compatible': { - 'hypervisors': ['*'], - 'networks': [], - 'storages': ['object:block:swift'], - 'additional_services': [] - } + 'name': 'hypervisor:test_hypervisor', + 'compatible': [ + {'name': 'hypervisors:*'}, + {'name': 'storages:object:block:swift'} + ], + 'incompatible': [ + {'name': 'networks:*'}, + {'name': 'additional_services:*'} + ] } ] diff --git a/nailgun/nailgun/test/integration/test_component_handler.py b/nailgun/nailgun/test/integration/test_component_handler.py index 7edf07196b..75b7f16005 100644 --- a/nailgun/nailgun/test/integration/test_component_handler.py +++ b/nailgun/nailgun/test/integration/test_component_handler.py @@ -26,7 +26,9 @@ class TestComponentHandler(base.BaseIntegrationTest): self.release = self.env.create_release( version='2015.1-8.0', operating_system='Ubuntu', - modes=[consts.CLUSTER_MODES.ha_compact]) + modes=[consts.CLUSTER_MODES.ha_compact], + components_metadata=self.env.get_default_components( + name='hypervisor:test_component_1')) self.plugin = self.env.create_plugin( name='compatible_plugin', fuel_version=['8.0'], @@ -35,47 +37,35 @@ class TestComponentHandler(base.BaseIntegrationTest): 'version': '2015.1-8.0', 'os': 'ubuntu', 'mode': ['ha'], - 'deployment_scripts_path': 'deployment_scripts/'}]) - self.core_component = self.env.create_component( - release=self.release, - name='test_component_1') - self.plugin_component = self.env.create_component( - plugin=self.plugin, - name='test_component_2', - type='additional_service') + 'deployment_scripts_path': 'deployment_scripts/'}], + components_metadata=self.env.get_default_components( + name='storage:test_component_2')) def test_get_components(self): - release_id = self.release.id - plugin_id = self.plugin.id - resp = self.app.get( reverse( 'ComponentCollectionHandler', - kwargs={'release_id': release_id}), + kwargs={'release_id': self.release.id}), headers=self.default_headers ) self.assertEqual(200, resp.status_code) self.assertEqual(resp.json_body, [ { - 'name': 'test_component_1', - 'type': 'hypervisor', - 'releases_ids': [release_id], - 'plugin_id': None, - 'compatible': { - 'hypervisors': '*', - 'networks': [], - 'storages': ['object:block:swift'], - 'additional_services': []}}, + 'name': 'hypervisor:test_component_1', + 'compatible': [ + {'name': 'hypervisors:*'}, + {'name': 'storages:object:block:swift'}], + 'incompatible': [ + {'name': 'networks:*'}, + {'name': 'additional_services:*'}]}, { - 'name': 'test_component_2', - 'type': 'additional_service', - 'releases_ids': [], - 'plugin_id': plugin_id, - 'compatible': { - 'hypervisors': '*', - 'networks': [], - 'storages': ['object:block:swift'], - 'additional_services': []}}]) + 'name': 'storage:test_component_2', + 'compatible': [ + {'name': 'hypervisors:*'}, + {'name': 'storages:object:block:swift'}], + 'incompatible': [ + {'name': 'networks:*'}, + {'name': 'additional_services:*'}]}]) def test_404_for_get_components_with_none_release_id(self): resp = self.app.get( @@ -89,12 +79,10 @@ class TestComponentHandler(base.BaseIntegrationTest): self.assertEqual(404, resp.status_code) def test_post_components_not_allowed(self): - release_id = self.release.id - resp = self.app.post( reverse( 'ComponentCollectionHandler', - kwargs={'release_id': release_id}), + kwargs={'release_id': self.release.id}), headers=self.default_headers, expect_errors=True ) diff --git a/nailgun/nailgun/test/integration/test_plugin_manager.py b/nailgun/nailgun/test/integration/test_plugin_manager.py index 2f6969af4e..9dab2cfa9f 100644 --- a/nailgun/nailgun/test/integration/test_plugin_manager.py +++ b/nailgun/nailgun/test/integration/test_plugin_manager.py @@ -29,7 +29,12 @@ class TestPluginManager(base.BaseIntegrationTest): def setUp(self): super(TestPluginManager, self).setUp() - self.env.create() + self.env.create( + release_kwargs={ + 'version': '2015.1-8.0', + 'operating_system': 'Ubuntu'}) + + self.release = self.env.releases[0] self.cluster = self.env.clusters[0] # Create two plugins with package verion 3.0.0 @@ -161,6 +166,67 @@ class TestPluginManager(base.BaseIntegrationTest): PluginManager.sync_plugins_metadata([self.env.plugins[0].id]) self.assertEqual(sync_mock.call_count, 1) + def test_get_components_metadata(self): + self.env.create_plugin( + name='plugin_with_components', + package_version='4.0.0', + fuel_version=['8.0'], + components_metadata=self.env.get_default_components()) + + components_metadata = PluginManager.get_components_metadata( + self.release) + self.assertEqual( + components_metadata, self.env.get_default_components()) + + def test_raise_exception_when_plugin_overlap_release_component(self): + release = self.env.create_release( + version='2015.1-8.1', + operating_system='Ubuntu', + modes=[consts.CLUSTER_MODES.ha_compact], + components_metadata=self.env.get_default_components()) + + self.env.create_plugin( + name='plugin_with_components', + package_version='4.0.0', + fuel_version=['8.0'], + releases=[{ + 'repository_path': 'repositories/ubuntu', + 'version': '2015.1-8.1', 'os': 'ubuntu', + 'mode': ['ha'], + 'deployment_scripts_path': 'deployment_scripts/'}], + components_metadata=self.env.get_default_components()) + + expected_message = ( + 'Plugin plugin_with_components-0.1.0 is overlapping with release ' + 'by introducing the same component with name ' + '"hypervisor:test_hypervisor"') + + with self.assertRaisesRegexp(errors.AlreadyExists, + expected_message): + PluginManager.get_components_metadata(release) + + def test_raise_exception_when_plugin_overlap_another_component(self): + self.env.create_plugin( + name='plugin_with_components_1', + package_version='4.0.0', + fuel_version=['8.0'], + components_metadata=self.env.get_default_components()) + + self.env.create_plugin( + name='plugin_with_components_2', + package_version='4.0.0', + fuel_version=['8.0'], + components_metadata=self.env.get_default_components()) + + expected_message = ( + 'Plugin plugin_with_components_2-0.1.0 is overlapping with ' + 'plugin_with_components_1-0.1.0 by introducing the same component ' + 'with name "hypervisor:test_hypervisor"') + + with self.assertRaisesRegexp(errors.AlreadyExists, + expected_message): + PluginManager.get_components_metadata(self.release) + class TestClusterPluginIntegration(base.BaseTestCase): diff --git a/nailgun/nailgun/test/unit/test_component.py b/nailgun/nailgun/test/unit/test_component.py deleted file mode 100644 index 8679606eaf..0000000000 --- a/nailgun/nailgun/test/unit/test_component.py +++ /dev/null @@ -1,112 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2015 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 nailgun import consts -from nailgun.objects import ComponentCollection -from nailgun.objects.serializers.component import ComponentSerializer -from nailgun.test import base - - -class BaseComponentTestCase(base.BaseTestCase): - - def setUp(self): - super(BaseComponentTestCase, self).setUp() - self.release = self.env.create_release( - version='2015.1-8.0', - operating_system='Ubuntu', - modes=[consts.CLUSTER_MODES.ha_compact]) - self.plugin = self.env.create_plugin( - name='compatible_plugin', - fuel_version=['8.0'], - releases=[{ - 'repository_path': 'repositories/ubuntu', - 'version': '2015.1-8.0', - 'os': 'ubuntu', - 'mode': ['ha'], - 'deployment_scripts_path': 'deployment_scripts/'}]) - self.core_component = self.env.create_component( - release=self.release, - name='test_component_1') - self.plugin_component = self.env.create_component( - plugin=self.plugin, - name='test_component_2', - type='additional_service') - - -class TestComponentCollection(BaseComponentTestCase): - - def test_get_all_by_release(self): - self.incompatible_plugin = self.env.create_plugin( - fuel_version=['6.0'], - releases=[{ - 'repository_path': 'repositories/centos', - 'version': '2014.2-6.0', - 'os': 'centos', - 'mode': ['ha'], - 'deployment_scripts_path': 'deployment_scripts/'}] - ) - self.incompatible_release = self.env.create_release( - operating_system='Ubuntu', - modes=[consts.CLUSTER_MODES.ha_compact]) - self.env.create_component( - plugin=self.incompatible_plugin, - name='incompatible_plugin_component') - self.env.create_component( - release=self.incompatible_release, - name='incompatible_core_component') - components = ComponentCollection.get_all_by_release(self.release.id) - for component in components: - self.assertIn( - component.name, ['test_component_1', 'test_component_2']) - self.assertNotIn(component.name, [ - 'incompatible_plugin_component', - 'incompatible_core_component']) - self.assertEqual(len(components), 2) - - -class TestComponentSerializer(BaseComponentTestCase): - - def test_core_component_serialization(self): - release_id = self.release.id - component_data = ComponentSerializer.serialize( - self.core_component) - self.assertDictEqual(component_data, { - 'name': 'test_component_1', - 'type': 'hypervisor', - 'compatible': { - 'additional_services': [], - 'networks': [], - 'storages': ['object:block:swift'], - 'hypervisors': '*' - }, - 'plugin_id': None, - 'releases_ids': [release_id]}) - - def test_plugin_component_serialization(self): - plugin_id = self.plugin.id - component_data = ComponentSerializer.serialize( - self.plugin_component) - self.assertDictEqual(component_data, { - 'name': 'test_component_2', - 'type': 'additional_service', - 'compatible': { - 'additional_services': [], - 'networks': [], - 'storages': ['object:block:swift'], - 'hypervisors': '*' - }, - 'plugin_id': plugin_id, - 'releases_ids': []}) diff --git a/nailgun/nailgun/test/unit/test_migration_fuel_8_0.py b/nailgun/nailgun/test/unit/test_migration_fuel_8_0.py index 9c578cdac9..ea1f166288 100644 --- a/nailgun/nailgun/test/unit/test_migration_fuel_8_0.py +++ b/nailgun/nailgun/test/unit/test_migration_fuel_8_0.py @@ -68,7 +68,7 @@ def prepare(): 'status': 'new', 'net_provider': 'neutron', 'grouping': 'roles', - 'fuel_version': '6.1', + 'fuel_version': '7.0', } ) @@ -200,159 +200,6 @@ def insert_table_row(table, row_data): return result.inserted_primary_key[0] -class TestComponentTableMigration(base.BaseAlembicMigrationTest): - def test_table_fields_and_default_values(self): - component_table = self.meta.tables['components'] - insert_table_row(component_table, - {'name': 'test_component', 'type': 'network'}) - columns = [t.name for t in component_table.columns] - self.assertItemsEqual(columns, ['id', 'name', 'type', 'hypervisors', - 'networks', 'storages', - 'plugin_id', 'additional_services']) - - column_with_default_values = [ - (component_table.c.hypervisors, []), - (component_table.c.networks, []), - (component_table.c.storages, []), - (component_table.c.additional_services, []) - ] - result = db.execute( - sa.select([item[0] for item in column_with_default_values])) - db_values = result.fetchone() - - for idx, db_value in enumerate(db_values): - self.assertEqual(db_value, column_with_default_values[idx][1]) - - def test_unique_name_type_constraint(self): - test_name = six.text_type(uuid.uuid4()) - test_type = 'storage' - component_table = self.meta.tables['components'] - insert_table_row(component_table, - {'name': test_name, 'type': test_type}) - - insert_table_row(component_table, - {'name': six.text_type(uuid.uuid4()), - 'type': test_type}) - same_type_components_count = db.execute( - sa.select([sa.func.count(component_table.c.name)]). - where(component_table.c.type == test_type) - ).fetchone()[0] - self.assertEqual(same_type_components_count, 2) - - with self.assertRaisesRegexp(IntegrityError, - 'duplicate key value violates unique ' - 'constraint "_component_name_type_uc"'): - insert_table_row(component_table, - {'name': test_name, 'type': test_type}) - - def test_component_types_enum(self): - allow_type_name = ('hypervisor', 'network', 'storage', - 'additional_service') - component_table = self.meta.tables['components'] - for type in allow_type_name: - name = six.text_type(uuid.uuid4()) - insert_table_row(component_table, {'name': name, 'type': type}) - inserted_count = db.execute( - sa.select([sa.func.count(component_table.c.name)]). - where(sa.and_(component_table.c.type == type, - component_table.c.name == name)) - ).fetchone()[0] - self.assertEqual(inserted_count, 1) - - with self.assertRaisesRegexp(DataError, 'invalid input value for ' - 'enum component_types'): - insert_table_row(component_table, - {'name': 'test', 'type': 'wrong_type_name'}) - - def test_cascade_plugin_deletion(self): - plugin_table = self.meta.tables['plugins'] - plugin_id = insert_table_row( - plugin_table, - { - 'name': 'test_plugin', - 'title': 'Test plugin', - 'version': '1.0.0', - 'description': 'Test plugin for Fuel', - 'homepage': 'http://fuel_plugins.test_plugin.com', - 'package_version': '3.0.0' - } - ) - component_table = self.meta.tables['components'] - insert_table_row( - component_table, - {'name': 'test_name', 'plugin_id': plugin_id, 'type': 'storage'}) - db.execute( - sa.delete(plugin_table).where(plugin_table.c.id == plugin_id)) - deleted_plugin_components = db.execute( - sa.select([sa.func.count(component_table.c.name)]). - where(component_table.c.plugin_id == plugin_id) - ).fetchone()[0] - self.assertEqual(deleted_plugin_components, 0) - - -class TestReleaseComponentTableMigration(base.BaseAlembicMigrationTest): - def test_component_foreign_key_constraints(self): - release_component_table = self.meta.tables['release_components'] - component_id = insert_table_row( - self.meta.tables['components'], - {'name': 'test_name', 'type': 'network'} - ) - with self.assertRaisesRegexp(IntegrityError, - 'violates foreign key constraint ' - '"release_components_release_id_fkey"'): - insert_table_row( - release_component_table, - {'release_id': -1, 'component_id': component_id} - ) - - def test_release_foreign_key_constraints(self): - release_component_table = self.meta.tables['release_components'] - release_table = self.meta.tables['releases'] - release_id = db.execute(sa.select([release_table.c.id])).fetchone()[0] - - with self.assertRaisesRegexp(IntegrityError, - 'violates foreign key constraint ' - '"release_components_component_id_fkey"'): - insert_table_row( - release_component_table, - {'release_id': release_id, 'component_id': -1} - ) - - def test_non_null_fields(self): - release_component_table = self.meta.tables['release_components'] - with self.assertRaisesRegexp(IntegrityError, - 'violates not-null constraint'): - insert_table_row(release_component_table, {}) - - def test_cascade_release_deletion(self): - release_component_table = self.meta.tables['release_components'] - release_table = self.meta.tables['releases'] - release_id = insert_table_row( - release_table, - { - 'name': 'release_with_components', - 'version': '2014.2.2-6.1', - 'operating_system': 'ubuntu', - 'state': 'available' - } - ) - component_id = insert_table_row( - self.meta.tables['components'], - {'name': six.text_type(uuid.uuid4()), 'type': 'hypervisor'} - ) - insert_table_row( - release_component_table, - {'release_id': release_id, 'component_id': component_id} - ) - db.execute( - sa.delete(release_table).where(release_table.c.id == release_id)) - deleted_plugin_components = db.execute( - sa.select([sa.func.count(release_component_table.c.id)]). - where(release_component_table.c.release_id == release_id) - ).fetchone()[0] - self.assertEqual(deleted_plugin_components, 0) - - class TestNodeGroupsMigration(base.BaseAlembicMigrationTest): def test_name_cluster_unique_constraint_migration(self): @@ -396,6 +243,12 @@ class TestReleaseMigrations(base.BaseAlembicMigrationTest): for state in states: self.assertEqual(state, 'manageonly') + def test_new_component_metadata_field_exists_and_empty(self): + result = db.execute( + sa.select([self.meta.tables['releases'].c.components_metadata])) + self.assertEqual( + jsonutils.loads(result.fetchone()[0]), []) + class TestTaskStatus(base.BaseAlembicMigrationTest): @@ -592,3 +445,12 @@ class TestBaremetalFields(base.BaseAlembicMigrationTest): self.meta.tables['neutron_config'].c.baremetal_range])).\ fetchall() self.assertIn((baremetal_gateway, baremetal_range), result) + + +class TestPluginMigration(base.BaseAlembicMigrationTest): + + def test_new_component_metadata_field_exists_and_empty(self): + result = db.execute( + sa.select([self.meta.tables['plugins'].c.components_metadata])) + self.assertEqual( + jsonutils.loads(result.fetchone()[0]), []) diff --git a/nailgun/nailgun/test/unit/test_object_plugin.py b/nailgun/nailgun/test/unit/test_object_plugin.py index e01bb3b71b..bae41a887d 100644 --- a/nailgun/nailgun/test/unit/test_object_plugin.py +++ b/nailgun/nailgun/test/unit/test_object_plugin.py @@ -89,6 +89,14 @@ class TestPluginCollection(ExtraFunctions): self.assertListEqual( [plugin.id for plugin in plugins], ids) + def test_get_by_release(self): + release = self.env.create_release( + version='2015.1-8.0', + operating_system='Ubuntu', + modes=[consts.CLUSTER_MODES.ha_compact]) + for plugin in PluginCollection.get_by_release(release): + self.assertNotEqual(plugin.name, 'incompatible_plugin') + class TestClusterPlugins(ExtraFunctions): diff --git a/nailgun/nailgun/test/unit/test_objects.py b/nailgun/nailgun/test/unit/test_objects.py index 74ac848b60..36874f2725 100644 --- a/nailgun/nailgun/test/unit/test_objects.py +++ b/nailgun/nailgun/test/unit/test_objects.py @@ -1426,3 +1426,48 @@ class TestNetworkGroup(BaseTestCase): def test_get_default_networkgroup(self): ng = objects.NetworkGroup.get_default_admin_network() self.assertIsNotNone(ng) + + +class TestRelease(BaseTestCase): + + def test_get_all_components(self): + release = self.env.create_release( + version='2015.1-8.0', + operating_system='Ubuntu', + modes=[consts.CLUSTER_MODES.ha_compact], + components_metadata=self.env.get_default_components( + name='hypervisor:test_component_1')) + + self.env.create_plugin( + name='plugin_with_components', + package_version='4.0.0', + fuel_version=['8.0'], + releases=[{ + 'repository_path': 'repositories/ubuntu', + 'version': '2015.1-8.0', + 'os': 'ubuntu', + 'mode': ['ha'], + 'deployment_scripts_path': 'deployment_scripts/'}], + components_metadata=self.env.get_default_components( + name='storage:test_component_2') + ) + + components = objects.Release.get_all_components(release) + + self.assertListEqual(components, [ + { + 'name': 'hypervisor:test_component_1', + 'compatible': [ + {'name': 'hypervisors:*'}, + {'name': 'storages:object:block:swift'}], + 'incompatible': [ + {'name': 'networks:*'}, + {'name': 'additional_services:*'}]}, + { + 'name': 'storage:test_component_2', + 'compatible': [ + {'name': 'hypervisors:*'}, + {'name': 'storages:object:block:swift'}], + 'incompatible': [ + {'name': 'networks:*'}, + {'name': 'additional_services:*'}]}]) diff --git a/nailgun/nailgun/test/unit/test_plugin_adapters.py b/nailgun/nailgun/test/unit/test_plugin_adapters.py index ef6d2a5a8d..fad540ef3e 100644 --- a/nailgun/nailgun/test/unit/test_plugin_adapters.py +++ b/nailgun/nailgun/test/unit/test_plugin_adapters.py @@ -24,7 +24,6 @@ from nailgun.db import db from nailgun.errors import errors from nailgun.expression import Expression from nailgun.objects import ClusterPlugins -from nailgun.objects import Component from nailgun.objects import Plugin from nailgun.plugins import adapters from nailgun.settings import settings @@ -312,26 +311,8 @@ class TestPluginV4(TestPluginBase): self.plugin.deployment_tasks, deployment_tasks) self.assertEqual( self.plugin.tasks, tasks) - - component = Component.get_by_name_and_type( - components_metadata[0].get('name'), - components_metadata[0].get('type')) - compatible = components_metadata[0].get('compatible') - self.assertEqual( - component.name, components_metadata[0].get('name')) - self.assertEqual( - component.type, components_metadata[0].get('type')) - self.assertEqual( - component.hypervisors, compatible.get('hypervisors')) - self.assertEqual( - component.networks, compatible.get('networks')) - self.assertEqual( - component.storages, compatible.get('storages')) - self.assertEqual( - component.additional_services, - compatible.get('additional_services')) - self.assertEqual(component.plugin_id, self.plugin.id) + self.plugin.components_metadata, components_metadata) class TestClusterCompatibilityValidation(base.BaseTestCase):