Router gw port subnet extension

Change-Id: I22c7dee488861fc98be55a3e4b0d2012e7df756c
This commit is contained in:
Christopher Collins 2023-11-01 17:41:14 -07:00
parent 5e605dae20
commit f1b0dcc5f5
9 changed files with 151 additions and 6 deletions

View File

@ -1 +1 @@
fd3e605a7232
e70e4d426ce0

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.
#
"""adds router gw ip pool subnet extension support
Revision ID: e70e4d426ce0
Revises: fd3e605a7232
"""
# revision identifiers, used by Alembic.
revision = 'e70e4d426ce0'
down_revision = 'fd3e605a7232'
from alembic import op
import sqlalchemy as sa
from sqlalchemy import sql
def upgrade():
op.add_column('apic_aim_subnet_extensions',
sa.Column('router_gw_ip_pool', sa.Boolean,
nullable=False, server_default=sql.false()))
pass
def downgrade():
pass

View File

@ -57,6 +57,7 @@ NO_NAT_CIDRS = 'apic:no_nat_cidrs'
MULTI_EXT_NETS = 'apic:multi_ext_nets'
ADVERTISED_EXTERNALLY = 'apic:advertised_externally'
SHARED_BETWEEN_VRFS = 'apic:shared_between_vrfs'
ROUTER_GW_IP_POOL = 'apic:router_gw_ip_pool'
BD = 'BridgeDomain'
EPG = 'EndpointGroup'
@ -424,6 +425,12 @@ EXT_SUBNET_ATTRIBUTES = {
'allow_post': True, 'allow_put': True,
'is_visible': True, 'default': False,
'convert_to': conv.convert_to_boolean,
},
ROUTER_GW_IP_POOL: {
# Whether this subnet is used for snat ip allocation
'allow_post': True, 'allow_put': True,
'is_visible': True, 'default': False,
'convert_to': conv.convert_to_boolean,
}
}

View File

@ -57,6 +57,10 @@ class SnatPoolCannotBeUsedForFloatingIp(exceptions.InvalidInput):
message = _("Floating IP cannot be allocated in SNAT host pool subnet.")
class RouterGWIPPoolCannotBeUsedForFloatingIp(exceptions.InvalidInput):
message = _("Floating IP cannot be allocated in router gw ip pool subnet.")
class PreExistingSVICannotBeConnectedToRouter(exceptions.BadRequest):
message = _("A SVI network with pre-existing l3out is not allowed to "
"be connected to a router.")

View File

