Support for multi external networks extention

The multi external networks extention allows multiple external networks
to be associated with a single L3Outside.

Change-Id: Ib872d8661fae321270130b4986d7d21249919ae6
This commit is contained in:
christides11 2023-07-03 16:51:09 -07:00
parent f9c7a63f7a
commit 2edc1ab5c5
8 changed files with 270 additions and 38 deletions

View File

@ -0,0 +1,38 @@
# 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 l3out multiple epgs network extension
Revision ID: 8c5b556b4df1
Revises: e29a84f6a15f
"""
# revision identifiers, used by Alembic.
revision = '8c5b556b4df1'
down_revision = 'e29a84f6a15f'
from alembic import op
import sqlalchemy as sa
from sqlalchemy import sql
def upgrade():
op.add_column('apic_aim_network_extensions',
sa.Column('multi_ext_nets', sa.Boolean,
nullable=False, server_default=sql.false()))
pass
def downgrade():
pass

View File

@ -1 +1 @@
e29a84f6a15f
8c5b556b4df1

View File

@ -54,6 +54,7 @@ POLICY_ENFORCEMENT_PREF = 'apic:policy_enforcement_pref'
SNAT_SUBNET_ONLY = 'apic:snat_subnet_only'
EPG_SUBNET = 'apic:epg_subnet'
NO_NAT_CIDRS = 'apic:no_nat_cidrs'
MULTI_EXT_NETS = 'apic:multi_ext_nets'
BD = 'BridgeDomain'
EPG = 'EndpointGroup'
@ -361,6 +362,11 @@ NET_ATTRIBUTES = {
'convert_to': convert_apic_none_to_empty_list,
'validate': {'type:list_of_unique_strings': None},
},
MULTI_EXT_NETS: {
'allow_post': True, 'allow_put': False,
'is_visible': True, 'default': False,
'convert_to': conv.convert_to_boolean,
},
}
EXT_NET_ATTRIBUTES = {

View File

@ -153,3 +153,8 @@ class InvalidNetworkForErspanSession(exceptions.BadRequest):
class SnatPoolCannotBeUsedForGatewayIp(exceptions.BadRequest):
message = _("Snat only subnet cannot be used to assign network gateway.")
class MultiExtNetworkMixing(exceptions.BadRequest):
message = _("All external networks associated with a l3out must "
"use the same apic:multi_ext_nets setting. ")

View File

@ -79,6 +79,7 @@ class NetworkExtensionDb(model_base.BASEV2):
nested_domain_infra_vlan = sa.Column(sa.Integer, nullable=True)
nested_domain_service_vlan = sa.Column(sa.Integer, nullable=True)
nested_domain_node_network_vlan = sa.Column(sa.Integer, nullable=True)
multi_ext_nets = sa.Column(sa.Boolean, default=False, nullable=False)
class NetworkExtensionCidrDb(model_base.BASEV2):
@ -369,6 +370,7 @@ class ExtensionDbMixin(object):
'policy_enforcement_pref']
net_res[cisco_apic.NO_NAT_CIDRS] = [
c.cidr for c in db_no_nat_cidrs]
net_res[cisco_apic.MULTI_EXT_NETS] = db_obj['multi_ext_nets']
if net_res.get(cisco_apic.EXTERNAL_NETWORK):
net_res[cisco_apic.EXTERNAL_CIDRS] = [c.cidr for c in db_cidrs]
return net_res
@ -417,6 +419,8 @@ class ExtensionDbMixin(object):
if cisco_apic.POLICY_ENFORCEMENT_PREF in res_dict:
db_obj['policy_enforcement_pref'] = res_dict[
cisco_apic.POLICY_ENFORCEMENT_PREF]
if cisco_apic.MULTI_EXT_NETS in res_dict:
db_obj['multi_ext_nets'] = res_dict[cisco_apic.MULTI_EXT_NETS]
session.add(db_obj)
if cisco_apic.EXTERNAL_CIDRS in res_dict:
@ -513,6 +517,15 @@ class ExtensionDbMixin(object):
return [c[0] for c in cidrs]
def get_external_cidrs_by_net_id(self, session, nid):
query = BAKERY(lambda s: s.query(
NetworkExtensionCidrDb.cidr))
query += lambda q: q.filter_by(
network_id=sa.bindparam('nid'))
cidrs = query(session).params(nid=nid)
return [i[0] for i in cidrs]
def get_subnet_extn_db(self, session, subnet_id):
query = BAKERY(lambda s: s.query(
SubnetExtensionDb))
@ -590,6 +603,17 @@ class ExtensionDbMixin(object):
c_contracts.append(db_contract['contract_name'])
return attr_dict
def get_network_ids_and_multi_by_l3out_dn(self, session, dn):
query = BAKERY(lambda s: s.query(
NetworkExtensionDb.network_id,
NetworkExtensionDb.multi_ext_nets))
query += lambda q: q.filter(
NetworkExtensionDb.external_network_dn.like(
sa.bindparam('dn') + "/%"))
ids_and_multis = query(session).params(dn=dn)
return [(i[0], i[1]) for i in ids_and_multis]
def _update_list_attr(self, session, db_model, column,
new_values, **filters):
if new_values is None:

View File

@ -137,6 +137,7 @@ class ApicExtensionDriver(api_plus.ExtensionDriver,
is_bgp_enabled = data.get(cisco_apic.BGP, False)
bgp_type = data.get(cisco_apic.BGP_TYPE, "default_export")
asn = data.get(cisco_apic.BGP_ASN, "0")
use_multi_ext_nets = data.get(cisco_apic.MULTI_EXT_NETS, False)
self.validate_bgp_params(data)
res_dict = {cisco_apic.SVI: is_svi,
cisco_apic.BGP: is_bgp_enabled,
@ -162,6 +163,7 @@ class ApicExtensionDriver(api_plus.ExtensionDriver,
data.get(cisco_apic.POLICY_ENFORCEMENT_PREF, "unenforced"),
cisco_apic.NO_NAT_CIDRS:
data.get(cisco_apic.NO_NAT_CIDRS),
cisco_apic.MULTI_EXT_NETS: use_multi_ext_nets,
}
if cisco_apic.VLANS_LIST in (data.get(
cisco_apic.NESTED_DOMAIN_ALLOWED_VLANS) or {}):
@ -225,8 +227,9 @@ class ApicExtensionDriver(api_plus.ExtensionDriver,
cisco_apic.EXTRA_CONSUMED_CONTRACTS,
cisco_apic.EPG_CONTRACT_MASTERS,
cisco_apic.POLICY_ENFORCEMENT_PREF,
cisco_apic.NO_NAT_CIDRS]
if not(set(update_attrs) & set(data.keys())):
cisco_apic.NO_NAT_CIDRS,
cisco_apic.MULTI_EXT_NETS]
if not (set(update_attrs) & set(data.keys())):
return
res_dict = {}
@ -246,7 +249,8 @@ class ApicExtensionDriver(api_plus.ExtensionDriver,
cisco_apic.EXTRA_CONSUMED_CONTRACTS,
cisco_apic.EPG_CONTRACT_MASTERS,
cisco_apic.POLICY_ENFORCEMENT_PREF,
cisco_apic.NO_NAT_CIDRS]
cisco_apic.NO_NAT_CIDRS,
cisco_apic.MULTI_EXT_NETS]
for e_k in ext_keys:
if e_k in data:
res_dict.update({e_k: data[e_k]})

View File

@ -809,6 +809,8 @@ class ApicMechanismDriver(api_plus.MechanismDriver,
aim_ctx = aim_context.AimContext(session)
is_ext = self._is_external(current)
multi_ext_nets_enb = self._is_multi_ext_nets_enabled(current)
wanted_epg_name = current['id'] if multi_ext_nets_enb else None
is_svi = self._is_svi(current)
if ((current[cisco_apic.EXTRA_PROVIDED_CONTRACTS] or
@ -830,17 +832,28 @@ class ApicMechanismDriver(api_plus.MechanismDriver,
l3out, ext_net, ns = self._get_aim_nat_strategy(current)
if not ext_net:
return # Unmanaged external network
other_nets = self.get_network_ids_and_multi_by_l3out_dn(
session, l3out.dn)
other_nets = [value for value in other_nets
if value[0] != current['id']]
if len(other_nets) > 0 and other_nets[0][1] != multi_ext_nets_enb:
raise exceptions.MultiExtNetworkMixing()
domains = self._get_vmm_domains(aim_ctx, ns)
ns.create_l3outside(aim_ctx, l3out, vmm_domains=domains)
ns.create_external_network(aim_ctx, ext_net)
ns.create_l3outside(aim_ctx, l3out, vmm_domains=domains,
epg_name=wanted_epg_name)
ns.create_external_network(aim_ctx, ext_net,
epg_name=wanted_epg_name)
# Get external CIDRs for all external networks that share
# this APIC external network.
cidrs = sorted(
self.get_external_cidrs_by_ext_net_dn(
session, ext_net.dn, lock_update=True))
session, ext_net.dn))
ns.update_external_cidrs(aim_ctx, ext_net, cidrs)
for resource in ns.get_l3outside_resources(aim_ctx, l3out):
for resource in ns.get_l3outside_resources(aim_ctx, l3out,
epg_name=wanted_epg_name):
if isinstance(resource, aim_resource.BridgeDomain):
bd = resource
elif isinstance(resource, aim_resource.EndpointGroup):
@ -1207,21 +1220,33 @@ class ApicMechanismDriver(api_plus.MechanismDriver,
l3out, ext_net, ns = self._get_aim_nat_strategy(current)
if not ext_net:
return # Unmanaged external network
# REVISIT: lock_update=True is needed to handle races. Find
# alternative solutions since Neutron discourages using such
# queries.
other_nets = set(
self.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(
self.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)
multi_ext_nets_enb = self._is_multi_ext_nets_enabled(current)
if multi_ext_nets_enb:
cidrs_to_delete = self.get_external_cidrs_by_net_id(
session, current['id'])
ns.delete_external_network(aim_ctx, ext_net,
epg_name=current['id'],
cidrs=cidrs_to_delete)
ns.delete_l3outside(aim_ctx, l3out, epg_name=current['id'],
cidrs=cidrs_to_delete)
else:
# REVISIT: lock_update=True is needed to handle races. Find
# alternative solutions since Neutron discourages using such
# queries.
other_nets = set(
self.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(
self.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_objects(current)
aim_l3out = self.aim.get(aim_ctx, l3out)
@ -4768,6 +4793,9 @@ class ApicMechanismDriver(api_plus.MechanismDriver,
def _is_bgp_enabled(self, network):
return network.get(cisco_apic.BGP)
def _is_multi_ext_nets_enabled(self, network):
return network.get(cisco_apic.MULTI_EXT_NETS)
def _nat_type_to_strategy(self, nat_type):
ns_cls = nat_strategy.DistributedNatStrategy
if nat_type == '':
@ -7130,6 +7158,7 @@ class ApicMechanismDriver(api_plus.MechanismDriver,
cisco_apic.NESTED_DOMAIN_INFRA_VLAN: None,
cisco_apic.NESTED_DOMAIN_SERVICE_VLAN: None,
cisco_apic.NESTED_DOMAIN_NODE_NETWORK_VLAN: None,
cisco_apic.MULTI_EXT_NETS: False,
}
if net_db.aim_mapping and net_db.aim_mapping.get(cisco_apic.BD):
res_dict.update({cisco_apic.BD: net_db.aim_mapping[cisco_apic.BD]})

View File

@ -359,7 +359,8 @@ class ApicAimTestCase(test_address_scope.AddressScopeTestCase,
ACTIVE_ACTIVE_AAP, EPG_SUBNET,
CIDR, PROV, CONS, SVI,
BGP, BGP_TYPE, ASN,
'provider:network_type'
'provider:network_type',
'apic:multi_ext_nets'
)
self.name_mapper = apic_mapper.APICNameMapper()
self.t1_aname = self.name_mapper.project(None, 't1')
@ -449,7 +450,8 @@ class ApicAimTestCase(test_address_scope.AddressScopeTestCase,
self.fmt)
return self.deserialize(self.fmt, req.get_response(self.api))
def _make_ext_network(self, name, dn=None, nat_type=None, cidrs=None):
def _make_ext_network(self, name, dn=None, nat_type=None, cidrs=None,
multi_ext_nets=False):
kwargs = {'router:external': True}
if dn:
kwargs[DN] = {'ExternalNetwork': dn}
@ -459,6 +461,8 @@ class ApicAimTestCase(test_address_scope.AddressScopeTestCase,
kwargs['apic:nat_type'] = self.nat_type
if cidrs:
kwargs[CIDR] = cidrs
if multi_ext_nets:
kwargs['apic:multi_ext_nets'] = True
return self._make_network(self.fmt, name, True,
arg_list=self.extension_attributes,
@ -4114,8 +4118,10 @@ class TestSyncState(ApicAimTestCase):
TestSyncState._mocked_get_statuses):
self._test_router('error')
def _test_external_network(self, expected_state, dn=None, msg=None):
net = self._make_ext_network('net1', dn=dn)
def _test_external_network(self, expected_state, dn=None, msg=None,
multi_ext_nets=True):
net = self._make_ext_network('net1', dn=dn,
multi_ext_nets=multi_ext_nets)
self.assertEqual(expected_state, net['apic:synchronization_state'],
msg)
net = self._show('networks', net['id'])['network']
@ -4156,6 +4162,38 @@ class TestSyncState(ApicAimTestCase):
dn=self.dn_t1_l1_n1,
msg='%s' % a_res)
def test_external_network_multi_ext_networks(self):
ext_net = aim_resource.ExternalNetwork.from_dn(self.dn_t1_l1_n1)
ext_net.monitored = True
aim_ctx = aim_context.AimContext(self.db_session)
self.aim_mgr.create(aim_ctx, ext_net)
with mock.patch('aim.aim_manager.AimManager.get_status',
TestSyncState._get_synced_status):
with mock.patch('aim.aim_manager.AimManager.get_statuses',
TestSyncState._mocked_get_statuses):
self._test_external_network('synced',
dn=self.dn_t1_l1_n1,
multi_ext_nets=True)
for expected_status, status_func in [
('build', TestSyncState._get_pending_status_for_type),
('error', TestSyncState._get_failed_status_for_type)]:
for a_res in [aim_resource.ExternalNetwork,
aim_resource.EndpointGroup,
aim_resource.BridgeDomain,
aim_resource.VRF]:
def get_status(self, context, resource, create_if_absent=True):
return status_func(context, resource, a_res)
with mock.patch('aim.aim_manager.AimManager.get_status',
get_status):
with mock.patch('aim.aim_manager.AimManager.get_statuses',
TestSyncState._mocked_get_statuses):
self._test_external_network(expected_status,
dn=self.dn_t1_l1_n1,
msg='%s' % a_res,
multi_ext_nets=True)
def test_unmanaged_external_network(self):
self._test_external_network('build')
@ -7262,6 +7300,94 @@ class TestExtensionAttributes(ApicAimTestCase):
self.assertFalse(extn.get_network_extn_db(session, net1['id']))
self.assertFalse(extn.get_network_extn_db(session, net2['id']))
def test_external_network_with_multi_nets_lifecycle(self):
session = db_api.get_reader_session()
extn = extn_db.ExtensionDbMixin()
# create with APIC DN, nat_typeand default CIDR
net1 = self._make_ext_network('net1',
dn=self.dn_t1_l1_n1,
nat_type='',
multi_ext_nets=True)
self.assertEqual(self.dn_t1_l1_n1,
net1[DN]['ExternalNetwork'])
self.assertEqual('', net1['apic:nat_type'])
self.assertEqual(['0.0.0.0/0'], net1[CIDR])
net1 = self._list(
'networks', query_params=('id=%s' % net1['id']))['networks'][0]
self.assertEqual(self.dn_t1_l1_n1,
net1[DN]['ExternalNetwork'])
self.assertEqual('', net1['apic:nat_type'])
self.assertEqual(['0.0.0.0/0'], net1[CIDR])
# create with nat_type set to default, and CIDR specified
net2 = self._make_ext_network('net2',
dn=self.dn_t1_l2_n2,
cidrs=['5.5.5.0/24', '10.20.0.0/16'],
multi_ext_nets=True)
self.assertEqual('distributed', net2['apic:nat_type'])
self.assertEqual(['10.20.0.0/16', '5.5.5.0/24'],
sorted(net2[CIDR]))
net2 = self._list(
'networks', query_params=('id=%s' % net2['id']))['networks'][0]
self.assertEqual('distributed', net2['apic:nat_type'])
self.assertEqual(['10.20.0.0/16', '5.5.5.0/24'],
sorted(net2[CIDR]))
# update CIDR
net2 = self._update('networks', net2['id'],
{'network': {CIDR: ['20.20.30.0/24']}})['network']
self.assertEqual('distributed', net2['apic:nat_type'])
self.assertEqual(['20.20.30.0/24'], net2[CIDR])
net2 = self._list(
'networks', query_params=('id=%s' % net2['id']))['networks'][0]
self.assertEqual('distributed', net2['apic:nat_type'])
self.assertEqual(['20.20.30.0/24'], net2[CIDR])
net2 = self._update('networks', net2['id'],
{'network': {CIDR: []}})['network']
self.assertEqual([], net2[CIDR])
net2 = self._list(
'networks', query_params=('id=%s' % net2['id']))['networks'][0]
self.assertEqual([], net2[CIDR])
# create without APIC DN -> this is an unmanaged network
net3 = self._make_ext_network('net3')
self.assertTrue(DN not in net3 or 'ExternalNetwork' not in net3[DN])
self.assertFalse('apic:nat_type' in net3)
self.assertFalse(CIDR in net3)
net3 = self._list(
'networks', query_params=('id=%s' % net3['id']))['networks'][0]
self.assertTrue(DN not in net3 or 'ExternalNetwork' not in net3[DN])
self.assertFalse('apic:nat_type' in net3)
self.assertFalse(CIDR in net3)
# updating CIDR of unmanaged network is no-op
net3 = self._update('networks', net3['id'],
{'network': {CIDR: ['30.30.20.0/24']}})['network']
self.assertTrue(DN not in net3 or 'ExternalNetwork' not in net3[DN])
self.assertFalse('apic:nat_type' in net3)
self.assertFalse(CIDR in net3)
net3 = self._list(
'networks', query_params=('id=%s' % net3['id']))['networks'][0]
self.assertTrue(DN not in net3 or 'ExternalNetwork' not in net3[DN])
self.assertFalse('apic:nat_type' in net3)
self.assertFalse(CIDR in net3)
# delete the external networks
self._delete('networks', net2['id'])
self._delete('networks', net1['id'])
self.assertFalse(extn.get_network_extn_db(session, net1['id']))
self.assertFalse(extn.get_network_extn_db(session, net2['id']))
def test_external_network_fail(self):
# APIC DN not specified
resp = self._create_network(self.fmt, 'net1', True,
@ -8033,11 +8159,11 @@ class TestExternalConnectivityBase(object):
self.mock_ns.create_l3outside.assert_called_once_with(
mock.ANY,
aim_resource.L3Outside(tenant_name=self.t1_aname, name='l1'),
vmm_domains=[])
vmm_domains=[], epg_name=None)
a_ext_net = aim_resource.ExternalNetwork(
tenant_name=self.t1_aname, l3out_name='l1', name='n1')
self.mock_ns.create_external_network.assert_called_once_with(
mock.ANY, a_ext_net)
mock.ANY, a_ext_net, epg_name=None)
self.mock_ns.update_external_cidrs.assert_called_once_with(
mock.ANY, a_ext_net, ['20.10.0.0/16', '4.4.4.0/24'])
ext_epg = aim_resource.EndpointGroup(
@ -8086,7 +8212,7 @@ class TestExternalConnectivityBase(object):
self._make_ext_network('net2',
dn=self.dn_t1_l1_n1)
self.mock_ns.create_external_network.assert_called_once_with(
mock.ANY, a_ext_net)
mock.ANY, a_ext_net, epg_name=None)
self.mock_ns.update_external_cidrs.assert_called_once_with(
mock.ANY, a_ext_net, ['0.0.0.0/0'])
self._validate()
@ -9161,11 +9287,11 @@ class TestExternalConnectivityBase(object):
self.mock_ns.create_l3outside.assert_called_once_with(
mock.ANY,
aim_resource.L3Outside(tenant_name=self.t1_aname, name='l1'),
vmm_domains=vmm_domains)
vmm_domains=vmm_domains, epg_name=None)
a_ext_net = aim_resource.ExternalNetwork(
tenant_name=self.t1_aname, l3out_name='l1', name='n1')
self.mock_ns.create_external_network.assert_called_once_with(
mock.ANY, a_ext_net)
mock.ANY, a_ext_net, epg_name=None)
self.mock_ns.update_external_cidrs.assert_called_once_with(
mock.ANY, a_ext_net, ['20.10.0.0/16', '4.4.4.0/24'])
ext_epg = self.aim_mgr.find(aim_ctx, aim_resource.EndpointGroup,
@ -9214,17 +9340,17 @@ class TestExternalConnectivityBase(object):
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=[])
mock.ANY, a_l3out, vmm_domains=[], epg_name=None)
self.mock_ns.create_external_network.assert_called_once_with(
mock.ANY, a_ext_net)
mock.ANY, a_ext_net, epg_name=None)
# 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=[])
mock.ANY, a_l3out, vmm_domains=[], epg_name=None)
self.mock_ns.create_external_network.assert_called_once_with(
mock.ANY, a_ext_net)
mock.ANY, a_ext_net, epg_name=None)
# create third network that shares APIC l3out only
self.mock_ns.reset_mock()
@ -9233,9 +9359,9 @@ class TestExternalConnectivityBase(object):
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=[])
mock.ANY, a_l3out, vmm_domains=[], epg_name=None)
self.mock_ns.create_external_network.assert_called_once_with(
mock.ANY, a_ext_net3)
mock.ANY, a_ext_net3, epg_name=None)
# delete net2
self.mock_ns.reset_mock()