Merge "[AIM] Support sharing of L3Outs"

This commit is contained in:
Zuul 2018-02-22 01:04:41 +00:00 committed by Gerrit Code Review
commit c61fa15ad7
5 changed files with 231 additions and 32 deletions

View File

@ -62,3 +62,8 @@ class PreExistingSVICannotBeConnectedToRouter(exceptions.BadRequest):
class OnlyOneSubnetInSVINetwork(exceptions.BadRequest):
message = _("Only one subnet is allowed in SVI network.")
class ExternalSubnetOverlapInL3Out(exceptions.BadRequest):
message = _("External subnet CIDR %(cidr)s overlaps with existing "
"subnets in APIC L3Outside %(l3out)s.")

View File

@ -112,6 +112,30 @@ class ExtensionDbMixin(object):
res_dict[cisco_apic.EXTERNAL_CIDRS],
network_id=network_id)
def get_network_ids_by_ext_net_dn(self, session, dn, lock_update=False):
ids = session.query(NetworkExtensionDb.network_id).filter_by(
external_network_dn=dn)
if lock_update:
ids = ids.with_lockmode('update')
return [i[0] for i in ids]
def get_network_ids_by_l3out_dn(self, session, dn, lock_update=False):
ids = session.query(NetworkExtensionDb.network_id).filter(
NetworkExtensionDb.external_network_dn.like(dn + "/%"))
if lock_update:
ids = ids.with_lockmode('update')
return [i[0] for i in ids]
def get_external_cidrs_by_ext_net_dn(self, session, dn, lock_update=False):
ctab = NetworkExtensionCidrDb
ntab = NetworkExtensionDb
cidrs = session.query(ctab.cidr).join(
ntab, ntab.network_id == ctab.network_id).filter(
ntab.external_network_dn == dn).distinct()
if lock_update:
cidrs = cidrs.with_lockmode('update')
return [c[0] for c in cidrs]
def get_subnet_extn_db(self, session, subnet_id):
db_obj = (session.query(SubnetExtensionDb).filter_by(
subnet_id=subnet_id).first())

View File

