Merge "Add flavor/service provider support to routers"

This commit is contained in:
Jenkins 2016-07-28 23:48:35 +00:00 committed by Gerrit Code Review
commit f3cde0c31b
19 changed files with 613 additions and 6 deletions

View File

@ -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',

View File

@ -1 +1 @@
a963b38d82f4
3d0e74aa7d37

View File

@ -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))

View File

@ -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"]

View File

@ -30,9 +30,11 @@ from neutron.db import l3_dvr_ha_scheduler_db
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 @@ class L3RouterPlugin(service_base.ServicePluginBase,
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 @@ class L3RouterPlugin(service_base.ServicePluginBase,
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])

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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."""

View File

@ -603,7 +603,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()
@ -699,7 +700,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(

View File

@ -231,7 +231,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
@ -1448,7 +1452,11 @@ class OvsL3AgentNotifierTestCase(test_l3.L3NatTestCaseMixin,
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(

View File

@ -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()

View File

@ -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(

View File

@ -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'])