diff --git a/etc/policy.json b/etc/policy.json index eaf6d685ffe..72756bdb630 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -163,5 +163,16 @@ "get_service_provider": "rule:regular_user", "get_lsn": "rule:admin_only", - "create_lsn": "rule:admin_only" + "create_lsn": "rule:admin_only", + + "create_flavor": "rule:admin_only", + "update_flavor": "rule:admin_only", + "delete_flavor": "rule:admin_only", + "get_flavors": "rule:regular_user", + "get_flavor": "rule:regular_user", + "create_service_profile": "rule:admin_only", + "update_service_profile": "rule:admin_only", + "delete_service_profile": "rule:admin_only", + "get_service_profiles": "rule:admin_only", + "get_service_profile": "rule:admin_only" } diff --git a/neutron/api/v2/base.py b/neutron/api/v2/base.py index 48dea6bf6d0..c3415161966 100644 --- a/neutron/api/v2/base.py +++ b/neutron/api/v2/base.py @@ -414,6 +414,9 @@ class Controller(object): action, item[self._resource], pluralized=self._collection) + if 'tenant_id' not in item[self._resource]: + # no tenant_id - no quota check + continue try: tenant_id = item[self._resource]['tenant_id'] count = quota.QUOTAS.count(request.context, self._resource, @@ -571,8 +574,7 @@ class Controller(object): return result @staticmethod - def _populate_tenant_id(context, res_dict, is_create): - + def _populate_tenant_id(context, res_dict, attr_info, is_create): if (('tenant_id' in res_dict and res_dict['tenant_id'] != context.tenant_id and not context.is_admin)): @@ -583,9 +585,9 @@ class Controller(object): if is_create and 'tenant_id' not in res_dict: if context.tenant_id: res_dict['tenant_id'] = context.tenant_id - else: + elif 'tenant_id' in attr_info: msg = _("Running without keystone AuthN requires " - " that tenant_id is specified") + "that tenant_id is specified") raise webob.exc.HTTPBadRequest(msg) @staticmethod @@ -627,7 +629,7 @@ class Controller(object): msg = _("Unable to find '%s' in request body") % resource raise webob.exc.HTTPBadRequest(msg) - Controller._populate_tenant_id(context, res_dict, is_create) + Controller._populate_tenant_id(context, res_dict, attr_info, is_create) Controller._verify_attributes(res_dict, attr_info) if is_create: # POST diff --git a/neutron/db/flavors_db.py b/neutron/db/flavors_db.py new file mode 100644 index 00000000000..75f5241be9b --- /dev/null +++ b/neutron/db/flavors_db.py @@ -0,0 +1,356 @@ +# All Rights Reserved. +# +# 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 oslo_log import log as logging +from oslo_serialization import jsonutils +from oslo_utils import importutils +from oslo_utils import uuidutils +import sqlalchemy as sa +from sqlalchemy import orm +from sqlalchemy.orm import exc as sa_exc + +from neutron.common import exceptions as qexception +from neutron.db import common_db_mixin +from neutron.db import model_base +from neutron.db import models_v2 +from neutron.plugins.common import constants + + +LOG = logging.getLogger(__name__) + + +# Flavor Exceptions +class FlavorNotFound(qexception.NotFound): + message = _("Flavor %(flavor_id)s could not be found") + + +class FlavorInUse(qexception.InUse): + message = _("Flavor %(flavor_id)s is used by some service instance") + + +class ServiceProfileNotFound(qexception.NotFound): + message = _("Service Profile %(sp_id)s could not be found") + + +class ServiceProfileInUse(qexception.InUse): + message = _("Service Profile %(sp_id)s is used by some service instance") + + +class FlavorServiceProfileBindingExists(qexception.Conflict): + message = _("Service Profile %(sp_id)s is already associated " + "with flavor %(fl_id)s") + + +class FlavorServiceProfileBindingNotFound(qexception.NotFound): + message = _("Service Profile %(sp_id)s is not associated " + "with flavor %(fl_id)s") + + +class DummyCorePlugin(object): + pass + + +class DummyServicePlugin(object): + + def driver_loaded(self, driver, service_profile): + pass + + def get_plugin_type(self): + return constants.DUMMY + + def get_plugin_description(self): + return "Dummy service plugin, aware of flavors" + + +class DummyServiceDriver(object): + + @staticmethod + def get_service_type(): + return constants.DUMMY + + def __init__(self, plugin): + pass + + +class Flavor(model_base.BASEV2, models_v2.HasId): + name = sa.Column(sa.String(255)) + description = sa.Column(sa.String(1024)) + enabled = sa.Column(sa.Boolean, nullable=False, default=True, + server_default=sa.sql.true()) + # Make it True for multi-type flavors + service_type = sa.Column(sa.String(36), nullable=True) + service_profiles = orm.relationship("FlavorServiceProfileBinding", + cascade="all, delete-orphan") + + +class ServiceProfile(model_base.BASEV2, models_v2.HasId): + description = sa.Column(sa.String(1024)) + driver = sa.Column(sa.String(1024), nullable=False) + enabled = sa.Column(sa.Boolean, nullable=False, default=True, + server_default=sa.sql.true()) + metainfo = sa.Column(sa.String(4096)) + flavors = orm.relationship("FlavorServiceProfileBinding") + + +class FlavorServiceProfileBinding(model_base.BASEV2): + flavor_id = sa.Column(sa.String(36), + sa.ForeignKey("flavors.id", + ondelete="CASCADE"), + nullable=False, primary_key=True) + flavor = orm.relationship(Flavor) + service_profile_id = sa.Column(sa.String(36), + sa.ForeignKey("serviceprofiles.id", + ondelete="CASCADE"), + nullable=False, primary_key=True) + service_profile = orm.relationship(ServiceProfile) + + +class FlavorManager(common_db_mixin.CommonDbMixin): + """Class to support flavors and service profiles.""" + + supported_extension_aliases = ["flavors"] + + def __init__(self, manager=None): + # manager = None is UT usage where FlavorManager is loaded as + # a core plugin + self.manager = manager + + def get_plugin_name(self): + return constants.FLAVORS + + def get_plugin_type(self): + return constants.FLAVORS + + def get_plugin_description(self): + return "Neutron Flavors and Service Profiles manager plugin" + + def _get_flavor(self, context, flavor_id): + try: + return self._get_by_id(context, Flavor, flavor_id) + except sa_exc.NoResultFound: + raise FlavorNotFound(flavor_id=flavor_id) + + def _get_service_profile(self, context, sp_id): + try: + return self._get_by_id(context, ServiceProfile, sp_id) + except sa_exc.NoResultFound: + raise ServiceProfileNotFound(sp_id=sp_id) + + def _make_flavor_dict(self, flavor_db, fields=None): + res = {'id': flavor_db['id'], + 'name': flavor_db['name'], + 'description': flavor_db['description'], + 'enabled': flavor_db['enabled'], + 'service_profiles': []} + if flavor_db.service_profiles: + res['service_profiles'] = [sp['service_profile_id'] + for sp in flavor_db.service_profiles] + return self._fields(res, fields) + + def _make_service_profile_dict(self, sp_db, fields=None): + res = {'id': sp_db['id'], + 'description': sp_db['description'], + 'driver': sp_db['driver'], + 'enabled': sp_db['enabled'], + 'metainfo': sp_db['metainfo']} + if sp_db.flavors: + res['flavors'] = [fl['flavor_id'] + for fl in sp_db.flavors] + return self._fields(res, fields) + + def _ensure_flavor_not_in_use(self, context, flavor_id): + """Checks that flavor is not associated with service instance.""" + # Future TODO(enikanorov): check that there is no binding to + # instances. Shall address in future upon getting the right + # flavor supported driver + pass + + def _ensure_service_profile_not_in_use(self, context, sp_id): + # Future TODO(enikanorov): check that there is no binding to instances + # and no binding to flavors. Shall be addressed in future + fl = (context.session.query(FlavorServiceProfileBinding). + filter_by(service_profile_id=sp_id).first()) + if fl: + raise ServiceProfileInUse(sp_id=sp_id) + + def create_flavor(self, context, flavor): + fl = flavor['flavor'] + with context.session.begin(subtransactions=True): + fl_db = Flavor(id=uuidutils.generate_uuid(), + name=fl['name'], + description=fl['description'], + enabled=fl['enabled']) + context.session.add(fl_db) + return self._make_flavor_dict(fl_db) + + def update_flavor(self, context, flavor_id, flavor): + fl = flavor['flavor'] + with context.session.begin(subtransactions=True): + self._ensure_flavor_not_in_use(context, flavor_id) + fl_db = self._get_flavor(context, flavor_id) + fl_db.update(fl) + + return self._make_flavor_dict(fl_db) + + def get_flavor(self, context, flavor_id, fields=None): + fl = self._get_flavor(context, flavor_id) + return self._make_flavor_dict(fl, fields) + + def delete_flavor(self, context, flavor_id): + with context.session.begin(subtransactions=True): + self._ensure_flavor_not_in_use(context, flavor_id) + fl_db = self._get_flavor(context, flavor_id) + context.session.delete(fl_db) + + def get_flavors(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, page_reverse=False): + return self._get_collection(context, Flavor, self._make_flavor_dict, + filters=filters, fields=fields, + sorts=sorts, limit=limit, + marker_obj=marker, + page_reverse=page_reverse) + + def create_flavor_service_profile(self, context, + service_profile, flavor_id): + sp = service_profile['service_profile'] + with context.session.begin(subtransactions=True): + bind_qry = context.session.query(FlavorServiceProfileBinding) + binding = bind_qry.filter_by(service_profile_id=sp['id'], + flavor_id=flavor_id).first() + if binding: + raise FlavorServiceProfileBindingExists( + sp_id=sp['id'], fl_id=flavor_id) + binding = FlavorServiceProfileBinding( + service_profile_id=sp['id'], + flavor_id=flavor_id) + context.session.add(binding) + fl_db = self._get_flavor(context, flavor_id) + sps = [x['service_profile_id'] for x in fl_db.service_profiles] + return sps + + def delete_flavor_service_profile(self, context, + service_profile_id, flavor_id): + with context.session.begin(subtransactions=True): + binding = (context.session.query(FlavorServiceProfileBinding). + filter_by(service_profile_id=service_profile_id, + flavor_id=flavor_id).first()) + if not binding: + raise FlavorServiceProfileBindingNotFound( + sp_id=service_profile_id, fl_id=flavor_id) + context.session.delete(binding) + + def get_flavor_service_profile(self, context, + service_profile_id, flavor_id, fields=None): + with context.session.begin(subtransactions=True): + binding = (context.session.query(FlavorServiceProfileBinding). + filter_by(service_profile_id=service_profile_id, + flavor_id=flavor_id).first()) + if not binding: + raise FlavorServiceProfileBindingNotFound( + sp_id=service_profile_id, fl_id=flavor_id) + res = {'service_profile_id': service_profile_id, + 'flavor_id': flavor_id} + return self._fields(res, fields) + + def _load_dummy_driver(self, driver): + driver = DummyServiceDriver + driver_klass = driver + return driver_klass + + def _load_driver(self, profile): + driver_klass = importutils.import_class(profile.driver) + return driver_klass + + def create_service_profile(self, context, service_profile): + sp = service_profile['service_profile'] + with context.session.begin(subtransactions=True): + driver_klass = self._load_dummy_driver(sp['driver']) + # 'get_service_type' must be a static method so it cant be changed + svc_type = DummyServiceDriver.get_service_type() + + sp_db = ServiceProfile(id=uuidutils.generate_uuid(), + description=sp['description'], + driver=svc_type, + enabled=sp['enabled'], + metainfo=jsonutils.dumps(sp['metainfo'])) + context.session.add(sp_db) + try: + # driver_klass = self._load_dummy_driver(sp_db) + # Future TODO(madhu_ak): commented for now to load dummy driver + # until there is flavor supported driver + # plugin = self.manager.get_service_plugins()[svc_type] + # plugin.driver_loaded(driver_klass(plugin), sp_db) + # svc_type = DummyServiceDriver.get_service_type() + # plugin = self.manager.get_service_plugins()[svc_type] + # plugin = FlavorManager(manager.NeutronManager().get_instance()) + # plugin = DummyServicePlugin.get_plugin_type(svc_type) + plugin = DummyServicePlugin() + plugin.driver_loaded(driver_klass(svc_type), sp_db) + except Exception: + # Future TODO(enikanorov): raise proper exception + self.delete_service_profile(context, sp_db['id']) + raise + return self._make_service_profile_dict(sp_db) + + def unit_create_service_profile(self, context, service_profile): + # Note: Triggered by unit tests pointing to dummy driver + sp = service_profile['service_profile'] + with context.session.begin(subtransactions=True): + sp_db = ServiceProfile(id=uuidutils.generate_uuid(), + description=sp['description'], + driver=sp['driver'], + enabled=sp['enabled'], + metainfo=sp['metainfo']) + context.session.add(sp_db) + try: + driver_klass = self._load_driver(sp_db) + # require get_service_type be a static method + svc_type = driver_klass.get_service_type() + plugin = self.manager.get_service_plugins()[svc_type] + plugin.driver_loaded(driver_klass(plugin), sp_db) + except Exception: + # Future TODO(enikanorov): raise proper exception + self.delete_service_profile(context, sp_db['id']) + raise + return self._make_service_profile_dict(sp_db) + + def update_service_profile(self, context, + service_profile_id, service_profile): + sp = service_profile['service_profile'] + with context.session.begin(subtransactions=True): + self._ensure_service_profile_not_in_use(context, + service_profile_id) + sp_db = self._get_service_profile(context, service_profile_id) + sp_db.update(sp) + return self._make_service_profile_dict(sp_db) + + def get_service_profile(self, context, sp_id, fields=None): + sp_db = self._get_service_profile(context, sp_id) + return self._make_service_profile_dict(sp_db, fields) + + def delete_service_profile(self, context, sp_id): + with context.session.begin(subtransactions=True): + self._ensure_service_profile_not_in_use(context, sp_id) + sp_db = self._get_service_profile(context, sp_id) + context.session.delete(sp_db) + + def get_service_profiles(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + return self._get_collection(context, ServiceProfile, + self._make_service_profile_dict, + filters=filters, fields=fields, + sorts=sorts, limit=limit, + marker_obj=marker, + page_reverse=page_reverse) diff --git a/neutron/db/migration/alembic_migrations/versions/HEADS b/neutron/db/migration/alembic_migrations/versions/HEADS index 81c411e63b6..816f3916df6 100644 --- a/neutron/db/migration/alembic_migrations/versions/HEADS +++ b/neutron/db/migration/alembic_migrations/versions/HEADS @@ -1,3 +1,3 @@ 30018084ec99 -52c5312f6baf +313373c0ffee kilo diff --git a/neutron/db/migration/alembic_migrations/versions/liberty/expand/31337ec0ffee_flavors.py b/neutron/db/migration/alembic_migrations/versions/liberty/expand/31337ec0ffee_flavors.py new file mode 100644 index 00000000000..4ac5ac8063a --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/liberty/expand/31337ec0ffee_flavors.py @@ -0,0 +1,62 @@ +# Copyright 2014-2015 OpenStack Foundation +# +# 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. +# + +"""Flavor framework + +Revision ID: 313373c0ffee +Revises: 52c5312f6baf + +Create Date: 2014-07-17 03:00:00.00 +""" +# revision identifiers, used by Alembic. +revision = '313373c0ffee' +down_revision = '52c5312f6baf' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table( + 'flavors', + sa.Column('id', sa.String(36)), + sa.Column('name', sa.String(255)), + sa.Column('description', sa.String(1024)), + sa.Column('enabled', sa.Boolean, nullable=False, + server_default=sa.sql.true()), + sa.Column('service_type', sa.String(36), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + + op.create_table( + 'serviceprofiles', + sa.Column('id', sa.String(36)), + sa.Column('description', sa.String(1024)), + sa.Column('driver', sa.String(1024), nullable=False), + sa.Column('enabled', sa.Boolean, nullable=False, + server_default=sa.sql.true()), + sa.Column('metainfo', sa.String(4096)), + sa.PrimaryKeyConstraint('id') + ) + + op.create_table( + 'flavorserviceprofilebindings', + sa.Column('service_profile_id', sa.String(36), nullable=False), + sa.Column('flavor_id', sa.String(36), nullable=False), + sa.ForeignKeyConstraint(['service_profile_id'], + ['serviceprofiles.id']), + sa.ForeignKeyConstraint(['flavor_id'], ['flavors.id']), + sa.PrimaryKeyConstraint('service_profile_id', 'flavor_id') + ) diff --git a/neutron/db/migration/models/head.py b/neutron/db/migration/models/head.py index 7119b4d5b2e..09e1c73b793 100644 --- a/neutron/db/migration/models/head.py +++ b/neutron/db/migration/models/head.py @@ -28,6 +28,7 @@ from neutron.db import dvr_mac_db # noqa from neutron.db import external_net_db # noqa from neutron.db import extradhcpopt_db # noqa from neutron.db import extraroute_db # noqa +from neutron.db import flavors_db # noqa from neutron.db import l3_agentschedulers_db # noqa from neutron.db import l3_attrs_db # noqa from neutron.db import l3_db # noqa diff --git a/neutron/extensions/flavors.py b/neutron/extensions/flavors.py new file mode 100644 index 00000000000..8de5fd08fe1 --- /dev/null +++ b/neutron/extensions/flavors.py @@ -0,0 +1,152 @@ +# All rights reserved. +# +# 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 neutron.api import extensions +from neutron.api.v2 import attributes as attr +from neutron.api.v2 import base +from neutron.api.v2 import resource_helper +from neutron import manager +from neutron.plugins.common import constants + + +FLAVORS = 'flavors' +SERVICE_PROFILES = 'service_profiles' +FLAVORS_PREFIX = "" + +RESOURCE_ATTRIBUTE_MAP = { + FLAVORS: { + 'id': {'allow_post': False, 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + 'primary_key': True}, + 'name': {'allow_post': True, 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True, 'default': ''}, + 'description': {'allow_post': True, 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True, 'default': ''}, + 'service_type': {'allow_post': True, 'allow_put': False, + 'validate': {'type:string': None}, + 'is_visible': True}, + 'tenant_id': {'allow_post': True, 'allow_put': False, + 'required_by_policy': True, + 'validate': {'type:string': attr.TENANT_ID_MAX_LEN}, + 'is_visible': True}, + 'service_profiles': {'allow_post': True, 'allow_put': True, + 'validate': {'type:uuid_list': None}, + 'is_visible': True, 'default': []}, + 'enabled': {'allow_post': True, 'allow_put': True, + 'validate': {'type:boolean': None}, + 'default': True, + 'is_visible': True}, + }, + SERVICE_PROFILES: { + 'id': {'allow_post': False, 'allow_put': False, + 'is_visible': True, + 'primary_key': True}, + 'description': {'allow_post': True, 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True}, + # service_profile belong to one service type for now + #'service_types': {'allow_post': False, 'allow_put': False, + # 'is_visible': True}, + 'driver': {'allow_post': True, 'allow_put': False, + 'validate': {'type:string': None}, + 'is_visible': True, + 'default': attr.ATTR_NOT_SPECIFIED}, + 'metainfo': {'allow_post': True, 'allow_put': True, + 'is_visible': True}, + 'tenant_id': {'allow_post': True, 'allow_put': False, + 'required_by_policy': True, + 'validate': {'type:string': attr.TENANT_ID_MAX_LEN}, + 'is_visible': True}, + 'enabled': {'allow_post': True, 'allow_put': True, + 'validate': {'type:boolean': None}, + 'is_visible': True, 'default': True}, + }, +} + + +SUB_RESOURCE_ATTRIBUTE_MAP = { + 'service_profiles': { + 'parent': {'collection_name': 'flavors', + 'member_name': 'flavor'}, + 'parameters': {'id': {'allow_post': True, 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True}} + } +} + + +class Flavors(extensions.ExtensionDescriptor): + + @classmethod + def get_name(cls): + return "Neutron Service Flavors" + + @classmethod + def get_alias(cls): + return "flavors" + + @classmethod + def get_description(cls): + return "Service specification for advanced services" + + @classmethod + def get_updated(cls): + return "2014-07-06T10:00:00-00:00" + + @classmethod + def get_resources(cls): + """Returns Ext Resources.""" + plural_mappings = resource_helper.build_plural_mappings( + {}, RESOURCE_ATTRIBUTE_MAP) + attr.PLURALS.update(plural_mappings) + resources = resource_helper.build_resource_info( + plural_mappings, + RESOURCE_ATTRIBUTE_MAP, + constants.FLAVORS) + plugin = manager.NeutronManager.get_service_plugins()[ + constants.FLAVORS] + for collection_name in SUB_RESOURCE_ATTRIBUTE_MAP: + # Special handling needed for sub-resources with 'y' ending + # (e.g. proxies -> proxy) + resource_name = collection_name[:-1] + parent = SUB_RESOURCE_ATTRIBUTE_MAP[collection_name].get('parent') + params = SUB_RESOURCE_ATTRIBUTE_MAP[collection_name].get( + 'parameters') + + controller = base.create_resource(collection_name, resource_name, + plugin, params, + allow_bulk=True, + parent=parent) + + resource = extensions.ResourceExtension( + collection_name, + controller, parent, + path_prefix=FLAVORS_PREFIX, + attr_map=params) + resources.append(resource) + + return resources + + def update_attributes_map(self, attributes): + super(Flavors, self).update_attributes_map( + attributes, extension_attrs_map=RESOURCE_ATTRIBUTE_MAP) + + def get_extended_resources(self, version): + if version == "2.0": + return RESOURCE_ATTRIBUTE_MAP + else: + return {} diff --git a/neutron/manager.py b/neutron/manager.py index 50beae09868..0e3a16cb2ed 100644 --- a/neutron/manager.py +++ b/neutron/manager.py @@ -23,6 +23,7 @@ from oslo_utils import importutils import six from neutron.common import utils +from neutron.db import flavors_db from neutron.i18n import _LE, _LI from neutron.plugins.common import constants @@ -165,6 +166,11 @@ class NeutronManager(object): LOG.info(_LI("Service %s is supported by the core plugin"), service_type) + def _load_flavors_manager(self): + # pass manager instance to resolve cyclical import dependency + self.service_plugins[constants.FLAVORS] = ( + flavors_db.FlavorManager(self)) + def _load_service_plugins(self): """Loads service plugins. @@ -204,6 +210,9 @@ class NeutronManager(object): "Description: %(desc)s", {"type": plugin_inst.get_plugin_type(), "desc": plugin_inst.get_plugin_description()}) + # do it after the loading from conf to avoid conflict with + # configuration provided by unit tests. + self._load_flavors_manager() @classmethod @utils.synchronized("manager") diff --git a/neutron/plugins/common/constants.py b/neutron/plugins/common/constants.py index 63947ae6fd1..edf52f5932b 100644 --- a/neutron/plugins/common/constants.py +++ b/neutron/plugins/common/constants.py @@ -22,6 +22,7 @@ FIREWALL = "FIREWALL" VPN = "VPN" METERING = "METERING" L3_ROUTER_NAT = "L3_ROUTER_NAT" +FLAVORS = "FLAVORS" # Maps extension alias to service type EXT_TO_SERVICE_MAPPING = { @@ -31,7 +32,8 @@ EXT_TO_SERVICE_MAPPING = { 'fwaas': FIREWALL, 'vpnaas': VPN, 'metering': METERING, - 'router': L3_ROUTER_NAT + 'router': L3_ROUTER_NAT, + 'flavors': FLAVORS } # Service operation status constants diff --git a/neutron/tests/api/base.py b/neutron/tests/api/base.py index 25ae565e580..bf71a56c34e 100644 --- a/neutron/tests/api/base.py +++ b/neutron/tests/api/base.py @@ -82,6 +82,8 @@ class BaseNetworkTest(neutron.tests.tempest.test.BaseTestCase): cls.ikepolicies = [] cls.floating_ips = [] cls.metering_labels = [] + cls.service_profiles = [] + cls.flavors = [] cls.metering_label_rules = [] cls.fw_rules = [] cls.fw_policies = [] @@ -146,6 +148,16 @@ class BaseNetworkTest(neutron.tests.tempest.test.BaseTestCase): cls._try_delete_resource( cls.admin_client.delete_metering_label, metering_label['id']) + # Clean up flavors + for flavor in cls.flavors: + cls._try_delete_resource( + cls.admin_client.delete_flavor, + flavor['id']) + # Clean up service profiles + for service_profile in cls.service_profiles: + cls._try_delete_resource( + cls.admin_client.delete_service_profile, + service_profile['id']) # Clean up ports for port in cls.ports: cls._try_delete_resource(cls.client.delete_port, @@ -464,3 +476,22 @@ class BaseAdminNetworkTest(BaseNetworkTest): metering_label_rule = body['metering_label_rule'] cls.metering_label_rules.append(metering_label_rule) return metering_label_rule + + @classmethod + def create_flavor(cls, name, description, service_type): + """Wrapper utility that returns a test flavor.""" + body = cls.admin_client.create_flavor( + description=description, service_type=service_type, + name=name) + flavor = body['flavor'] + cls.flavors.append(flavor) + return flavor + + @classmethod + def create_service_profile(cls, description, metainfo, driver): + """Wrapper utility that returns a test service profile.""" + body = cls.admin_client.create_service_profile( + driver=driver, metainfo=metainfo, description=description) + service_profile = body['service_profile'] + cls.service_profiles.append(service_profile) + return service_profile diff --git a/neutron/tests/api/test_flavors_extensions.py b/neutron/tests/api/test_flavors_extensions.py new file mode 100644 index 00000000000..8575c6f31d8 --- /dev/null +++ b/neutron/tests/api/test_flavors_extensions.py @@ -0,0 +1,154 @@ +# Copyright 2015 Hewlett-Packard Development Company, L.P. +# +# 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 oslo_log import log as logging + +from neutron.tests.api import base +from neutron.tests.tempest import test + + +LOG = logging.getLogger(__name__) + + +class TestFlavorsJson(base.BaseAdminNetworkTest): + + """ + Tests the following operations in the Neutron API using the REST client for + Neutron: + + List, Show, Create, Update, Delete Flavors + List, Show, Create, Update, Delete service profiles + """ + + @classmethod + def resource_setup(cls): + super(TestFlavorsJson, cls).resource_setup() + if not test.is_extension_enabled('flavors', 'network'): + msg = "flavors extension not enabled." + raise cls.skipException(msg) + service_type = "LOADBALANCER" + description_flavor = "flavor is created by tempest" + name_flavor = "Best flavor created by tempest" + cls.flavor = cls.create_flavor(name_flavor, description_flavor, + service_type) + description_sp = "service profile created by tempest" + # Future TODO(madhu_ak): Right now the dummy driver is loaded. Will + # make changes as soon I get to know the flavor supported drivers + driver = "" + metainfo = '{"data": "value"}' + cls.service_profile = cls.create_service_profile( + description=description_sp, metainfo=metainfo, driver=driver) + + def _delete_service_profile(self, service_profile_id): + # Deletes a service profile and verifies if it is deleted or not + self.admin_client.delete_service_profile(service_profile_id) + # Asserting that service profile is not found in list after deletion + labels = self.admin_client.list_service_profiles(id=service_profile_id) + self.assertEqual(len(labels['service_profiles']), 0) + + @test.attr(type='smoke') + @test.idempotent_id('ec8e15ff-95d0-433b-b8a6-b466bddb1e50') + def test_create_update_delete_service_profile(self): + # Creates a service profile + description = "service_profile created by tempest" + driver = "" + metainfo = '{"data": "value"}' + body = self.admin_client.create_service_profile( + description=description, driver=driver, metainfo=metainfo) + service_profile = body['service_profile'] + # Updates a service profile + self.admin_client.update_service_profile(service_profile['id'], + enabled=False) + self.assertTrue(service_profile['enabled']) + # Deletes a service profile + self.addCleanup(self._delete_service_profile, + service_profile['id']) + # Assert whether created service profiles are found in service profile + # lists or fail if created service profiles are not found in service + # profiles list + labels = (self.admin_client.list_service_profiles( + id=service_profile['id'])) + self.assertEqual(len(labels['service_profiles']), 1) + + @test.attr(type='smoke') + @test.idempotent_id('ec8e15ff-95d0-433b-b8a6-b466bddb1e50') + def test_create_update_delete_flavor(self): + # Creates a flavor + description = "flavor created by tempest" + service = "LOADBALANCERS" + name = "Best flavor created by tempest" + body = self.admin_client.create_flavor(name=name, service_type=service, + description=description) + flavor = body['flavor'] + # Updates a flavor + self.admin_client.update_flavor(flavor['id'], enabled=False) + self.assertTrue(flavor['enabled']) + # Deletes a flavor + self.addCleanup(self._delete_flavor, flavor['id']) + # Assert whether created flavors are found in flavor lists or fail + # if created flavors are not found in flavors list + labels = (self.admin_client.list_flavors(id=flavor['id'])) + self.assertEqual(len(labels['flavors']), 1) + + @test.attr(type='smoke') + @test.idempotent_id('30abb445-0eea-472e-bd02-8649f54a5968') + def test_show_service_profile(self): + # Verifies the details of a service profile + body = self.admin_client.show_service_profile( + self.service_profile['id']) + service_profile = body['service_profile'] + self.assertEqual(self.service_profile['id'], service_profile['id']) + self.assertEqual(self.service_profile['description'], + service_profile['description']) + self.assertEqual(self.service_profile['metainfo'], + service_profile['metainfo']) + self.assertEqual(True, service_profile['enabled']) + + @test.attr(type='smoke') + @test.idempotent_id('30abb445-0eea-472e-bd02-8649f54a5968') + def test_show_flavor(self): + # Verifies the details of a flavor + body = self.admin_client.show_flavor(self.flavor['id']) + flavor = body['flavor'] + self.assertEqual(self.flavor['id'], flavor['id']) + self.assertEqual(self.flavor['description'], flavor['description']) + self.assertEqual(self.flavor['name'], flavor['name']) + self.assertEqual(True, flavor['enabled']) + + @test.attr(type='smoke') + @test.idempotent_id('e2fb2f8c-45bf-429a-9f17-171c70444612') + def test_list_flavors(self): + # Verify flavor lists + body = self.admin_client.list_flavors(id=33) + flavors = body['flavors'] + self.assertEqual(0, len(flavors)) + + @test.attr(type='smoke') + @test.idempotent_id('e2fb2f8c-45bf-429a-9f17-171c70444612') + def test_list_service_profiles(self): + # Verify service profiles lists + body = self.admin_client.list_service_profiles(id=33) + service_profiles = body['service_profiles'] + self.assertEqual(0, len(service_profiles)) + + def _delete_flavor(self, flavor_id): + # Deletes a flavor and verifies if it is deleted or not + self.admin_client.delete_flavor(flavor_id) + # Asserting that the flavor is not found in list after deletion + labels = self.admin_client.list_flavors(id=flavor_id) + self.assertEqual(len(labels['flavors']), 0) + + +class TestFlavorsIpV6TestJSON(TestFlavorsJson): + _ip_version = 6 diff --git a/neutron/tests/etc/policy.json b/neutron/tests/etc/policy.json index eaf6d685ffe..72756bdb630 100644 --- a/neutron/tests/etc/policy.json +++ b/neutron/tests/etc/policy.json @@ -163,5 +163,16 @@ "get_service_provider": "rule:regular_user", "get_lsn": "rule:admin_only", - "create_lsn": "rule:admin_only" + "create_lsn": "rule:admin_only", + + "create_flavor": "rule:admin_only", + "update_flavor": "rule:admin_only", + "delete_flavor": "rule:admin_only", + "get_flavors": "rule:regular_user", + "get_flavor": "rule:regular_user", + "create_service_profile": "rule:admin_only", + "update_service_profile": "rule:admin_only", + "delete_service_profile": "rule:admin_only", + "get_service_profiles": "rule:admin_only", + "get_service_profile": "rule:admin_only" } diff --git a/neutron/tests/tempest/services/network/json/network_client.py b/neutron/tests/tempest/services/network/json/network_client.py index 54f264c82f1..4958bc51c03 100644 --- a/neutron/tests/tempest/services/network/json/network_client.py +++ b/neutron/tests/tempest/services/network/json/network_client.py @@ -45,7 +45,7 @@ class NetworkClientJSON(service_client.ServiceClient): # The following list represents resource names that do not require # changing underscore to a hyphen hyphen_exceptions = ["health_monitors", "firewall_rules", - "firewall_policies"] + "firewall_policies", "service_profiles"] # the following map is used to construct proper URI # for the given neutron resource service_resource_prefix_map = { diff --git a/neutron/tests/unit/extensions/test_flavors.py b/neutron/tests/unit/extensions/test_flavors.py new file mode 100644 index 00000000000..8de2cf5cacc --- /dev/null +++ b/neutron/tests/unit/extensions/test_flavors.py @@ -0,0 +1,459 @@ +# +# 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 fixtures +import mock + +from oslo_config import cfg +from oslo_utils import uuidutils + +from neutron import context +from neutron.db import api as dbapi +from neutron.db import flavors_db +from neutron.extensions import flavors +from neutron import manager +from neutron.plugins.common import constants +from neutron.tests import base +from neutron.tests.unit.api.v2 import test_base +from neutron.tests.unit.db import test_db_base_plugin_v2 +from neutron.tests.unit.extensions import base as extension + +_uuid = uuidutils.generate_uuid +_get_path = test_base._get_path + + +class FlavorExtensionTestCase(extension.ExtensionTestCase): + + def setUp(self): + super(FlavorExtensionTestCase, self).setUp() + self._setUpExtension( + 'neutron.db.flavors_db.FlavorManager', + constants.FLAVORS, flavors.RESOURCE_ATTRIBUTE_MAP, + flavors.Flavors, '', supported_extension_aliases='flavors') + + def test_create_flavor(self): + tenant_id = uuidutils.generate_uuid() + data = {'flavor': {'name': 'GOLD', + 'service_type': constants.LOADBALANCER, + 'description': 'the best flavor', + 'tenant_id': tenant_id, + 'enabled': True}} + + expected = copy.deepcopy(data) + expected['flavor']['service_profiles'] = [] + + instance = self.plugin.return_value + instance.create_flavor.return_value = expected['flavor'] + res = self.api.post(_get_path('flavors', fmt=self.fmt), + self.serialize(data), + content_type='application/%s' % self.fmt) + + instance.create_flavor.assert_called_with(mock.ANY, + flavor=expected) + res = self.deserialize(res) + self.assertIn('flavor', res) + self.assertEqual(expected, res) + + def test_update_flavor(self): + flavor_id = 'fake_id' + data = {'flavor': {'name': 'GOLD', + 'description': 'the best flavor', + 'enabled': True}} + expected = copy.copy(data) + expected['flavor']['service_profiles'] = [] + + instance = self.plugin.return_value + instance.update_flavor.return_value = expected['flavor'] + res = self.api.put(_get_path('flavors', id=flavor_id, fmt=self.fmt), + self.serialize(data), + content_type='application/%s' % self.fmt) + + instance.update_flavor.assert_called_with(mock.ANY, + flavor_id, + flavor=expected) + res = self.deserialize(res) + self.assertIn('flavor', res) + self.assertEqual(expected, res) + + def test_delete_flavor(self): + flavor_id = 'fake_id' + instance = self.plugin.return_value + self.api.delete(_get_path('flavors', id=flavor_id, fmt=self.fmt), + content_type='application/%s' % self.fmt) + + instance.delete_flavor.assert_called_with(mock.ANY, + flavor_id) + + def test_show_flavor(self): + flavor_id = 'fake_id' + expected = {'flavor': {'id': flavor_id, + 'name': 'GOLD', + 'description': 'the best flavor', + 'enabled': True, + 'service_profiles': ['profile-1']}} + instance = self.plugin.return_value + instance.get_flavor.return_value = expected['flavor'] + res = self.api.get(_get_path('flavors', id=flavor_id, fmt=self.fmt)) + instance.get_flavor.assert_called_with(mock.ANY, + flavor_id, + fields=mock.ANY) + res = self.deserialize(res) + self.assertEqual(expected, res) + + def test_get_flavors(self): + data = {'flavors': [{'id': 'id1', + 'name': 'GOLD', + 'description': 'the best flavor', + 'enabled': True, + 'service_profiles': ['profile-1']}, + {'id': 'id2', + 'name': 'GOLD', + 'description': 'the best flavor', + 'enabled': True, + 'service_profiles': ['profile-2', 'profile-1']}]} + instance = self.plugin.return_value + instance.get_flavors.return_value = data['flavors'] + res = self.api.get(_get_path('flavors', fmt=self.fmt)) + instance.get_flavors.assert_called_with(mock.ANY, + fields=mock.ANY, + filters=mock.ANY) + res = self.deserialize(res) + self.assertEqual(data, res) + + def test_create_service_profile(self): + tenant_id = uuidutils.generate_uuid() + expected = {'service_profile': {'description': 'the best sp', + 'driver': '', + 'tenant_id': tenant_id, + 'enabled': True, + 'metainfo': '{"data": "value"}'}} + + instance = self.plugin.return_value + instance.create_service_profile.return_value = ( + expected['service_profile']) + res = self.api.post(_get_path('service_profiles', fmt=self.fmt), + self.serialize(expected), + content_type='application/%s' % self.fmt) + instance.create_service_profile.assert_called_with( + mock.ANY, + service_profile=expected) + res = self.deserialize(res) + self.assertIn('service_profile', res) + self.assertEqual(expected, res) + + def test_update_service_profile(self): + sp_id = "fake_id" + expected = {'service_profile': {'description': 'the best sp', + 'enabled': False, + 'metainfo': '{"data1": "value3"}'}} + + instance = self.plugin.return_value + instance.update_service_profile.return_value = ( + expected['service_profile']) + res = self.api.put(_get_path('service_profiles', + id=sp_id, fmt=self.fmt), + self.serialize(expected), + content_type='application/%s' % self.fmt) + + instance.update_service_profile.assert_called_with( + mock.ANY, + sp_id, + service_profile=expected) + res = self.deserialize(res) + self.assertIn('service_profile', res) + self.assertEqual(expected, res) + + def test_delete_service_profile(self): + sp_id = 'fake_id' + instance = self.plugin.return_value + self.api.delete(_get_path('service_profiles', id=sp_id, fmt=self.fmt), + content_type='application/%s' % self.fmt) + instance.delete_service_profile.assert_called_with(mock.ANY, + sp_id) + + def test_show_service_profile(self): + sp_id = 'fake_id' + expected = {'service_profile': {'id': 'id1', + 'driver': 'entrypoint1', + 'description': 'desc', + 'metainfo': '{}', + 'enabled': True}} + instance = self.plugin.return_value + instance.get_service_profile.return_value = ( + expected['service_profile']) + res = self.api.get(_get_path('service_profiles', + id=sp_id, fmt=self.fmt)) + instance.get_service_profile.assert_called_with(mock.ANY, + sp_id, + fields=mock.ANY) + res = self.deserialize(res) + self.assertEqual(expected, res) + + def test_get_service_profiles(self): + expected = {'service_profiles': [{'id': 'id1', + 'driver': 'entrypoint1', + 'description': 'desc', + 'metainfo': '{}', + 'enabled': True}, + {'id': 'id2', + 'driver': 'entrypoint2', + 'description': 'desc', + 'metainfo': '{}', + 'enabled': True}]} + instance = self.plugin.return_value + instance.get_service_profiles.return_value = ( + expected['service_profiles']) + res = self.api.get(_get_path('service_profiles', fmt=self.fmt)) + instance.get_service_profiles.assert_called_with(mock.ANY, + fields=mock.ANY, + filters=mock.ANY) + res = self.deserialize(res) + self.assertEqual(expected, res) + + def test_associate_service_profile_with_flavor(self): + expected = {'service_profile': {'id': _uuid()}} + instance = self.plugin.return_value + instance.create_flavor_service_profile.return_value = ( + expected['service_profile']) + res = self.api.post('/flavors/fl_id/service_profiles', + self.serialize(expected), + content_type='application/%s' % self.fmt) + instance.create_flavor_service_profile.assert_called_with( + mock.ANY, service_profile=expected, flavor_id='fl_id') + res = self.deserialize(res) + self.assertEqual(expected, res) + + def test_disassociate_service_profile_with_flavor(self): + instance = self.plugin.return_value + instance.delete_flavor_service_profile.return_value = None + self.api.delete('/flavors/fl_id/service_profiles/%s' % 'fake_spid', + content_type='application/%s' % self.fmt) + instance.delete_flavor_service_profile.assert_called_with( + mock.ANY, + 'fake_spid', + flavor_id='fl_id') + + +class DummyCorePlugin(object): + pass + + +class DummyServicePlugin(object): + + def driver_loaded(self, driver, service_profile): + pass + + def get_plugin_type(self): + return constants.DUMMY + + def get_plugin_description(self): + return "Dummy service plugin, aware of flavors" + + +class DummyServiceDriver(object): + + @staticmethod + def get_service_type(): + return constants.DUMMY + + def __init__(self, plugin): + pass + + +class FlavorManagerTestCase(test_db_base_plugin_v2.NeutronDbPluginV2TestCase, + base.PluginFixture): + def setUp(self): + super(FlavorManagerTestCase, self).setUp() + + self.config_parse() + cfg.CONF.set_override( + 'core_plugin', + 'neutron.tests.unit.extensions.test_flavors.DummyCorePlugin') + cfg.CONF.set_override( + 'service_plugins', + ['neutron.tests.unit.extensions.test_flavors.DummyServicePlugin']) + + self.useFixture( + fixtures.MonkeyPatch('neutron.manager.NeutronManager._instance')) + + self.plugin = flavors_db.FlavorManager( + manager.NeutronManager().get_instance()) + self.ctx = context.get_admin_context() + dbapi.get_engine() + + def _create_flavor(self, description=None): + flavor = {'flavor': {'name': 'GOLD', + 'service_type': constants.LOADBALANCER, + 'description': description or 'the best flavor', + 'enabled': True}} + return self.plugin.create_flavor(self.ctx, flavor), flavor + + def test_create_flavor(self): + self._create_flavor() + res = self.ctx.session.query(flavors_db.Flavor).all() + self.assertEqual(1, len(res)) + self.assertEqual('GOLD', res[0]['name']) + + def test_update_flavor(self): + fl, flavor = self._create_flavor() + flavor = {'flavor': {'name': 'Silver', + 'enabled': False}} + self.plugin.update_flavor(self.ctx, fl['id'], flavor) + res = (self.ctx.session.query(flavors_db.Flavor). + filter_by(id=fl['id']).one()) + self.assertEqual('Silver', res['name']) + self.assertFalse(res['enabled']) + + def test_delete_flavor(self): + fl, data = self._create_flavor() + self.plugin.delete_flavor(self.ctx, fl['id']) + res = (self.ctx.session.query(flavors_db.Flavor).all()) + self.assertFalse(res) + + def test_show_flavor(self): + fl, data = self._create_flavor() + show_fl = self.plugin.get_flavor(self.ctx, fl['id']) + self.assertEqual(fl, show_fl) + + def test_get_flavors(self): + fl, flavor = self._create_flavor() + flavor['flavor']['name'] = 'SILVER' + self.plugin.create_flavor(self.ctx, flavor) + show_fl = self.plugin.get_flavors(self.ctx) + self.assertEqual(2, len(show_fl)) + + def _create_service_profile(self, description=None): + data = {'service_profile': + {'description': description or 'the best sp', + 'driver': + ('neutron.tests.unit.extensions.test_flavors.' + 'DummyServiceDriver'), + 'enabled': True, + 'metainfo': '{"data": "value"}'}} + sp = self.plugin.unit_create_service_profile(self.ctx, + data) + return sp, data + + def test_create_service_profile(self): + sp, data = self._create_service_profile() + res = (self.ctx.session.query(flavors_db.ServiceProfile). + filter_by(id=sp['id']).one()) + self.assertEqual(data['service_profile']['driver'], res['driver']) + self.assertEqual(data['service_profile']['metainfo'], res['metainfo']) + + def test_update_service_profile(self): + sp, data = self._create_service_profile() + data['service_profile']['metainfo'] = '{"data": "value1"}' + sp = self.plugin.update_service_profile(self.ctx, sp['id'], + data) + res = (self.ctx.session.query(flavors_db.ServiceProfile). + filter_by(id=sp['id']).one()) + self.assertEqual(data['service_profile']['metainfo'], res['metainfo']) + + def test_delete_service_profile(self): + sp, data = self._create_service_profile() + self.plugin.delete_service_profile(self.ctx, sp['id']) + res = self.ctx.session.query(flavors_db.ServiceProfile).all() + self.assertFalse(res) + + def test_show_service_profile(self): + sp, data = self._create_service_profile() + sp_show = self.plugin.get_service_profile(self.ctx, sp['id']) + self.assertEqual(sp, sp_show) + + def test_get_service_profiles(self): + self._create_service_profile() + self._create_service_profile(description='another sp') + self.assertEqual(2, len(self.plugin.get_service_profiles(self.ctx))) + + def test_associate_service_profile_with_flavor(self): + sp, data = self._create_service_profile() + fl, data = self._create_flavor() + self.plugin.create_flavor_service_profile( + self.ctx, + {'service_profile': {'id': sp['id']}}, + fl['id']) + binding = ( + self.ctx.session.query(flavors_db.FlavorServiceProfileBinding). + first()) + self.assertEqual(fl['id'], binding['flavor_id']) + self.assertEqual(sp['id'], binding['service_profile_id']) + + res = self.plugin.get_flavor(self.ctx, fl['id']) + self.assertEqual(1, len(res['service_profiles'])) + self.assertEqual(sp['id'], res['service_profiles'][0]) + + res = self.plugin.get_service_profile(self.ctx, sp['id']) + self.assertEqual(1, len(res['flavors'])) + self.assertEqual(fl['id'], res['flavors'][0]) + + def test_autodelete_flavor_associations(self): + sp, data = self._create_service_profile() + fl, data = self._create_flavor() + self.plugin.create_flavor_service_profile( + self.ctx, + {'service_profile': {'id': sp['id']}}, + fl['id']) + self.plugin.delete_flavor(self.ctx, fl['id']) + binding = ( + self.ctx.session.query(flavors_db.FlavorServiceProfileBinding). + first()) + self.assertIsNone(binding) + + def test_associate_service_profile_with_flavor_exists(self): + sp, data = self._create_service_profile() + fl, data = self._create_flavor() + self.plugin.create_flavor_service_profile( + self.ctx, + {'service_profile': {'id': sp['id']}}, + fl['id']) + self.assertRaises(flavors_db.FlavorServiceProfileBindingExists, + self.plugin.create_flavor_service_profile, + self.ctx, + {'service_profile': {'id': sp['id']}}, + fl['id']) + + def test_disassociate_service_profile_with_flavor(self): + sp, data = self._create_service_profile() + fl, data = self._create_flavor() + self.plugin.create_flavor_service_profile( + self.ctx, + {'service_profile': {'id': sp['id']}}, + fl['id']) + self.plugin.delete_flavor_service_profile( + self.ctx, sp['id'], fl['id']) + binding = ( + self.ctx.session.query(flavors_db.FlavorServiceProfileBinding). + first()) + self.assertIsNone(binding) + + self.assertRaises( + flavors_db.FlavorServiceProfileBindingNotFound, + self.plugin.delete_flavor_service_profile, + self.ctx, sp['id'], fl['id']) + + def test_delete_service_profile_in_use(self): + sp, data = self._create_service_profile() + fl, data = self._create_flavor() + self.plugin.create_flavor_service_profile( + self.ctx, + {'service_profile': {'id': sp['id']}}, + fl['id']) + self.assertRaises( + flavors_db.ServiceProfileInUse, + self.plugin.delete_service_profile, + self.ctx, + sp['id']) diff --git a/neutron/tests/unit/test_manager.py b/neutron/tests/unit/test_manager.py index b3d3916f964..2020804fd4f 100644 --- a/neutron/tests/unit/test_manager.py +++ b/neutron/tests/unit/test_manager.py @@ -105,7 +105,7 @@ class NeutronManagerTestCase(base.BaseTestCase): "MultiServiceCorePlugin") mgr = manager.NeutronManager.get_instance() svc_plugins = mgr.get_service_plugins() - self.assertEqual(3, len(svc_plugins)) + self.assertEqual(4, len(svc_plugins)) self.assertIn(constants.CORE, svc_plugins.keys()) self.assertIn(constants.LOADBALANCER, svc_plugins.keys()) self.assertIn(constants.DUMMY, svc_plugins.keys())