From 0deec6621e6485e0a7c219953850fd14b83a7c41 Mon Sep 17 00:00:00 2001 From: Flavio Fernandes Date: Fri, 17 Jul 2020 17:58:17 -0400 Subject: [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 --- .../ovn/mech_driver/ovsdb/test_maintenance.py | 274 +++++++++++++++++++-- 1 file changed, 248 insertions(+), 26 deletions(-) (limited to 'neutron') diff --git a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py index d0b75e9c8b..f6c618126e 100644 --- a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py +++ b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py @@ -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)) -- cgit v1.2.1