[ovn] Implement support for external-gateway-multihoming extension

The general approach is to update the ovn_client and ovn_db_sync
code to handle multiple gateway ports wherever a single gateway
port is handled today.

In this patch set multiple static routes for default gateway will
be added by default when multiple gw ports are present.  Support
for the `enable_default_route_ecmp` attribute to control this
behavior will be added in subsequent patch set to avoid making
this change too large.

Partial-Bug: #2002687
Change-Id: I00b1f29172be5a0034b921b11af3a8d502273766
This commit is contained in:
Frode Nordahl 2023-03-21 08:54:22 +01:00 committed by Brian Haley
parent 3ef02cc2fb
commit 0bc9a71387
10 changed files with 237 additions and 140 deletions

View File

@ -41,6 +41,7 @@ from neutron_lib.api.definitions import floating_ip_port_forwarding
from neutron_lib.api.definitions import floatingip_pools
from neutron_lib.api.definitions import l3
from neutron_lib.api.definitions import l3_ext_gw_mode
from neutron_lib.api.definitions import l3_ext_gw_multihoming
from neutron_lib.api.definitions import l3_flavors
from neutron_lib.api.definitions import logging
from neutron_lib.api.definitions import multiprovidernet
@ -118,6 +119,7 @@ ML2_SUPPORTED_API_EXTENSIONS_OVN_L3 = [
raz_def.ALIAS,
flavors.ALIAS,
l3_flavors.ALIAS,
l3_ext_gw_multihoming.ALIAS,
]
ML2_SUPPORTED_API_EXTENSIONS = [
address_group.ALIAS,

View File

@ -19,6 +19,7 @@ import datetime
import netaddr
from neutron_lib.api.definitions import l3
from neutron_lib.api.definitions import l3_ext_gw_multihoming
from neutron_lib.api.definitions import port_security as psec
from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import provider_net as pnet
@ -1180,17 +1181,21 @@ class OVNClient(object):
LOG.error('Unable to disassociate floating ip in gateway '
'router. Error: %s', e)
def _get_gw_info(self, context, router):
def _get_gw_info(self, context, port_dict):
gateways_info = []
ext_gw_info = router.get(l3.EXTERNAL_GW_INFO, {})
network_id = ext_gw_info.get('network_id', '')
for ext_fixed_ip in ext_gw_info.get('external_fixed_ips', []):
subnet_id = ext_fixed_ip['subnet_id']
subnet = self._plugin.get_subnet(context, subnet_id)
network_id = port_dict.get('network_id')
subnet_by_id = {
subnet['id']: subnet
for subnet in self._plugin.get_subnets_by_network(
context, network_id)}
for fixed_ip in port_dict.get('fixed_ips'):
subnet_id = fixed_ip.get('subnet_id')
subnet = subnet_by_id.get(subnet_id)
ip_version = subnet.get('ip_version')
gateways_info.append(GW_INFO(
network_id, subnet_id, ext_fixed_ip['ip_address'],
subnet.get('gateway_ip'), subnet['ip_version'],
const.IPv4_ANY if subnet['ip_version'] == const.IP_VERSION_4
network_id, subnet_id, fixed_ip.get('ip_address'),
subnet.get('gateway_ip'), ip_version,
const.IPv4_ANY if ip_version == const.IP_VERSION_4
else const.IPv6_ANY))
return gateways_info
@ -1199,21 +1204,23 @@ class OVNClient(object):
if not networks:
networks = []
router_id = router['id']
gw_port_id = router['gw_port_id']
gw_lrouter_name = utils.ovn_name(router_id)
gateways = self._get_gw_info(context, router)
for gw_info in gateways:
if gw_info.ip_version == const.IP_VERSION_4:
for network in networks:
txn.add(self._nb_idl.delete_nat_rule_in_lrouter(
gw_lrouter_name, type='snat', logical_ip=network,
external_ip=gw_info.router_ip))
txn.add(self._nb_idl.delete_static_route(
gw_lrouter_name, ip_prefix=gw_info.ip_prefix,
nexthop=gw_info.gateway_ip))
txn.add(self._nb_idl.delete_lrouter_port(
utils.ovn_lrouter_port_name(gw_port_id),
gw_lrouter_name))
deleted_ports = []
for gw_port in self._get_router_gw_ports(context, router_id):
for gw_info in self._get_gw_info(context, gw_port):
if gw_info.ip_version == const.IP_VERSION_4:
for network in networks:
txn.add(self._nb_idl.delete_nat_rule_in_lrouter(
gw_lrouter_name, type='snat', logical_ip=network,
external_ip=gw_info.router_ip))
txn.add(self._nb_idl.delete_static_route(
gw_lrouter_name, ip_prefix=gw_info.ip_prefix,
nexthop=gw_info.gateway_ip))
txn.add(self._nb_idl.delete_lrouter_port(
utils.ovn_lrouter_port_name(gw_port['id']),
gw_lrouter_name))
deleted_ports.append(gw_port['id'])
return deleted_ports
def _get_nets_and_ipv6_ra_confs_for_router_port(self, context, port):
port_fixed_ips = port['fixed_ips']
@ -1249,34 +1256,35 @@ class OVNClient(object):
return list(networks), ipv6_ra_configs
def _add_router_ext_gw(self, context, router, networks, txn):
lrouter_name = utils.ovn_name(router['id'])
# 1. Add the external gateway router port.
admin_context = context.elevated()
gateways = self._get_gw_info(admin_context, router)
gw_port_id = router['gw_port_id']
port = self._plugin.get_port(admin_context, gw_port_id)
self._create_lrouter_port(admin_context, router, port, txn=txn)
added_ports = []
for gw_port in self._get_router_gw_ports(admin_context, router['id']):
port = self._plugin.get_port(admin_context, gw_port['id'])
self._create_lrouter_port(admin_context, router, port, txn=txn)
added_ports.append(port)
# 2. Add default route with nexthop as gateway ip
lrouter_name = utils.ovn_name(router['id'])
for gw_info in gateways:
if gw_info.gateway_ip is None:
continue
columns = {'external_ids': {
ovn_const.OVN_ROUTER_IS_EXT_GW: 'true',
ovn_const.OVN_SUBNET_EXT_ID_KEY: gw_info.subnet_id}}
txn.add(self._nb_idl.add_static_route(
lrouter_name, ip_prefix=gw_info.ip_prefix,
nexthop=gw_info.gateway_ip, **columns))
# 2. Add default route with nexthop as gateway ip
for gw_info in self._get_gw_info(admin_context, gw_port):
if gw_info.gateway_ip is None:
continue
columns = {'external_ids': {
ovn_const.OVN_ROUTER_IS_EXT_GW: 'true',
ovn_const.OVN_SUBNET_EXT_ID_KEY: gw_info.subnet_id}}
txn.add(self._nb_idl.add_static_route(
lrouter_name, ip_prefix=gw_info.ip_prefix,
nexthop=gw_info.gateway_ip, **columns))
# 3. Add snat rules for tenant networks in lrouter if snat is enabled
if utils.is_snat_enabled(router) and networks:
self.update_nat_rules(router, networks, enable_snat=True, txn=txn)
return port
return added_ports
def _check_external_ips_changed(self, ovn_snats,
ovn_static_routes, router):
context = n_context.get_admin_context()
gateways = self._get_gw_info(context, router)
ovn_gw_subnets = None
if self._nb_idl.is_col_present('Logical_Router_Static_Route',
'external_ids'):
@ -1285,15 +1293,29 @@ class OVNClient(object):
ovn_const.OVN_SUBNET_EXT_ID_KEY) for route in
ovn_static_routes]
for gw_info in gateways:
if ovn_gw_subnets and gw_info.subnet_id not in ovn_gw_subnets:
return True
if gw_info.ip_version == const.IP_VERSION_6:
continue
for snat in ovn_snats:
if snat.external_ip != gw_info.router_ip:
for gw_port in self._get_router_gw_ports(context, router['id']):
gw_infos = self._get_gw_info(context, gw_port)
if not gw_infos:
# The router is attached to a external network without a subnet
lrp = self._nb_idl.get_lrouter_port(
utils.ovn_lrouter_port_name(gw_port['id']))
if not lrp:
continue
lrp_ext_ids = getattr(lrp, 'external_ids', {})
if (ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY in lrp_ext_ids and
lrp_ext_ids[ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY] != (
utils.ovn_name(gw_port['network_id']))):
return True
for gw_info in gw_infos:
if ovn_gw_subnets and gw_info.subnet_id not in ovn_gw_subnets:
return True
if gw_info.ip_version == const.IP_VERSION_6:
continue
for snat in ovn_snats:
if snat.external_ip != gw_info.router_ip:
return True
return False
def update_router_routes(self, context, router_id, add, remove,
@ -1315,7 +1337,9 @@ class OVNClient(object):
self._transaction(commands, txn=txn)
def _get_router_gw_ports(self, context, router_id):
return self._plugin.get_ports(context, filters={
# NOTE(fnordahl): an elevated context is required here to ensure we
# have access to the data.
return self._plugin.get_ports(context.elevated(), filters={
'device_owner': [const.DEVICE_OWNER_ROUTER_GW],
'device_id': [router_id]})
@ -1367,7 +1391,7 @@ class OVNClient(object):
external_ids = self._gen_router_ext_ids(router)
enabled = router.get('admin_state_up')
lrouter_name = utils.ovn_name(router['id'])
added_gw_port = None
added_gw_ports = []
options = {'always_learn_from_arp_request': 'false',
'dynamic_neigh_routers': 'true',
ovn_const.LR_OPTIONS_MAC_AGE_LIMIT:
@ -1383,14 +1407,15 @@ class OVNClient(object):
if add_external_gateway:
networks = self._get_v4_network_of_all_router_ports(
context, router['id'])
if router.get(l3.EXTERNAL_GW_INFO) and networks is not None:
added_gw_port = self._add_router_ext_gw(
if (router.get(l3_ext_gw_multihoming.EXTERNAL_GATEWAYS) and
networks is not None):
added_gw_ports = self._add_router_ext_gw(
context, router, networks, txn)
self._qos_driver.create_router(txn, router)
if added_gw_port:
db_rev.bump_revision(context, added_gw_port,
for gw_port in added_gw_ports:
db_rev.bump_revision(context, gw_port,
ovn_const.TYPE_ROUTER_PORTS)
db_rev.bump_revision(context, router, ovn_const.TYPE_ROUTERS)
@ -1402,13 +1427,16 @@ class OVNClient(object):
router_id = new_router['id']
router_name = utils.ovn_name(router_id)
ovn_router = self._nb_idl.get_lrouter(router_name)
gateway_new = new_router.get(l3.EXTERNAL_GW_INFO)
# Note that this needs to be retrieved from the request
gateway_new = new_router.get(l3_ext_gw_multihoming.EXTERNAL_GATEWAYS)
gateway_old = utils.get_lrouter_ext_gw_static_route(ovn_router)
added_gw_port = None
deleted_gw_port_id = None
added_gw_ports = []
deleted_gw_port_ids = []
if router_object:
gateway_old = gateway_old or router_object.get(l3.EXTERNAL_GW_INFO)
gateway_old = gateway_old or router_object.get(
l3_ext_gw_multihoming.EXTERNAL_GATEWAYS)
ovn_snats = utils.get_lrouter_snats(ovn_router)
networks = self._get_v4_network_of_all_router_ports(context, router_id)
try:
@ -1418,15 +1446,14 @@ class OVNClient(object):
txn.add(check_rev_cmd)
if gateway_new and not gateway_old:
# Route gateway is set
added_gw_port = self._add_router_ext_gw(
added_gw_ports = self._add_router_ext_gw(
context, new_router, networks, txn)
elif gateway_old and not gateway_new:
# router gateway is removed
txn.add(self._nb_idl.delete_lrouter_ext_gw(router_name))
if router_object:
self._delete_router_ext_gw(
deleted_gw_port_ids = self._delete_router_ext_gw(
router_object, networks, txn)
deleted_gw_port_id = router_object['gw_port_id']
elif gateway_new and gateway_old:
# Check if external gateway has changed, if yes, delete
# the old gateway and add the new gateway
@ -1435,14 +1462,13 @@ class OVNClient(object):
txn.add(self._nb_idl.delete_lrouter_ext_gw(
router_name))
if router_object:
self._delete_router_ext_gw(
deleted_gw_port_ids = self._delete_router_ext_gw(
router_object, networks, txn)
deleted_gw_port_id = router_object['gw_port_id']
added_gw_port = self._add_router_ext_gw(
added_gw_ports = self._add_router_ext_gw(
context, new_router, networks, txn)
else:
# Check if snat has been enabled/disabled and update
new_snat_state = gateway_new.get('enable_snat', True)
new_snat_state = utils.is_snat_enabled(new_router)
if bool(ovn_snats) != new_snat_state and networks:
self.update_nat_rules(
new_router, networks,
@ -1465,12 +1491,12 @@ class OVNClient(object):
db_rev.bump_revision(context, new_router,
ovn_const.TYPE_ROUTERS)
if added_gw_port:
db_rev.bump_revision(context, added_gw_port,
for gw_port in added_gw_ports:
db_rev.bump_revision(context, gw_port,
ovn_const.TYPE_ROUTER_PORTS)
if deleted_gw_port_id:
db_rev.delete_revision(context, deleted_gw_port_id,
for gw_port in deleted_gw_port_ids:
db_rev.delete_revision(context, gw_port,
ovn_const.TYPE_ROUTER_PORTS)
except Exception as e:
@ -1692,7 +1718,8 @@ class OVNClient(object):
else:
self._create_lrouter_port(context, router, port, txn=txn)
if router.get(l3.EXTERNAL_GW_INFO):
gw_ports = self._get_router_gw_ports(context, router_id)
if gw_ports:
cidr = None
for fixed_ip in port['fixed_ips']:
subnet = self._plugin.get_subnet(context,
@ -1705,9 +1732,10 @@ class OVNClient(object):
cidr = subnet['cidr']
if ovn_conf.is_ovn_emit_need_to_frag_enabled():
provider_net = self._plugin.get_network(
context, router[l3.EXTERNAL_GW_INFO]['network_id'])
self.set_gateway_mtu(context, provider_net)
for gw_port in gw_ports:
provider_net = self._plugin.get_network(
context, gw_port['network_id'])
self.set_gateway_mtu(context, provider_net)
if utils.is_snat_enabled(router) and cidr:
self.update_nat_rules(router, networks=[cidr],
@ -1816,14 +1844,16 @@ class OVNClient(object):
router_id = port.get('device_id')
router = None
gw_ports = []
if router_id:
try:
router = self._l3_plugin.get_router(context, router_id)
gw_ports = self._get_router_gw_ports(context, router_id)
except l3_exc.RouterNotFound:
# If the router is gone, the router port is also gone
port_removed = True
if not router or not router.get(l3.EXTERNAL_GW_INFO):
if not router or not gw_ports:
if port_removed:
self._delete_lrouter_port(context, port_id, router_id,
txn=txn)
@ -1838,11 +1868,11 @@ class OVNClient(object):
elif port:
subnet_ids = utils.get_port_subnet_ids(port)
if (ovn_conf.is_ovn_emit_need_to_frag_enabled() and
router.get('gw_port_id')):
provider_net = self._plugin.get_network(
context, router[l3.EXTERNAL_GW_INFO]['network_id'])
self.set_gateway_mtu(context, provider_net, txn=txn)
if ovn_conf.is_ovn_emit_need_to_frag_enabled():
for gw_port in gw_ports:
provider_net = self._plugin.get_network(
context, gw_port['network_id'])
self.set_gateway_mtu(context, provider_net, txn=txn)
cidr = None
for sid in subnet_ids:
@ -1881,10 +1911,12 @@ class OVNClient(object):
func = (self._nb_idl.add_nat_rule_in_lrouter if enable_snat else
self._nb_idl.delete_nat_rule_in_lrouter)
gw_lrouter_name = utils.ovn_name(router['id'])
gateways = self._get_gw_info(context, router)
# Update NAT rules only for IPv4 subnets
commands = [func(gw_lrouter_name, type='snat', logical_ip=network,
external_ip=gw_info.router_ip) for gw_info in gateways
external_ip=gw_info.router_ip)
for gw_port in self._get_router_gw_ports(context,
router['id'])
for gw_info in self._get_gw_info(context, gw_port)
if gw_info.ip_version != const.IP_VERSION_6
for network in networks]
self._transaction(commands, txn=txn)

View File

@ -15,7 +15,6 @@ from datetime import datetime
import itertools
from eventlet import greenthread
from neutron_lib.api.definitions import l3
from neutron_lib.api.definitions import segment as segment_def
from neutron_lib import constants
from neutron_lib import context
@ -527,32 +526,28 @@ class OvnNbSynchronizer(OvnDbSynchronizer):
db_extends[router['id']]['snats'] = []
db_extends[router['id']]['fips'] = []
db_extends[router['id']]['fips_pfs'] = []
if not router.get(l3.EXTERNAL_GW_INFO):
continue
gateways = self._ovn_client._get_gw_info(ctx, router)
for gw_info in gateways:
prefix = (constants.IPv4_ANY if
gw_info.ip_version == constants.IP_VERSION_4 else
constants.IPv6_ANY)
if gw_info.gateway_ip:
db_extends[router['id']]['routes'].append(
{'destination': prefix,
'nexthop': gw_info.gateway_ip,
'external_ids': {
ovn_const.OVN_ROUTER_IS_EXT_GW: 'true',
ovn_const.OVN_SUBNET_EXT_ID_KEY:
gw_info.subnet_id}})
if gw_info.ip_version == constants.IP_VERSION_6:
continue
if gw_info.router_ip and utils.is_snat_enabled(router):
networks = (
self._ovn_client._get_v4_network_of_all_router_ports(
ctx, router['id']))
for network in networks:
db_extends[router['id']]['snats'].append({
'logical_ip': network,
'external_ip': gw_info.router_ip,
'type': 'snat'})
for gw_port in self._ovn_client._get_router_gw_ports(ctx,
router['id']):
for gw_info in self._ovn_client._get_gw_info(ctx, gw_port):
if gw_info.gateway_ip:
db_extends[router['id']]['routes'].append(
{'destination': gw_info.ip_prefix,
'nexthop': gw_info.gateway_ip,
'external_ids': {
ovn_const.OVN_ROUTER_IS_EXT_GW: 'true',
ovn_const.OVN_SUBNET_EXT_ID_KEY:
gw_info.subnet_id}})
if gw_info.ip_version == constants.IP_VERSION_6:
continue
if gw_info.router_ip and utils.is_snat_enabled(router):
networks = self._ovn_client.\
_get_v4_network_of_all_router_ports(
ctx, router['id'])
for network in networks:
db_extends[router['id']]['snats'].append({
'logical_ip': network,
'external_ip': gw_info.router_ip,
'type': 'snat'})
fips = self.l3_plugin.get_floatingips(
ctx, {'router_id': list(db_routers.keys())})

View File

@ -139,7 +139,7 @@ class TestOVNFunctionalBase(test_plugin.Ml2PluginV2TestCase,
OVN_SCHEMA_FILES = ['ovn-nb.ovsschema', 'ovn-sb.ovsschema']
_mechanism_drivers = ['logger', 'ovn']
_extension_drivers = ['port_security']
_extension_drivers = ['port_security', 'external-gateway-multihoming']
_counter = 0
l3_plugin = 'neutron.services.ovn_l3.plugin.OVNL3RouterPlugin'

View File

@ -19,7 +19,6 @@ from neutron_lib.api.definitions import dns as dns_apidef
from neutron_lib.api.definitions import fip_pf_description as ext_pf_def
from neutron_lib.api.definitions import fip_pf_port_range as ranges_pf_def
from neutron_lib.api.definitions import floating_ip_port_forwarding as pf_def
from neutron_lib.api.definitions import l3
from neutron_lib.api.definitions import port_security as ps
from neutron_lib import constants
from neutron_lib import context
@ -1192,10 +1191,10 @@ class TestOvnNbSync(base.TestOVNFunctionalBase):
db_route['nexthop']
for db_route in db_router['routes']]
db_nats[db_router['id']] = []
if db_router.get(l3.EXTERNAL_GW_INFO):
gateways = self.l3_plugin._ovn_client._get_gw_info(
self.context, db_router)
for gw_info in gateways:
for gw_port in self.l3_plugin._ovn_client._get_router_gw_ports(
self.context, db_router['id']):
for gw_info in self.l3_plugin._ovn_client._get_gw_info(
self.context, gw_port):
# Add gateway default route and snats
if gw_info.gateway_ip:
db_routes[db_router['id']].append(gw_info.ip_prefix +

View File

@ -53,6 +53,16 @@ class TestRouter(base.TestOVNFunctionalBase):
router['router']['external_gateway_info'] = gw_info
return self.l3_plugin.create_router(self.context, router)
def _add_external_gateways(self, router_id, external_gateways):
router = {'router': {'external_gateways': external_gateways}}
return self.l3_plugin.add_external_gateways(
self.context, router_id, body=router)
def _remove_external_gateways(self, router_id, external_gateways):
router = {'router': {'external_gateways': external_gateways}}
return self.l3_plugin.remove_external_gateways(
self.context, router_id, body=router)
def _create_ext_network(self, name, net_type, physnet, seg,
gateway, cidr):
arg_list = (pnet.NETWORK_TYPE, external_net.EXTERNAL,)
@ -504,6 +514,27 @@ class TestRouter(base.TestOVNFunctionalBase):
self._test_router_port_ipv6_ra_configs_helper(
ip_version=4)
def test_create_delete_router_multiple_gw_ports(self):
ext4 = self._create_ext_network(
'ext4', 'flat', 'physnet4', None, "40.0.0.1", "40.0.0.0/24")
router = self._create_router('router4')
gws = self._add_external_gateways(
router['id'],
[
{'network_id': ext4['network']['id']},
{'network_id': ext4['network']['id']},
]
)
lr = self.nb_api.lookup('Logical_Router',
ovn_utils.ovn_name(router['id']))
self.assertEqual(
len(lr.ports),
len(gws['router']['external_gateways']))
self.l3_plugin.delete_router(self.context, id=router['id'])
self.assertRaises(idlutils.RowNotFound, self.nb_api.lookup,
'Logical_Router', ovn_utils.ovn_name(router['id']))
def test_gateway_chassis_rebalance(self):
def _get_result_dict():
sched_info = {}

View File

@ -158,6 +158,7 @@ class FakeOvsdbNbOvnIdl(object):
self.ha_chassis_group_del = mock.Mock()
self.ha_chassis_group_add_chassis = mock.Mock()
self.ha_chassis_group_del_chassis = mock.Mock()
self.get_lrouter_port = mock.Mock()
self.get_lrouter_gw_ports = mock.Mock()
self.lrp_get = mock.Mock()
self.get_schema_version = mock.Mock(return_value='3.6.0')

View File

@ -21,6 +21,7 @@ from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf
from neutron.plugins.ml2 import db as ml2_db
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovn_client
from neutron.tests import base
from neutron.tests.unit import fake_resources as fakes
from neutron.tests.unit.services.logapi.drivers.ovn \
import test_driver as test_log_driver
from neutron_lib.api.definitions import l3
@ -55,24 +56,28 @@ class TestOVNClient(TestOVNClientBase):
plugin = mock.MagicMock()
self.get_plugin.return_value = plugin
subnet = {
'subnet_id': 'fake-subnet-id',
'id': 'fake-subnet-id',
'gateway_ip': '10.42.0.1',
'ip_version': const.IP_VERSION_4,
}
plugin.get_subnet.return_value = subnet
plugin.get_subnets_by_network.return_value = [subnet]
router = {
'id': 'fake-router-id',
l3.EXTERNAL_GW_INFO: {
'external_fixed_ips': [{
'subnet_id': subnet.get('subnet_id'),
'ip_address': '10.42.0.42'}],
},
'gw_port_id': 'fake-port-id',
}
networks = mock.MagicMock()
txn = mock.MagicMock()
self.ovn_client._get_router_gw_ports = mock.MagicMock()
gw_port = fakes.FakePort().create_one_port(
attrs={
'fixed_ips': [{
'subnet_id': subnet.get('id'),
'ip_address': '10.42.0.42'}]
})
self.ovn_client._get_router_gw_ports.return_value = [gw_port]
self.assertEqual(
self.get_plugin().get_port(),
[self.get_plugin().get_port()],
self.ovn_client._add_router_ext_gw(mock.Mock(), router, networks,
txn))
self.nb_idl.add_static_route.assert_called_once_with(
@ -81,30 +86,39 @@ class TestOVNClient(TestOVNClientBase):
nexthop='10.42.0.1',
external_ids={
'neutron:is_ext_gw': 'true',
'neutron:subnet_id': subnet['subnet_id']})
'neutron:subnet_id': subnet['id']})
def test__add_router_ext_gw_no_default_route(self):
plugin = mock.MagicMock()
self.get_plugin.return_value = plugin
subnet = {
'subnet_id': 'fake-subnet-id',
'id': 'fake-subnet-id',
'gateway_ip': None,
'ip_version': const.IP_VERSION_4
}
plugin.get_subnet.return_value = subnet
plugin.get_subnets_by_network.return_value = [subnet]
router = {
'id': 'fake-router-id',
l3.EXTERNAL_GW_INFO: {
'external_fixed_ips': [{
'subnet_id': subnet.get('subnet_id'),
'subnet_id': subnet.get('id'),
'ip_address': '10.42.0.42'}],
},
'gw_port_id': 'fake-port-id',
}
networks = mock.MagicMock()
txn = mock.MagicMock()
self.ovn_client._get_router_gw_ports = mock.MagicMock()
gw_port = fakes.FakePort().create_one_port(
attrs={
'fixed_ips': [{
'subnet_id': subnet.get('id'),
'ip_address': '10.42.0.42'}]
})
self.ovn_client._get_router_gw_ports.return_value = [gw_port]
self.assertEqual(
self.get_plugin().get_port(),
[self.get_plugin().get_port()],
self.ovn_client._add_router_ext_gw(mock.Mock(), router, networks,
txn))
self.nb_idl.add_static_route.assert_not_called()

View File

@ -353,19 +353,19 @@ class TestOvnNbSyncML2(test_mech_driver.OVNMechanismDriverTestCase):
'external_ids': {'subnet_id': 'n1-s1'}}
return {'cidr': '', 'options': '', 'external_ids': {}}
def _fake_get_gw_info(self, ctx, router):
def _fake_get_gw_info(self, ctx, port):
return {
'r1': [ovn_client.GW_INFO(router_ip='90.0.0.2',
gateway_ip='90.0.0.1',
network_id='', subnet_id='ext-subnet',
ip_version=4,
ip_prefix=const.IPv4_ANY)],
'r2': [ovn_client.GW_INFO(router_ip='100.0.0.2',
gateway_ip='100.0.0.1',
network_id='', subnet_id='ext-subnet',
ip_version=4,
ip_prefix=const.IPv4_ANY)]
}.get(router['id'], [])
'p1r1': [ovn_client.GW_INFO(router_ip='90.0.0.2',
gateway_ip='90.0.0.1',
network_id='', subnet_id='ext-subnet',
ip_version=4,
ip_prefix=const.IPv4_ANY)],
'p1r2': [ovn_client.GW_INFO(router_ip='100.0.0.2',
gateway_ip='100.0.0.1',
network_id='', subnet_id='ext-subnet',
ip_version=4,
ip_prefix=const.IPv4_ANY)]
}.get(port['id'], [])
def _fake_get_v4_network_of_all_router_ports(self, ctx, router_id):
return {'r1': ['172.16.0.0/24', '172.16.2.0/24'],
@ -532,6 +532,11 @@ class TestOvnNbSyncML2(test_mech_driver.OVNMechanismDriverTestCase):
ovn_api.delete_dhcp_options = mock.Mock()
ovn_nb_synchronizer._ovn_client.get_port_dns_records = mock.Mock()
ovn_nb_synchronizer._ovn_client.get_port_dns_records.return_value = {}
ovn_nb_synchronizer._ovn_client._get_router_gw_ports.side_effect = (
[self.get_sync_router_ports[0]],
[self.get_sync_router_ports[1]],
[self.get_sync_router_ports[2]],
)
def _test_ovn_nb_sync_helper(self, ovn_nb_synchronizer,
networks, ports,

View File

@ -58,9 +58,11 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
_mechanism_drivers = ['ovn']
l3_plugin = 'neutron.services.ovn_l3.plugin.OVNL3RouterPlugin'
def _start_mock(self, path, return_value, new_callable=None):
def _start_mock(self, path, return_value=None, new_callable=None,
side_effect=None):
patcher = mock.patch(path, return_value=return_value,
new_callable=new_callable)
new_callable=new_callable,
side_effect=side_effect)
patch = patcher.start()
self.addCleanup(patcher.stop)
return patch
@ -128,6 +130,7 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
'admin_state_up': True,
'flavor_id': None,
'external_gateway_info': self.fake_external_fixed_ips,
'external_gateways': [self.fake_external_fixed_ips],
'gw_port_id': 'gw-port-id'
}
self.fake_router_without_ext_gw = {
@ -144,8 +147,9 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
'fixed_ips': [{'ip_address': '192.168.1.1',
'subnet_id': 'ext-subnet-id'}],
'mac_address': '00:00:00:02:04:06',
'network_id': self.fake_network['id'],
'network_id': 'ext-network-id',
'id': 'gw-port-id'}
self.fake_ext_gw_ports = [self.fake_ext_gw_port]
self.fake_ext_gw_port_assert = {
'lrouter': 'neutron-router-id',
'mac': '00:00:00:02:04:06',
@ -156,7 +160,7 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
ovn_const.OVN_SUBNET_EXT_IDS_KEY: 'ext-subnet-id',
ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1',
ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY:
utils.ovn_name(self.fake_network['id'])},
utils.ovn_name('ext-network-id')},
'gateway_chassis': ['hv1'],
'options': {}}
self.fake_floating_ip_attrs = {'floating_ip_address': '192.168.0.10',
@ -250,6 +254,13 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
self.get_subnet = self._start_mock(
'neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_subnet',
return_value=self.fake_subnet)
self.get_subnets_by_network = self._start_mock(
'neutron.db.db_base_plugin_v2.NeutronDbPluginV2.'
'get_subnets_by_network',
side_effect=lambda _, x: {
self.fake_network['id']: [self.fake_subnet],
'ext-network-id': [self.fake_ext_subnet],
}[x])
self.get_router = self._start_mock(
'neutron.db.l3_db.L3_NAT_dbonly_mixin.get_router',
return_value=self.fake_router)
@ -318,6 +329,10 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
self._start_mock(
'neutron.common.ovn.utils.is_nat_gateway_port_supported',
return_value=False)
self._get_router_gw_ports = self._start_mock(
'neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.ovn_client.'
'OVNClient._get_router_gw_ports',
return_value=self.fake_ext_gw_ports)
def test__plugin_driver(self):
# No valid mech drivers should raise an exception.
@ -818,6 +833,9 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
self.fake_subnet)
self.get_port.return_value = self.fake_ext_gw_port
grps.return_value = self.fake_router_ports
fake_old_ext_gw_port = self.fake_ext_gw_port.copy()
fake_old_ext_gw_port['id'] = 'old-gw-port-id'
self._get_router_gw_ports.return_value = [fake_old_ext_gw_port]
payload = self._create_payload_for_router_update(
self.get_router.return_value, self.fake_router_with_ext_gw)