[ovn]: port forwarding -- functional tests for maintenance task

This is a subset of the changes for implementing the floating IP
port forwarding feature in neutron, using OVN as the backend.

This changeset adds functional tests in maintenance task to ensure
that floating ips as well as port forwarding are properly handled.

Depends-On: https://review.opendev.org/#/c/741303/
Change-Id: I0671ed8d73ca8c0e3ba4aded81f395e20957d598
Partially-implements: ovn/port_forwarding
Partial-Bug: #1877447
This commit is contained in:
Flavio Fernandes 2020-07-17 17:58:17 -04:00 committed by Flavio Fernandes
parent 7e37ee92ac
commit 0deec6621e
1 changed files with 248 additions and 26 deletions

View File

@ -19,8 +19,10 @@ from oslo_config import cfg
from futurist import periodics
from neutron_lib.api.definitions import external_net as extnet_apidef
from neutron_lib.api.definitions import floating_ip_port_forwarding as pf_def
from neutron_lib import constants as n_const
from neutron_lib import context as n_context
from neutron_lib.exceptions import l3 as lib_l3_exc
from neutron.common.ovn import constants as ovn_const
from neutron.common.ovn import utils
@ -97,34 +99,36 @@ class _TestMaintenanceHelper(base.TestOVNFunctionalBase):
opt_string = ','.join(['{0}:{1}'.format(key, value)
for key, value
in opts.items()])
if ip_version == 6:
if ip_version == n_const.IP_VERSION_6:
ovn_config.cfg.CONF.set_override('ovn_dhcp6_global_options',
opt_string,
group='ovn')
if ip_version == 4:
if ip_version == n_const.IP_VERSION_4:
ovn_config.cfg.CONF.set_override('ovn_dhcp4_global_options',
opt_string,
group='ovn')
def _unset_global_dhcp_opts(self, ip_version):
if ip_version == 6:
if ip_version == n_const.IP_VERSION_6:
ovn_config.cfg.CONF.clear_override('ovn_dhcp6_global_options',
group='ovn')
if ip_version == 4:
if ip_version == n_const.IP_VERSION_4:
ovn_config.cfg.CONF.clear_override('ovn_dhcp4_global_options',
group='ovn')
def _create_subnet(self, name, net_id, ip_version=4):
def _create_subnet(self, name, net_id, ip_version=n_const.IP_VERSION_4,
**kwargs):
if ip_version == n_const.IP_VERSION_4:
cidr = '10.0.0.0/24'
else:
cidr = '2001:db8::/64'
data = {'subnet': {'name': name,
'tenant_id': self._tenant_id,
'network_id': net_id,
'ip_version': ip_version,
'tenant_id': self._tenant_id,
'cidr': cidr,
'enable_dhcp': True}}
if ip_version == 4:
data['subnet']['cidr'] = '10.0.0.0/24'
else:
data['subnet']['cidr'] = 'eef0::/64'
data['subnet'].update(kwargs)
req = self.new_create_request('subnets', data, self.fmt)
res = req.get_response(self.api)
return self.deserialize(self.fmt, res)['subnet']
@ -210,6 +214,21 @@ class _TestMaintenanceHelper(base.TestOVNFunctionalBase):
if row.name == utils.ovn_lrouter_port_name(port_id):
return row
def _find_nat_rule(self, router_id, external_ip, logical_ip=None,
nat_type='dnat_and_snat'):
rules = self.nb_api.get_lrouter_nat_rules(utils.ovn_name(router_id))
return next((r for r in rules
if r['type'] == nat_type and
r['external_ip'] == external_ip and
(not logical_ip or r['logical_ip'] == logical_ip)),
None)
def _find_pf_lb(self, router_id, fip_id=None):
lbs = self.nb_api.get_router_floatingip_lbs(utils.ovn_name(router_id))
return [lb for lb in lbs
if (not fip_id or
fip_id == lb.external_ids[ovn_const.OVN_FIP_EXT_ID_KEY])]
class TestMaintenance(_TestMaintenanceHelper):
@ -357,7 +376,8 @@ class TestMaintenance(_TestMaintenanceHelper):
self.assertIsNone(ovn_obj.options.get('ntp_server', None))
# Set some global DHCP Options
self._set_global_dhcp_opts(ip_version=4, opts=options)
self._set_global_dhcp_opts(ip_version=n_const.IP_VERSION_4,
opts=options)
# Run the maintenance task to add the new options
self.assertRaises(periodics.NeverAgain,
@ -366,12 +386,13 @@ class TestMaintenance(_TestMaintenanceHelper):
# Assert that the option was added
ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
self.assertEqual(
ovn_obj.options.get('ntp_server', None),
'1.2.3.4')
'1.2.3.4',
ovn_obj.options.get('ntp_server', None))
# Change the global option
new_options = {'ntp_server': '4.3.2.1'}
self._set_global_dhcp_opts(ip_version=4, opts=new_options)
self._set_global_dhcp_opts(ip_version=n_const.IP_VERSION_4,
opts=new_options)
# Run the maintenance task to update the options
self.assertRaises(periodics.NeverAgain,
@ -380,12 +401,13 @@ class TestMaintenance(_TestMaintenanceHelper):
# Assert that the option was changed
ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
self.assertEqual(
ovn_obj.options.get('ntp_server', None),
'4.3.2.1')
'4.3.2.1',
ovn_obj.options.get('ntp_server', None))
# Change the global option to null
new_options = {'ntp_server': ''}
self._set_global_dhcp_opts(ip_version=4, opts=new_options)
self._set_global_dhcp_opts(ip_version=n_const.IP_VERSION_4,
opts=new_options)
# Run the maintenance task to update the options
self.assertRaises(periodics.NeverAgain,
@ -401,14 +423,16 @@ class TestMaintenance(_TestMaintenanceHelper):
neutron_net = self._create_network('network1')
# Create a subnet without global options
neutron_sub = self._create_subnet(obj_name, neutron_net['id'], 6)
neutron_sub = self._create_subnet(obj_name, neutron_net['id'],
n_const.IP_VERSION_6)
# Assert that the option is not set
ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
self.assertIsNone(ovn_obj.options.get('ntp_server', None))
# Set some global DHCP Options
self._set_global_dhcp_opts(ip_version=6, opts=options)
self._set_global_dhcp_opts(ip_version=n_const.IP_VERSION_6,
opts=options)
# Run the maintenance task to add the new options
self.assertRaises(periodics.NeverAgain,
@ -417,12 +441,13 @@ class TestMaintenance(_TestMaintenanceHelper):
# Assert that the option was added
ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
self.assertEqual(
ovn_obj.options.get('ntp_server', None),
'1.2.3.4')
'1.2.3.4',
ovn_obj.options.get('ntp_server', None))
# Change the global option
new_options = {'ntp_server': '4.3.2.1'}
self._set_global_dhcp_opts(ip_version=6, opts=new_options)
self._set_global_dhcp_opts(ip_version=n_const.IP_VERSION_6,
opts=new_options)
# Run the maintenance task to update the options
self.assertRaises(periodics.NeverAgain,
@ -431,12 +456,13 @@ class TestMaintenance(_TestMaintenanceHelper):
# Assert that the option was changed
ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
self.assertEqual(
ovn_obj.options.get('ntp_server', None),
'4.3.2.1')
'4.3.2.1',
ovn_obj.options.get('ntp_server', None))
# Change the global option to null
new_options = {'ntp_server': ''}
self._set_global_dhcp_opts(ip_version=6, opts=new_options)
self._set_global_dhcp_opts(ip_version=n_const.IP_VERSION_6,
opts=new_options)
# Run the maintenance task to update the options
self.assertRaises(periodics.NeverAgain,
@ -821,3 +847,199 @@ class TestMaintenance(_TestMaintenanceHelper):
self.assertEqual('true', ls['other_config'][ovn_const.MCAST_SNOOP])
self.assertEqual(
'true', ls['other_config'][ovn_const.MCAST_FLOOD_UNREGISTERED])
def test_floating_ip(self):
ext_net = self._create_network('ext_networktest', external=True)
ext_subnet = self._create_subnet(
'ext_subnettest',
ext_net['id'],
**{'cidr': '100.0.0.0/24',
'gateway_ip': '100.0.0.254',
'allocation_pools': [
{'start': '100.0.0.2', 'end': '100.0.0.253'}],
'enable_dhcp': False})
net1 = self._create_network('network1test', external=False)
subnet1 = self._create_subnet('subnet1test', net1['id'])
external_gateway_info = {
'enable_snat': True,
'network_id': ext_net['id'],
'external_fixed_ips': [
{'ip_address': '100.0.0.2', 'subnet_id': ext_subnet['id']}]}
router = self._create_router(
'routertest', external_gateway_info=external_gateway_info)
self._add_router_interface(router['id'], subnet1['id'])
p1 = self._create_port('testp1', net1['id'])
logical_ip = p1['fixed_ips'][0]['ip_address']
fip_info = {'floatingip': {
'description': 'test_fip',
'tenant_id': self._tenant_id,
'floating_network_id': ext_net['id'],
'port_id': p1['id'],
'fixed_ip_address': logical_ip}}
# > Create
with mock.patch.object(self._l3_ovn_client, 'create_floatingip'):
fip = self.l3_plugin.create_floatingip(self.context, fip_info)
floating_ip_address = fip['floating_ip_address']
self.assertEqual(router['id'], fip['router_id'])
self.assertEqual('testp1', fip['port_details']['name'])
self.assertIsNotNone(self.nb_api.get_lswitch_port(fip['port_id']))
# Assert the dnat_and_snat rule doesn't exist in OVN
self.assertIsNone(
self._find_nat_rule(router['id'], floating_ip_address, logical_ip))
# Call the maintenance thread to fix the problem
self.maint.check_for_inconsistencies()
# Assert the rule for the fip is now present
self.assertIsNotNone(
self._find_nat_rule(router['id'], floating_ip_address, logical_ip))
# > Update
p2 = self._create_port('testp2', net1['id'])
logical_ip = p2['fixed_ips'][0]['ip_address']
fip_info = {'floatingip': {
'port_id': p2['id'],
'fixed_ip_address': logical_ip}}
with mock.patch.object(self._l3_ovn_client, 'update_floatingip'):
self.l3_plugin.update_floatingip(self.context, fip['id'], fip_info)
# Assert the dnat_and_snat rule in OVN is still using p1's address
stale_nat_rule = self._find_nat_rule(router['id'], floating_ip_address)
self.assertEqual(p1['fixed_ips'][0]['ip_address'],
stale_nat_rule['logical_ip'])
# Call the maintenance thread to fix the problem
self.maint.check_for_inconsistencies()
# Assert the rule for the fip is now updated
self.assertIsNotNone(
self._find_nat_rule(router['id'], floating_ip_address, logical_ip))
# > Delete
with mock.patch.object(self._l3_ovn_client, 'delete_floatingip'):
self.l3_plugin.delete_floatingip(self.context, fip['id'])
self.assertRaises(
lib_l3_exc.FloatingIPNotFound,
self.l3_plugin.get_floatingip, self.context, fip['id'])
# Assert the dnat_and_snat rule in OVN is still present
self.assertIsNotNone(
self._find_nat_rule(router['id'], floating_ip_address, logical_ip))
# Call the maintenance thread to fix the problem
self.maint.check_for_inconsistencies()
# Assert the rule for the fip is now gone
self.assertIsNone(
self._find_nat_rule(router['id'], floating_ip_address))
# Assert the router snat rule is still there
snat_rule = self._find_nat_rule(
router['id'], '100.0.0.2', nat_type='snat')
self.assertEqual(subnet1['cidr'], snat_rule['logical_ip'])
def test_port_forwarding(self):
fip_attrs = lambda args: {
pf_def.RESOURCE_NAME: {pf_def.RESOURCE_NAME: args}}
def _verify_lb(test, protocol, vip_ext_port, vip_int_port):
ovn_lbs = self._find_pf_lb(router_id, fip_id)
test.assertEqual(1, len(ovn_lbs))
test.assertEqual('pf-floatingip-{}-{}'.format(fip_id, protocol),
ovn_lbs[0].name)
test.assertEqual(
{'{}:{}'.format(fip_ip, vip_ext_port):
'{}:{}'.format(p1_ip, vip_int_port)},
ovn_lbs[0].vips)
ext_net = self._create_network('ext_networktest', external=True)
ext_subnet = self._create_subnet(
'ext_subnettest',
ext_net['id'],
**{'cidr': '100.0.0.0/24',
'gateway_ip': '100.0.0.254',
'allocation_pools': [
{'start': '100.0.0.2', 'end': '100.0.0.253'}],
'enable_dhcp': False})
net1 = self._create_network('network1test', external=False)
subnet1 = self._create_subnet('subnet1test', net1['id'])
external_gateway_info = {
'enable_snat': True,
'network_id': ext_net['id'],
'external_fixed_ips': [
{'ip_address': '100.0.0.2', 'subnet_id': ext_subnet['id']}]}
router = self._create_router(
'routertest', external_gateway_info=external_gateway_info)
router_id = router['id']
self._add_router_interface(router['id'], subnet1['id'])
fip_info = {'floatingip': {
'tenant_id': self._tenant_id,
'floating_network_id': ext_net['id'],
'port_id': None,
'fixed_ip_address': None}}
fip = self.l3_plugin.create_floatingip(self.context, fip_info)
fip_id = fip['id']
fip_ip = fip['floating_ip_address']
p1 = self._create_port('testp1', net1['id'])
p1_ip = p1['fixed_ips'][0]['ip_address']
with mock.patch('neutron_lib.callbacks.registry.notify') as m_notify:
# > Create
fip_pf_args = {
pf_def.EXTERNAL_PORT: 2222,
pf_def.INTERNAL_PORT: 22,
pf_def.INTERNAL_PORT_ID: p1['id'],
pf_def.PROTOCOL: 'tcp',
pf_def.INTERNAL_IP_ADDRESS: p1_ip}
pf_obj = self.pf_plugin.create_floatingip_port_forwarding(
self.context, fip_id, **fip_attrs(fip_pf_args))
m_notify.assert_called_once()
# Assert load balancer for port forwarding was not created
self.assertFalse(self._find_pf_lb(router_id, fip_id))
# Call the maintenance thread to fix the problem
self.maint.check_for_inconsistencies()
# Assert load balancer for port forwarding was created
_verify_lb(self, 'tcp', 2222, 22)
# > Update
fip_pf_args = {pf_def.EXTERNAL_PORT: 5353,
pf_def.INTERNAL_PORT: 53,
pf_def.PROTOCOL: 'udp'}
m_notify.reset_mock()
self.pf_plugin.update_floatingip_port_forwarding(
self.context, pf_obj['id'], fip_id, **fip_attrs(fip_pf_args))
m_notify.assert_called_once()
# Assert load balancer for port forwarding is stale
_verify_lb(self, 'tcp', 2222, 22)
# Call the maintenance thread to fix the problem
self.maint.check_for_inconsistencies()
# Assert load balancer for port forwarding was updated
_verify_lb(self, 'udp', 5353, 53)
# > Delete
m_notify.reset_mock()
self.pf_plugin.delete_floatingip_port_forwarding(
self.context, pf_obj['id'], fip_id)
m_notify.assert_called_once()
# Assert load balancer for port forwarding is stale
_verify_lb(self, 'udp', 5353, 53)
# Call the maintenance thread to fix the problem
self.maint.check_for_inconsistencies()
# Assert load balancer for port forwarding is gone
self.assertFalse(self._find_pf_lb(router_id, fip_id))