@ -452,8 +452,13 @@ class ApicMechanismDriver(api_plus.MechanismDriver,
ns.create_l3outside(aim_ctx, l3out, vmm_domains=domains)
ns.create_external_network(aim_ctx, ext_net)
ns.update_external_cidrs(aim_ctx, ext_net,
current[cisco_apic.EXTERNAL_CIDRS])
# Get external CIDRs for all external networks that share
# this APIC external network.
ext_db = extension_db.ExtensionDbMixin()
cidrs = sorted(
ext_db.get_external_cidrs_by_ext_net_dn(session, ext_net.dn,
lock_update=True))
ns.update_external_cidrs(aim_ctx, ext_net, cidrs)
for resource in ns.get_l3outside_resources(aim_ctx, l3out):
if isinstance(resource, aim_resource.BridgeDomain):
@ -554,7 +559,13 @@ class ApicMechanismDriver(api_plus.MechanismDriver,
old = sorted(original[cisco_apic.EXTERNAL_CIDRS])
new = sorted(current[cisco_apic.EXTERNAL_CIDRS])
if old != new:
ns.update_external_cidrs(aim_ctx, ext_net, new)
# Get external CIDRs for all external networks that share
# this APIC external network.
ext_db = extension_db.ExtensionDbMixin()
cidrs = sorted(
ext_db.get_external_cidrs_by_ext_net_dn(
session, ext_net.dn, lock_update=True))
ns.update_external_cidrs(aim_ctx, ext_net, cidrs)
# TODO(amitbose) Propagate name updates to AIM
def delete_network_precommit(self, context):
@ -568,10 +579,22 @@ class ApicMechanismDriver(api_plus.MechanismDriver,
l3out, ext_net, ns = self._get_aim_nat_strategy(current)
if not ext_net:
return # Unmanaged external network
ns.delete_external_network(aim_ctx, ext_net)
# TODO(amitbose) delete L3out only if no other Neutron
# network is using the L3out
ns.delete_l3outside(aim_ctx, l3out)
extn_db = extension_db.ExtensionDbMixin()
# REVISIT: lock_update=True is needed to handle races. Find
# alternative solutions since Neutron discourages using such
# queries.
other_nets = set(
extn_db.get_network_ids_by_ext_net_dn(session, ext_net.dn,
lock_update=True))
other_nets.discard(current['id'])
if not other_nets:
ns.delete_external_network(aim_ctx, ext_net)
other_nets = set(
extn_db.get_network_ids_by_l3out_dn(session, l3out.dn,
lock_update=True))
other_nets.discard(current['id'])
if not other_nets:
ns.delete_l3outside(aim_ctx, l3out)
elif self._is_svi(current):
l3out, ext_net, _ = self._get_aim_external_stuff(current)
aim_l3out = self.aim.get(aim_ctx, l3out)
@ -657,6 +680,19 @@ class ApicMechanismDriver(api_plus.MechanismDriver,
network_db)
if not ext_net:
return # Unmanaged external network
# Check subnet overlap with subnets from other Neutron
# external networks that map to the same APIC L3Out
ext_db = extension_db.ExtensionDbMixin()
other_nets = set(
ext_db.get_network_ids_by_l3out_dn(session, l3out.dn,
lock_update=True))
other_nets.discard(network_id)
cidrs = (session.query(models_v2.Subnet.cidr).filter(
models_v2.Subnet.network_id.in_(other_nets)).all())
cidrs = netaddr.IPSet([c[0] for c in cidrs])
if cidrs & netaddr.IPSet([current['cidr']]):
raise exceptions.ExternalSubnetOverlapInL3Out(
cidr=current['cidr'], l3out=l3out.dn)
ns.create_subnet(aim_ctx, l3out,
self._subnet_to_gw_ip_mask(current))
@ -2782,9 +2818,12 @@ class ApicMechanismDriver(api_plus.MechanismDriver,
if old_network:
_, ext_net, ns = self._get_aim_nat_strategy(old_network)
if ext_net:
# Find Neutron networks that share the APIC external network.
eqv_nets = ext_db.get_network_ids_by_ext_net_dn(
session, ext_net.dn, lock_update=True)
rtr_old = [r for r in rtr_dbs
if (r.gw_port_id and
r.gw_port.network_id == old_network['id'])]
r.gw_port.network_id in eqv_nets)]
prov = set()
cons = set()
for r in rtr_old:
@ -2799,9 +2838,12 @@ class ApicMechanismDriver(api_plus.MechanismDriver,
if new_network:
_, ext_net, ns = self._get_aim_nat_strategy(new_network)
if ext_net:
# Find Neutron networks that share the APIC external network.
eqv_nets = ext_db.get_network_ids_by_ext_net_dn(
session, ext_net.dn, lock_update=True)
rtr_new = [r for r in rtr_dbs
if (r.gw_port_id and
r.gw_port.network_id == new_network['id'])]
r.gw_port.network_id in eqv_nets)]
prov = set()
cons = set()
for r in rtr_new:

View File

@ -2930,13 +2930,13 @@ class TestAimMapping(ApicAimTestCase):
class TestSyncState(ApicAimTestCase):
@staticmethod
def _get_synced_status(self, context, resource, create_if_absent=True):
def _get_synced_status(self, context, resource, **kwargs):
status = aim_status.AciStatus.SYNCED
return aim_status.AciStatus(resource_root=resource.root,
sync_status=status)
@staticmethod
def _get_pending_status_for_type(resource, type, create_if_absent=True):
def _get_pending_status_for_type(resource, type, **kwargs):
status = (isinstance(resource, type) and
aim_status.AciStatus.SYNC_PENDING or
aim_status.AciStatus.SYNCED)
@ -2944,7 +2944,7 @@ class TestSyncState(ApicAimTestCase):
sync_status=status)
@staticmethod
def _get_failed_status_for_type(resource, type, create_if_absent=True):
def _get_failed_status_for_type(resource, type, **kwargs):
status = (isinstance(resource, type) and
aim_status.AciStatus.SYNC_FAILED or
aim_status.AciStatus.SYNC_PENDING)
@ -3196,6 +3196,7 @@ class TestSyncState(ApicAimTestCase):
subnet = self._show('subnets', subnet['id'])['subnet']
self.assertEqual(expected_state, subnet['apic:synchronization_state'])
self._delete("subnets", subnet['id'])
def test_external_subnet(self):
with mock.patch('aim.aim_manager.AimManager.get_status',
@ -3206,7 +3207,7 @@ class TestSyncState(ApicAimTestCase):
for expected_status, status_func in [
('build', TestSyncState._get_pending_status_for_type),
('error', TestSyncState._get_failed_status_for_type)]:
def get_status(self, context, resource, create_if_absent=True):
def get_status(self, context, resource, **kwargs):
return status_func(resource, aim_resource.Subnet)
with mock.patch('aim.aim_manager.AimManager.get_status',
get_status):
@ -4815,7 +4816,7 @@ class TestExternalConnectivityBase(object):
self._make_subnet(
self.fmt, {'network': ext_net1}, '100.100.100.1',
'100.100.100.0/24')
ext_net2 = self._make_ext_network('ext-net1',
ext_net2 = self._make_ext_network('ext-net2',
dn=self.dn_t1_l2_n2)
self._make_subnet(
self.fmt, {'network': ext_net2}, '200.200.200.1',
@ -4894,7 +4895,7 @@ class TestExternalConnectivityBase(object):
def test_router_gateway(self):
self._do_test_router_gateway(use_addr_scope=False)
def test_router_gateway_addr_scope(self,):
def test_router_gateway_addr_scope(self):
self._do_test_router_gateway(use_addr_scope=True)
def test_router_with_unmanaged_external_network(self):
@ -4921,7 +4922,8 @@ class TestExternalConnectivityBase(object):
self._router_interface_action('remove', router['id'], sub1['id'], None)
self.mock_ns.disconnect_vrf.assert_not_called()
def _do_test_multiple_router(self, use_addr_scope=False):
def _do_test_multiple_router(self, use_addr_scope=False,
shared_l3out=False):
cv = self.mock_ns.connect_vrf
dv = self.mock_ns.disconnect_vrf
@ -4951,17 +4953,18 @@ class TestExternalConnectivityBase(object):
ext_nets = []
a_ext_nets = []
for x in range(0, 2):
ext_net = self._make_ext_network('ext-net%d' % x,
dn='uni/tn-%s/out-l%d/instP-n%d' % (self.t1_aname, x, x))
dn = (self.dn_t1_l1_n1 if shared_l3out else
'uni/tn-%s/out-l%d/instP-n%d' % (self.t1_aname, x, x))
ext_net = self._make_ext_network('ext-net%d' % x, dn=dn)
self._make_subnet(
self.fmt, {'network': ext_net}, '100.%d.100.1' % x,
'100.%d.100.0/24' % x)
ext_nets.append(ext_net['id'])
a_ext_net = aim_resource.ExternalNetwork(
tenant_name=self.t1_aname,
l3out_name='l%d' % x, name='n%d' % x)
a_ext_net = aim_resource.ExternalNetwork.from_dn(dn)
a_ext_nets.append(a_ext_net)
self.fix_l3out_vrf(self.t1_aname, 'l%d' % x, a_vrf.name)
# Note: With shared_l3out=True, a_ext_net[0] and a_ext_net[1] are one
# and the same APIC external network
routers = []
contracts = []
@ -4988,16 +4991,26 @@ class TestExternalConnectivityBase(object):
self.mock_ns.reset_mock()
self._add_external_gateway_to_router(routers[1], ext_nets[1])
a_ext_nets[1].provided_contract_names = [contracts[1]]
a_ext_nets[1].consumed_contract_names = [contracts[1]]
if shared_l3out:
a_ext_nets[1].provided_contract_names = sorted(contracts)
a_ext_nets[1].consumed_contract_names = sorted(contracts)
else:
a_ext_nets[1].provided_contract_names = [contracts[1]]
a_ext_nets[1].consumed_contract_names = [contracts[1]]
cv.assert_called_once_with(mock.ANY, a_ext_nets[1], a_vrf)
self.mock_ns.reset_mock()
self._router_interface_action('remove', routers[0], sub1['id'], None)
a_ext_nets[0].provided_contract_names = []
a_ext_nets[0].consumed_contract_names = []
dv.assert_called_once_with(mock.ANY, a_ext_nets[0], a_vrf)
cv.assert_not_called()
if shared_l3out:
a_ext_nets[0].provided_contract_names = [contracts[1]]
a_ext_nets[0].consumed_contract_names = [contracts[1]]
cv.assert_called_once_with(mock.ANY, a_ext_nets[0], a_vrf)
dv.assert_not_called()
else:
a_ext_nets[0].provided_contract_names = []
a_ext_nets[0].consumed_contract_names = []
dv.assert_called_once_with(mock.ANY, a_ext_nets[0], a_vrf)
cv.assert_not_called()
self.mock_ns.reset_mock()
self._router_interface_action('remove', routers[1], sub1['id'], None)
@ -5011,6 +5024,14 @@ class TestExternalConnectivityBase(object):
def test_multiple_router_addr_scope(self):
self._do_test_multiple_router(use_addr_scope=True)
def test_multiple_router_shared_l3out(self):
self._do_test_multiple_router(use_addr_scope=False,
shared_l3out=True)
def test_multiple_router_addr_scope_shared_l3out(self):
self._do_test_multiple_router(use_addr_scope=True,
shared_l3out=True)
def test_floatingip(self):
net1 = self._make_network(self.fmt, 'pvt-net1', True)['network']
sub1 = self._make_subnet(
@ -5377,6 +5398,111 @@ class TestExternalConnectivityBase(object):
def test_external_network_host_domains(self):
self._test_external_network_lifecycle_with_domains(create_hds=True)
def test_shared_l3out_external_cidr(self):
net1 = self._make_ext_network('net1',
dn=self.dn_t1_l1_n1,
cidrs=['1.2.3.0/20', '20.10.0.0/16',
'4.4.4.0/24'])
a_ext_net = aim_resource.ExternalNetwork.from_dn(self.dn_t1_l1_n1)
self.mock_ns.update_external_cidrs.assert_called_once_with(
mock.ANY, a_ext_net, ['1.2.3.0/20', '20.10.0.0/16', '4.4.4.0/24'])
self.mock_ns.reset_mock()
self._make_ext_network('net2',
dn=self.dn_t1_l1_n1,
cidrs=['50.60.0.0/28', '1.2.3.0/20'])
self.mock_ns.update_external_cidrs.assert_called_once_with(
mock.ANY, a_ext_net,
['1.2.3.0/20', '20.10.0.0/16', '4.4.4.0/24', '50.60.0.0/28'])
self.mock_ns.reset_mock()
net1 = self._update('networks', net1['id'],
{'network': {CIDR: ['4.3.3.0/30', '20.10.0.0/16']}})['network']
self.mock_ns.update_external_cidrs.assert_called_once_with(
mock.ANY, a_ext_net,
['1.2.3.0/20', '20.10.0.0/16', '4.3.3.0/30', '50.60.0.0/28'])
def test_shared_l3out_network_delete(self):
# Test reference counting of shared L3outs
net1 = self._make_ext_network('net1', dn=self.dn_t1_l1_n1)
a_l3out = aim_resource.L3Outside(tenant_name=self.t1_aname, name='l1')
a_ext_net = aim_resource.ExternalNetwork(
tenant_name=self.t1_aname, l3out_name='l1', name='n1')
self.mock_ns.create_l3outside.assert_called_once_with(
mock.ANY, a_l3out, vmm_domains=[])
self.mock_ns.create_external_network.assert_called_once_with(
mock.ANY, a_ext_net)
# create second network that shares APIC l3out and external-network
self.mock_ns.reset_mock()
net2 = self._make_ext_network('net2', dn=self.dn_t1_l1_n1)
self.mock_ns.create_l3outside.assert_called_once_with(
mock.ANY, a_l3out, vmm_domains=[])
self.mock_ns.create_external_network.assert_called_once_with(
mock.ANY, a_ext_net)
# create third network that shares APIC l3out only
self.mock_ns.reset_mock()
net3 = self._make_ext_network(
'net3', dn=('uni/tn-%s/out-l1/instP-n2' % self.t1_aname))
a_ext_net3 = aim_resource.ExternalNetwork(
tenant_name=self.t1_aname, l3out_name='l1', name='n2')
self.mock_ns.create_l3outside.assert_called_once_with(
mock.ANY, a_l3out, vmm_domains=[])
self.mock_ns.create_external_network.assert_called_once_with(
mock.ANY, a_ext_net3)
# delete net2
self.mock_ns.reset_mock()
self._delete('networks', net2['id'])
self.mock_ns.delete_l3outside.assert_not_called()
self.mock_ns.delete_external_network.assert_not_called()
# delete net1
self.mock_ns.reset_mock()
self._delete('networks', net1['id'])
self.mock_ns.delete_l3outside.assert_not_called()
self.mock_ns.delete_external_network.assert_called_once_with(
mock.ANY, a_ext_net)
# delete net3
self.mock_ns.reset_mock()
self._delete('networks', net3['id'])
self.mock_ns.delete_l3outside.assert_called_once_with(
mock.ANY, a_l3out)
self.mock_ns.delete_external_network.assert_called_once_with(
mock.ANY, a_ext_net3)
def test_shared_l3out_external_subnet_overlap(self):
net1 = self._make_ext_network('net1', dn=self.dn_t1_l1_n1)
self._make_subnet(
self.fmt, {'network': net1}, '100.100.100.1',
'100.100.100.0/24')
net2 = self._make_ext_network('net2', dn=self.dn_t1_l1_n1)
self._make_subnet(
self.fmt, {'network': net2}, '200.200.200.1',
'200.200.200.0/24')
res = self._create_subnet(self.fmt,
net_id=net2['id'],
cidr="100.100.100.128/28",
gateway_ip="100.100.100.129",
expected_res_status=400)
res = self.deserialize(self.fmt, res)
self.assertEqual('ExternalSubnetOverlapInL3Out',
res['NeutronError']['type'])
res = self._create_subnet(self.fmt,
net_id=net1['id'],
cidr="200.200.0.0/16",
gateway_ip="200.200.0.129",
expected_res_status=400)
res = self.deserialize(self.fmt, res)
self.assertEqual('ExternalSubnetOverlapInL3Out',
res['NeutronError']['type'])
class TestExternalDistributedNat(TestExternalConnectivityBase,
ApicAimTestCase):

View File

@ -4195,7 +4195,7 @@ class TestImplicitExternalSegment(AIMBaseTestCase):
def _create_default_es(self, **kwargs):
es_sub = self._make_ext_subnet(
'net1', '90.90.0.0/16',
'net1', kwargs.pop('subnet_cidr', '90.90.0.0/16'),
tenant_id=(kwargs.get('tenant_id') or self._tenant_id),
dn=self._dn_t1_l1_n1)
return self.create_external_segment(name=self._default_es_name,
@ -4244,11 +4244,12 @@ class TestImplicitExternalSegment(AIMBaseTestCase):
self.assertEqual(1, len(l3p['external_segments']))
# Verify only one visible ES can exist
res = self._create_default_es(expected_res_status=400)
res = self._create_default_es(expected_res_status=400,
subnet_cidr='10.10.0.0/28')
self.assertEqual('DefaultExternalSegmentAlreadyExists',
res['NeutronError']['type'])
def test_impicit_lifecycle(self):
def test_implicit_lifecycle(self):
self._test_implicit_lifecycle()
def test_implicit_lifecycle_shared(self):
@ -4268,7 +4269,8 @@ class TestImplicitExternalSegment(AIMBaseTestCase):
self.assertEqual(1, len(ep['external_segments']))
res = self._create_default_es(expected_res_status=400,
tenant_id='anothertenant')
tenant_id='anothertenant',
subnet_cidr='10.10.0.0/28')
self.assertEqual('DefaultExternalSegmentAlreadyExists',
res['NeutronError']['type'])
@ -4566,7 +4568,7 @@ class TestNatPool(AIMBaseTestCase):
sub_id = nat_pool['subnet_id']
sub2 = self._make_ext_subnet('net1', '192.167.0.0/24',
dn='uni/tn-t1/out-l1/instP-n2')
dn='uni/tn-t1/out-l2/instP-n2')
es2 = self.create_external_segment(
name="nondefault", subnet_id=sub2['id'],
external_routes=routes,