452 lines
20 KiB
Python
452 lines
20 KiB
Python
# Copyright (c) 2014 Red Hat, Inc.
|
|
# 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.
|
|
|
|
import copy
|
|
|
|
import mock
|
|
from neutron_lib.callbacks import events
|
|
from neutron_lib.callbacks import registry
|
|
from neutron_lib.callbacks import resources
|
|
from neutron_lib import constants as lib_constants
|
|
|
|
from neutron.agent.l3 import agent as l3_agent
|
|
from neutron.agent.l3 import namespace_manager
|
|
from neutron.agent.l3 import namespaces
|
|
from neutron.agent.linux import ip_lib
|
|
from neutron.common import utils
|
|
from neutron.tests.common import machine_fixtures
|
|
from neutron.tests.common import net_helpers
|
|
from neutron.tests.functional.agent.l3 import framework
|
|
|
|
|
|
class L3AgentTestCase(framework.L3AgentTestFramework):
|
|
|
|
def _test_agent_notifications_for_router_events(self, enable_ha=False):
|
|
"""Test notifications for router create, update, and delete.
|
|
|
|
Make sure that when the agent sends notifications of router events
|
|
for router create, update, and delete, that the correct handler is
|
|
called with the right resource, event, and router information.
|
|
"""
|
|
event_handler = mock.Mock()
|
|
registry.subscribe(event_handler,
|
|
resources.ROUTER, events.BEFORE_CREATE)
|
|
registry.subscribe(event_handler,
|
|
resources.ROUTER, events.AFTER_CREATE)
|
|
registry.subscribe(event_handler,
|
|
resources.ROUTER, events.BEFORE_UPDATE)
|
|
registry.subscribe(event_handler,
|
|
resources.ROUTER, events.AFTER_UPDATE)
|
|
registry.subscribe(event_handler,
|
|
resources.ROUTER, events.BEFORE_DELETE)
|
|
registry.subscribe(event_handler,
|
|
resources.ROUTER, events.AFTER_DELETE)
|
|
|
|
router_info = self.generate_router_info(enable_ha=enable_ha)
|
|
router = self.manage_router(self.agent, router_info)
|
|
with mock.patch.object(self.agent,
|
|
'check_ha_state_for_router') as check:
|
|
self.agent._process_updated_router(router.router)
|
|
self._delete_router(self.agent, router.router_id)
|
|
if enable_ha:
|
|
check.assert_called_once_with(router.router_id, None)
|
|
|
|
expected_calls = [
|
|
mock.call('router', 'before_create', self.agent, router=router),
|
|
mock.call('router', 'after_create', self.agent, router=router),
|
|
mock.call('router', 'before_update', self.agent, router=router),
|
|
mock.call('router', 'after_update', self.agent, router=router),
|
|
mock.call('router', 'before_delete', self.agent, payload=mock.ANY),
|
|
mock.call('router', 'after_delete', self.agent, router=router)]
|
|
event_handler.assert_has_calls(expected_calls)
|
|
|
|
def test_agent_notifications_for_router_events(self):
|
|
self._test_agent_notifications_for_router_events()
|
|
|
|
def test_agent_notifications_for_router_events_ha(self):
|
|
self._test_agent_notifications_for_router_events(enable_ha=True)
|
|
|
|
def test_legacy_router_update_floatingip_statuses(self):
|
|
self._test_update_floatingip_statuses(
|
|
self.generate_router_info(enable_ha=False))
|
|
|
|
def test_legacy_router_lifecycle(self):
|
|
self._router_lifecycle(enable_ha=False, dual_stack=True)
|
|
|
|
def test_legacy_router_lifecycle_with_no_gateway_subnet(self):
|
|
self.agent.conf.set_override('ipv6_gateway',
|
|
'fe80::f816:3eff:fe2e:1')
|
|
self._router_lifecycle(enable_ha=False, dual_stack=True,
|
|
v6_ext_gw_with_sub=False)
|
|
|
|
def test_legacy_router_gateway_update_to_none(self):
|
|
router_info = self.generate_router_info(False)
|
|
router = self.manage_router(self.agent, router_info)
|
|
gw_port = router.get_ex_gw_port()
|
|
interface_name = router.get_external_device_name(gw_port['id'])
|
|
device = ip_lib.IPDevice(interface_name, namespace=router.ns_name)
|
|
self.assertIn('gateway', device.route.get_gateway())
|
|
|
|
# Make this copy, so that the agent will think there is change in
|
|
# external gateway port.
|
|
router.ex_gw_port = copy.deepcopy(router.ex_gw_port)
|
|
for subnet in gw_port['subnets']:
|
|
subnet['gateway_ip'] = None
|
|
router.process()
|
|
|
|
self.assertIsNone(device.route.get_gateway())
|
|
|
|
def test_router_processing_pool_size(self):
|
|
router_info_1 = self.generate_router_info(False)
|
|
r1 = self.manage_router(self.agent, router_info_1)
|
|
self.assertEqual(l3_agent.ROUTER_PROCESS_GREENLET_MIN,
|
|
self.agent._pool.size)
|
|
|
|
router_info_2 = self.generate_router_info(False)
|
|
r2 = self.manage_router(self.agent, router_info_2)
|
|
self.assertEqual(l3_agent.ROUTER_PROCESS_GREENLET_MIN,
|
|
self.agent._pool.size)
|
|
|
|
router_info_list = [r1, r2]
|
|
for _i in range(l3_agent.ROUTER_PROCESS_GREENLET_MAX + 1):
|
|
ri = self.generate_router_info(False)
|
|
rtr = self.manage_router(self.agent, ri)
|
|
router_info_list.append(rtr)
|
|
|
|
self.assertEqual(l3_agent.ROUTER_PROCESS_GREENLET_MAX,
|
|
self.agent._pool.size)
|
|
|
|
for router in router_info_list:
|
|
self.agent._router_removed(router, router.router_id)
|
|
|
|
self.assertEqual(l3_agent.ROUTER_PROCESS_GREENLET_MIN,
|
|
self.agent._pool.size)
|
|
|
|
def _make_bridge(self):
|
|
bridge = framework.get_ovs_bridge(utils.get_rand_name())
|
|
bridge.create()
|
|
self.addCleanup(bridge.destroy)
|
|
return bridge
|
|
|
|
def test_external_network_bridge_change(self):
|
|
bridge1, bridge2 = self._make_bridge(), self._make_bridge()
|
|
self.agent.conf.set_override('external_network_bridge',
|
|
bridge1.br_name)
|
|
router_info = self.generate_router_info(False)
|
|
router = self.manage_router(self.agent, router_info)
|
|
gw_port = router.router['gw_port']
|
|
gw_inf_name = router.get_external_device_name(gw_port['id'])
|
|
|
|
self.assertIn(gw_inf_name,
|
|
[v.port_name for v in bridge1.get_vif_ports()])
|
|
# changeing the external_network_bridge should have no impact since
|
|
# the interface exists.
|
|
self.agent.conf.set_override('external_network_bridge',
|
|
bridge2.br_name)
|
|
self.manage_router(self.agent, router_info)
|
|
self.assertIn(gw_inf_name,
|
|
[v.port_name for v in bridge1.get_vif_ports()])
|
|
self.assertNotIn(gw_inf_name,
|
|
[v.port_name for v in bridge2.get_vif_ports()])
|
|
namespaces.Namespace.delete(router.router_namespace)
|
|
self.manage_router(self.agent, router_info)
|
|
self.assertIn(gw_inf_name,
|
|
[v.port_name for v in bridge2.get_vif_ports()])
|
|
self.assertNotIn(gw_inf_name,
|
|
[v.port_name for v in bridge1.get_vif_ports()])
|
|
|
|
def test_legacy_router_ns_rebuild(self):
|
|
router_info = self.generate_router_info(False)
|
|
router = self.manage_router(self.agent, router_info)
|
|
gw_port = router.router['gw_port']
|
|
gw_inf_name = router.get_external_device_name(gw_port['id'])
|
|
gw_device = ip_lib.IPDevice(gw_inf_name, namespace=router.ns_name)
|
|
router_ports = [gw_device]
|
|
for i_port in router_info.get(lib_constants.INTERFACE_KEY, []):
|
|
interface_name = router.get_internal_device_name(i_port['id'])
|
|
router_ports.append(
|
|
ip_lib.IPDevice(interface_name, namespace=router.ns_name))
|
|
|
|
namespaces.Namespace.delete(router.router_namespace)
|
|
|
|
# l3 agent should be able to rebuild the ns when it is deleted
|
|
self.manage_router(self.agent, router_info)
|
|
# Assert the router ports are there in namespace
|
|
self.assertTrue(all([port.exists() for port in router_ports]))
|
|
|
|
self._delete_router(self.agent, router.router_id)
|
|
|
|
def test_conntrack_disassociate_fip_legacy_router(self):
|
|
self._test_conntrack_disassociate_fip(ha=False)
|
|
|
|
def _test_periodic_sync_routers_task(self,
|
|
routers_to_keep,
|
|
routers_deleted,
|
|
routers_deleted_during_resync):
|
|
ns_names_to_retrieve = set()
|
|
deleted_routers_info = []
|
|
for r in routers_to_keep:
|
|
ri = self.manage_router(self.agent, r)
|
|
ns_names_to_retrieve.add(ri.ns_name)
|
|
for r in routers_deleted + routers_deleted_during_resync:
|
|
ri = self.manage_router(self.agent, r)
|
|
deleted_routers_info.append(ri)
|
|
ns_names_to_retrieve.add(ri.ns_name)
|
|
|
|
mocked_get_router_ids = self.mock_plugin_api.get_router_ids
|
|
mocked_get_router_ids.return_value = [r['id'] for r in
|
|
routers_to_keep +
|
|
routers_deleted_during_resync]
|
|
mocked_get_routers = self.mock_plugin_api.get_routers
|
|
mocked_get_routers.return_value = (routers_to_keep +
|
|
routers_deleted_during_resync)
|
|
# clear agent router_info as it will be after restart
|
|
self.agent.router_info = {}
|
|
|
|
# Synchronize the agent with the plug-in
|
|
with mock.patch.object(namespace_manager.NamespaceManager, 'list_all',
|
|
return_value=ns_names_to_retrieve):
|
|
self.agent.periodic_sync_routers_task(self.agent.context)
|
|
|
|
# Mock the plugin RPC API so a known external network id is returned
|
|
# when the router updates are processed by the agent
|
|
external_network_id = framework._uuid()
|
|
self.mock_plugin_api.get_external_network_id.return_value = (
|
|
external_network_id)
|
|
|
|
# Plug external_gateway_info in the routers that are not going to be
|
|
# deleted by the agent when it processes the updates. Otherwise,
|
|
# _process_router_if_compatible in the agent fails
|
|
for r in routers_to_keep:
|
|
r['external_gateway_info'] = {'network_id': external_network_id}
|
|
|
|
# while sync updates are still in the queue, higher priority
|
|
# router_deleted events may be added there as well
|
|
for r in routers_deleted_during_resync:
|
|
self.agent.router_deleted(self.agent.context, r['id'])
|
|
|
|
# make sure all events are processed
|
|
while not self.agent._queue._queue.empty():
|
|
self.agent._process_router_update()
|
|
|
|
for r in routers_to_keep:
|
|
self.assertIn(r['id'], self.agent.router_info)
|
|
self.assertTrue(self._namespace_exists(namespaces.NS_PREFIX +
|
|
r['id']))
|
|
for ri in deleted_routers_info:
|
|
self.assertNotIn(ri.router_id,
|
|
self.agent.router_info)
|
|
self._assert_router_does_not_exist(ri)
|
|
|
|
def test_periodic_sync_routers_task(self):
|
|
routers_to_keep = []
|
|
for i in range(2):
|
|
routers_to_keep.append(self.generate_router_info(False))
|
|
self._test_periodic_sync_routers_task(routers_to_keep,
|
|
routers_deleted=[],
|
|
routers_deleted_during_resync=[])
|
|
|
|
def test_periodic_sync_routers_task_routers_deleted_while_agent_down(self):
|
|
routers_to_keep = []
|
|
routers_deleted = []
|
|
for i in range(2):
|
|
routers_to_keep.append(self.generate_router_info(False))
|
|
for i in range(2):
|
|
routers_deleted.append(self.generate_router_info(False))
|
|
self._test_periodic_sync_routers_task(routers_to_keep,
|
|
routers_deleted,
|
|
routers_deleted_during_resync=[])
|
|
|
|
def test_periodic_sync_routers_task_routers_deleted_while_agent_sync(self):
|
|
routers_to_keep = []
|
|
routers_deleted_during_resync = []
|
|
for i in range(2):
|
|
routers_to_keep.append(self.generate_router_info(False))
|
|
for i in range(2):
|
|
routers_deleted_during_resync.append(
|
|
self.generate_router_info(False))
|
|
self._test_periodic_sync_routers_task(
|
|
routers_to_keep,
|
|
routers_deleted=[],
|
|
routers_deleted_during_resync=routers_deleted_during_resync)
|
|
|
|
def _setup_fip_with_fixed_ip_from_same_subnet(self, enable_snat):
|
|
"""Setup 2 FakeMachines from same subnet, one with floatingip
|
|
associated.
|
|
"""
|
|
router_info = self.generate_router_info(enable_ha=False,
|
|
enable_snat=enable_snat)
|
|
router = self.manage_router(self.agent, router_info)
|
|
router_ip_cidr = self._port_first_ip_cidr(router.internal_ports[0])
|
|
router_ip = router_ip_cidr.partition('/')[0]
|
|
|
|
br_int = framework.get_ovs_bridge(
|
|
self.agent.conf.ovs_integration_bridge)
|
|
|
|
src_machine, dst_machine = self.useFixture(
|
|
machine_fixtures.PeerMachines(
|
|
br_int,
|
|
net_helpers.increment_ip_cidr(router_ip_cidr),
|
|
router_ip)).machines
|
|
|
|
dst_fip = '19.4.4.10'
|
|
router.router[lib_constants.FLOATINGIP_KEY] = []
|
|
self._add_fip(router, dst_fip, fixed_address=dst_machine.ip)
|
|
router.process()
|
|
|
|
return src_machine, dst_machine, dst_fip
|
|
|
|
def test_fip_connection_from_same_subnet(self):
|
|
'''Test connection to floatingip which is associated with
|
|
fixed_ip on the same subnet of the source fixed_ip.
|
|
In other words it confirms that return packets surely
|
|
go through the router.
|
|
'''
|
|
src_machine, dst_machine, dst_fip = (
|
|
self._setup_fip_with_fixed_ip_from_same_subnet(enable_snat=True))
|
|
protocol_port = net_helpers.get_free_namespace_port(
|
|
lib_constants.PROTO_NAME_TCP, dst_machine.namespace)
|
|
# client sends to fip
|
|
netcat = net_helpers.NetcatTester(
|
|
src_machine.namespace, dst_machine.namespace,
|
|
dst_fip, protocol_port,
|
|
protocol=net_helpers.NetcatTester.TCP)
|
|
self.addCleanup(netcat.stop_processes)
|
|
self.assertTrue(netcat.test_connectivity())
|
|
|
|
def test_ping_floatingip_reply_with_floatingip(self):
|
|
src_machine, _, dst_fip = (
|
|
self._setup_fip_with_fixed_ip_from_same_subnet(enable_snat=False))
|
|
|
|
# Verify that the ping replys with fip
|
|
ns_ip_wrapper = ip_lib.IPWrapper(src_machine.namespace)
|
|
result = ns_ip_wrapper.netns.execute(
|
|
['ping', '-c', 1, '-W', 5, dst_fip])
|
|
self._assert_ping_reply_from_expected_address(result, dst_fip)
|
|
|
|
def _setup_address_scope(self, internal_address_scope1,
|
|
internal_address_scope2, gw_address_scope=None):
|
|
router_info = self.generate_router_info(enable_ha=False,
|
|
num_internal_ports=2)
|
|
address_scope1 = {
|
|
str(lib_constants.IP_VERSION_4): internal_address_scope1}
|
|
address_scope2 = {
|
|
str(lib_constants.IP_VERSION_4): internal_address_scope2}
|
|
if gw_address_scope:
|
|
router_info['gw_port']['address_scopes'] = {
|
|
str(lib_constants.IP_VERSION_4): gw_address_scope}
|
|
router_info[lib_constants.INTERFACE_KEY][0]['address_scopes'] = (
|
|
address_scope1)
|
|
router_info[lib_constants.INTERFACE_KEY][1]['address_scopes'] = (
|
|
address_scope2)
|
|
|
|
router = self.manage_router(self.agent, router_info)
|
|
router_ip_cidr1 = self._port_first_ip_cidr(router.internal_ports[0])
|
|
router_ip1 = router_ip_cidr1.partition('/')[0]
|
|
router_ip_cidr2 = self._port_first_ip_cidr(router.internal_ports[1])
|
|
router_ip2 = router_ip_cidr2.partition('/')[0]
|
|
|
|
br_int = framework.get_ovs_bridge(
|
|
self.agent.conf.ovs_integration_bridge)
|
|
test_machine1 = self.useFixture(
|
|
machine_fixtures.FakeMachine(
|
|
br_int,
|
|
net_helpers.increment_ip_cidr(router_ip_cidr1),
|
|
router_ip1))
|
|
test_machine2 = self.useFixture(
|
|
machine_fixtures.FakeMachine(
|
|
br_int,
|
|
net_helpers.increment_ip_cidr(router_ip_cidr2),
|
|
router_ip2))
|
|
|
|
return test_machine1, test_machine2, router
|
|
|
|
def test_connection_from_same_address_scope(self):
|
|
test_machine1, test_machine2, _ = self._setup_address_scope(
|
|
'scope1', 'scope1')
|
|
# Internal networks that are in the same address scope can connected
|
|
# each other
|
|
net_helpers.assert_ping(test_machine1.namespace, test_machine2.ip)
|
|
net_helpers.assert_ping(test_machine2.namespace, test_machine1.ip)
|
|
|
|
def test_connection_from_diff_address_scope(self):
|
|
test_machine1, test_machine2, _ = self._setup_address_scope(
|
|
'scope1', 'scope2')
|
|
# Internal networks that are not in the same address scope should
|
|
# not reach each other
|
|
test_machine1.assert_no_ping(test_machine2.ip)
|
|
test_machine2.assert_no_ping(test_machine1.ip)
|
|
|
|
def test_fip_connection_for_address_scope(self):
|
|
(machine_same_scope, machine_diff_scope,
|
|
router) = self._setup_address_scope('scope1', 'scope2', 'scope1')
|
|
|
|
router.router[lib_constants.FLOATINGIP_KEY] = []
|
|
fip_same_scope = '19.4.4.10'
|
|
self._add_fip(router, fip_same_scope,
|
|
fixed_address=machine_same_scope.ip,
|
|
fixed_ip_address_scope='scope1')
|
|
fip_diff_scope = '19.4.4.11'
|
|
self._add_fip(router, fip_diff_scope,
|
|
fixed_address=machine_diff_scope.ip,
|
|
fixed_ip_address_scope='scope2')
|
|
router.process()
|
|
|
|
br_ex = framework.get_ovs_bridge(
|
|
self.agent.conf.external_network_bridge)
|
|
src_machine = self.useFixture(
|
|
machine_fixtures.FakeMachine(br_ex, '19.4.4.12/24'))
|
|
# Floating ip should work no matter of address scope
|
|
net_helpers.assert_ping(src_machine.namespace, fip_same_scope)
|
|
net_helpers.assert_ping(src_machine.namespace, fip_diff_scope)
|
|
|
|
def test_direct_route_for_address_scope(self):
|
|
(machine_same_scope, machine_diff_scope,
|
|
router) = self._setup_address_scope('scope1', 'scope2', 'scope1')
|
|
|
|
gw_port = router.get_ex_gw_port()
|
|
gw_ip = self._port_first_ip_cidr(gw_port).partition('/')[0]
|
|
br_ex = framework.get_ovs_bridge(
|
|
self.agent.conf.external_network_bridge)
|
|
|
|
src_machine = self.useFixture(
|
|
machine_fixtures.FakeMachine(br_ex, '19.4.4.12/24', gw_ip))
|
|
# For the internal networks that are in the same address scope as
|
|
# external network, they can directly route to external network
|
|
net_helpers.assert_ping(src_machine.namespace, machine_same_scope.ip)
|
|
# For the internal networks that are not in the same address scope as
|
|
# external networks. SNAT will be used. Direct route will not work
|
|
# here.
|
|
src_machine.assert_no_ping(machine_diff_scope.ip)
|
|
|
|
def test_connection_from_diff_address_scope_with_fip(self):
|
|
(machine_same_scope, machine_diff_scope,
|
|
router) = self._setup_address_scope('scope1', 'scope2', 'scope1')
|
|
|
|
router.router[lib_constants.FLOATINGIP_KEY] = []
|
|
fip = '19.4.4.11'
|
|
self._add_fip(router, fip,
|
|
fixed_address=machine_diff_scope.ip,
|
|
fixed_ip_address_scope='scope2')
|
|
router.process()
|
|
|
|
# For the internal networks that are in the same address scope as
|
|
# external network, they should be able to reach the floating ip
|
|
net_helpers.assert_ping(machine_same_scope.namespace, fip)
|
|
# For the port with fip, it should be able to reach the internal
|
|
# networks that are in the same address scope as external network
|
|
net_helpers.assert_ping(machine_diff_scope.namespace,
|
|
machine_same_scope.ip)
|