[apic-aim] Isomorphic address scopes

Allow a single IPv4 address scope and a single IPv6 address scope to
reference the same VRF, which may be pre-existing or mapped from one
of the address scopes.

Change-Id: Ibe5288a3a6d5032e4c0ac509a0857ce5defafa9c
This commit is contained in:
Robert Kukura 2017-04-27 18:15:29 -04:00
parent ace2a20d7b
commit 71b3b2df67
5 changed files with 117 additions and 38 deletions

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from neutron.db.models import address_scope as as_db
from neutron_lib.db import model_base
import sqlalchemy as sa
@ -181,7 +182,8 @@ class ExtensionDbMixin(object):
db_obj.vrf_dn = res_dict[cisco_apic.VRF]
session.add(db_obj)
def get_address_scope_by_vrf_dn(self, session, vrf_dn):
db_obj = (session.query(AddressScopeExtensionDb)
.filter_by(vrf_dn=vrf_dn).first())
return db_obj.address_scope_id if db_obj else None
def get_address_scopes_by_vrf_dn(self, session, vrf_dn):
return (session.query(as_db.AddressScope).
join(AddressScopeExtensionDb).
filter(AddressScopeExtensionDb.vrf_dn == vrf_dn).
all())

View File

@ -152,13 +152,16 @@ class ApicExtensionDriver(api_plus.ExtensionDriver,
raise n_exc.InvalidInput(
error_message=('%s is not valid VRF DN' % dn))
session = plugin_context.session
# check if there is another address-scope mapping to same VRF
# Check if there is another address scope of the same
# address family mapping to same VRF.
#
# Case 1: Another address-scope with pre-existing VRF
scope = self.get_address_scope_by_vrf_dn(session, dn)
if scope:
raise n_exc.InvalidInput(
error_message=('VRF %s is already in use by '
'address-scope %s' % (dn, scope)))
scopes = self.get_address_scopes_by_vrf_dn(session, dn)
for scope in scopes:
if scope.ip_version == data['ip_version']:
raise n_exc.InvalidInput(
error_message=('VRF %s is already in use by '
'address-scope %s' % (dn, scope)))
# Case 2: Another address-scope with orchestrated VRF
#
# REVISIT: We don't filter by the project ID because the
@ -170,7 +173,7 @@ class ApicExtensionDriver(api_plus.ExtensionDriver,
scope = (session.query(address_scope_db.AddressScope)
.filter_by(id=scope_id)
.first())
if scope:
if scope and scope.ip_version == data['ip_version']:
raise n_exc.InvalidInput(
error_message=('VRF %s is already in use by '
'address-scope %s' % (dn, scope)))

View File

@ -568,6 +568,16 @@ class ApicMechanismDriver(api_plus.MechanismDriver):
vrf = self.aim.get(aim_ctx, self._map_address_scope(session, current))
if vrf and not vrf.monitored:
# Don't delete VRF if isomorphic scope uses this scope's
# VRF.
id = self.name_mapper.reverse_address_scope(
session, vrf.name, enforce=False)
if id != current['id'] and self._scope_by_id(session, id):
return
extn_db = extension_db.ExtensionDbMixin()
for scope in extn_db.get_address_scopes_by_vrf_dn(session, vrf.dn):
if scope.id != current['id']:
return
self.aim.delete(aim_ctx, vrf)
def extend_address_scope_dict(self, session, scope_db, result):
@ -1445,8 +1455,8 @@ class ApicMechanismDriver(api_plus.MechanismDriver):
if network_db:
return self._get_routed_vrf_for_network(session, network_db)
def _get_address_scope_id_for_vrf(self, session, vrf):
# Returns the ID of an address-scope that corresponds to a VRF,
def _get_address_scope_ids_for_vrf(self, session, vrf):
# Returns the IDs of address-scopes that corresponds to a VRF,
# if any.
# Check if unrouted VRF
@ -1455,19 +1465,20 @@ class ApicMechanismDriver(api_plus.MechanismDriver):
# Check if pre-existing VRF for an address-scope
extn_db = extension_db.ExtensionDbMixin()
scope_id = extn_db.get_address_scope_by_vrf_dn(session, vrf.dn)
scope_ids = [scope.id for scope in
extn_db.get_address_scopes_by_vrf_dn(session, vrf.dn)]
# Check if orchestrated VRF for an address-scope
if not scope_id and vrf.name != DEFAULT_VRF_NAME:
scope_id = self.name_mapper.reverse_address_scope(session,
vrf.name)
return scope_id
if not scope_ids and vrf.name != DEFAULT_VRF_NAME:
scope_ids = [self.name_mapper.reverse_address_scope(session,
vrf.name)]
return scope_ids
def _get_routers_for_vrf(self, session, vrf):
# REVISIT: Persist router/VRF relationship?
scope_id = self._get_address_scope_id_for_vrf(session, vrf)
if scope_id:
scope_ids = self._get_address_scope_ids_for_vrf(session, vrf)
if scope_ids:
rtr_dbs = (session.query(l3_db.Router)
.join(l3_db.RouterPort)
.join(models_v2.Port)
@ -1478,8 +1489,8 @@ class ApicMechanismDriver(api_plus.MechanismDriver):
models_v2.SubnetPool.id)
.filter(l3_db.RouterPort.port_type ==
n_constants.DEVICE_OWNER_ROUTER_INTF)
.filter(models_v2.SubnetPool.address_scope_id ==
scope_id)
.filter(models_v2.SubnetPool.address_scope_id.in_(
scope_ids))
.distinct())
else:
# For an unscoped VRF, first find all the routed BDs
@ -1752,7 +1763,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).
one())
first())
def _map_network(self, session, network, vrf, bd_only=False):
tenant_aname = (vrf.tenant_name

View File

@ -2002,17 +2002,18 @@ class AIMMappingDriver(nrd.CommonNeutronBase, aim_rpc.AIMMappingRPCMixin):
# get all subnets of the specified VRF
with session.begin(subtransactions=True):
# Find VRF's address_scope first
address_scope_id = (
self.aim_mech_driver._get_address_scope_id_for_vrf(
address_scope_ids = (
self.aim_mech_driver._get_address_scope_ids_for_vrf(
session,
aim_resource.VRF(tenant_name=vrf_tenant_name,
name=vrf_name)))
if address_scope_id:
subnetpools = self._get_subnetpools(
plugin_context,
filters={'address_scope_id': [address_scope_id]})
for pool in subnetpools:
result.extend(pool['prefixes'])
if address_scope_ids:
for address_scope_id in address_scope_ids:
subnetpools = self._get_subnetpools(
plugin_context,
filters={'address_scope_id': [address_scope_id]})
for pool in subnetpools:
result.extend(pool['prefixes'])
else:
aim_ctx = aim_context.AimContext(db_session=session)
if vrf_tenant_name != md.COMMON_TENANT_NAME:

View File

@ -2790,22 +2790,84 @@ class TestExtensionAttributes(ApicAimTestCase):
extn = extn_db.ExtensionDbMixin()
aim_ctx = aim_context.AimContext(db_session=session)
# create with APIC DN
# Create VRF.
self.aim_mgr.create(
aim_ctx, aim_resource.Tenant(name=self.t1_aname, monitored=True))
vrf = aim_resource.VRF(tenant_name=self.t1_aname, name='ctx1',
monitored=True)
self.aim_mgr.create(aim_ctx, vrf)
scope = self._make_address_scope_for_vrf(vrf.dn)['address_scope']
# Create v4 scope with APIC DN.
scope4 = self._make_address_scope_for_vrf(vrf.dn)['address_scope']
self.assertEqual({'VRF': vrf.dn},
extn.get_address_scope_extn_db(session, scope['id']))
extn.get_address_scope_extn_db(session, scope4['id']))
self._check_dn(scope4, vrf, 'VRF')
scope = self._show('address-scopes', scope4['id'])['address_scope']
self._check_dn(scope, vrf, 'VRF')
scope = self._show('address-scopes', scope['id'])['address_scope']
self._check_dn(scope, vrf, 'VRF')
# Create (isomorphic) v6 scope with same APIC DN.
scope6 = self._make_address_scope_for_vrf(
vrf.dn, n_constants.IP_VERSION_6)['address_scope']
self.assertEqual({'VRF': vrf.dn},
extn.get_address_scope_extn_db(session, scope6['id']))
self._check_dn(scope6, vrf, 'VRF')
self._delete('address-scopes', scope['id'])
self.assertFalse(extn.get_address_scope_extn_db(session, scope['id']))
scope = self._show('address-scopes', scope6['id'])['address_scope']
self._check_dn(scope6, vrf, 'VRF')
# Delete scopes.
self._delete('address-scopes', scope4['id'])
self.assertFalse(extn.get_address_scope_extn_db(session, scope4['id']))
self._delete('address-scopes', scope6['id'])
self.assertFalse(extn.get_address_scope_extn_db(session, scope6['id']))
vrf = self.aim_mgr.get(aim_ctx, vrf)
self.assertIsNotNone(vrf)
def test_isomorphic_address_scopes_lifecycle(self):
# Create initial v4 scope.
scope4 = self._make_address_scope(
self.fmt, 4, name='as')['address_scope']
dn = scope4['apic:distinguished_names']['VRF']
# Create isomorphic v6 scope, using v4 scope's pre-existing
# DN.
scope6 = self._make_address_scope_for_vrf(
dn, n_constants.IP_VERSION_6)['address_scope']
self.assertEqual(dn, scope6['apic:distinguished_names']['VRF'])
# Delete v4 scope.
self._delete('address-scopes', scope4['id'])
vrf = self._find_by_dn(dn, aim_resource.VRF)
self.assertIsNotNone(vrf)
# Delete v6 scope.
self._delete('address-scopes', scope6['id'])
vrf = self._find_by_dn(dn, aim_resource.VRF)
self.assertIsNone(vrf)
# Create another initial v4 scope.
scope4 = self._make_address_scope(
self.fmt, 4, name='as')['address_scope']
dn = scope4['apic:distinguished_names']['VRF']
# Create isomorphic v6 scope, using v4 scope's pre-existing
# DN.
scope6 = self._make_address_scope_for_vrf(
dn, n_constants.IP_VERSION_6)['address_scope']
self.assertEqual(dn, scope6['apic:distinguished_names']['VRF'])
# Delete v6 scope.
self._delete('address-scopes', scope6['id'])
vrf = self._find_by_dn(dn, aim_resource.VRF)
self.assertIsNotNone(vrf)
# Delete v4 scope.
self._delete('address-scopes', scope4['id'])
vrf = self._find_by_dn(dn, aim_resource.VRF)
self.assertIsNone(vrf)
def test_address_scope_fail(self):
# APIC DN not specified