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
This commit is contained in:
Ryan Tidwell 2016-03-01 07:59:14 -08:00
parent f0b042f58a
commit 8a9d83dc0b
3 changed files with 315 additions and 4 deletions

View File

@ -34,6 +34,7 @@ from neutron.db import l3_db
from neutron.db import model_base from neutron.db import model_base
from neutron.db import models_v2 from neutron.db import models_v2
from neutron.extensions import bgp as bgp_ext from neutron.extensions import bgp as bgp_ext
from neutron.plugins.ml2 import models as ml2_models
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -464,7 +465,10 @@ class BgpDbMixin(common_db.CommonDbMixin):
fip_routes = self._get_central_fip_host_routes_by_bgp_speaker( fip_routes = self._get_central_fip_host_routes_by_bgp_speaker(
context, context,
bgp_speaker_id) 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, def get_routes_by_bgp_speaker_binding(self, context,
bgp_speaker_id, network_id): bgp_speaker_id, network_id):
@ -478,7 +482,11 @@ class BgpDbMixin(common_db.CommonDbMixin):
context, context,
network_id, network_id,
bgp_speaker_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): def _get_routes_by_router(self, context, router_id):
bgp_speaker_ids = self._get_bgp_speaker_ids_by_router(context, bgp_speaker_ids = self._get_bgp_speaker_ids_by_router(context,
@ -493,8 +501,12 @@ class BgpDbMixin(common_db.CommonDbMixin):
context, context,
router_id, router_id,
bgp_speaker_id) bgp_speaker_id)
routes = list(itertools.chain(fip_routes, net_routes)) dvr_fip_routes = self._get_dvr_fip_host_routes_by_router(
route_dict[bgp_speaker_id] = routes 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 return route_dict
def _get_central_fip_host_routes_by_router(self, context, router_id, 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()) query = query.filter(router_attrs.distributed != sa.sql.true())
return self._host_route_list_from_tuples(query.all()) 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, def _get_central_fip_host_routes_by_binding(self, context,
network_id, bgp_speaker_id): network_id, bgp_speaker_id):
"""Get all floating IP host routes for the given network binding.""" """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()) query = query.filter(router_attrs.distributed != sa.sql.true())
return self._host_route_list_from_tuples(query.all()) 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, def _get_central_fip_host_routes_by_bgp_speaker(self, context,
bgp_speaker_id): bgp_speaker_id):
"""Get all the floating IP host routes advertised by a BgpSpeaker.""" """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()) query = query.filter(router_attrs.distributed != sa.sql.true())
return self._host_route_list_from_tuples(query.all()) 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, def _get_tenant_network_routes_by_binding(self, context,
network_id, bgp_speaker_id): network_id, bgp_speaker_id):
"""Get all tenant network routes for the given network.""" """Get all tenant network routes for the given network."""

View File

@ -20,6 +20,7 @@ from neutron.api.v2 import attributes as attrs
from neutron.common import exceptions as n_exc from neutron.common import exceptions as n_exc
from neutron.extensions import bgp from neutron.extensions import bgp
from neutron.extensions import external_net from neutron.extensions import external_net
from neutron.extensions import portbindings
from neutron import manager from neutron import manager
from neutron.plugins.common import constants as p_const from neutron.plugins.common import constants as p_const
from neutron.services.bgp import bgp_plugin from neutron.services.bgp import bgp_plugin
@ -145,6 +146,11 @@ class BgpTests(test_plugin.Ml2PluginV2TestCase,
BgpEntityCreationMixin): BgpEntityCreationMixin):
fmt = 'json' fmt = 'json'
def setup_parent(self):
self.l3_plugin = ('neutron.tests.unit.extensions.test_l3.'
'TestL3NatAgentSchedulingServicePlugin')
super(BgpTests, self).setup_parent()
def setUp(self): def setUp(self):
super(BgpTests, self).setUp() super(BgpTests, self).setUp()
self.l3plugin = manager.NeutronManager.get_service_plugins().get( self.l3plugin = manager.NeutronManager.get_service_plugins().get(
@ -775,6 +781,184 @@ class BgpTests(test_plugin.Ml2PluginV2TestCase,
self.assertTrue(tenant_prefix_found) self.assertTrue(tenant_prefix_found)
self.assertTrue(fip_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): def test_get_routes_by_bgp_speaker_binding_with_fip(self):
gw_prefix = '172.16.10.0/24' gw_prefix = '172.16.10.0/24'
tenant_prefix = '10.10.10.0/24' tenant_prefix = '10.10.10.0/24'

View File

@ -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.