From 0e3f4b833502d0f501cfc3dd384642e46a3bf78a Mon Sep 17 00:00:00 2001 From: Kevin Benton Date: Sat, 30 Apr 2016 18:49:17 -0700 Subject: [PATCH] Add flavor/service provider support to routers This is the initial support for flavors and multiple service providers with the built-in L3 service plugin. This patch handles a few key components: * Adds an optional flavor_id to the router data model * Adds a new driver controller that performs the following tasks: * Loads up the configured drivers and 4 default drivers representing the current matrix of ha/dvr options (single node, ha, dvr, and ha+dvr) * Associates every router with a driver based on ha/dvr attributes or the flavor_id if specified Note that the current drivers are very limited because they don't do anything. All of the complex logic for the in-tree drivers is still tied up in the giant mixin the service plugin inherits. Breaking that apart will be in follow-up patches. Partially-Implements: blueprint multi-l3-backends Change-Id: Idce75bf0fc1375dcbbff9b9803fd2fe97d158cff --- neutron/db/l3_db.py | 2 + .../alembic_migrations/versions/EXPAND_HEAD | 2 +- .../3d0e74aa7d37_add_flavor_id_to_routers.py | 43 +++ neutron/extensions/l3_flavors.py | 55 ++++ .../services/l3_router/l3_router_plugin.py | 14 +- .../l3_router/service_providers/__init__.py | 0 .../l3_router/service_providers/base.py | 55 ++++ .../service_providers/driver_controller.py | 249 ++++++++++++++++++ .../l3_router/service_providers/dvr.py | 19 ++ .../l3_router/service_providers/dvrha.py | 22 ++ .../l3_router/service_providers/ha.py | 19 ++ .../service_providers/single_node.py | 19 ++ .../functional/pecan_wsgi/test_controllers.py | 6 +- .../tests/unit/db/test_agentschedulers_db.py | 12 +- neutron/tests/unit/plugins/ml2/base.py | 6 + neutron/tests/unit/plugins/ml2/test_plugin.py | 5 + .../tests/unit/services/l3_router/__init__.py | 0 .../l3_router/service_providers/__init__.py | 0 .../test_driver_controller.py | 91 +++++++ 19 files changed, 613 insertions(+), 6 deletions(-) create mode 100644 neutron/db/migration/alembic_migrations/versions/newton/expand/3d0e74aa7d37_add_flavor_id_to_routers.py create mode 100644 neutron/extensions/l3_flavors.py create mode 100644 neutron/services/l3_router/service_providers/__init__.py create mode 100644 neutron/services/l3_router/service_providers/base.py create mode 100644 neutron/services/l3_router/service_providers/driver_controller.py create mode 100644 neutron/services/l3_router/service_providers/dvr.py create mode 100644 neutron/services/l3_router/service_providers/dvrha.py create mode 100644 neutron/services/l3_router/service_providers/ha.py create mode 100644 neutron/services/l3_router/service_providers/single_node.py create mode 100644 neutron/tests/unit/services/l3_router/__init__.py create mode 100644 neutron/tests/unit/services/l3_router/service_providers/__init__.py create mode 100644 neutron/tests/unit/services/l3_router/service_providers/test_driver_controller.py diff --git a/neutron/db/l3_db.py b/neutron/db/l3_db.py index 98b7eea4b99..ff1c78d70e7 100644 --- a/neutron/db/l3_db.py +++ b/neutron/db/l3_db.py @@ -95,6 +95,8 @@ class Router(model_base.HasStandardAttributes, model_base.BASEV2, admin_state_up = sa.Column(sa.Boolean) gw_port_id = sa.Column(sa.String(36), sa.ForeignKey('ports.id')) gw_port = orm.relationship(models_v2.Port, lazy='joined') + flavor_id = sa.Column(sa.String(36), + sa.ForeignKey("flavors.id"), nullable=True) attached_ports = orm.relationship( RouterPort, backref='router', diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index a10d1a58cf3..4d397c1c085 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -a963b38d82f4 +3d0e74aa7d37 diff --git a/neutron/db/migration/alembic_migrations/versions/newton/expand/3d0e74aa7d37_add_flavor_id_to_routers.py b/neutron/db/migration/alembic_migrations/versions/newton/expand/3d0e74aa7d37_add_flavor_id_to_routers.py new file mode 100644 index 00000000000..1359e2c2ca7 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/newton/expand/3d0e74aa7d37_add_flavor_id_to_routers.py @@ -0,0 +1,43 @@ +# Copyright 2016 Mirantis +# +# 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. +# + +"""Add flavor_id to Router + +Revision ID: 3d0e74aa7d37 +Revises: a963b38d82f4 +Create Date: 2016-05-05 00:22:47.618593 + +""" + +from alembic import op +import sqlalchemy as sa + +from neutron.db import migration + + +# revision identifiers, used by Alembic. +revision = '3d0e74aa7d37' +down_revision = 'a963b38d82f4' + +# milestone identifier, used by neutron-db-manage +neutron_milestone = [migration.NEWTON] + + +def upgrade(): + op.add_column('routers', + sa.Column('flavor_id', + sa.String(length=36), + sa.ForeignKey('flavors.id'), + nullable=True)) diff --git a/neutron/extensions/l3_flavors.py b/neutron/extensions/l3_flavors.py new file mode 100644 index 00000000000..b1f9cd6242c --- /dev/null +++ b/neutron/extensions/l3_flavors.py @@ -0,0 +1,55 @@ +# +# 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_lib import constants + +from neutron.api import extensions + +EXTENDED_ATTRIBUTES_2_0 = { + 'routers': { + 'flavor_id': {'allow_post': True, 'allow_put': False, + 'default': constants.ATTR_NOT_SPECIFIED, + 'is_visible': True, 'enforce_policy': True} + + } +} + + +class L3_flavors(extensions.ExtensionDescriptor): + """Extension class supporting flavors for routers.""" + + @classmethod + def get_name(cls): + return "Router Flavor Extension" + + @classmethod + def get_alias(cls): + return 'l3-flavors' + + @classmethod + def get_description(cls): + return "Flavor support for routers." + + @classmethod + def get_updated(cls): + return "2016-05-17T00:00:00-00:00" + + def get_extended_resources(self, version): + if version == "2.0": + return EXTENDED_ATTRIBUTES_2_0 + else: + return {} + + def get_required_extensions(self): + return ["router", "flavors"] diff --git a/neutron/services/l3_router/l3_router_plugin.py b/neutron/services/l3_router/l3_router_plugin.py index 27330626493..dd7c8223c9e 100644 --- a/neutron/services/l3_router/l3_router_plugin.py +++ b/neutron/services/l3_router/l3_router_plugin.py @@ -30,9 +30,11 @@ from neutron.db import l3_dvrscheduler_db from neutron.db import l3_gwmode_db from neutron.db import l3_hamode_db +from neutron.extensions import l3 from neutron.plugins.common import constants from neutron.quota import resource_registry from neutron import service +from neutron.services.l3_router.service_providers import driver_controller from neutron.services import service_base @@ -55,7 +57,8 @@ class L3RouterPlugin(service_base.ServicePluginBase, """ supported_extension_aliases = ["dvr", "router", "ext-gw-mode", "extraroute", "l3_agent_scheduler", - "l3-ha", "router_availability_zone"] + "l3-ha", "router_availability_zone", + "l3-flavors"] __native_pagination_support = True __native_sorting_support = True @@ -76,6 +79,7 @@ def __init__(self): rpc_worker = service.RpcWorker([self], worker_process_count=0) self.add_worker(rpc_worker) + self.l3_driver_controller = driver_controller.DriverController(self) @log_helpers.log_method_call def start_rpc_listeners(self): @@ -111,3 +115,11 @@ def create_floatingip(self, context, floatingip): return super(L3RouterPlugin, self).create_floatingip( context, floatingip, initial_status=n_const.FLOATINGIP_STATUS_DOWN) + + +def add_flavor_id(plugin, router_res, router_db): + router_res['flavor_id'] = router_db['flavor_id'] + + +common_db_mixin.CommonDbMixin.register_dict_extend_funcs( + l3.ROUTERS, [add_flavor_id]) diff --git a/neutron/services/l3_router/service_providers/__init__.py b/neutron/services/l3_router/service_providers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/services/l3_router/service_providers/base.py b/neutron/services/l3_router/service_providers/base.py new file mode 100644 index 00000000000..d264300648f --- /dev/null +++ b/neutron/services/l3_router/service_providers/base.py @@ -0,0 +1,55 @@ +# 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._i18n import _ + + +class _FeatureFlag(object): + + def is_compatible(self, value): + if value == self.requires: + return True + if value and self.supports: + return True + return False + + def __init__(self, supports, requires): + self.supports = supports + self.requires = requires + if requires and not supports: + raise RuntimeError(_("A driver can't require a feature and not " + "support it.")) + +UNSUPPORTED = _FeatureFlag(supports=False, requires=False) +OPTIONAL = _FeatureFlag(supports=True, requires=False) +MANDATORY = _FeatureFlag(supports=True, requires=True) + + +class L3ServiceProvider(object): + """Base class for L3 service providers. + + On __init__ this will be given a handle to the l3 plugin. It is then the + responsibility of the driver to subscribe to the events it is interested + in (e.g. router_create, router_update, router_delete, etc). + + The 'ha' and 'distributed' attributes below are used to determine if a + router request with the 'ha' or 'distributed' attribute can be supported + by this particular driver. These attributes must be present. + """ + + ha_support = UNSUPPORTED + distributed_support = UNSUPPORTED + + def __init__(self, l3plugin): + self.l3plugin = l3plugin diff --git a/neutron/services/l3_router/service_providers/driver_controller.py b/neutron/services/l3_router/service_providers/driver_controller.py new file mode 100644 index 00000000000..a4b6de55cd9 --- /dev/null +++ b/neutron/services/l3_router/service_providers/driver_controller.py @@ -0,0 +1,249 @@ +# 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_lib import constants as lib_const +from oslo_config import cfg +from oslo_log import log as logging + +from neutron._i18n import _ +from neutron.callbacks import events +from neutron.callbacks import registry +from neutron.callbacks import resources +from neutron.common import exceptions as n_exc +from neutron.db import servicetype_db as st_db +from neutron import manager +from neutron.plugins.common import constants +from neutron.services import provider_configuration +from neutron.services import service_base + +LOG = logging.getLogger(__name__) + + +class DriverController(object): + """Driver controller for the L3 service plugin. + + This component is responsible for dispatching router requests to L3 + service providers and for performing the bookkeeping about which + driver is associated with a given router. + + This is not intended to be accessed by the drivers or the l3 plugin. + All of the methods are marked as private to reflect this. + """ + + def __init__(self, l3_plugin): + self.l3_plugin = l3_plugin + self._stm = st_db.ServiceTypeManager.get_instance() + self._stm.add_provider_configuration( + constants.L3_ROUTER_NAT, _LegacyPlusProviderConfiguration()) + self._load_drivers() + registry.subscribe(self._set_router_provider, + resources.ROUTER, events.PRECOMMIT_CREATE) + registry.subscribe(self._update_router_provider, + resources.ROUTER, events.PRECOMMIT_UPDATE) + registry.subscribe(self._clear_router_provider, + resources.ROUTER, events.PRECOMMIT_DELETE) + + def _load_drivers(self): + self.drivers, self.default_provider = ( + service_base.load_drivers(constants.L3_ROUTER_NAT, self.l3_plugin)) + # store the provider name on each driver to make finding inverse easy + for provider_name, driver in self.drivers.items(): + setattr(driver, 'name', provider_name) + + @property + def _flavor_plugin(self): + if not hasattr(self, '_flavor_plugin_ref'): + self._flavor_plugin = manager.NeutronManager.get_service_plugins()[ + constants.FLAVORS] + return self._flavor_plugin_ref + + def _set_router_provider(self, resource, event, trigger, context, router, + router_db, **kwargs): + """Associates a router with a service provider. + + Association is done by flavor_id if it's specified, otherwise it will + fallback to determining which loaded driver supports the ha/distributed + attributes associated with the router. + """ + if _flavor_specified(router): + router_db.flavor_id = router['flavor_id'] + drv = self._get_provider_for_create(context, router) + _ensure_driver_supports_request(drv, router) + self._stm.add_resource_association(context, 'L3_ROUTER_NAT', + drv.name, router['id']) + + def _clear_router_provider(self, resource, event, trigger, context, + router_id, **kwargs): + """Remove the association between a router and a service provider.""" + self._stm.del_resource_associations(context, [router_id]) + + def _update_router_provider(self, resource, event, trigger, context, + router_id, router, old_router, router_db, + **kwargs): + """Handle transition between providers. + + The provider can currently be changed only by the caller updating + 'ha' and/or 'distributed' attributes. If we allow updates of flavor_id + directly in the future those requests will also land here. + """ + drv = self._get_provider_for_router(context, router_id) + new_drv = None + if _flavor_specified(router): + if router['flavor_id'] != old_router['flavor_id']: + # TODO(kevinbenton): this is currently disallowed by the API + # so we shouldn't hit it but this is a placeholder to add + # support later. + raise NotImplementedError() + + # the following is to support updating the 'ha' and 'distributed' + # attributes via the API. + try: + _ensure_driver_supports_request(drv, router) + except n_exc.Invalid: + # the current driver does not support this request, we need to + # migrate to a new provider. populate the distributed and ha + # flags from the previous state if not in the update so we can + # determine the target provider appropriately. + # NOTE(kevinbenton): if the router is associated with a flavor + # we bail because changing the provider without changing + # the flavor will make things inconsistent. We can probably + # update the flavor automatically in the future. + if old_router['flavor_id']: + raise n_exc.Invalid(_( + "Changing the 'ha' and 'distributed' attributes on a " + "router associated with a flavor is not supported.")) + if 'distributed' not in router: + router['distributed'] = old_router['distributed'] + if 'ha' not in router: + router['ha'] = old_router['distributed'] + new_drv = self._attrs_to_driver(router) + if new_drv: + LOG.debug("Router %(id)s migrating from %(old)s provider to " + "%(new)s provider.", {'id': router_id, 'old': drv, + 'new': new_drv}) + _ensure_driver_supports_request(new_drv, router) + # TODO(kevinbenton): notify old driver explicity of driver change + with context.session.begin(subtransactions=True): + self._stm.del_resource_associations(context, [router_id]) + self._stm.add_resource_association( + context, 'L3_ROUTER_NAT', new_drv.name, router_id) + + def _get_provider_for_router(self, context, router_id): + """Return the provider driver handle for a router id.""" + driver_name = self._stm.get_provider_names_by_resource_ids( + context, [router_id]).get(router_id) + if not driver_name: + # this is an old router that hasn't been mapped to a provider + # yet so we do this now + router = self.l3_plugin.get_router(context, router_id) + driver = self._attrs_to_driver(router) + driver_name = driver.name + self._stm.add_resource_association(context, 'L3_ROUTER_NAT', + driver_name, router_id) + return self.drivers[driver_name] + + def _get_provider_for_create(self, context, router): + """Get provider based on flavor or ha/distributed flags.""" + if not _flavor_specified(router): + return self._attrs_to_driver(router) + return self._get_l3_driver_by_flavor(context, router['flavor_id']) + + def _get_l3_driver_by_flavor(self, context, flavor_id): + """Get a provider driver handle for a given flavor_id.""" + flavor = self._flavor_plugin.get_flavor(context, flavor_id) + provider = self._flavor_plugin.get_flavor_next_provider( + context, flavor['id'])[0] + # TODO(kevinbenton): the callback framework suppresses the nice errors + # these generate when they fail to lookup. carry them through + driver = self.drivers[provider['provider']] + return driver + + def _attrs_to_driver(self, router): + """Get a provider driver handle based on the ha/distributed flags.""" + distributed = _is_distributed(router['distributed']) + ha = _is_ha(router['ha']) + drivers = self.drivers.values() + # make sure default is tried before the rest if defined + if self.default_provider: + drivers.insert(0, self.drivers[self.default_provider]) + for driver in drivers: + if _is_driver_compatible(distributed, ha, driver): + return driver + raise NotImplementedError( + _("Could not find a service provider that supports " + "distributed=%(d)s and ha=%(h)s") % {'d': distributed, 'h': ha} + ) + + +class _LegacyPlusProviderConfiguration( + provider_configuration.ProviderConfiguration): + + def __init__(self): + # loads up ha, dvr, and single_node service providers automatically. + # If an operator has setup explicit values that conflict with these, + # the operator defined values will take priority. + super(_LegacyPlusProviderConfiguration, self).__init__() + for name, driver in (('dvrha', 'dvrha.DvrHaDriver'), + ('dvr', 'dvr.DvrDriver'), ('ha', 'ha.HaDriver'), + ('single_node', 'single_node.SingleNodeDriver')): + path = 'neutron.services.l3_router.service_providers.%s' % driver + try: + self.add_provider({'service_type': constants.L3_ROUTER_NAT, + 'name': name, 'driver': path, + 'default': False}) + except n_exc.Invalid: + LOG.debug("Could not add L3 provider '%s', it may have " + "already been explicitly defined.", name) + + +def _is_driver_compatible(distributed, ha, driver): + if not driver.distributed_support.is_compatible(distributed): + return False + if not driver.ha_support.is_compatible(ha): + return False + return True + + +def _is_distributed(distributed_attr): + if distributed_attr is False: + return False + if distributed_attr == lib_const.ATTR_NOT_SPECIFIED: + return cfg.CONF.router_distributed + return True + + +def _is_ha(ha_attr): + if ha_attr is False: + return False + if ha_attr == lib_const.ATTR_NOT_SPECIFIED: + return cfg.CONF.l3_ha + return True + + +def _flavor_specified(router): + return ('flavor_id' in router and + router['flavor_id'] != lib_const.ATTR_NOT_SPECIFIED) + + +def _ensure_driver_supports_request(drv, router_body): + r = router_body + for key, attr in (('distributed', 'distributed_support'), + ('ha', 'ha_support')): + flag = r.get(key) + if flag not in [True, False]: + continue # not specified in body + if not getattr(drv, attr).is_compatible(flag): + raise n_exc.Invalid( + _("Provider %(name)s does not support %(key)s=%(flag)s") + % dict(name=drv.name, key=key, flag=flag)) diff --git a/neutron/services/l3_router/service_providers/dvr.py b/neutron/services/l3_router/service_providers/dvr.py new file mode 100644 index 00000000000..38027694f1f --- /dev/null +++ b/neutron/services/l3_router/service_providers/dvr.py @@ -0,0 +1,19 @@ +# 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.services.l3_router.service_providers import base + + +class DvrDriver(base.L3ServiceProvider): + distributed_support = base.MANDATORY diff --git a/neutron/services/l3_router/service_providers/dvrha.py b/neutron/services/l3_router/service_providers/dvrha.py new file mode 100644 index 00000000000..05789cf9bd8 --- /dev/null +++ b/neutron/services/l3_router/service_providers/dvrha.py @@ -0,0 +1,22 @@ +# 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.services.l3_router.service_providers import base +from neutron.services.l3_router.service_providers import dvr +from neutron.services.l3_router.service_providers import ha + + +class DvrHaDriver(dvr.DvrDriver, ha.HaDriver): + ha_support = base.MANDATORY + dvr_support = base.MANDATORY diff --git a/neutron/services/l3_router/service_providers/ha.py b/neutron/services/l3_router/service_providers/ha.py new file mode 100644 index 00000000000..ee7e94edfc5 --- /dev/null +++ b/neutron/services/l3_router/service_providers/ha.py @@ -0,0 +1,19 @@ +# 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.services.l3_router.service_providers import base + + +class HaDriver(base.L3ServiceProvider): + ha_support = base.MANDATORY diff --git a/neutron/services/l3_router/service_providers/single_node.py b/neutron/services/l3_router/service_providers/single_node.py new file mode 100644 index 00000000000..8c6c9c5e6ab --- /dev/null +++ b/neutron/services/l3_router/service_providers/single_node.py @@ -0,0 +1,19 @@ +# 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.services.l3_router.service_providers import base + + +class SingleNodeDriver(base.L3ServiceProvider): + """Provider for single L3 agent routers.""" diff --git a/neutron/tests/functional/pecan_wsgi/test_controllers.py b/neutron/tests/functional/pecan_wsgi/test_controllers.py index b45c863ddf4..b1656b8502a 100644 --- a/neutron/tests/functional/pecan_wsgi/test_controllers.py +++ b/neutron/tests/functional/pecan_wsgi/test_controllers.py @@ -601,7 +601,8 @@ class TestRouterController(TestResourceController): def setUp(self): cfg.CONF.set_override( 'service_plugins', - ['neutron.services.l3_router.l3_router_plugin.L3RouterPlugin']) + ['neutron.services.l3_router.l3_router_plugin.L3RouterPlugin', + 'neutron.services.flavors.flavors_plugin.FlavorsPlugin']) super(TestRouterController, self).setUp() plugin = manager.NeutronManager.get_plugin() ctx = context.get_admin_context() @@ -697,7 +698,8 @@ class TestL3AgentShimControllers(test_functional.PecanFunctionalTest): def setUp(self): cfg.CONF.set_override( 'service_plugins', - ['neutron.services.l3_router.l3_router_plugin.L3RouterPlugin']) + ['neutron.services.l3_router.l3_router_plugin.L3RouterPlugin', + 'neutron.services.flavors.flavors_plugin.FlavorsPlugin']) super(TestL3AgentShimControllers, self).setUp() policy.init() policy._ENFORCER.set_rules( diff --git a/neutron/tests/unit/db/test_agentschedulers_db.py b/neutron/tests/unit/db/test_agentschedulers_db.py index 374f2ffc97c..dd486ed0e6a 100644 --- a/neutron/tests/unit/db/test_agentschedulers_db.py +++ b/neutron/tests/unit/db/test_agentschedulers_db.py @@ -232,7 +232,11 @@ class OvsAgentSchedulerTestCaseBase(test_l3.L3NatTestCaseMixin, def setUp(self): self.useFixture(tools.AttributeMapMemento()) if self.l3_plugin: - service_plugins = {'l3_plugin_name': self.l3_plugin} + service_plugins = { + 'l3_plugin_name': self.l3_plugin, + 'flavors_plugin_name': 'neutron.services.flavors.' + 'flavors_plugin.FlavorsPlugin' + } else: service_plugins = None # NOTE(ivasilevskaya) mocking this way allows some control over mocked @@ -1452,7 +1456,11 @@ def setUp(self): self.useFixture(tools.AttributeMapMemento()) if self.l3_plugin: - service_plugins = {'l3_plugin_name': self.l3_plugin} + service_plugins = { + 'l3_plugin_name': self.l3_plugin, + 'flavors_plugin_name': 'neutron.services.flavors.' + 'flavors_plugin.FlavorsPlugin' + } else: service_plugins = None super(OvsL3AgentNotifierTestCase, self).setUp( diff --git a/neutron/tests/unit/plugins/ml2/base.py b/neutron/tests/unit/plugins/ml2/base.py index 6c193a4a095..6f10c658c89 100644 --- a/neutron/tests/unit/plugins/ml2/base.py +++ b/neutron/tests/unit/plugins/ml2/base.py @@ -22,6 +22,12 @@ class ML2TestFramework(test_plugin.Ml2PluginV2TestCase): 'L3RouterPlugin') _mechanism_drivers = ['openvswitch'] + def get_additional_service_plugins(self): + p = super(ML2TestFramework, self).get_additional_service_plugins() + p.update({'flavors_plugin_name': 'neutron.services.flavors.' + 'flavors_plugin.FlavorsPlugin'}) + return p + def setUp(self): super(ML2TestFramework, self).setUp() self.core_plugin = manager.NeutronManager.get_instance().get_plugin() diff --git a/neutron/tests/unit/plugins/ml2/test_plugin.py b/neutron/tests/unit/plugins/ml2/test_plugin.py index fccc33eea43..b21b4c7d978 100644 --- a/neutron/tests/unit/plugins/ml2/test_plugin.py +++ b/neutron/tests/unit/plugins/ml2/test_plugin.py @@ -109,9 +109,14 @@ class Ml2PluginV2TestCase(test_plugin.NeutronDbPluginV2TestCase): l3_plugin = ('neutron.tests.unit.extensions.test_l3.' 'TestL3NatServicePlugin') + def get_additional_service_plugins(self): + """Subclasses can return a dictionary of service plugins to load.""" + return {} + def setup_parent(self): """Perform parent setup with the common plugin configuration class.""" service_plugins = {'l3_plugin_name': self.l3_plugin} + service_plugins.update(self.get_additional_service_plugins()) # Ensure that the parent setup can be called without arguments # by the common configuration setUp. parent_setup = functools.partial( diff --git a/neutron/tests/unit/services/l3_router/__init__.py b/neutron/tests/unit/services/l3_router/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/unit/services/l3_router/service_providers/__init__.py b/neutron/tests/unit/services/l3_router/service_providers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/unit/services/l3_router/service_providers/test_driver_controller.py b/neutron/tests/unit/services/l3_router/service_providers/test_driver_controller.py new file mode 100644 index 00000000000..c12ca2256a0 --- /dev/null +++ b/neutron/tests/unit/services/l3_router/service_providers/test_driver_controller.py @@ -0,0 +1,91 @@ +# 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. + +import mock +from neutron_lib import constants +import testtools + +from neutron import context +from neutron.services.l3_router.service_providers import driver_controller +from neutron.tests.unit import testlib_api + + +class TestDriverController(testlib_api.SqlTestCase): + + def setUp(self): + super(TestDriverController, self).setUp() + self.fake_l3 = mock.Mock() + self.dc = driver_controller.DriverController(self.fake_l3) + self.ctx = context.get_admin_context() + + def _return_provider_for_flavor(self, provider): + self.dc._flavor_plugin_ref = mock.Mock() + self.dc._flavor_plugin_ref.get_flavor.return_value = {'id': 'abc'} + provider = {'provider': provider} + self.dc._flavor_plugin_ref.get_flavor_next_provider.return_value = [ + provider] + + def test__set_router_provider_flavor_specified(self): + self._return_provider_for_flavor('dvrha') + router_db = mock.Mock() + router = dict(id='router_id', flavor_id='abc123') + self.dc._set_router_provider('router', 'PRECOMMIT_CREATE', self, + self.ctx, router, router_db) + self.assertEqual('abc123', router_db.flavor_id) + self.assertEqual(self.dc.drivers['dvrha'], + self.dc._get_provider_for_router(self.ctx, + 'router_id')) + + def test__set_router_provider_attr_lookups(self): + # ensure correct drivers are looked up based on attrs + cases = [ + ('dvrha', dict(id='router_id1', distributed=True, ha=True)), + ('dvr', dict(id='router_id2', distributed=True, ha=False)), + ('ha', dict(id='router_id3', distributed=False, ha=True)), + ('single_node', dict(id='router_id4', distributed=False, + ha=False)), + ('ha', dict(id='router_id5', ha=True, + distributed=constants.ATTR_NOT_SPECIFIED)), + ('dvr', dict(id='router_id6', distributed=True, + ha=constants.ATTR_NOT_SPECIFIED)), + ('single_node', dict(id='router_id7', ha=False, + distributed=constants.ATTR_NOT_SPECIFIED)), + ('single_node', dict(id='router_id8', distributed=False, + ha=constants.ATTR_NOT_SPECIFIED)), + ('single_node', dict(id='router_id9', + distributed=constants.ATTR_NOT_SPECIFIED, + ha=constants.ATTR_NOT_SPECIFIED)), + ] + for driver, body in cases: + self.dc._set_router_provider('router', 'PRECOMMIT_CREATE', self, + self.ctx, body, mock.Mock()) + self.assertEqual(self.dc.drivers[driver], + self.dc._get_provider_for_router(self.ctx, + body['id']), + 'Expecting %s for body %s' % (driver, body)) + + def test__clear_router_provider(self): + # ensure correct drivers are looked up based on attrs + body = dict(id='router_id1', distributed=True, ha=True) + self.dc._set_router_provider('router', 'PRECOMMIT_CREATE', self, + self.ctx, body, mock.Mock()) + self.assertEqual(self.dc.drivers['dvrha'], + self.dc._get_provider_for_router(self.ctx, + body['id'])) + self.dc._clear_router_provider('router', 'PRECOMMIT_DELETE', self, + self.ctx, body['id']) + with testtools.ExpectedException(ValueError): + # if association was cleared, get_router will be called + self.fake_l3.get_router.side_effect = ValueError + self.dc._get_provider_for_router(self.ctx, body['id'])