# Copyright 2018 OpenStack Foundation # All Rights Reserved. # # 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 import netaddr from neutron_lib import constants as lib_const from neutron_lib import context from oslo_utils import uuidutils from neutron.agent.l3 import agent as l3_agent from neutron.agent.l3.extensions import port_forwarding as pf from neutron.agent.l3 import l3_agent_extension_api as l3_ext_api from neutron.agent.l3 import router_info as l3router from neutron.agent.linux import iptables_manager from neutron.api.rpc.callbacks.consumer import registry from neutron.api.rpc.callbacks import resources from neutron.api.rpc.handlers import resources_rpc from neutron.objects import port_forwarding as pf_obj from neutron.objects import router from neutron.tests import base from neutron.tests.unit.agent.l3 import test_agent _uuid = uuidutils.generate_uuid TEST_FIP = '10.100.2.45' BINARY_NAME = iptables_manager.get_binary_name() DEFAULT_RULE = ('PREROUTING', '-j %s-fip-pf' % BINARY_NAME) DEFAULT_CHAIN = 'fip-pf' HOSTNAME = 'testhost' class PortForwardingExtensionBaseTestCase( test_agent.BasicRouterOperationsFramework): def setUp(self): super(PortForwardingExtensionBaseTestCase, self).setUp() self.fip_pf_ext = pf.PortForwardingAgentExtension() self.context = context.get_admin_context() self.connection = mock.Mock() self.floatingip2 = router.FloatingIP(context=None, id=_uuid(), floating_ip_address='172.24.6.12', floating_network_id=_uuid(), router_id=_uuid(), status='ACTIVE') self.portforwarding1 = pf_obj.PortForwarding( context=None, id=_uuid(), floatingip_id=self.floatingip2.id, external_port=1111, protocol='tcp', internal_port_id=_uuid(), external_port_range='1111:1111', internal_port_range='11111:11111', internal_ip_address='1.1.1.1', internal_port=11111, floating_ip_address=self.floatingip2.floating_ip_address, router_id=self.floatingip2.router_id) self.agent = l3_agent.L3NATAgent(HOSTNAME, self.conf) self.ex_gw_port = {'id': _uuid()} self.fip = {'id': _uuid(), 'floating_ip_address': TEST_FIP, 'fixed_ip_address': '192.168.0.1', 'floating_network_id': _uuid(), 'port_id': _uuid(), 'host': HOSTNAME} self.router = {'id': self.floatingip2.router_id, 'gw_port': self.ex_gw_port, 'ha': False, 'distributed': False, lib_const.FLOATINGIP_KEY: [self.fip]} self.router_info = l3router.RouterInfo( self.agent, self.floatingip2.router_id, self.router, **self.ri_kwargs) self.centralized_port_forwarding_fip_set = set( [str(self.floatingip2.floating_ip_address) + '/32']) self.pf_managed_fips = [self.floatingip2.id] self.router_info.ex_gw_port = self.ex_gw_port self.router_info.fip_managed_by_port_forwardings = self.pf_managed_fips self.agent.router_info[self.router['id']] = self.router_info self.get_router_info = mock.patch( 'neutron.agent.l3.l3_agent_extension_api.' 'L3AgentExtensionAPI.get_router_info').start() self.get_router_info.return_value = self.router_info self.agent_api = l3_ext_api.L3AgentExtensionAPI(None, None) self.fip_pf_ext.consume_api(self.agent_api) self.port_forwardings = [self.portforwarding1] class FipPortForwardingExtensionInitializeTestCase( PortForwardingExtensionBaseTestCase): @mock.patch.object(registry, 'register') @mock.patch.object(resources_rpc, 'ResourcesPushRpcCallback') def test_initialize_subscribed_to_rpc(self, rpc_mock, subscribe_mock): call_to_patch = 'neutron_lib.rpc.Connection' with mock.patch(call_to_patch, return_value=self.connection) as create_connection: self.fip_pf_ext.initialize( self.connection, lib_const.L3_AGENT_MODE) create_connection.assert_has_calls([mock.call()]) self.connection.create_consumer.assert_has_calls( [mock.call( resources_rpc.resource_type_versioned_topic( resources.PORTFORWARDING), [rpc_mock()], fanout=True)] ) subscribe_mock.assert_called_with( mock.ANY, resources.PORTFORWARDING) class FipPortForwardingExtensionTestCase(PortForwardingExtensionBaseTestCase): def setUp(self): super(FipPortForwardingExtensionTestCase, self).setUp() self.fip_pf_ext.initialize( self.connection, lib_const.L3_AGENT_MODE) self._set_bulk_pull_mock() def _set_bulk_pull_mock(self): def _bulk_pull_mock(context, resource_type, filter_kwargs=None): if 'floatingip_id' in filter_kwargs: result = [] for pfobj in self.port_forwardings: if pfobj.floatingip_id in filter_kwargs['floatingip_id']: result.append(pfobj) return result return self.port_forwardings self.bulk_pull = mock.patch( 'neutron.api.rpc.handlers.resources_rpc.' 'ResourcesPullRpcApi.bulk_pull').start() self.bulk_pull.side_effect = _bulk_pull_mock def _get_chainrule_tag_from_pf_obj(self, target_obj): rule_tag = 'fip_portforwarding-' + target_obj.id chain_name = ( 'pf-' + target_obj.id)[:lib_const.MAX_IPTABLES_CHAIN_LEN_WRAP] ports = pf.PortForwardingAgentExtension.extract_ports(target_obj) chain_rule = (chain_name, '-d %s/32 -p %s -m %s --dport %s ' '-j DNAT --to-destination %s:%s' % ( target_obj.floating_ip_address, target_obj.protocol, target_obj.protocol, ports[0], target_obj.internal_ip_address, ports[1])) return chain_name, chain_rule, rule_tag def _assert_called_iptables_process(self, mock_add_chain, mock_add_rule, mock_add_fip, mock_send_fip_status, target_obj=None): if target_obj: obj = target_obj else: obj = self.portforwarding1 (chain_name, chain_rule, rule_tag) = self._get_chainrule_tag_from_pf_obj(obj) mock_add_chain.assert_has_calls([mock.call('fip-pf'), mock.call(chain_name)]) mock_add_rule.assert_has_calls( [mock.call(DEFAULT_RULE[0], DEFAULT_RULE[1]), mock.call(DEFAULT_CHAIN, ('-j %s-' % BINARY_NAME) + chain_name, tag=rule_tag), mock.call(chain_name, chain_rule[1], tag=rule_tag)]) mock_add_fip.assert_called_once_with( {'floating_ip_address': str(obj.floating_ip_address)}, mock.ANY, mock.ANY) fip_status = { obj.floatingip_id: lib_const.FLOATINGIP_STATUS_ACTIVE} mock_send_fip_status.assert_called_once_with(mock.ANY, fip_status) def _test_extract_ports(self, internal_port_range, external_port_range, expected_internal_port, expected_external_port, log_called=False): mock_pf = mock.Mock() mock_log = mock.patch.object(pf.LOG, 'warning').start() mock_pf.internal_port_range = internal_port_range mock_pf.external_port_range = external_port_range ports = pf.PortForwardingAgentExtension.extract_ports(mock_pf) self.assertEqual(expected_internal_port, ports[1]) self.assertEqual(expected_external_port, ports[0]) if log_called: mock_log.assert_called_with( "Port forwarding rule with different internal " "and external port ranges applied. Internal " "port range: [%s], external port range: [%s].", ports[1], ports[0]) else: self.assertFalse(mock_log.called) def test_extract_ports(self): test_cases = { 'internal port range = external port range': { 'internal': '10:12', 'external': '13:15', 'expected_internal': '10-12/13', 'expected_external': '13:15', }, 'internal port range = external port range = 1': { 'internal': '10:10', 'external': '13:13', 'expected_internal': '10', 'expected_external': '13', }, 'internal port range < external port range': { 'internal': '10:12', 'external': '13:16', 'expected_internal': '10-12/13', 'expected_external': '13:16', 'log_called': True }, 'internal port range > external port range': { 'internal': '9:12', 'external': '13:15', 'expected_internal': '9-12/13', 'expected_external': '13:15', 'log_called': True }, 'internal port range = 1': { 'internal': '10:10', 'external': '13:15', 'expected_internal': '10', 'expected_external': '13:15', }, 'external port range = 1': { 'internal': '10:12', 'external': '13:13', 'expected_internal': '10-12', 'expected_external': '13', } } for case in test_cases.values(): self._test_extract_ports(*case.values()) @mock.patch.object(pf.PortForwardingAgentExtension, '_sending_port_forwarding_fip_status') @mock.patch.object(iptables_manager.IptablesTable, 'add_rule') @mock.patch.object(iptables_manager.IptablesTable, 'add_chain') @mock.patch.object(l3router.RouterInfo, 'add_floating_ip') def test_add_update_router(self, mock_add_fip, mock_add_chain, mock_add_rule, mock_send_fip_status): # simulate the router add and already there is a port forwarding # resource association. mock_add_fip.return_value = lib_const.FLOATINGIP_STATUS_ACTIVE self.fip_pf_ext.add_router(self.context, self.router) self._assert_called_iptables_process( mock_add_chain, mock_add_rule, mock_add_fip, mock_send_fip_status, target_obj=self.portforwarding1) # Then we create another port forwarding with the same fip mock_add_fip.reset_mock() mock_send_fip_status.reset_mock() mock_add_chain.reset_mock() mock_add_rule.reset_mock() test_portforwarding = pf_obj.PortForwarding( context=None, id=_uuid(), floatingip_id=self.floatingip2.id, external_port=2222, protocol='tcp', internal_port_id=_uuid(), external_port_range='2222:2222', internal_port_range='22222:22222', internal_ip_address='2.2.2.2', internal_port=22222, floating_ip_address=self.floatingip2.floating_ip_address, router_id=self.floatingip2.router_id) self.pf_managed_fips.append(self.floatingip2.id) self.port_forwardings.append(test_portforwarding) self.fip_pf_ext.update_router(self.context, self.router) self._assert_called_iptables_process( mock_add_chain, mock_add_rule, mock_add_fip, mock_send_fip_status, target_obj=test_portforwarding) @mock.patch.object(iptables_manager.IptablesTable, 'add_rule') @mock.patch.object(iptables_manager.IptablesTable, 'add_chain') @mock.patch('neutron.agent.linux.ip_lib.IPDevice') @mock.patch.object(iptables_manager.IptablesTable, 'remove_chain') def test_add_update_router_port_forwarding_change( self, mock_remove_chain, mock_ip_device, mock_add_chain, mock_add_rule): self.fip_pf_ext.add_router(self.context, self.router) update_portforwarding = pf_obj.PortForwarding( context=None, id=self.portforwarding1.id, floatingip_id=self.portforwarding1.floatingip_id, external_port_range='2222:2223', protocol='tcp', internal_port_id=_uuid(), internal_ip_address='2.2.2.2', internal_port_range='22222:22223', floating_ip_address=self.portforwarding1.floating_ip_address, router_id=self.portforwarding1.router_id) self.port_forwardings = [update_portforwarding] mock_delete = mock.Mock() mock_ip_device.return_value = mock_delete self.fip_pf_ext.update_router(self.context, self.router) current_chain = ('pf-' + self.portforwarding1.id)[ :lib_const.MAX_IPTABLES_CHAIN_LEN_WRAP] mock_remove_chain.assert_called_once_with(current_chain) mock_delete.delete_socket_conntrack_state.assert_called_once_with( str(self.portforwarding1.floating_ip_address), self.portforwarding1.external_port, protocol=self.portforwarding1.protocol) (chain_name, chain_rule, rule_tag) = self._get_chainrule_tag_from_pf_obj( update_portforwarding) mock_add_chain.assert_has_calls([mock.call('fip-pf'), mock.call(chain_name)]) mock_add_rule.assert_has_calls( [mock.call(DEFAULT_RULE[0], DEFAULT_RULE[1]), mock.call(DEFAULT_CHAIN, ('-j %s-' % BINARY_NAME) + chain_name, tag=rule_tag), mock.call(chain_name, chain_rule[1], tag=rule_tag)]) @mock.patch.object(pf.PortForwardingAgentExtension, '_sending_port_forwarding_fip_status') @mock.patch('neutron.agent.linux.ip_lib.IPDevice') @mock.patch.object(iptables_manager.IptablesTable, 'remove_chain') def test_add_update_router_port_forwarding_remove( self, mock_remove_chain, mock_ip_device, mock_send_fip_status): self.fip_pf_ext.add_router(self.context, self.router) mock_send_fip_status.reset_mock() self.port_forwardings = [] mock_device = mock.Mock() mock_ip_device.return_value = mock_device self.fip_pf_ext.update_router(self.context, self.router) current_chain = ('pf-' + self.portforwarding1.id)[ :lib_const.MAX_IPTABLES_CHAIN_LEN_WRAP] mock_remove_chain.assert_called_once_with(current_chain) mock_device.delete_socket_conntrack_state.assert_called_once_with( str(self.portforwarding1.floating_ip_address), self.portforwarding1.external_port, protocol=self.portforwarding1.protocol) mock_device.delete_addr_and_conntrack_state.assert_called_once_with( str(netaddr.IPNetwork(self.portforwarding1.floating_ip_address))) fip_status = { self.portforwarding1.floatingip_id: lib_const.FLOATINGIP_STATUS_DOWN} mock_send_fip_status.assert_called_once_with(mock.ANY, fip_status) @mock.patch.object(pf.PortForwardingAgentExtension, '_sending_port_forwarding_fip_status') @mock.patch.object(iptables_manager.IptablesTable, 'add_rule') @mock.patch.object(iptables_manager.IptablesTable, 'add_chain') @mock.patch.object(l3router.RouterInfo, 'add_floating_ip') def test_add_delete_router(self, mock_add_fip, mock_add_chain, mock_add_rule, mock_send_fip_status): # simulate the router add and already there is a port forwarding # resource association. mock_add_fip.return_value = lib_const.FLOATINGIP_STATUS_ACTIVE self.fip_pf_ext.add_router(self.context, self.router) self._assert_called_iptables_process( mock_add_chain, mock_add_rule, mock_add_fip, mock_send_fip_status, target_obj=self.portforwarding1) router_fip_ids = self.fip_pf_ext.mapping.router_fip_mapping.get( self.router['id']) self.assertIsNotNone(router_fip_ids) for fip_id in router_fip_ids: pf_ids = self.fip_pf_ext.mapping.fip_port_forwarding.get(fip_id) self.assertIsNotNone(pf_ids) for pf_id in pf_ids: pf = self.fip_pf_ext.mapping.managed_port_forwardings.get( pf_id) self.assertIsNotNone(pf) self.fip_pf_ext.delete_router(self.context, self.router) self.assertIsNone( self.fip_pf_ext.mapping.router_fip_mapping.get(self.router['id'])) def test_check_if_need_process_no_snat_ns(self): ex_gw_port = {'id': _uuid()} router_id = _uuid() router = {'id': router_id, 'gw_port': ex_gw_port, 'ha': False, 'distributed': True} router_info = l3router.RouterInfo( self.agent, router_id, router, **self.ri_kwargs) router_info.agent_conf.agent_mode = lib_const.L3_AGENT_MODE_DVR_SNAT router_info.fip_managed_by_port_forwardings = True router_info.snat_namespace = mock.Mock() router_info.snat_namespace.exists.return_value = False self.assertFalse(self.fip_pf_ext._check_if_need_process(router_info)) class RouterFipPortForwardingMappingTestCase(base.BaseTestCase): def setUp(self): super(RouterFipPortForwardingMappingTestCase, self).setUp() self.mapping = pf.RouterFipPortForwardingMapping() self.router1 = _uuid() self.router2 = _uuid() self.floatingip1 = _uuid() self.floatingip2 = _uuid() self.floatingip3 = _uuid() self.portforwarding1 = pf_obj.PortForwarding( context=None, id=_uuid(), floatingip_id=self.floatingip1, external_port=1111, protocol='tcp', internal_port_id=_uuid(), internal_ip_address='1.1.1.1', internal_port=11111, floating_ip_address='111.111.111.111', router_id=self.router1, description='Some description') self.portforwarding2 = pf_obj.PortForwarding( context=None, id=_uuid(), floatingip_id=self.floatingip1, external_port=1112, protocol='tcp', internal_port_id=_uuid(), internal_ip_address='1.1.1.2', internal_port=11112, floating_ip_address='111.111.111.111', router_id=self.router1, description='Some description') self.portforwarding3 = pf_obj.PortForwarding( context=None, id=_uuid(), floatingip_id=self.floatingip2, external_port=1113, protocol='tcp', internal_port_id=_uuid(), internal_ip_address='1.1.1.3', internal_port=11113, floating_ip_address='111.222.111.222', router_id=self.router1, description=None) self.portforwarding4 = pf_obj.PortForwarding( context=None, id=_uuid(), floatingip_id=self.floatingip3, external_port=2222, protocol='tcp', internal_port_id=_uuid(), internal_ip_address='2.2.2.2', internal_port=22222, floating_ip_address='222.222.222.222', router_id=self.router2, description='') self.portforwardings_dict = { self.portforwarding1.id: self.portforwarding1, self.portforwarding2.id: self.portforwarding2, self.portforwarding3.id: self.portforwarding3, self.portforwarding4.id: self.portforwarding4} def _set_pf(self): self.mapping.set_port_forwardings(self.portforwardings_dict.values()) def test_set_port_forwardings(self): self._set_pf() pf_ids = self.portforwardings_dict.keys() for pf_id, obj in self.mapping.managed_port_forwardings.items(): self.assertIn(pf_id, pf_ids) self.assertEqual(obj, self.portforwardings_dict[pf_id]) self.assertEqual( len(pf_ids), len(self.mapping.managed_port_forwardings.keys())) fip_pf_set = { self.floatingip1: set( [self.portforwarding1.id, self.portforwarding2.id]), self.floatingip2: set([self.portforwarding3.id]), self.floatingip3: set([self.portforwarding4.id]) } for fip_id, pf_set in self.mapping.fip_port_forwarding.items(): self.assertIn( fip_id, [self.floatingip1, self.floatingip2, self.floatingip3]) self.assertEqual(0, len(pf_set - fip_pf_set[fip_id])) self.assertEqual( len([self.floatingip1, self.floatingip2, self.floatingip3]), len(self.mapping.fip_port_forwarding)) router_fip = { self.router1: set([self.floatingip1, self.floatingip2]), self.router2: set([self.floatingip3]) } for router_id, fip_set in self.mapping.router_fip_mapping.items(): self.assertIn(router_id, [self.router1, self.router2]) self.assertEqual(0, len(fip_set - router_fip[router_id])) self.assertEqual( len([self.router1, self.router2]), len(self.mapping.router_fip_mapping.keys())) def test_update_port_forwarding(self): self._set_pf() description = 'Some description' new_pf1 = pf_obj.PortForwarding( context=None, id=self.portforwarding2.id, floatingip_id=self.floatingip1, external_port=11122, protocol='tcp', internal_port_id=self.portforwarding2.internal_port_id, internal_ip_address='1.1.1.22', internal_port=11122, floating_ip_address='111.111.111.111', router_id=self.router1, description=description) self.mapping.update_port_forwardings([new_pf1]) self.assertEqual( new_pf1, self.mapping.managed_port_forwardings[self.portforwarding2.id]) def test_del_port_forwardings(self): self._set_pf() del_pfs = [self.portforwarding3, self.portforwarding2, self.portforwarding4] self.mapping.del_port_forwardings(del_pfs) self.assertEqual( [self.portforwarding1.id], list(self.mapping.managed_port_forwardings.keys())) self.assertEqual({self.floatingip1: set([self.portforwarding1.id])}, self.mapping.fip_port_forwarding) self.assertEqual({self.router1: set([self.floatingip1])}, self.mapping.router_fip_mapping) def test_clear_by_fip(self): self._set_pf() self.mapping.clear_by_fip(self.floatingip1, self.router1) router_fip = { self.router1: set([self.floatingip2]), self.router2: set([self.floatingip3]) } for router_id, fip_set in self.mapping.router_fip_mapping.items(): self.assertIn(router_id, [self.router1, self.router2]) self.assertEqual(0, len(fip_set - router_fip[router_id])) fip_pf_set = { self.floatingip2: set([self.portforwarding3.id]), self.floatingip3: set([self.portforwarding4.id]) } for fip_id, pf_set in self.mapping.fip_port_forwarding.items(): self.assertIn( fip_id, [self.floatingip2, self.floatingip3]) self.assertEqual(0, len(pf_set - fip_pf_set[fip_id])) self.assertEqual( len([self.floatingip2, self.floatingip3]), len(self.mapping.fip_port_forwarding)) pfs_dict = {self.portforwarding3.id: self.portforwarding3, self.portforwarding4.id: self.portforwarding4} for pf_id, obj in self.mapping.managed_port_forwardings.items(): self.assertIn(pf_id, [self.portforwarding3.id, self.portforwarding4.id]) self.assertEqual(obj, pfs_dict[pf_id]) self.assertEqual( len([self.portforwarding3.id, self.portforwarding4.id]), len(self.mapping.managed_port_forwardings.keys()))