From 2e11d85d6b3be3cf3be0dc988e0dfeb004ebce18 Mon Sep 17 00:00:00 2001 From: Ryan Tidwell Date: Tue, 1 Mar 2016 07:59:14 -0800 Subject: [PATCH] Queries for DVR-aware floating IP next-hop lookups Introduce calls to collect host routes for floating IP's in a DVR-aware way. When a floating IP is associated through a distributed router, the next-hop for the floating IP is announced as the floating IP agent gateway port on the corresponding host. Change-Id: I654e4d2496e19c011653eedb87c9b6026f801e9f Implements: blueprint bgp-dynamic-routing DocImpact: Neutron BGP should have docs created for the feature --- neutron/db/bgp_db.py | 112 ++++++++++- neutron/tests/unit/db/test_bgp_db.py | 184 ++++++++++++++++++ .../notes/bgp-support-ef361825ca63f28b.yaml | 23 +++ 3 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/bgp-support-ef361825ca63f28b.yaml diff --git a/neutron/db/bgp_db.py b/neutron/db/bgp_db.py index 94c26f94064..ddc281adb61 100644 --- a/neutron/db/bgp_db.py +++ b/neutron/db/bgp_db.py @@ -34,6 +34,7 @@ from neutron.db import l3_db from neutron.db import model_base from neutron.db import models_v2 from neutron.extensions import bgp as bgp_ext +from neutron.plugins.ml2 import models as ml2_models LOG = logging.getLogger(__name__) @@ -464,7 +465,10 @@ class BgpDbMixin(common_db.CommonDbMixin): fip_routes = self._get_central_fip_host_routes_by_bgp_speaker( context, bgp_speaker_id) - return itertools.chain(fip_routes, net_routes) + dvr_fip_routes = self._get_dvr_fip_host_routes_by_bgp_speaker( + context, + bgp_speaker_id) + return itertools.chain(fip_routes, net_routes, dvr_fip_routes) def get_routes_by_bgp_speaker_binding(self, context, bgp_speaker_id, network_id): @@ -478,7 +482,11 @@ class BgpDbMixin(common_db.CommonDbMixin): context, network_id, bgp_speaker_id) - return itertools.chain(fip_routes, net_routes) + dvr_fip_routes = self._get_dvr_fip_host_routes_by_binding( + context, + network_id, + bgp_speaker_id) + return itertools.chain(fip_routes, net_routes, dvr_fip_routes) def _get_routes_by_router(self, context, router_id): bgp_speaker_ids = self._get_bgp_speaker_ids_by_router(context, @@ -493,8 +501,12 @@ class BgpDbMixin(common_db.CommonDbMixin): context, router_id, bgp_speaker_id) - routes = list(itertools.chain(fip_routes, net_routes)) - route_dict[bgp_speaker_id] = routes + dvr_fip_routes = self._get_dvr_fip_host_routes_by_router( + context, + router_id, + bgp_speaker_id) + routes = itertools.chain(fip_routes, net_routes, dvr_fip_routes) + route_dict[bgp_speaker_id] = list(routes) return route_dict def _get_central_fip_host_routes_by_router(self, context, router_id, @@ -533,6 +545,21 @@ class BgpDbMixin(common_db.CommonDbMixin): query = query.filter(router_attrs.distributed != sa.sql.true()) return self._host_route_list_from_tuples(query.all()) + def _get_dvr_fip_host_routes_by_router(self, context, bgp_speaker_id, + router_id): + with context.session.begin(subtransactions=True): + gw_query = self._get_gateway_query(context, bgp_speaker_id) + + fip_query = self._get_fip_query(context, bgp_speaker_id) + fip_query.filter(l3_db.FloatingIP.router_id == router_id) + + #Create the join query + join_query = self._join_fip_by_host_binding_to_agent_gateway( + context, + fip_query.subquery(), + gw_query.subquery()) + return self._host_route_list_from_tuples(join_query.all()) + def _get_central_fip_host_routes_by_binding(self, context, network_id, bgp_speaker_id): """Get all floating IP host routes for the given network binding.""" @@ -572,6 +599,24 @@ class BgpDbMixin(common_db.CommonDbMixin): query = query.filter(router_attrs.distributed != sa.sql.true()) return self._host_route_list_from_tuples(query.all()) + def _get_dvr_fip_host_routes_by_binding(self, context, network_id, + bgp_speaker_id): + with context.session.begin(subtransactions=True): + BgpBinding = BgpSpeakerNetworkBinding + + gw_query = self._get_gateway_query(context, bgp_speaker_id) + gw_query.filter(BgpBinding.network_id == network_id) + + fip_query = self._get_fip_query(context, bgp_speaker_id) + fip_query.filter(BgpBinding.network_id == network_id) + + #Create the join query + join_query = self._join_fip_by_host_binding_to_agent_gateway( + context, + fip_query.subquery(), + gw_query.subquery()) + return self._host_route_list_from_tuples(join_query.all()) + def _get_central_fip_host_routes_by_bgp_speaker(self, context, bgp_speaker_id): """Get all the floating IP host routes advertised by a BgpSpeaker.""" @@ -613,6 +658,65 @@ class BgpDbMixin(common_db.CommonDbMixin): query = query.filter(router_attrs.distributed != sa.sql.true()) return self._host_route_list_from_tuples(query.all()) + def _get_gateway_query(self, context, bgp_speaker_id): + BgpBinding = BgpSpeakerNetworkBinding + ML2PortBinding = ml2_models.PortBinding + IpAllocation = models_v2.IPAllocation + Port = models_v2.Port + gw_query = context.session.query(Port.network_id, + ML2PortBinding.host, + IpAllocation.ip_address) + + #Subquery for FIP agent gateway ports + gw_query = gw_query.filter( + ML2PortBinding.port_id == Port.id, + IpAllocation.port_id == Port.id, + IpAllocation.subnet_id == models_v2.Subnet.id, + models_v2.Subnet.ip_version == 4, + Port.device_owner == lib_consts.DEVICE_OWNER_AGENT_GW, + Port.network_id == BgpBinding.network_id, + BgpBinding.bgp_speaker_id == bgp_speaker_id, + BgpBinding.ip_version == 4) + return gw_query + + def _get_fip_query(self, context, bgp_speaker_id): + BgpBinding = BgpSpeakerNetworkBinding + ML2PortBinding = ml2_models.PortBinding + + #Subquery for floating IP's + fip_query = context.session.query( + l3_db.FloatingIP.floating_network_id, + ML2PortBinding.host, + l3_db.FloatingIP.floating_ip_address) + fip_query = fip_query.filter( + l3_db.FloatingIP.fixed_port_id == ML2PortBinding.port_id, + l3_db.FloatingIP.floating_network_id == BgpBinding.network_id, + BgpBinding.bgp_speaker_id == bgp_speaker_id) + return fip_query + + def _get_dvr_fip_host_routes_by_bgp_speaker(self, context, + bgp_speaker_id): + with context.session.begin(subtransactions=True): + gw_query = self._get_gateway_query(context, bgp_speaker_id) + fip_query = self._get_fip_query(context, bgp_speaker_id) + + #Create the join query + join_query = self._join_fip_by_host_binding_to_agent_gateway( + context, + fip_query.subquery(), + gw_query.subquery()) + return self._host_route_list_from_tuples(join_query.all()) + + def _join_fip_by_host_binding_to_agent_gateway(self, context, + fip_subq, gw_subq): + join_query = context.session.query(fip_subq.c.floating_ip_address, + gw_subq.c.ip_address) + and_cond = and_( + gw_subq.c.host == fip_subq.c.host, + gw_subq.c.network_id == fip_subq.c.floating_network_id) + + return join_query.join(gw_subq, and_cond) + def _get_tenant_network_routes_by_binding(self, context, network_id, bgp_speaker_id): """Get all tenant network routes for the given network.""" diff --git a/neutron/tests/unit/db/test_bgp_db.py b/neutron/tests/unit/db/test_bgp_db.py index 1b426f48d62..6f4258f5bd5 100644 --- a/neutron/tests/unit/db/test_bgp_db.py +++ b/neutron/tests/unit/db/test_bgp_db.py @@ -20,6 +20,7 @@ from neutron.api.v2 import attributes as attrs from neutron.common import exceptions as n_exc from neutron.extensions import bgp from neutron.extensions import external_net +from neutron.extensions import portbindings from neutron import manager from neutron.plugins.common import constants as p_const from neutron.services.bgp import bgp_plugin @@ -145,6 +146,11 @@ class BgpTests(test_plugin.Ml2PluginV2TestCase, BgpEntityCreationMixin): fmt = 'json' + def setup_parent(self): + self.l3_plugin = ('neutron.tests.unit.extensions.test_l3.' + 'TestL3NatAgentSchedulingServicePlugin') + super(BgpTests, self).setup_parent() + def setUp(self): super(BgpTests, self).setUp() self.l3plugin = manager.NeutronManager.get_service_plugins().get( @@ -775,6 +781,184 @@ class BgpTests(test_plugin.Ml2PluginV2TestCase, self.assertTrue(tenant_prefix_found) self.assertTrue(fip_prefix_found) + def test_get_routes_by_bgp_speaker_id_with_fip_dvr(self): + gw_prefix = '172.16.10.0/24' + tenant_prefix = '10.10.10.0/24' + tenant_id = _uuid() + scope_data = {'tenant_id': tenant_id, 'ip_version': 4, + 'shared': True, 'name': 'bgp-scope'} + scope = self.plugin.create_address_scope( + self.context, + {'address_scope': scope_data}) + with self.router_with_external_and_tenant_networks( + tenant_id=tenant_id, + gw_prefix=gw_prefix, + tenant_prefix=tenant_prefix, + address_scope=scope, + distributed=True) as res: + router, ext_net, int_net = res + ext_gw_info = router['external_gateway_info'] + gw_net_id = ext_net['network']['id'] + tenant_net_id = int_net['network']['id'] + fixed_port_data = {'port': + {'name': 'test', + 'network_id': tenant_net_id, + 'tenant_id': tenant_id, + 'admin_state_up': True, + 'device_id': _uuid(), + 'device_owner': 'compute:nova', + 'mac_address': attrs.ATTR_NOT_SPECIFIED, + 'fixed_ips': attrs.ATTR_NOT_SPECIFIED, + portbindings.HOST_ID: 'test-host'}} + fixed_port = self.plugin.create_port(self.context, + fixed_port_data) + self.plugin._create_or_update_agent(self.context, + {'agent_type': 'L3 agent', + 'host': 'test-host', + 'binary': 'neutron-l3-agent', + 'topic': 'test'}) + fip_gw = self.l3plugin.create_fip_agent_gw_port_if_not_exists( + self.context, + gw_net_id, + 'test-host') + fip_data = {'floatingip': {'floating_network_id': gw_net_id, + 'tenant_id': tenant_id, + 'port_id': fixed_port['id']}} + fip = self.l3plugin.create_floatingip(self.context, fip_data) + fip_prefix = fip['floating_ip_address'] + '/32' + with self.bgp_speaker(4, 1234, networks=[gw_net_id]) as speaker: + bgp_speaker_id = speaker['id'] + routes = self.bgp_plugin.get_routes_by_bgp_speaker_id( + self.context, + bgp_speaker_id) + routes = list(routes) + cvr_gw_ip = ext_gw_info['external_fixed_ips'][0]['ip_address'] + dvr_gw_ip = fip_gw['fixed_ips'][0]['ip_address'] + self.assertEqual(2, len(routes)) + tenant_route_verified = False + fip_route_verified = False + for route in routes: + if route['destination'] == tenant_prefix: + self.assertEqual(cvr_gw_ip, route['next_hop']) + tenant_route_verified = True + if route['destination'] == fip_prefix: + self.assertEqual(dvr_gw_ip, route['next_hop']) + fip_route_verified = True + self.assertTrue(tenant_route_verified) + self.assertTrue(fip_route_verified) + + def test__get_dvr_fip_host_routes_by_binding(self): + gw_prefix = '172.16.10.0/24' + tenant_prefix = '10.10.10.0/24' + tenant_id = _uuid() + scope_data = {'tenant_id': tenant_id, 'ip_version': 4, + 'shared': True, 'name': 'bgp-scope'} + scope = self.plugin.create_address_scope( + self.context, + {'address_scope': scope_data}) + with self.router_with_external_and_tenant_networks( + tenant_id=tenant_id, + gw_prefix=gw_prefix, + tenant_prefix=tenant_prefix, + address_scope=scope, + distributed=True) as res: + router, ext_net, int_net = res + gw_net_id = ext_net['network']['id'] + tenant_net_id = int_net['network']['id'] + fixed_port_data = {'port': + {'name': 'test', + 'network_id': tenant_net_id, + 'tenant_id': tenant_id, + 'admin_state_up': True, + 'device_id': _uuid(), + 'device_owner': 'compute:nova', + 'mac_address': attrs.ATTR_NOT_SPECIFIED, + 'fixed_ips': attrs.ATTR_NOT_SPECIFIED, + portbindings.HOST_ID: 'test-host'}} + fixed_port = self.plugin.create_port(self.context, + fixed_port_data) + self.plugin._create_or_update_agent(self.context, + {'agent_type': 'L3 agent', + 'host': 'test-host', + 'binary': 'neutron-l3-agent', + 'topic': 'test'}) + fip_gw = self.l3plugin.create_fip_agent_gw_port_if_not_exists( + self.context, + gw_net_id, + 'test-host') + fip_data = {'floatingip': {'floating_network_id': gw_net_id, + 'tenant_id': tenant_id, + 'port_id': fixed_port['id']}} + fip = self.l3plugin.create_floatingip(self.context, fip_data) + fip_prefix = fip['floating_ip_address'] + '/32' + with self.bgp_speaker(4, 1234, networks=[gw_net_id]) as speaker: + bgp_speaker_id = speaker['id'] + routes = self.bgp_plugin._get_dvr_fip_host_routes_by_binding( + self.context, + gw_net_id, + bgp_speaker_id) + routes = list(routes) + dvr_gw_ip = fip_gw['fixed_ips'][0]['ip_address'] + self.assertEqual(1, len(routes)) + self.assertEqual(dvr_gw_ip, routes[0]['next_hop']) + self.assertEqual(fip_prefix, routes[0]['destination']) + + def test__get_dvr_fip_host_routes_by_router(self): + gw_prefix = '172.16.10.0/24' + tenant_prefix = '10.10.10.0/24' + tenant_id = _uuid() + scope_data = {'tenant_id': tenant_id, 'ip_version': 4, + 'shared': True, 'name': 'bgp-scope'} + scope = self.plugin.create_address_scope( + self.context, + {'address_scope': scope_data}) + with self.router_with_external_and_tenant_networks( + tenant_id=tenant_id, + gw_prefix=gw_prefix, + tenant_prefix=tenant_prefix, + address_scope=scope, + distributed=True) as res: + router, ext_net, int_net = res + gw_net_id = ext_net['network']['id'] + tenant_net_id = int_net['network']['id'] + fixed_port_data = {'port': + {'name': 'test', + 'network_id': tenant_net_id, + 'tenant_id': tenant_id, + 'admin_state_up': True, + 'device_id': _uuid(), + 'device_owner': 'compute:nova', + 'mac_address': attrs.ATTR_NOT_SPECIFIED, + 'fixed_ips': attrs.ATTR_NOT_SPECIFIED, + portbindings.HOST_ID: 'test-host'}} + fixed_port = self.plugin.create_port(self.context, + fixed_port_data) + self.plugin._create_or_update_agent(self.context, + {'agent_type': 'L3 agent', + 'host': 'test-host', + 'binary': 'neutron-l3-agent', + 'topic': 'test'}) + fip_gw = self.l3plugin.create_fip_agent_gw_port_if_not_exists( + self.context, + gw_net_id, + 'test-host') + fip_data = {'floatingip': {'floating_network_id': gw_net_id, + 'tenant_id': tenant_id, + 'port_id': fixed_port['id']}} + fip = self.l3plugin.create_floatingip(self.context, fip_data) + fip_prefix = fip['floating_ip_address'] + '/32' + with self.bgp_speaker(4, 1234, networks=[gw_net_id]) as speaker: + bgp_speaker_id = speaker['id'] + routes = self.bgp_plugin._get_dvr_fip_host_routes_by_router( + self.context, + bgp_speaker_id, + router['id']) + routes = list(routes) + dvr_gw_ip = fip_gw['fixed_ips'][0]['ip_address'] + self.assertEqual(1, len(routes)) + self.assertEqual(dvr_gw_ip, routes[0]['next_hop']) + self.assertEqual(fip_prefix, routes[0]['destination']) + def test_get_routes_by_bgp_speaker_binding_with_fip(self): gw_prefix = '172.16.10.0/24' tenant_prefix = '10.10.10.0/24' diff --git a/releasenotes/notes/bgp-support-ef361825ca63f28b.yaml b/releasenotes/notes/bgp-support-ef361825ca63f28b.yaml new file mode 100644 index 00000000000..33cdf7fcd2c --- /dev/null +++ b/releasenotes/notes/bgp-support-ef361825ca63f28b.yaml @@ -0,0 +1,23 @@ +--- +prelude: > + Announcement of tenant prefixes and host routes for floating + IP's via BGP is supported +features: + - Announcement of tenant subnets via BGP using centralized Neutron + router gateway port as the next-hop + - Announcement of floating IP host routes via BGP using the centralized + Neutron router gateway port as the next-hop + - Announcement of floating IP host routes via BGP using the floating + IP agent gateway as the next-hop when the floating IP is associated + through a distributed router +issues: + - When using DVR, if a floating IP is associated to a fixed IP direct + access to the fixed IP is not possible when traffic is sent from + outside of a Neutron tenant network (north-south traffic). Traffic + sent between tenant networks (east-west traffic) is not affected. + When using a distributed router, the floating IP will mask the fixed + IP making it inaccessible, even though the tenant subnet is being + announced as accessible through the centralized SNAT router. In such + a case, traffic sent to the instance should be directed to the + floating IP. This is a limitation of the Neutron L3 agent when using + DVR and will be addressed in a future release.