[apic_aim] Multi-scope routing

Allow subnets with different address scopes, as well as unscoped
subnets, to be attached as interfaces to the same router. Note that no
East/West routing is provided between differently scoped interfaces of
a router, but East/West routing is provided within each scope and
North/South routing is provided between each scope and the router's
gateway.

Routed IPv4 and IPv6 subnets on the same network currently either must
both be unscoped or each must be associated with isomorphic address
scopes (referencing the same VRF). Adding a subnet to a router results
in a NonIsomorphicNetworkRoutingUnsupported exception if this
constraint would be violated. Eventually, use of identity NAT to move
IPv6 traffic from the network's IPv4 VRF to its IPv6 VRF will allow
this constraint to be removed or relaxed.

A flag in interface_info is added for GBP to override network routing
topology validation when adding router interfaces. This should not be
used for any other purpose, and will eventually be removed without
warning.

External connectivity for routers associated with multiple VRFs will
require some follow-on work to correctly handle all cases.

Change-Id: Idbbd4400e570654937c2bee4577422a91224430e
This commit is contained in:
Robert Kukura 2017-04-28 16:46:59 -04:00
parent 8b510e32f2
commit 824d897f37
6 changed files with 631 additions and 197 deletions

View File

@ -26,7 +26,8 @@ EXTERNAL_CONSUMED_CONTRACTS = 'apic:external_consumed_contracts'
CONTRACT = 'Contract'
CONTRACT_SUBJECT = 'ContractSubject'
VRF = 'VRF'
UNSCOPED_VRF = 'no_scope-VRF'
SCOPED_VRF = 'as_%s-VRF'
EXT_GW_ATTRIBUTES = {
EXTERNAL_PROVIDED_CONTRACTS: {
@ -50,6 +51,17 @@ EXTENDED_ATTRIBUTES_2_0 = {
EXT_GW_ATTRIBUTES.items())
}
# Pass this key with the value True in the interface_info parameter to
# add_router_interface to override validation that normally disallows
# topologies where the same network has multiple subnets connected to
# multiple routers, which would result in unintended routing. This is
# intended only for use in the aim_mapping GBP policy driver to get
# from one valid topology to another within a transaction, and is not
# supported for any other use. It may be removed in a future release
# without any deprecation notice.
OVERRIDE_NETWORK_ROUTING_TOPOLOGY_VALIDATION = (
'apic_aim_override_network_routing_topology_validation')
class Cisco_apic_l3(extensions.ExtensionDescriptor):

View File

@ -31,13 +31,9 @@ class UnscopedSharedNetworkProjectConflict(exceptions.BadRequest):
"in the same topology.")
class IPv6RoutingNotSupported(exceptions.BadRequest):
message = _("IPv6 routing is not currently supported.")
class MultiScopeRoutingNotSupported(exceptions.BadRequest):
message = _("Attaching interfaces from multiple address_scopes to the "
"same router is not currently supported.")
class NonIsomorphicNetworkRoutingUnsupported(exceptions.BadRequest):
message = _("All router interfaces for a network must utilize the same "
"VRF.")
class ScopeUpdateNotSupported(exceptions.BadRequest):

View File

