From e0eed14a1e8e63a8b6bbcc6f6a474036c1ef52f9 Mon Sep 17 00:00:00 2001 From: Eugene Nikanorov Date: Thu, 17 Jul 2014 14:23:49 +0400 Subject: [PATCH] Flavor Framework implementation This patch introduces API and DB plugin for flavor framework. API adds Flavors and Service Profiles which are resources available only for admins to operate. This framework then should be leveraged by advanced services. Included tempest API tests in neutron tree Implements: blueprint neutron-flavor-framework Change-Id: I99ba0ce520ae3d8696eca5c994777c7d5ba3d4b1 Co-Authored-By: Doug Wiegley Co-Authored-By: Madhusudhan Kandadai --- etc/policy.json | 13 +- neutron/api/v2/base.py | 12 +- neutron/db/flavors_db.py | 356 ++++++++++++++ .../alembic_migrations/versions/HEADS | 2 +- .../liberty/expand/31337ec0ffee_flavors.py | 62 +++ neutron/db/migration/models/head.py | 1 + neutron/extensions/flavors.py | 152 ++++++ neutron/manager.py | 9 + neutron/plugins/common/constants.py | 4 +- neutron/tests/api/base.py | 31 ++ neutron/tests/api/test_flavors_extensions.py | 154 ++++++ neutron/tests/etc/policy.json | 13 +- .../services/network/json/network_client.py | 2 +- neutron/tests/unit/extensions/test_flavors.py | 459 ++++++++++++++++++ neutron/tests/unit/test_manager.py | 2 +- 15 files changed, 1261 insertions(+), 11 deletions(-) create mode 100644 neutron/db/flavors_db.py create mode 100644 neutron/db/migration/alembic_migrations/versions/liberty/expand/31337ec0ffee_flavors.py create mode 100644 neutron/extensions/flavors.py create mode 100644 neutron/tests/api/test_flavors_extensions.py create mode 100644 neutron/tests/unit/extensions/test_flavors.py 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())