neutron/neutron/tests/functional/services/ovn_l3/test_plugin.py

553 lines
26 KiB
Python

# Copyright 2020 Red Hat, Inc.
#
# 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.
from unittest import mock
from neutron.common.ovn import constants as ovn_const
from neutron.common.ovn import utils as ovn_utils
from neutron.common import utils as n_utils
from neutron.scheduler import l3_ovn_scheduler as l3_sched
from neutron.tests.functional import base
from neutron.tests.functional.resources.ovsdb import events
from neutron_lib.api.definitions import external_net
from neutron_lib.api.definitions import l3 as l3_apidef
from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import provider_net as pnet
from neutron_lib import constants as n_consts
from neutron_lib.plugins import directory
from ovsdbapp.backend.ovs_idl import idlutils
class TestRouter(base.TestOVNFunctionalBase):
def setUp(self):
super(TestRouter, self).setUp()
self.chassis1 = self.add_fake_chassis(
'ovs-host1', physical_nets=['physnet1', 'physnet3'])
self.chassis2 = self.add_fake_chassis(
'ovs-host2', physical_nets=['physnet2', 'physnet3'])
self.cr_lrp_pb_event = events.WaitForCrLrpPortBindingEvent()
self.sb_api.idl.notify_handler.watch_event(self.cr_lrp_pb_event)
def _create_router(self, name, gw_info=None):
router = {'router':
{'name': name,
'admin_state_up': True,
'tenant_id': self._tenant_id}}
if gw_info:
router['router']['external_gateway_info'] = gw_info
return self.l3_plugin.create_router(self.context, router)
def _create_ext_network(self, name, net_type, physnet, seg,
gateway, cidr):
arg_list = (pnet.NETWORK_TYPE, external_net.EXTERNAL,)
net_arg = {pnet.NETWORK_TYPE: net_type,
external_net.EXTERNAL: True}
if seg:
arg_list = arg_list + (pnet.SEGMENTATION_ID,)
net_arg[pnet.SEGMENTATION_ID] = seg
if physnet:
arg_list = arg_list + (pnet.PHYSICAL_NETWORK,)
net_arg[pnet.PHYSICAL_NETWORK] = physnet
network = self._make_network(self.fmt, name, True,
arg_list=arg_list, **net_arg)
if cidr:
self._make_subnet(self.fmt, network, gateway, cidr,
ip_version=n_consts.IP_VERSION_4)
return network
def _set_redirect_chassis_to_invalid_chassis(self, ovn_client):
with ovn_client._nb_idl.transaction(check_error=True) as txn:
for lrp in self.nb_api.tables[
'Logical_Router_Port'].rows.values():
txn.add(ovn_client._nb_idl.update_lrouter_port(
lrp.name,
gateway_chassis=[ovn_const.OVN_GATEWAY_INVALID_CHASSIS]))
def test_gateway_chassis_on_router_gateway_port(self):
ext2 = self._create_ext_network(
'ext2', 'flat', 'physnet3', None, "20.0.0.1", "20.0.0.0/24")
gw_info = {'network_id': ext2['network']['id']}
self._create_router('router1', gw_info=gw_info)
expected = [row.name for row in
self.sb_api.tables['Chassis'].rows.values()]
for row in self.nb_api.tables[
'Logical_Router_Port'].rows.values():
if self._l3_ha_supported():
chassis = [gwc.chassis_name for gwc in row.gateway_chassis]
self.assertItemsEqual(expected, chassis)
else:
rc = row.options.get(ovn_const.OVN_GATEWAY_CHASSIS_KEY)
self.assertIn(rc, expected)
def _check_gateway_chassis_candidates(self, candidates):
# In this test, fake_select() is called once from _create_router()
# and later from schedule_unhosted_gateways()
ovn_client = self.l3_plugin._ovn_client
ext1 = self._create_ext_network(
'ext1', 'vlan', 'physnet1', 1, "10.0.0.1", "10.0.0.0/24")
# mock select function and check if it is called with expected
# candidates.
def fake_select(*args, **kwargs):
self.assertItemsEqual(candidates, kwargs['candidates'])
# We are not interested in further processing, let us return
# INVALID_CHASSIS to avoid erros
return [ovn_const.OVN_GATEWAY_INVALID_CHASSIS]
with mock.patch.object(ovn_client._ovn_scheduler, 'select',
side_effect=fake_select) as client_select,\
mock.patch.object(self.l3_plugin.scheduler, 'select',
side_effect=fake_select) as plugin_select:
gw_info = {'network_id': ext1['network']['id']}
self._create_router('router1', gw_info=gw_info)
self.assertFalse(plugin_select.called)
self.assertTrue(client_select.called)
client_select.reset_mock()
plugin_select.reset_mock()
# set redirect-chassis to neutron-ovn-invalid-chassis, so
# that schedule_unhosted_gateways will try to schedule it
self._set_redirect_chassis_to_invalid_chassis(ovn_client)
self.l3_plugin.schedule_unhosted_gateways()
self.assertFalse(client_select.called)
self.assertTrue(plugin_select.called)
def test_gateway_chassis_with_cms_and_bridge_mappings(self):
# Both chassis1 and chassis3 are having proper bridge mappings,
# but only chassis3 is having enable-chassis-as-gw.
# Test if chassis3 is selected as candidate or not.
self.chassis3 = self.add_fake_chassis(
'ovs-host3', physical_nets=['physnet1'],
external_ids={'ovn-cms-options': 'enable-chassis-as-gw'})
self._check_gateway_chassis_candidates([self.chassis3])
def test_gateway_chassis_with_cms_and_no_bridge_mappings(self):
# chassis1 is having proper bridge mappings.
# chassis3 is having enable-chassis-as-gw, but no bridge mappings.
self.chassis3 = self.add_fake_chassis(
'ovs-host3',
external_ids={'ovn-cms-options': 'enable-chassis-as-gw'})
ovn_client = self.l3_plugin._ovn_client
ext1 = self._create_ext_network(
'ext1', 'vlan', 'physnet1', 1, "10.0.0.1", "10.0.0.0/24")
# As we have 'gateways' in the system, but without required
# chassis we should not schedule gw in that case at all.
self._set_redirect_chassis_to_invalid_chassis(ovn_client)
with mock.patch.object(ovn_client._ovn_scheduler, 'select',
return_value=[self.chassis1]), \
mock.patch.object(self.l3_plugin.scheduler, 'select',
side_effect=[self.chassis1]):
gw_info = {'network_id': ext1['network']['id']}
self._create_router('router1', gw_info=gw_info)
with mock.patch.object(
ovn_client._nb_idl, 'update_lrouter_port') as ulrp:
self.l3_plugin.schedule_unhosted_gateways()
# Make sure that we don't schedule on chassis3
# and do not updated the lrp port.
ulrp.assert_not_called()
def test_gateway_chassis_with_bridge_mappings_and_no_cms(self):
# chassis1 is configured with proper bridge mappings,
# but none of the chassis having enable-chassis-as-gw.
# Test if chassis1 is selected as candidate or not.
self._check_gateway_chassis_candidates([self.chassis1])
def _l3_ha_supported(self):
# If the Gateway_Chassis table exists in SB database, then it
# means that L3 HA is supported.
return self.nb_api.tables.get('Gateway_Chassis')
def test_gateway_chassis_least_loaded_scheduler(self):
# This test will create 4 routers each with its own gateway.
# Using the least loaded policy for scheduling gateway ports, we
# expect that they are equally distributed across the two available
# chassis.
ovn_client = self.l3_plugin._ovn_client
ovn_client._ovn_scheduler = l3_sched.OVNGatewayLeastLoadedScheduler()
ext1 = self._create_ext_network(
'ext1', 'flat', 'physnet3', None, "20.0.0.1", "20.0.0.0/24")
gw_info = {'network_id': ext1['network']['id']}
# Create 4 routers with a gateway. Since we're using physnet3, the
# chassis candidates will be chassis1 and chassis2.
for i in range(1, 5):
self._create_router('router%d' % i, gw_info=gw_info)
# At this point we expect two gateways to be present in chassis1
# and two in chassis2. If schema supports L3 HA, we expect each
# chassis to host 2 priority 2 gateways and 2 priority 1 ones.
if self._l3_ha_supported():
# Each chassis contains a dict of (priority, # of ports hosted).
# {1: 2, 2: 2} means that this chassis hosts 2 ports of prio 1
# and two ports of prio 2.
expected = {self.chassis1: {1: 2, 2: 2},
self.chassis2: {1: 2, 2: 2}}
else:
# For non L3 HA, each chassis should contain two gateway ports.
expected = {self.chassis1: 2,
self.chassis2: 2}
sched_info = {}
for row in self.nb_api.tables[
'Logical_Router_Port'].rows.values():
if self._l3_ha_supported():
for gwc in row.gateway_chassis:
chassis = sched_info.setdefault(gwc.chassis_name, {})
chassis[gwc.priority] = chassis.get(gwc.priority, 0) + 1
else:
rc = row.options.get(ovn_const.OVN_GATEWAY_CHASSIS_KEY)
sched_info[rc] = sched_info.get(rc, 0) + 1
self.assertEqual(expected, sched_info)
def _get_gw_port(self, router_id):
router = self.l3_plugin._get_router(self.context, router_id)
gw_port_id = router.get('gw_port_id', '')
for row in self.nb_api.tables['Logical_Router_Port'].rows.values():
if row.name == 'lrp-%s' % gw_port_id:
return row
def test_gateway_chassis_with_subnet_changes(self):
"""Launchpad bug #1843485: logical router port is getting lost
Test cases when subnets are added to an external network after router
has been configured to use that network via "set --external-gateway"
"""
ovn_client = self.l3_plugin._ovn_client
with mock.patch.object(
ovn_client._ovn_scheduler, 'select',
return_value=[ovn_const.OVN_GATEWAY_INVALID_CHASSIS]) as \
client_select:
router1 = self._create_router('router1', gw_info=None)
router_id = router1['id']
self.assertIsNone(self._get_gw_port(router_id),
"router logical port unexpected before ext net")
# Create external network with no subnets and assign it to router
ext1 = self._create_ext_network(
'ext1', 'flat', 'physnet3', None, gateway=None, cidr=None)
net_id = ext1['network']['id']
gw_info = {'network_id': ext1['network']['id']}
self.l3_plugin.update_router(
self.context, router_id,
{'router': {l3_apidef.EXTERNAL_GW_INFO: gw_info}})
self.assertIsNotNone(self._get_gw_port(router_id),
"router logical port must exist after gw add")
# Add subnets to external network. This should percolate
# into l3_plugin.update_router()
kwargs = {'ip_version': n_consts.IP_VERSION_4,
'gateway_ip': '10.0.0.1', 'cidr': '10.0.0.0/24'}
subnet4_res = self._create_subnet(
self.fmt, net_id, **kwargs)
subnet4 = self.deserialize(self.fmt, subnet4_res).get('subnet')
self.assertIsNotNone(self._get_gw_port(router_id),
"router logical port must exist after v4 add")
kwargs = {'ip_version': n_consts.IP_VERSION_6,
'gateway_ip': 'fe81::1', 'cidr': 'fe81::/64',
'ipv6_ra_mode': n_consts.IPV6_SLAAC,
'ipv6_address_mode': n_consts.IPV6_SLAAC}
subnet6_res = self._create_subnet(
self.fmt, net_id, **kwargs)
subnet6 = self.deserialize(self.fmt, subnet6_res).get('subnet')
self.assertIsNotNone(self._get_gw_port(router_id),
"router logical port must exist after v6 add")
self.assertGreaterEqual(client_select.call_count, 3)
# Verify that ports have had the subnets created
kwargs = {'device_owner': n_consts.DEVICE_OWNER_ROUTER_GW}
ports_res = self._list_ports(self.fmt, net_id=net_id, **kwargs)
ports = self.deserialize(self.fmt, ports_res).get('ports')
subnet4_ip = None
subnet6_ip = None
for port in ports:
for fixed_ip in port.get('fixed_ips', []):
if fixed_ip.get('subnet_id') == subnet4['id']:
subnet4_ip = fixed_ip.get('ip_address')
if fixed_ip.get('subnet_id') == subnet6['id']:
subnet6_ip = fixed_ip.get('ip_address')
self.assertIsNotNone(subnet4_ip)
self.assertIsNotNone(subnet6_ip)
# Verify that logical router port is properly configured
gw_port = self._get_gw_port(router_id)
self.assertIsNotNone(gw_port)
expected_networks = ['%s/24' % subnet4_ip, '%s/64' % subnet6_ip]
self.assertItemsEqual(
expected_networks, gw_port.networks,
'networks in ovn port must match fixed_ips in neutron')
def test_logical_router_port_creation(self):
"""Launchpad bug #1844652: Verify creation and removal of lrp
This test verifies that logical router port is created and removed
based on attaching and detaching the external network to a router.
"""
router = self._create_router('router1', gw_info=None)
router_id = router['id']
self.assertIsNone(self._get_gw_port(router_id),
"router logical port unexpected before ext net")
# Create external network and assign it to router
ext1 = self._create_ext_network(
'ext1', 'flat', 'physnet3', None, gateway=None, cidr=None)
gw_info = {'network_id': ext1['network']['id']}
self.l3_plugin.update_router(
self.context, router_id,
{'router': {l3_apidef.EXTERNAL_GW_INFO: gw_info}})
self.assertIsNotNone(self._get_gw_port(router_id),
"router logical port missing after ext net add")
# Un-assign external network from router
self.l3_plugin.update_router(
self.context, router_id,
{'router': {l3_apidef.EXTERNAL_GW_INFO: None}})
self.assertIsNone(self._get_gw_port(router_id),
"router logical port exists after ext net removal")
def test_gateway_chassis_with_bridge_mappings(self):
"""Check selected ovn chassis based on external network
This test sets different gateway values to ensure that the proper
chassis are candidates, based on the physical network mappings.
"""
ovn_client = self.l3_plugin._ovn_client
# Create external networks with vlan, flat and geneve network types
ext1 = self._create_ext_network(
'ext1', 'vlan', 'physnet1', 1, "10.0.0.1", "10.0.0.0/24")
ext2 = self._create_ext_network(
'ext2', 'flat', 'physnet3', None, "20.0.0.1", "20.0.0.0/24")
ext3 = self._create_ext_network(
'ext3', 'geneve', None, 10, "30.0.0.1", "30.0.0.0/24")
# mock select function and check if it is called with expected
# candidates.
self.candidates = []
def fake_select(*args, **kwargs):
self.assertItemsEqual(self.candidates, kwargs['candidates'])
# We are not interested in further processing, let us return
# INVALID_CHASSIS to avoid erros
return [ovn_const.OVN_GATEWAY_INVALID_CHASSIS]
with mock.patch.object(ovn_client._ovn_scheduler, 'select',
side_effect=fake_select) as client_select,\
mock.patch.object(self.l3_plugin.scheduler, 'select',
side_effect=fake_select) as plugin_select:
self.candidates = [self.chassis1]
gw_info = {'network_id': ext1['network']['id']}
router1 = self._create_router('router1', gw_info=gw_info)
# set redirect-chassis to neutron-ovn-invalid-chassis, so
# that schedule_unhosted_gateways will try to schedule it
self._set_redirect_chassis_to_invalid_chassis(ovn_client)
self.l3_plugin.schedule_unhosted_gateways()
self.candidates = [self.chassis1, self.chassis2]
gw_info = {'network_id': ext2['network']['id']}
self.l3_plugin.update_router(
self.context, router1['id'],
{'router': {l3_apidef.EXTERNAL_GW_INFO: gw_info}})
self._set_redirect_chassis_to_invalid_chassis(ovn_client)
self.l3_plugin.schedule_unhosted_gateways()
self.candidates = []
gw_info = {'network_id': ext3['network']['id']}
self.l3_plugin.update_router(
self.context, router1['id'],
{'router': {l3_apidef.EXTERNAL_GW_INFO: gw_info}})
self._set_redirect_chassis_to_invalid_chassis(ovn_client)
self.l3_plugin.schedule_unhosted_gateways()
# We can't test call_count for these mocks, as we have disabled
# maintenance_worker which will trigger chassis events
# and eventually calling schedule_unhosted_gateways.
# However, we know for sure that these mocks must have been
# called at least 3 times because that is the number of times
# this test invokes them: 1x create_router + 2x update_router
# for client_select mock; and 3x schedule_unhosted_gateways for
# plugin_select mock.
self.assertGreaterEqual(client_select.call_count, 3)
self.assertGreaterEqual(plugin_select.call_count, 3)
def test_router_gateway_port_binding_host_id(self):
# Test setting chassis on chassisredirect port in Port_Binding table,
# will update host_id of corresponding router gateway port
# with this chassis.
chassis = idlutils.row_by_value(self.sb_api.idl, 'Chassis',
'name', self.chassis1)
host_id = chassis.hostname
ext = self._create_ext_network(
'ext1', 'vlan', 'physnet1', 1, "10.0.0.1", "10.0.0.0/24")
gw_info = {'network_id': ext['network']['id']}
router = self._create_router('router1', gw_info=gw_info)
core_plugin = directory.get_plugin()
gw_port_id = router.get('gw_port_id')
# Set chassis on chassisredirect port in Port_Binding table
logical_port = 'cr-lrp-%s' % gw_port_id
self.assertTrue(self.cr_lrp_pb_event.wait(logical_port))
self.sb_api.lsp_bind(logical_port, self.chassis1,
may_exist=True).execute(check_error=True)
def check_port_binding_host_id(port_id):
port = core_plugin.get_ports(
self.context, filters={'id': [port_id]})[0]
return port[portbindings.HOST_ID] == host_id
# Test if router gateway port updated with this chassis
n_utils.wait_until_true(lambda: check_port_binding_host_id(
gw_port_id))
def _validate_router_ipv6_ra_configs(self, lrp_name, expected_ra_confs):
lrp = idlutils.row_by_value(self.nb_api.idl,
'Logical_Router_Port', 'name', lrp_name)
self.assertEqual(expected_ra_confs, lrp.ipv6_ra_configs)
def _test_router_port_ipv6_ra_configs_helper(
self, cidr='aef0::/64', ip_version=6,
address_mode=n_consts.IPV6_SLAAC,):
router1 = self._create_router('router1')
n1 = self._make_network(self.fmt, 'n1', True)
if ip_version == 6:
kwargs = {'ip_version': 6, 'cidr': 'aef0::/64',
'ipv6_address_mode': address_mode,
'ipv6_ra_mode': address_mode}
else:
kwargs = {'ip_version': 4, 'cidr': '10.0.0.0/24'}
res = self._create_subnet(self.fmt, n1['network']['id'],
**kwargs)
n1_s1 = self.deserialize(self.fmt, res)
n1_s1_id = n1_s1['subnet']['id']
router_iface_info = self.l3_plugin.add_router_interface(
self.context, router1['id'], {'subnet_id': n1_s1_id})
lrp_name = ovn_utils.ovn_lrouter_port_name(
router_iface_info['port_id'])
if ip_version == 6:
expected_ra_configs = {
'address_mode': ovn_utils.get_ovn_ipv6_address_mode(
address_mode),
'send_periodic': 'true',
'mtu': '1450'}
else:
expected_ra_configs = {}
self._validate_router_ipv6_ra_configs(lrp_name, expected_ra_configs)
def test_router_port_ipv6_ra_configs_addr_mode_slaac(self):
self._test_router_port_ipv6_ra_configs_helper()
def test_router_port_ipv6_ra_configs_addr_mode_dhcpv6_stateful(self):
self._test_router_port_ipv6_ra_configs_helper(
address_mode=n_consts.DHCPV6_STATEFUL)
def test_router_port_ipv6_ra_configs_addr_mode_dhcpv6_stateless(self):
self._test_router_port_ipv6_ra_configs_helper(
address_mode=n_consts.DHCPV6_STATELESS)
def test_router_port_ipv6_ra_configs_ipv4(self):
self._test_router_port_ipv6_ra_configs_helper(
ip_version=4)
def test_gateway_chassis_rebalance(self):
def _get_result_dict():
sched_info = {}
for row in self.nb_api.tables[
'Logical_Router_Port'].rows.values():
for gwc in row.gateway_chassis:
chassis = sched_info.setdefault(gwc.chassis_name, {})
chassis[gwc.priority] = chassis.get(gwc.priority, 0) + 1
return sched_info
if not self._l3_ha_supported():
self.skipTest('L3 HA not supported')
ovn_client = self.l3_plugin._ovn_client
chassis4 = self.add_fake_chassis(
'ovs-host4', physical_nets=['physnet4'], external_ids={
'ovn-cms-options': 'enable-chassis-as-gw'})
ovn_client._ovn_scheduler = l3_sched.OVNGatewayLeastLoadedScheduler()
ext1 = self._create_ext_network(
'ext1', 'flat', 'physnet4', None, "30.0.0.1", "30.0.0.0/24")
gw_info = {'network_id': ext1['network']['id']}
# Create 20 routers with a gateway. Since we're using physnet4, the
# chassis candidates will be chassis4 initially.
for i in range(20):
router = self._create_router('router%d' % i, gw_info=gw_info)
gw_port_id = router.get('gw_port_id')
logical_port = 'cr-lrp-%s' % gw_port_id
self.assertTrue(self.cr_lrp_pb_event.wait(logical_port))
self.sb_api.lsp_bind(logical_port, chassis4,
may_exist=True).execute(check_error=True)
self.l3_plugin.schedule_unhosted_gateways()
expected = {chassis4: {1: 20}}
self.assertEqual(expected, _get_result_dict())
# Add another chassis as a gateway chassis
chassis5 = self.add_fake_chassis(
'ovs-host5', physical_nets=['physnet4'], external_ids={
'ovn-cms-options': 'enable-chassis-as-gw'})
# Add a node as compute node. Compute node wont be
# used to schedule the router gateway ports therefore
# priority values wont be changed. Therefore chassis4 would
# still have priority 2
self.add_fake_chassis('ovs-host6', physical_nets=['physnet4'])
# Chassis4 should have all ports at Priority 2
self.l3_plugin.schedule_unhosted_gateways()
self.assertEqual({2: 20}, _get_result_dict()[chassis4])
# Chassis5 should have all ports at Priority 1
self.assertEqual({1: 20}, _get_result_dict()[chassis5])
# delete chassis that hosts all the gateways
self.del_fake_chassis(chassis4)
self.l3_plugin.schedule_unhosted_gateways()
# As Chassis4 has been removed so all gateways that were
# hosted there are now masters on chassis5 and have
# priority 1.
self.assertEqual({1: 20}, _get_result_dict()[chassis5])
def test_gateway_chassis_rebalance_max_chassis(self):
chassis_list = []
# spawn 6 chassis and check if port has MAX_CHASSIS candidates.
for i in range(0, ovn_const.MAX_GW_CHASSIS + 1):
chassis_list.append(
self.add_fake_chassis(
'ovs-host%s' % i, physical_nets=['physnet1'],
external_ids={
'ovn-cms-options': 'enable-chassis-as-gw'}))
ext1 = self._create_ext_network(
'ext1', 'vlan', 'physnet1', 1, "10.0.0.1", "10.0.0.0/24")
gw_info = {'network_id': ext1['network']['id']}
router = self._create_router('router', gw_info=gw_info)
gw_port_id = router.get('gw_port_id')
logical_port = 'cr-lrp-%s' % gw_port_id
self.assertTrue(self.cr_lrp_pb_event.wait(logical_port))
self.sb_api.lsp_bind(logical_port, chassis_list[0],
may_exist=True).execute(check_error=True)
self.l3_plugin.schedule_unhosted_gateways()
for row in self.nb_api.tables[
'Logical_Router_Port'].rows.values():
self.assertEqual(ovn_const.MAX_GW_CHASSIS,
len(row.gateway_chassis))