@ -14,7 +14,7 @@
# under the License.
from neutron.api import extensions
from neutron.db import address_scope_db
from neutron.db.models import address_scope as as_db
from neutron import manager as n_manager
from neutron_lib import exceptions as n_exc
from oslo_log import log
@ -170,7 +170,7 @@ class ApicExtensionDriver(api_plus.ExtensionDriver,
scope_id = self._md.name_mapper.reverse_address_scope(
session, vrf.name, enforce=False)
if scope_id:
scope = (session.query(address_scope_db.AddressScope)
scope = (session.query(as_db.AddressScope)
.filter_by(id=scope_id)
.first())
if scope and scope.ip_version == data['ip_version']:

View File

@ -27,9 +27,9 @@ from aim import utils as aim_utils
from neutron.agent import securitygroups_rpc
from neutron.common import rpc as n_rpc
from neutron.common import topics as n_topics
from neutron.db import address_scope_db
from neutron.db import api as db_api
from neutron.db import l3_db
from neutron.db.models import address_scope as address_scope_db
from neutron.db.models import allowed_address_pair as n_addr_pair_db
from neutron.db import models_v2
from neutron.db import rbac_db_models
@ -744,49 +744,57 @@ class ApicMechanismDriver(api_plus.MechanismDriver):
dist_names[a_l3.CONTRACT_SUBJECT] = subject.dn
sync_state = self._merge_status(aim_ctx, sync_state, subject)
# REVISIT(rkukura): Consider moving the SubnetPool query below
# into this loop, although that might be less efficient when
# many subnets are from the same pool.
active = False
for intf in (session.query(models_v2.IPAllocation).
# REVISIT: Do we really need to include Subnet DNs in
# apic:distinguished_names and apic:synchronization_state?
# Eliminating these would reduce or potentially eliminate (if
# we persist the router->VRF mapping) the querying needed
# here.
unscoped_vrf = None
scope_ids = set()
for intf in (session.query(models_v2.IPAllocation.ip_address,
models_v2.Subnet,
models_v2.Network).
join(models_v2.Subnet, models_v2.Subnet.id ==
models_v2.IPAllocation.subnet_id).
join(models_v2.Network).
join(models_v2.Port).
join(l3_db.RouterPort).
filter(l3_db.RouterPort.router_id == router_db.id,
l3_db.RouterPort.port_type ==
n_constants.DEVICE_OWNER_ROUTER_INTF)):
ip_address, subnet_db, network_db = intf
active = True
# TODO(rkukura): Avoid separate queries for these.
subnet_db = (session.query(models_v2.Subnet).
filter_by(id=intf.subnet_id).
one())
network_db = (session.query(models_v2.Network).
filter_by(id=subnet_db.network_id).
one())
# TODO(rkukura): This can be very expensive, and there can
# be multiple subnets per network, so cache the BDs for
# networks that have been processed. Also, without
# multi-scope routing, there should be exactly one VRF per
# router. With multi-scope routing, we should be able to
# determine the set of VRFs and the subnets accociated
# with each.
vrf = self._get_routed_vrf_for_network(session, network_db)
bd = self._map_network(session, network_db, vrf, True)
sn = self._map_subnet(subnet_db, intf.ip_address, bd)
dist_names[intf.ip_address] = sn.dn
sn = self._map_subnet(subnet_db, ip_address, bd)
dist_names[ip_address] = sn.dn
sync_state = self._merge_status(aim_ctx, sync_state, sn)
if active:
# TODO(rkukura): With multi-scope routing, there will be
# multiple VRF DNs, so include the scope_id or 'no-scope'
# in the key.
dist_names[a_l3.VRF] = vrf.dn
scope_id = (subnet_db.subnetpool and
subnet_db.subnetpool.address_scope_id)
if scope_id:
scope_ids.add(scope_id)
else:
if unscoped_vrf and unscoped_vrf.identity != vrf.identity:
# This should never happen. If it does, it
# indicates an inconsistency in the DB state
# rather than any sort of user error. We log an
# error to aid debugging in case such an
# inconsistency somehow does occur.
LOG.error("Inconsistent unscoped VRFs %s and %s for "
"router %s.", vrf, unscoped_vrf, router_db)
unscoped_vrf = vrf
for scope_id in scope_ids:
scope_db = self._scope_by_id(session, scope_id)
vrf = self._map_address_scope(session, scope_db)
dist_names[a_l3.SCOPED_VRF % scope_id] = vrf.dn
sync_state = self._merge_status(aim_ctx, sync_state, vrf)
if unscoped_vrf:
dist_names[a_l3.UNSCOPED_VRF] = unscoped_vrf.dn
sync_state = self._merge_status(aim_ctx, sync_state, unscoped_vrf)
result[cisco_apic.DIST_NAMES] = dist_names
result[cisco_apic.SYNC_STATE] = sync_state
@ -803,28 +811,26 @@ class ApicMechanismDriver(api_plus.MechanismDriver):
# Find the address_scope(s) for the new interface.
#
# REVISIT: If dual-stack interfaces allowed, process each
# stack's scope separately, or at least raise an exception.
scope_id = self._get_address_scope_id_for_subnets(context, subnets)
# Find number of existing interface ports on the router,
# excluding the one we are adding.
#
# REVISIT: Just for this scope?
router_intf_count = self._get_router_intf_count(session, router)
# TODO(rkukura): Support multi-scope routing. For now, we
# reject adding an interface that does not match the scope of
# any existing interfaces.
if (router_intf_count and
(scope_id !=
self._get_address_scope_id_for_router(session, router))):
raise exceptions.MultiScopeRoutingNotSupported()
# Find up to two existing router interfaces for this
# network. The interface currently being added is not
# included, because the RouterPort has not yet been added to
# the DB session.
net_intfs = (session.query(l3_db.RouterPort.router_id,
models_v2.IPAllocation.subnet_id).
models_v2.Subnet).
join(models_v2.Port).
join(models_v2.IPAllocation).
join(models_v2.Subnet, models_v2.Subnet.id ==
models_v2.IPAllocation.subnet_id).
filter(models_v2.Port.network_id == network_id,
l3_db.RouterPort.port_type ==
n_constants.DEVICE_OWNER_ROUTER_INTF).
@ -838,19 +844,44 @@ class ApicMechanismDriver(api_plus.MechanismDriver):
# Neutron, would result in unintended routing. We
# therefore require that all router interfaces for a
# network share either the same router or the same subnet.
#
# REVISIT: Remove override flag when no longer needed for
# GBP.
if not context.override_network_routing_topology_validation:
different_router = False
different_subnet = False
router_id = router['id']
subnet_ids = [subnet['id'] for subnet in subnets]
for existing_router_id, existing_subnet in net_intfs:
if router_id != existing_router_id:
different_router = True
for subnet_id in subnet_ids:
if subnet_id != existing_subnet.id:
different_subnet = True
if different_router and different_subnet:
raise exceptions.UnsupportedRoutingTopology()
different_router = False
different_subnet = False
router_id = router['id']
subnet_ids = [subnet['id'] for subnet in subnets]
for existing_router_id, existing_subnet_id in net_intfs:
if router_id != existing_router_id:
different_router = True
for subnet_id in subnet_ids:
if subnet_id != existing_subnet_id:
different_subnet = True
if different_router and different_subnet:
raise exceptions.UnsupportedRoutingTopology()
# REVISIT: Remove this check for isomorphism once identity
# NAT can be used to move IPv6 traffic from an IPv4 VRF to
# the intended IPv6 VRF.
_, subnet = net_intfs[0]
existing_scope_id = (NO_ADDR_SCOPE if not subnet.subnetpool or
not subnet.subnetpool.address_scope_id else
subnet.subnetpool.address_scope_id)
if scope_id != existing_scope_id:
if (scope_id != NO_ADDR_SCOPE and
existing_scope_id != NO_ADDR_SCOPE):
scope_db = self._scope_by_id(session, scope_id)
vrf = self._map_address_scope(session, scope_db)
existing_scope_db = self._scope_by_id(
session, existing_scope_id)
existing_vrf = self._map_address_scope(
session, existing_scope_db)
if vrf.identity != existing_vrf.identity:
raise (exceptions.
NonIsomorphicNetworkRoutingUnsupported())
else:
raise exceptions.NonIsomorphicNetworkRoutingUnsupported()
nets_to_notify = set()
ports_to_notify = set()
@ -862,21 +893,9 @@ class ApicMechanismDriver(api_plus.MechanismDriver):
# and consume the router's Contract. This is handled
# differently for scoped and unscoped topologies.
if scope_id != NO_ADDR_SCOPE:
# TODO(rkukura): Scoped topologies are simple, until we
# support IPv6 routing. Then, we will need to figure out
# if a v6-only network should use its own v6 scope, or a
# v4 scope that's already part of the router
# topology. Adding a v4 interface to a previously v6-only
# network may require moving the existing network (and its
# subnets) from the v6 scope to the v4 scope. Multi-scope
# routers will further complicate this with multiple v4
# scopes to choose between for v6-only networks.
scope = self._scope_by_id(session, scope_id)
vrf = self._map_address_scope(session, scope)
else:
# TODO(rkukura): The unscoped case will need to handle
# IPv6 and, and will have to coexist with address_scopes
# for multi-scope routing.
intf_topology = self._network_topology(session, network_db)
router_topology = self._router_topology(session, router['id'])
@ -937,6 +956,11 @@ class ApicMechanismDriver(api_plus.MechanismDriver):
aim_ctx, network_db, vrf, nets_to_notify)
else:
# Network is already routed.
#
# REVISIT: For non-isomorphic dual-stack network, may need
# to move the BD and EPG from already-routed v6 VRF to
# newly-routed v4 VRF, and setup identity NAT for the v6
# traffic.
bd, epg = self._map_network(session, network_db, vrf)
# Create AIM Subnet(s) for each added Neutron subnet.
@ -1016,7 +1040,8 @@ class ApicMechanismDriver(api_plus.MechanismDriver):
# Find the address_scope(s) for the old interface.
#
# TODO(rkukura): Handle IPv6 and dual-stack routing.
# REVISIT: If dual-stack interfaces allowed, process each
# stack's scope separately, or at least raise an exception.
scope_id = self._get_address_scope_id_for_subnets(context, subnets)
old_vrf = self._get_routed_vrf_for_network(session, network_db)
@ -1063,6 +1088,11 @@ class ApicMechanismDriver(api_plus.MechanismDriver):
router_topo_moved = False
# If unscoped topologies have split, move VRFs as needed.
#
# REVISIT: For non-isomorphic dual-stack network, may need to
# move the BD and EPG from the previously-routed v4 VRF to the
# still-routed v6 VRF, and disable identity NAT for the v6
# traffic.
if scope_id == NO_ADDR_SCOPE:
# If the interface's network has not become unrouted, see
# if its topology must be moved.
@ -1703,8 +1733,10 @@ class ApicMechanismDriver(api_plus.MechanismDriver):
visited_router_ids |= added_ids
LOG.debug("Querying for networks interfaced to routers %s",
added_ids)
query = (session.query(models_v2.Network).
query = (session.query(models_v2.Network, models_v2.Subnet).
join(models_v2.Port).
join(models_v2.IPAllocation).
join(models_v2.Subnet).
join(l3_db.RouterPort).
filter(l3_db.RouterPort.router_id.in_(added_ids)))
if visited_networks:
@ -1715,7 +1747,9 @@ class ApicMechanismDriver(api_plus.MechanismDriver):
distinct().
all())
self._expand_topology_for_networks(
session, visited_networks, visited_router_ids, results)
session, visited_networks, visited_router_ids,
[network for network, subnet in results if not
(subnet.subnetpool and subnet.subnetpool.address_scope_id)])
def _expand_topology_for_networks(self, session, visited_networks,
visited_router_ids, new_networks):
@ -1771,7 +1805,7 @@ class ApicMechanismDriver(api_plus.MechanismDriver):
def _scope_by_id(self, session, scope_id):
return (session.query(address_scope_db.AddressScope).
filter_by(id=scope_id).
first())
one_or_none())
def _map_network(self, session, network, vrf, bd_only=False):
tenant_aname = (vrf.tenant_name

View File

@ -29,6 +29,7 @@ from sqlalchemy import inspect
from gbpservice._i18n import _LE
from gbpservice._i18n import _LI
from gbpservice.neutron import extensions as extensions_pkg
from gbpservice.neutron.extensions import cisco_apic_l3 as l3_ext
from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import (
extension_db as extn_db)
@ -149,8 +150,15 @@ class ApicL3Plugin(common_db_mixin.CommonDbMixin,
# generally not concerned with mechanism driver postcommit
# processing. Consider reimplementing the base
# funtionality to be completely transaction safe.
#
# REVISIT: Remove override flag when no longer needed for
# GBP.
context.override_network_routing_topology_validation = (
interface_info.get(
l3_ext.OVERRIDE_NETWORK_ROUTING_TOPOLOGY_VALIDATION))
info = super(ApicL3Plugin, self).add_router_interface(
context, router_id, interface_info)
del context.override_network_routing_topology_validation
return info
def _add_interface_by_subnet(self, context, router, subnet_id, owner):

View File

@ -49,6 +49,7 @@ from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import apic_mapper
from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import config # noqa
from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import exceptions
from gbpservice.neutron.extensions import cisco_apic_l3 as l3_ext
from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import (
extension_db as extn_db)
from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import (
@ -296,6 +297,10 @@ class ApicAimTestCase(test_address_scope.AddressScopeTestCase,
class TestAimMapping(ApicAimTestCase):
def setUp(self):
self._actual_scopes = {}
super(TestAimMapping, self).setUp()
def _get_tenant(self, tenant_name):
session = db_api.get_session()
aim_ctx = aim_context.AimContext(session)
@ -444,6 +449,7 @@ class TestAimMapping(ApicAimTestCase):
else:
vrf_tenant_dname = 'CommonTenant'
elif scope:
scope = self._actual_scopes.get(scope['id'], scope)
vrf_aname = self.name_mapper.address_scope(None, scope['id'])
vrf_dname = scope['name']
vrf_project = scope['tenant_id']
@ -509,6 +515,7 @@ class TestAimMapping(ApicAimTestCase):
scope=None, project=None):
prefix_len = subnet['cidr'].split('/')[1]
scope = scope and self._actual_scopes.get(scope['id'], scope)
project = project or (scope or net)['tenant_id']
tenant_aname = self.name_mapper.project(None, project)
self._get_tenant(tenant_aname)
@ -541,15 +548,18 @@ class TestAimMapping(ApicAimTestCase):
pass
def _check_address_scope(self, scope):
tenant_aname = self.name_mapper.project(None, scope['tenant_id'])
actual_scope = self._actual_scopes.get(scope['id'], scope)
tenant_aname = self.name_mapper.project(
None, actual_scope['tenant_id'])
self._get_tenant(tenant_aname)
aname = self.name_mapper.address_scope(None, scope['id'])
aname = self.name_mapper.address_scope(None, actual_scope['id'])
aim_vrf = self._get_vrf(aname, tenant_aname)
self.assertEqual(tenant_aname, aim_vrf.tenant_name)
self.assertEqual(aname, aim_vrf.name)
self.assertEqual(scope['name'], aim_vrf.display_name)
self.assertEqual(actual_scope['name'], aim_vrf.display_name)
self.assertEqual('enforced', aim_vrf.policy_enforcement_pref)
self._check_dn(scope, aim_vrf, 'VRF')
@ -558,7 +568,7 @@ class TestAimMapping(ApicAimTestCase):
self._vrf_should_not_exist(aname)
def _check_router(self, router, expected_gw_ips, unexpected_gw_ips,
scope=None, unscoped_project=None):
scopes=None, unscoped_project=None):
aname = self.name_mapper.router(None, router['id'])
aim_contract = self._get_contract(aname, 'common')
@ -581,36 +591,27 @@ class TestAimMapping(ApicAimTestCase):
self._check_any_filter()
dist_names = router.get('apic:distinguished_names')
vrf_dns = {k: v for (k, v) in six.iteritems(dist_names)
if k.endswith('-VRF')}
if expected_gw_ips:
if scope:
vrf_aname = self.name_mapper.address_scope(
None, scope['id'])
vrf_dname = scope['name']
vrf_project = scope['tenant_id']
else:
vrf_aname = 'DefaultVRF'
vrf_dname = 'DefaultRoutedVRF'
vrf_project = unscoped_project or router['tenant_id']
vrf_tenant_aname = self.name_mapper.project(None, vrf_project)
vrf_tenant_dname = TEST_TENANT_NAMES[vrf_project]
if unscoped_project:
self._check_router_vrf(
'DefaultVRF', 'DefaultRoutedVRF', unscoped_project,
vrf_dns, 'no_scope-VRF')
aim_tenant = self._get_tenant(vrf_tenant_aname)
self.assertEqual(vrf_tenant_aname, aim_tenant.name)
self.assertEqual(vrf_tenant_dname, aim_tenant.display_name)
for scope in scopes or []:
actual_scope = self._actual_scopes.get(scope['id'], scope)
self._check_router_vrf(
self.name_mapper.address_scope(None, actual_scope['id']),
actual_scope['name'], actual_scope['tenant_id'],
vrf_dns, 'as_%s-VRF' % scope['id'])
aim_vrf = self._get_vrf(vrf_aname,
vrf_tenant_aname)
self.assertEqual(vrf_tenant_aname, aim_vrf.tenant_name)
self.assertEqual(vrf_aname, aim_vrf.name)
self.assertEqual(vrf_dname, aim_vrf.display_name)
self.assertEqual('enforced', aim_vrf.policy_enforcement_pref)
self._check_dn(router, aim_vrf, 'VRF')
else:
self._check_no_dn(router, 'VRF')
self.assertFalse(vrf_dns)
# The AIM Subnets are validated in _check_subnet, so just
# check that their DNs are present and valid.
dist_names = router.get('apic:distinguished_names')
for gw_ip in expected_gw_ips:
self.assertIn(gw_ip, dist_names)
aim_subnet = self._find_by_dn(dist_names[gw_ip],
@ -619,6 +620,23 @@ class TestAimMapping(ApicAimTestCase):
for gw_ip in unexpected_gw_ips:
self.assertNotIn(gw_ip, dist_names)
def _check_router_vrf(self, aname, dname, project_id, vrf_dns, key):
tenant_aname = self.name_mapper.project(None, project_id)
tenant_dname = TEST_TENANT_NAMES[project_id]
aim_tenant = self._get_tenant(tenant_aname)
self.assertEqual(tenant_aname, aim_tenant.name)
self.assertEqual(tenant_dname, aim_tenant.display_name)
aim_vrf = self._get_vrf(aname, tenant_aname)
self.assertEqual(tenant_aname, aim_vrf.tenant_name)
self.assertEqual(aname, aim_vrf.name)
self.assertEqual(dname, aim_vrf.display_name)
self.assertEqual('enforced', aim_vrf.policy_enforcement_pref)
dn = vrf_dns.pop(key, None)
self.assertEqual(aim_vrf.dn, dn)
def _check_router_deleted(self, router):
aname = self.name_mapper.router(None, router['id'])
self._subject_should_not_exist('route', aname)
@ -792,7 +810,8 @@ class TestAimMapping(ApicAimTestCase):
# Check router.
router = self._show('routers', router_id)['router']
self._check_router(router, [gw1_ip], [])
self._check_router(router, [gw1_ip], [],
unscoped_project=self._tenant_id)
# Check network.
net = self._show('networks', net_id)['network']
@ -814,7 +833,8 @@ class TestAimMapping(ApicAimTestCase):
# Test router update.
data = {'router': {'name': 'newnameforrouter'}}
router = self._update('routers', router_id, data)['router']
self._check_router(router, [gw1_ip], [])
self._check_router(router, [gw1_ip], [],
unscoped_project=self._tenant_id)
self._check_subnet(subnet, net, [(gw1_ip, router)], [])
# Add subnet2 to router by port.
@ -831,7 +851,8 @@ class TestAimMapping(ApicAimTestCase):
# Check router.
router = self._show('routers', router_id)['router']
self._check_router(router, [gw1_ip, gw2_ip], [])
self._check_router(router, [gw1_ip, gw2_ip], [],
unscoped_project=self._tenant_id)
# Check network.
net = self._show('networks', net_id)['network']
@ -857,7 +878,8 @@ class TestAimMapping(ApicAimTestCase):
# Check router.
router = self._show('routers', router_id)['router']
self._check_router(router, [gw2_ip], [gw1_ip])
self._check_router(router, [gw2_ip], [gw1_ip],
unscoped_project=self._tenant_id)
# Check network.
net = self._show('networks', net_id)['network']
@ -923,7 +945,7 @@ class TestAimMapping(ApicAimTestCase):
router = self._make_router(
self.fmt, 'test-tenant', 'router1')['router']
router_id = router['id']
self._check_router(router, [], [], scope)
self._check_router(router, [], [], scopes=[scope])
# Create network.
net_resp = self._make_network(self.fmt, 'net1', True)
@ -975,7 +997,7 @@ class TestAimMapping(ApicAimTestCase):
# Check router.
router = self._show('routers', router_id)['router']
self._check_router(router, [gw1_ip], [], scope)
self._check_router(router, [gw1_ip], [], scopes=[scope])
# Check network.
net = self._show('networks', net_id)['network']
@ -997,7 +1019,7 @@ class TestAimMapping(ApicAimTestCase):
# Test router update.
data = {'router': {'name': 'newnameforrouter'}}
router = self._update('routers', router_id, data)['router']
self._check_router(router, [gw1_ip], [], scope)
self._check_router(router, [gw1_ip], [], scopes=[scope])
self._check_subnet(subnet, net, [(gw1_ip, router)], [], scope)
# Add subnet2 to router by port.
@ -1014,7 +1036,7 @@ class TestAimMapping(ApicAimTestCase):
# Check router.
router = self._show('routers', router_id)['router']
self._check_router(router, [gw1_ip, gw2_ip], [], scope)
self._check_router(router, [gw1_ip, gw2_ip], [], scopes=[scope])
# Check network.
net = self._show('networks', net_id)['network']
@ -1040,7 +1062,7 @@ class TestAimMapping(ApicAimTestCase):
# Check router.
router = self._show('routers', router_id)['router']
self._check_router(router, [gw2_ip], [gw1_ip], scope)
self._check_router(router, [gw2_ip], [gw1_ip], scopes=[scope])
# Check network.
net = self._show('networks', net_id)['network']
@ -1065,7 +1087,7 @@ class TestAimMapping(ApicAimTestCase):
# Check router.
router = self._show('routers', router_id)['router']
self._check_router(router, [], [gw1_ip, gw2_ip], scope)
self._check_router(router, [], [gw1_ip, gw2_ip], scopes=[scope])
# Check network.
net = self._show('networks', net_id)['network']
@ -1109,7 +1131,295 @@ class TestAimMapping(ApicAimTestCase):
mock.call(mock.ANY, tenant)]
self._check_call_list(exp_calls, self.driver.aim.delete.call_args_list)
# TODO(rkukura): Test IPv6 and dual stack router interfaces.
def test_multi_scope_routing(self):
# REVISIT: Re-enable testing with non-isomorphic scopes once
# they are supported. Also, test with shared scopes?
# Create a v6 scope and pool.
scope6 = self._make_address_scope(
self.fmt, 6, name='as6')['address_scope']
scope6_id = scope6['id']
self._check_address_scope(scope6)
scope6_vrf = scope6['apic:distinguished_names']['VRF']
pool6 = self._make_subnetpool(
self.fmt, ['2001:db8:1::0/56'], name='sp6',
tenant_id=self._tenant_id,
address_scope_id=scope6_id)['subnetpool']
pool6_id = pool6['id']
# Create isomorphic v4 scope and pool.
scope4i = self._make_address_scope_for_vrf(
scope6_vrf, 4, name='as4i')['address_scope']
scope4i_id = scope4i['id']
self._actual_scopes[scope4i_id] = scope6
self._check_address_scope(scope4i)
pool4i = self._make_subnetpool(
self.fmt, ['10.1.0.0/16'], name='sp4i', tenant_id=self._tenant_id,
address_scope_id=scope4i_id, default_prefixlen=24)['subnetpool']
pool4i_id = pool4i['id']
# Create non-isomorphic v4 scope and pool.
scope4n = self._make_address_scope(
self.fmt, 4, name='as4n')['address_scope']
scope4n_id = scope4n['id']
self._check_address_scope(scope4n)
pool4n = self._make_subnetpool(
self.fmt, ['10.2.0.0/16'], name='sp4n', tenant_id=self._tenant_id,
address_scope_id=scope4n_id, default_prefixlen=24)['subnetpool']
pool4n_id = pool4n['id']
# Create network with subnets using first v4 scope and v6 scope.
net_resp = self._make_network(self.fmt, 'net1', True)
net1 = net_resp['network']
self._check_network(net1)
gw4i1_ip = '10.1.1.1'
subnet4i1 = self._make_subnet(
self.fmt, net_resp, gw4i1_ip, '10.1.1.0/24',
subnetpool_id=pool4i_id)['subnet']
self._check_subnet(subnet4i1, net1, [], [gw4i1_ip])
gw61_ip = '2001:db8:1:1::1'
subnet61 = self._make_subnet(
self.fmt, net_resp, gw61_ip, '2001:db8:1:1::0/64',
ip_version=6, subnetpool_id=pool6_id)['subnet']
self._check_subnet(subnet61, net1, [], [gw61_ip])
# Create network with subnets using second v4 scope and v6 scope.
net_resp = self._make_network(self.fmt, 'net2', True)
net2 = net_resp['network']
self._check_network(net2)
gw4n2_ip = '10.2.1.1'
subnet4n2 = self._make_subnet(
self.fmt, net_resp, gw4n2_ip, '10.2.1.0/24',
subnetpool_id=pool4n_id)['subnet']
self._check_subnet(subnet4n2, net2, [], [gw4n2_ip])
gw62_ip = '2001:db8:1:2::1'
subnet62 = self._make_subnet(
self.fmt, net_resp, gw62_ip, '2001:db8:1:2::0/64',
ip_version=6, subnetpool_id=pool6_id)['subnet']
self._check_subnet(subnet62, net2, [], [gw62_ip])
# Create network with unscoped subnets.
net_resp = self._make_network(self.fmt, 'net3', True)
net3 = net_resp['network']
self._check_network(net3)
gw43_ip = '10.3.1.1'
subnet43 = self._make_subnet(
self.fmt, net_resp, gw43_ip, '10.3.1.0/24')['subnet']
self._check_subnet(subnet43, net3, [], [gw43_ip])
gw63_ip = '2001:db8:1:3::1'
subnet63 = self._make_subnet(
self.fmt, net_resp, gw63_ip, '2001:db8:1:3::0/64',
ip_version=6)['subnet']
self._check_subnet(subnet63, net3, [], [gw63_ip])
# Create shared network with unscoped subnets.
net_resp = self._make_network(
self.fmt, 'net4', True, tenant_id='tenant_2', shared=True)
net4 = net_resp['network']
self._check_network(net4)
gw44_ip = '10.4.1.1'
subnet44 = self._make_subnet(
self.fmt, net_resp, gw44_ip, '10.4.1.0/24')['subnet']
self._check_subnet(subnet44, net4, [], [gw44_ip])
gw64_ip = '2001:db8:1:4::1'
subnet64 = self._make_subnet(
self.fmt, net_resp, gw64_ip, '2001:db8:1:4::0/64',
ip_version=6)['subnet']
self._check_subnet(subnet64, net4, [], [gw64_ip])
def add(subnet):
# REVISIT: Adding by port would work, but adding shared
# network interface by subnet fails without admin context.
#
# router_ctx = n_context.Context(None, self._tenant_id)
router_ctx = n_context.get_admin_context()
info = self.l3_plugin.add_router_interface(
router_ctx, router_id, {'subnet_id': subnet['id']})
self.assertIn(subnet['id'], info['subnet_ids'])
def remove(subnet):
# REVISIT: Removing by port should work, but removing
# shared network interface by subnet fails without admin
# context.
#
# router_ctx = n_context.Context(None, self._tenant_id)
router_ctx = n_context.get_admin_context()
info = self.l3_plugin.remove_router_interface(
router_ctx, router_id, {'subnet_id': subnet['id']})
self.assertIn(subnet['id'], info['subnet_ids'])
def check(nets, scopes, unscoped_project):
router = self._show('routers', router_id)['router']
expected_gw_ips = []
unexpected_gw_ips = []
for net, routed_subnets, unrouted_subnets, scope, project in nets:
net = self._show('networks', net['id'])['network']
self._check_network(
net, [router] if routed_subnets else [], scope, project)
for subnet in routed_subnets:
gw_ip = subnet['gateway_ip']
expected_gw_ips.append(gw_ip)
subnet = self._show('subnets', subnet['id'])['subnet']
self._check_subnet(
subnet, net, [(gw_ip, router)], [], scope, project)
for subnet in unrouted_subnets:
gw_ip = subnet['gateway_ip']
unexpected_gw_ips.append(gw_ip)
subnet = self._show('subnets', subnet['id'])['subnet']
self._check_subnet(
subnet, net, [], [gw_ip], scope, project)
self._check_router(
router, expected_gw_ips, unexpected_gw_ips, scopes,
unscoped_project)
# Create router.
router = self._make_router(
self.fmt, self._tenant_id, 'router1')['router']
router_id = router['id']
check([(net1, [], [subnet4i1, subnet61], None, None),
(net2, [], [subnet4n2, subnet62], None, None),
(net3, [], [subnet43, subnet63], None, None),
(net4, [], [subnet44, subnet64], None, None)],
[], None)
# Add first scoped v4 subnet to router.
add(subnet4i1)
check([(net1, [subnet4i1], [subnet61], scope4i, None),
(net2, [], [subnet4n2, subnet62], None, None),
(net3, [], [subnet43, subnet63], None, None),
(net4, [], [subnet44, subnet64], None, None)],
[scope4i], None)
# Add first scoped v6 subnet to router.
add(subnet61)
check([(net1, [subnet4i1, subnet61], [], scope4i, None),
(net2, [], [subnet4n2, subnet62], None, None),
(net3, [], [subnet43, subnet63], None, None),
(net4, [], [subnet44, subnet64], None, None)],
[scope4i, scope6], None)
# Add first unscoped v6 subnet to router.
add(subnet63)
check([(net1, [subnet4i1, subnet61], [], scope4i, None),
(net2, [], [subnet4n2, subnet62], None, None),
(net3, [subnet63], [subnet43], None, None),
(net4, [], [subnet44, subnet64], None, None)],
[scope4i, scope6], self._tenant_id)
# Add second scoped v6 subnet to router.
add(subnet62)
check([(net1, [subnet4i1, subnet61], [], scope4i, None),
(net2, [subnet62], [subnet4n2], scope6, None),
(net3, [subnet63], [subnet43], None, None),
(net4, [], [subnet44, subnet64], None, None)],
[scope4i, scope6], self._tenant_id)
# REVISIT: Enable when non-isomorphic network routing is
# supported.
#
# Add second scoped v4 subnet to router.
# add(subnet4n2)
# check([(net1, [subnet4i1, subnet61], [], scope4i, None),
# (net2, [subnet4n2, subnet62], [], scope6, None),
# (net3, [subnet63], [subnet43], None, None),
# (net4, [], [subnet44, subnet64], None, None)],
# [scope4i, scope4n, scope6], self._tenant_id)
# Add first unscoped v4 subnet to router.
add(subnet43)
check([(net1, [subnet4i1, subnet61], [], scope4i, None),
(net2, [subnet62], [subnet4n2], scope6, None),
(net3, [subnet43, subnet63], [], None, None),
(net4, [], [subnet44, subnet64], None, None)],
[scope4i, scope6], self._tenant_id)
# Add shared unscoped v4 subnet to router, which should move
# unscoped topology but not scoped topologies.
add(subnet44)
# REVISIT: net2 should be scope4n.
check([(net1, [subnet4i1, subnet61], [], scope4i, None),
(net2, [subnet62], [subnet4n2], scope6, None),
(net3, [subnet43, subnet63], [], None, 'tenant_2'),
(net4, [subnet44], [subnet64], None, 'tenant_2')],
[scope4i, scope6], 'tenant_2')
# Add shared unscoped v6 subnet to router.
add(subnet64)
# REVISIT: net2 should be scope4n.
check([(net1, [subnet4i1, subnet61], [], scope4i, None),
(net2, [subnet62], [subnet4n2], scope6, None),
(net3, [subnet43, subnet63], [], None, 'tenant_2'),
(net4, [subnet44, subnet64], [], None, 'tenant_2')],
[scope4i, scope6], 'tenant_2')
# Remove first scoped v4 subnet from router.
remove(subnet4i1)
# REVISIT: net1 should be scope6, net2 should be scope4n.
check([(net1, [subnet61], [subnet4i1], scope4i, None),
(net2, [subnet62], [subnet4n2], scope6, None),
(net3, [subnet43, subnet63], [], None, 'tenant_2'),
(net4, [subnet44, subnet64], [], None, 'tenant_2')],
[scope6], 'tenant_2')
# Remove first scoped v6 subnet from router.
remove(subnet61)
check([(net1, [], [subnet4i1, subnet61], None, None),
(net2, [subnet62], [subnet4n2], scope6, None),
(net3, [subnet43, subnet63], [], None, 'tenant_2'),
(net4, [subnet44, subnet64], [], None, 'tenant_2')],
[scope6], 'tenant_2')
# Remove shared unscoped v4 subnet from router.
remove(subnet44)
check([(net1, [], [subnet4i1, subnet61], None, None),
(net2, [subnet62], [subnet4n2], scope6, None),
(net3, [subnet43, subnet63], [], None, 'tenant_2'),
(net4, [subnet64], [subnet44], None, 'tenant_2')],
[scope6], 'tenant_2')
# Remove shared unscoped v6 subnet from router, which should
# move remaining unscoped topology back to original tenant.
remove(subnet64)
check([(net1, [], [subnet4i1, subnet61], None, None),
(net2, [subnet62], [subnet4n2], scope6, None),
(net3, [subnet43, subnet63], [], None, None),
(net4, [], [subnet44, subnet64], None, None)],
[scope6], self._tenant_id)
# Remove first unscoped v6 subnet from router.
remove(subnet63)
check([(net1, [], [subnet4i1, subnet61], None, None),
(net2, [subnet62], [subnet4n2], scope6, None),
(net3, [subnet43], [subnet63], None, None),
(net4, [], [subnet44, subnet64], None, None)],
[scope6], self._tenant_id)
# REVISIT: Enable when non-isomorphic network routing is
# supported.
#
# Remove second scoped v4 subnet from router.
# remove(subnet4n2)
# check([(net1, [], [subnet4i1, subnet61], None, None),
# (net2, [subnet62], [subnet4n2], scope6, None),
# (net3, [subnet43], [subnet63], None, None),
# (net4, [], [subnet44, subnet64], None, None)],
# [scope6], self._tenant_id)
# Remove second unscoped v4 subnet from router.
remove(subnet43)
check([(net1, [], [subnet4i1, subnet61], None, None),
(net2, [subnet62], [subnet4n2], scope6, None),
(net3, [], [subnet43, subnet63], None, None),
(net4, [], [subnet44, subnet64], None, None)],
[scope6], None)
# Remove second scoped v6 subnet from router.
remove(subnet62)
check([(net1, [], [subnet4i1, subnet61], None, None),
(net2, [], [subnet4n2, subnet62], None, None),
(net3, [], [subnet43, subnet63], None, None),
(net4, [], [subnet44, subnet64], None, None)],
[], None)
def test_shared_address_scope(self):
# Create shared scope as tenant_1.
@ -1130,7 +1440,7 @@ class TestAimMapping(ApicAimTestCase):
router = self._make_router(
self.fmt, 'tenant_2', 'router1')['router']
router_id = router['id']
self._check_router(router, [], [], scope)
self._check_router(router, [], [], scopes=[scope])
# Create network as tenant_2.
net_resp = self._make_network(self.fmt, 'net1', True,
@ -1155,7 +1465,7 @@ class TestAimMapping(ApicAimTestCase):
# Check router.
router = self._show('routers', router_id)['router']
self._check_router(router, [gw1_ip], [], scope)
self._check_router(router, [gw1_ip], [], scopes=[scope])
# Check network.
net = self._show('networks', net_id)['network']
@ -1181,7 +1491,7 @@ class TestAimMapping(ApicAimTestCase):
# Check router.
router = self._show('routers', router_id)['router']
self._check_router(router, [gw1_ip, gw2_ip], [], scope)
self._check_router(router, [gw1_ip, gw2_ip], [], scopes=[scope])
# Check network.
net = self._show('networks', net_id)['network']
@ -1203,7 +1513,7 @@ class TestAimMapping(ApicAimTestCase):
# Check router.
router = self._show('routers', router_id)['router']
self._check_router(router, [gw2_ip], [gw1_ip], scope)
self._check_router(router, [gw2_ip], [gw1_ip], scopes=[scope])
# Check network.
net = self._show('networks', net_id)['network']
@ -2249,6 +2559,38 @@ class TestTopology(ApicAimTestCase):
self.l3_plugin.add_router_interface,
n_context.get_admin_context(), router2_id, {'port_id': port_id})
# REVISIT: The following tests are temporary. This override
# flag and these tests should be removed when a better
# solution is implemented for supporting multiple external
# segments with an L3P in GBP.
# Verify adding 3rd subnet to 2nd router succeeds with
# override flag.
self.l3_plugin.add_router_interface(
n_context.get_admin_context(), router2_id,
{'subnet_id': subnet3_id,
l3_ext.OVERRIDE_NETWORK_ROUTING_TOPOLOGY_VALIDATION: True})
# Verify adding 1st subnet to 2nd router succeeds with
# override flag.
fixed_ips = [{'subnet_id': subnet1_id, 'ip_address': '10.0.1.101'}]
port_id = self._make_port(
self.fmt, net_id, fixed_ips=fixed_ips)['port']['id']
self.l3_plugin.add_router_interface(
n_context.get_admin_context(), router2_id,
{'port_id': port_id,
l3_ext.OVERRIDE_NETWORK_ROUTING_TOPOLOGY_VALIDATION: True})
# Verify adding 2nd subnet to 2nd router succeeds with
# override flag.
fixed_ips = [{'subnet_id': subnet2_id, 'ip_address': '10.0.2.101'}]
port_id = self._make_port(
self.fmt, net_id, fixed_ips=fixed_ips)['port']['id']
self.l3_plugin.add_router_interface(
n_context.get_admin_context(), router2_id,
{'port_id': port_id,
l3_ext.OVERRIDE_NETWORK_ROUTING_TOPOLOGY_VALIDATION: True})
def test_network_subnet_on_multple_routers(self):
# Create network.
net_resp = self._make_network(self.fmt, 'net1', True)
@ -2291,85 +2633,27 @@ class TestTopology(ApicAimTestCase):
n_context.get_admin_context(), router2_id,
{'subnet_id': subnet2_id})
def test_reject_routing_multiple_address_scopes(self):
# TODO(rkukura): Remove this test when multi-scope routing is
# supported.
# REVISIT: The following tests are temporary. This override
# flag and these tests should be removed when a better
# solution is implemented for supporting multiple external
# segments with an L3P in GBP.
# Create subnet1 with address_scope.
scope = self._make_address_scope(
self.fmt, 4, name='as1')['address_scope']
scope_id = scope['id']
pool = self._make_subnetpool(self.fmt, ['10.1.0.0/16'], name='sp1',
tenant_id=scope['tenant_id'],
address_scope_id=scope_id,
default_prefixlen=24)['subnetpool']
pool_id = pool['id']
net_resp = self._make_network(self.fmt, 'net1', True)
subnet = self._make_subnet(
self.fmt, net_resp, '10.1.0.1', '10.1.0.0/24',
subnetpool_id=pool_id)['subnet']
subnet1_id = subnet['id']
# Create non-overlapping subnet2 with different address_scope.
scope = self._make_address_scope(
self.fmt, 4, name='as2')['address_scope']
scope_id = scope['id']
pool = self._make_subnetpool(self.fmt, ['10.2.0.0/16'], name='sp2',
tenant_id=scope['tenant_id'],
address_scope_id=scope_id,
default_prefixlen=24)['subnetpool']
pool_id = pool['id']
net_resp = self._make_network(self.fmt, 'net2', True)
subnet = self._make_subnet(
self.fmt, net_resp, '10.2.0.1', '10.2.0.0/24',
subnetpool_id=pool_id)['subnet']
subnet2_id = subnet['id']
# Create non-overlapping subnet3 with no address_scope.
net_resp = self._make_network(self.fmt, 'net3', True)
subnet = self._make_subnet(
self.fmt, net_resp, '10.3.0.1', '10.3.0.0/24')['subnet']
subnet3_id = subnet['id']
# Create router.
router_id = self._make_router(
self.fmt, 'test-tenant', 'router1')['router']['id']
# Add first scoped subnet to router.
# Verify adding 2nd subnet to 1st router succeeds with
# override flag.
self.l3_plugin.add_router_interface(
n_context.get_admin_context(), router_id,
{'subnet_id': subnet1_id})
n_context.get_admin_context(), router1_id,
{'subnet_id': subnet2_id,
l3_ext.OVERRIDE_NETWORK_ROUTING_TOPOLOGY_VALIDATION: True})
# Verify adding second scoped subnet to router fails.
self.assertRaises(
exceptions.MultiScopeRoutingNotSupported,
self.l3_plugin.add_router_interface,
n_context.get_admin_context(), router_id,
{'subnet_id': subnet2_id})
# Verify adding unscoped subnet to router fails.
self.assertRaises(
exceptions.MultiScopeRoutingNotSupported,
self.l3_plugin.add_router_interface,
n_context.get_admin_context(), router_id,
{'subnet_id': subnet3_id})
# Remove first scoped subnet from router.
self.l3_plugin.remove_router_interface(
n_context.get_admin_context(), router_id,
{'subnet_id': subnet1_id})
# Add unscoped subnet to router.
# Verify adding 2nd subnet to 2nd router succeeds with
# override flag.
fixed_ips = [{'subnet_id': subnet2_id, 'ip_address': '10.0.2.100'}]
port_id = self._make_port(
self.fmt, net_id, fixed_ips=fixed_ips)['port']['id']
self.l3_plugin.add_router_interface(
n_context.get_admin_context(), router_id,
{'subnet_id': subnet3_id})
# Verify adding scoped subnet to router fails.
self.assertRaises(
exceptions.MultiScopeRoutingNotSupported,
self.l3_plugin.add_router_interface,
n_context.get_admin_context(), router_id,
{'subnet_id': subnet1_id})
n_context.get_admin_context(), router2_id,
{'port_id': port_id,
l3_ext.OVERRIDE_NETWORK_ROUTING_TOPOLOGY_VALIDATION: True})
def test_reject_routing_shared_networks_from_different_projects(self):
# Create router as tenant_1.
@ -2474,6 +2758,106 @@ class TestTopology(ApicAimTestCase):
self.assertEqual('ScopeUpdateNotSupported',
result['NeutronError']['type'])
def test_reject_non_isomorphic_network_routing(self):
# Create v6 scope and pool.
scope = self._make_address_scope(
self.fmt, 6, name='as6')['address_scope']
scope6_id = scope['id']
scope6_vrf = scope['apic:distinguished_names']['VRF']
pool = self._make_subnetpool(
self.fmt, ['2001:db8:1::0/56'], name='sp6',
tenant_id=self._tenant_id,
address_scope_id=scope6_id)['subnetpool']
pool6_id = pool['id']
# Create isomorphic v4 scope and pool.
scope = self._make_address_scope_for_vrf(
scope6_vrf, 4, name='as4i')['address_scope']
scope4i_id = scope['id']
pool = self._make_subnetpool(
self.fmt, ['10.1.0.0/16'], name='sp4i', tenant_id=self._tenant_id,
address_scope_id=scope4i_id, default_prefixlen=24)['subnetpool']
pool4i_id = pool['id']
# Create non-isomorphic v4 scope and pool.
scope = self._make_address_scope(
self.fmt, 4, name='as4n')['address_scope']
scope4n_id = scope['id']
pool = self._make_subnetpool(
self.fmt, ['10.2.0.0/16'], name='sp4n', tenant_id=self._tenant_id,
address_scope_id=scope4n_id)['subnetpool']
pool4n_id = pool['id']
# Create network with isomorphic scoped v4 and v6 subnets.
net_resp = self._make_network(self.fmt, 'net1', True)
subnet = self._make_subnet(
self.fmt, net_resp, '10.1.1.1', '10.1.1.0/24',
subnetpool_id=pool4i_id)['subnet']
subnet14_id = subnet['id']
subnet = self._make_subnet(
self.fmt, net_resp, '2001:db8:1:1::1', '2001:db8:1:1::0/64',
ip_version=6, subnetpool_id=pool6_id)['subnet']
subnet16_id = subnet['id']
# Create network with non-isomorphic scoped v4 and v6 subnets.
net_resp = self._make_network(self.fmt, 'net2', True)
subnet = self._make_subnet(
self.fmt, net_resp, '10.2.1.1', '10.2.1.0/24',
subnetpool_id=pool4n_id)['subnet']
subnet24_id = subnet['id']
subnet = self._make_subnet(
self.fmt, net_resp, '2001:db8:1:2::1', '2001:db8:1:2::0/64',
ip_version=6, subnetpool_id=pool6_id)['subnet']
subnet26_id = subnet['id']
# Create network with unscoped v4 and scoped v6 subnets.
net_resp = self._make_network(self.fmt, 'net3', True)
subnet = self._make_subnet(
self.fmt, net_resp, '10.3.1.1', '10.3.1.0/24')['subnet']
subnet34_id = subnet['id']
subnet = self._make_subnet(
self.fmt, net_resp, '2001:db8:1:3::1', '2001:db8:1:3::0/64',
ip_version=6, subnetpool_id=pool6_id)['subnet']
subnet36_id = subnet['id']
# Create router.
router_id = self._make_router(
self.fmt, self._tenant_id, 'router1')['router']['id']
# Verify adding isomorphic scoped subnets on network succeeds.
info = self.l3_plugin.add_router_interface(
n_context.get_admin_context(), router_id,
{'subnet_id': subnet14_id})
self.assertIn(subnet14_id, info['subnet_ids'])
info = self.l3_plugin.add_router_interface(
n_context.get_admin_context(), router_id,
{'subnet_id': subnet16_id})
self.assertIn(subnet16_id, info['subnet_ids'])
# Verify adding non-isomorphic scoped subnets on network fails.
info = self.l3_plugin.add_router_interface(
n_context.get_admin_context(), router_id,
{'subnet_id': subnet24_id})
self.assertIn(subnet24_id, info['subnet_ids'])
self.assertRaises(
exceptions.NonIsomorphicNetworkRoutingUnsupported,
self.l3_plugin.add_router_interface,
n_context.get_admin_context(), router_id,
{'subnet_id': subnet26_id})
# Verify adding scoped and unscoped subnets on network fails.
info = self.l3_plugin.add_router_interface(
n_context.get_admin_context(), router_id,
{'subnet_id': subnet34_id})
self.assertIn(subnet34_id, info['subnet_ids'])
self.assertRaises(
exceptions.NonIsomorphicNetworkRoutingUnsupported,
self.l3_plugin.add_router_interface,
n_context.get_admin_context(), router_id,
{'subnet_id': subnet36_id})
# REVISIT: Add test_reject_v4_scope_with_different_v6_scopes.
def test_unscoped_subnetpool_subnets_with_router(self):
# Test that subnets from a subnetpool that has no address-scope
# can be connected to a router.