@ -169,6 +169,7 @@ class SubnetExtensionDb(model_base.BASEV2):
epg_subnet = sa.Column(sa.Boolean)
advertised_externally = sa.Column(sa.Boolean)
shared_between_vrfs = sa.Column(sa.Boolean)
router_gw_ip_pool = sa.Column(sa.Boolean)
subnet = orm.relationship(models_v2.Subnet,
backref=orm.backref(
'aim_extension_mapping', lazy='joined',
@ -580,6 +581,8 @@ class ExtensionDbMixin(object):
db_obj['advertised_externally'])
self._set_if_not_none(result, cisco_apic.SHARED_BETWEEN_VRFS,
db_obj['shared_between_vrfs'])
self._set_if_not_none(result, cisco_apic.ROUTER_GW_IP_POOL,
db_obj['router_gw_ip_pool'])
return result
def set_subnet_extn_db(self, session, subnet_id, res_dict):
@ -607,6 +610,9 @@ class ExtensionDbMixin(object):
if cisco_apic.SHARED_BETWEEN_VRFS in res_dict:
db_obj['shared_between_vrfs'] = res_dict[
cisco_apic.SHARED_BETWEEN_VRFS]
if cisco_apic.ROUTER_GW_IP_POOL in res_dict:
db_obj['router_gw_ip_pool'] = res_dict[
cisco_apic.ROUTER_GW_IP_POOL]
session.add(db_obj)
def get_router_extn_db(self, session, router_id):

View File

@ -282,6 +282,8 @@ class ApicExtensionDriver(api_plus.ExtensionDriver,
res_dict.get(cisco_apic.ADVERTISED_EXTERNALLY, True))
result[cisco_apic.SHARED_BETWEEN_VRFS] = (
res_dict.get(cisco_apic.SHARED_BETWEEN_VRFS, False))
result[cisco_apic.ROUTER_GW_IP_POOL] = (
res_dict.get(cisco_apic.ROUTER_GW_IP_POOL, False))
except Exception as e:
with excutils.save_and_reraise_exception():
if db_api.is_retriable(e):
@ -307,6 +309,8 @@ class ApicExtensionDriver(api_plus.ExtensionDriver,
res_dict.get(cisco_apic.ADVERTISED_EXTERNALLY, True))
result[cisco_apic.SHARED_BETWEEN_VRFS] = (
res_dict.get(cisco_apic.SHARED_BETWEEN_VRFS, False))
result[cisco_apic.ROUTER_GW_IP_POOL] = (
res_dict.get(cisco_apic.ROUTER_GW_IP_POOL, False))
except Exception as e:
with excutils.save_and_reraise_exception():
if db_api.is_retriable(e):
@ -327,7 +331,9 @@ class ApicExtensionDriver(api_plus.ExtensionDriver,
cisco_apic.ADVERTISED_EXTERNALLY:
data.get(cisco_apic.ADVERTISED_EXTERNALLY, True),
cisco_apic.SHARED_BETWEEN_VRFS:
data.get(cisco_apic.SHARED_BETWEEN_VRFS, False)}
data.get(cisco_apic.SHARED_BETWEEN_VRFS, False),
cisco_apic.ROUTER_GW_IP_POOL:
data.get(cisco_apic.ROUTER_GW_IP_POOL, False)}
self.set_subnet_extn_db(plugin_context.session, result['id'],
res_dict)
result.update(res_dict)
@ -336,7 +342,8 @@ class ApicExtensionDriver(api_plus.ExtensionDriver,
if (cisco_apic.SNAT_HOST_POOL not in data and
cisco_apic.SNAT_SUBNET_ONLY not in data and
cisco_apic.ADVERTISED_EXTERNALLY not in data and
cisco_apic.SHARED_BETWEEN_VRFS not in data):
cisco_apic.SHARED_BETWEEN_VRFS not in data and
cisco_apic.ROUTER_GW_IP_POOL not in data):
return
res_dict = {}
@ -356,6 +363,10 @@ class ApicExtensionDriver(api_plus.ExtensionDriver,
res_dict.update({cisco_apic.SHARED_BETWEEN_VRFS:
data[cisco_apic.SHARED_BETWEEN_VRFS]})
if cisco_apic.ROUTER_GW_IP_POOL in data:
res_dict.update({cisco_apic.ROUTER_GW_IP_POOL:
data[cisco_apic.ROUTER_GW_IP_POOL]})
self.set_subnet_extn_db(plugin_context.session, result['id'],
res_dict)
result.update(res_dict)

View File

@ -5537,6 +5537,8 @@ class ApicMechanismDriver(api_plus.MechanismDriver,
sn_ext = self.get_subnet_extn_db(session, floatingip['subnet_id'])
if sn_ext.get(cisco_apic.SNAT_HOST_POOL, False):
raise exceptions.SnatPoolCannotBeUsedForFloatingIp()
if sn_ext.get(cisco_apic.ROUTER_GW_IP_POOL, False):
raise exceptions.RouterGWIPPoolCannotBeUsedForFloatingIp()
elif floatingip.get('floating_ip_address'):
extn_db_sn = extension_db.SubnetExtensionDb
@ -5547,7 +5549,10 @@ class ApicMechanismDriver(api_plus.MechanismDriver,
extn_db_sn.subnet_id == models_v2.Subnet.id)
query += lambda q: q.filter(
models_v2.Subnet.network_id == sa.bindparam('network_id'))
query += lambda q: q.filter(extn_db_sn.snat_host_pool.is_(True))
query += lambda q: q.filter(sa.or_(
extn_db_sn.snat_host_pool.is_(True),
extn_db_sn.router_gw_ip_pool.is_(True)))
cidrs = query(session).params(
network_id=floatingip['floating_network_id']).all()

View File

@ -201,12 +201,20 @@ class ApicL3Plugin(extraroute_db.ExtraRoute_db_mixin,
# No subnet set, so look for at most one subnet in
# each address family to include in the request,
# skipping any subnets with the SNAT only extension
# set to True.
# set to True while prioritizing those with the
# router gw ip pool extension set to True.
subnets_in_nw = self._core_plugin.get_subnets_by_network(context,
gw_info['network_id'])
subs = {}
for ext_sn in subnets_in_nw:
if (ext_sn['apic:snat_subnet_only'] is False and
ext_sn['apic:snat_host_pool'] is False and
ext_sn['apic:router_gw_ip_pool'] is True and
not subs.get(ext_sn['ip_version'])):
subs[ext_sn['ip_version']] = {'subnet_id': ext_sn['id']}
for ext_sn in subnets_in_nw:
if (ext_sn['apic:snat_subnet_only'] is False and
ext_sn['apic:snat_host_pool'] is False and
not subs.get(ext_sn['ip_version'])):
subs[ext_sn['ip_version']] = {'subnet_id': ext_sn['id']}
gw_info.update({'external_fixed_ips': list(subs.values())})

View File

@ -134,6 +134,7 @@ SNAT_SUBNET_ONLY = 'apic:snat_subnet_only'
EPG_SUBNET = 'apic:epg_subnet'
ADVERTISED_EXTERNALLY = 'apic:advertised_externally'
SHARED_BETWEEN_VRFS = 'apic:shared_between_vrfs'
ROUTER_GW_IP_POOL = 'apic:router_gw_ip_pool'
def sort_if_list(attr):
@ -368,7 +369,8 @@ class ApicAimTestCase(test_address_scope.AddressScopeTestCase,
'provider:network_type',
'apic:multi_ext_nets',
ADVERTISED_EXTERNALLY,
SHARED_BETWEEN_VRFS
SHARED_BETWEEN_VRFS,
ROUTER_GW_IP_POOL
)
self.name_mapper = apic_mapper.APICNameMapper()
self.t1_aname = self.name_mapper.project(None, 't1')
@ -2443,6 +2445,70 @@ class TestAimMapping(ApicAimTestCase):
def test_subnetpool_lifecycle_with_scopes(self):
self._test_subnetpool_lifecycle(use_scopes=True)
def test_router_gw_port(self):
ext_net = self._make_ext_network(
'ext-net', dn=self.dn_t1_l1_n1)
aim_resource.L3Outside(tenant_name=self.t1_aname, name='l1')
subnet = self._create_subnet_with_extension(
self.fmt, ext_net, '10.0.0.1', '10.0.0.0/24',
**{'gateway_ip': '10.0.0.1',
ROUTER_GW_IP_POOL: False})['subnet']
subnet2 = self._create_subnet_with_extension(
self.fmt, ext_net, '20.0.0.1', '20.0.0.0/24',
**{'gateway_ip': '20.0.0.1',
ROUTER_GW_IP_POOL: False})['subnet']
subnet3 = self._create_subnet_with_extension(
self.fmt, ext_net, '30.0.0.1', '30.0.0.0/24',
**{'gateway_ip': '30.0.0.1',
ROUTER_GW_IP_POOL: True})['subnet']
router = self._make_router(
self.fmt, self._tenant_id, 'router1',
external_gateway_info={'network_id': ext_net['id']})['router']
routerb = self._make_router(
self.fmt, self._tenant_id, 'router2',
external_gateway_info={'network_id': ext_net['id']})['router']
self._check_router(router)
self._check_router(routerb)
self.assertEqual(subnet3['id'],
router['external_gateway_info']
['external_fixed_ips'][0]['subnet_id'])
self.assertEqual(subnet3['id'],
routerb['external_gateway_info']
['external_fixed_ips'][0]['subnet_id'])
# Test updating extension
self._update('subnets', subnet2['id'],
{'subnet': {ROUTER_GW_IP_POOL: True}})
router = self._make_router(
self.fmt, self._tenant_id, 'router3',
external_gateway_info={'network_id': ext_net['id']})['router']
self.assertEqual(subnet2['id'],
router['external_gateway_info']
['external_fixed_ips'][0]['subnet_id'])
self._update('subnets', subnet2['id'],
{'subnet': {ROUTER_GW_IP_POOL: False}})
router = self._make_router(
self.fmt, self._tenant_id, 'router4',
external_gateway_info={'network_id': ext_net['id']})['router']
self.assertEqual(subnet3['id'],
router['external_gateway_info']
['external_fixed_ips'][0]['subnet_id'])
self._update('subnets', subnet['id'],
{'subnet': {ROUTER_GW_IP_POOL: True}})
router = self._make_router(
self.fmt, self._tenant_id, 'router4',
external_gateway_info={'network_id': ext_net['id']})['router']
self.assertEqual(subnet['id'],
router['external_gateway_info']
['external_fixed_ips'][0]['subnet_id'])
def test_router_lifecycle(self):
# Test create.
router = self._make_router(