From 02439b6730444d29f139dcc12536f0d2d02a51f7 Mon Sep 17 00:00:00 2001 From: Carl Baldwin Date: Wed, 23 Dec 2015 10:16:15 -0700 Subject: [PATCH] Add address scope to floating IPs in RPC response to L3 agent The L3 agent needs to know the address scope of the fixed ip of each floating ip because floating ips are a way to cross scope boundaries. Without the scope information, there could be ambiguity and no way to know which scope to send it to. [1] https://review.openstack.org/#/c/189741/ Change-Id: Id9f8c12954a6efbf4d9b99c011652eefbe5f5145 Partially-Implements: blueprint address-scopes --- doc/source/devref/address_scopes.rst | 22 +++++++++++-- neutron/agent/l3/agent.py | 1 + neutron/api/rpc/handlers/l3_rpc.py | 3 +- neutron/db/l3_db.py | 46 ++++++++++++++++++++++++++-- neutron/tests/unit/db/test_l3_db.py | 34 ++++++++++++++++++++ 5 files changed, 101 insertions(+), 5 deletions(-) diff --git a/doc/source/devref/address_scopes.rst b/doc/source/devref/address_scopes.rst index 711864191..c3c39cc7f 100644 --- a/doc/source/devref/address_scopes.rst +++ b/doc/source/devref/address_scopes.rst @@ -100,8 +100,9 @@ Routing The reference implementation honors address scopes. Within an address scope, addresses route freely (barring any FW rules or other external restrictions). -Between scopes, routed is prevented unless address translation is used. Future -patches will expand on this. +Between scopes, routed is prevented unless address translation is used. For +now, floating IPs are the only place where traffic crosses scope boundaries. +The 1-1 NAT allows this to happen. .. TODO (Carl) Implement NAT for floating ips crossing scopes .. TODO (Carl) Implement SNAT for crossing scopes @@ -135,6 +136,23 @@ Here is an example of how the json will look in the context of a router port:: "6": null }, +To implement floating IPs crossing scope boundaries, the L3 agent needs to know +the target scope of the floating ip. The fixed address is not enough to +disambiguate because, theoritically, there could be overlapping addresses from +different scopes. The scope is computed [#]_ from the floating ip fixed port +and attached to the floating ip dict under the 'fixed_ip_address_scope' +attribute. Here's what the json looks like (trimmed):: + + { + ... + "floating_ip_address": "172.24.4.4", + "fixed_ip_address": "172.16.0.3", + "fixed_ip_address_scope": "d010a0ea-660e-4df4-86ca-ae2ed96da5c1", + ... + } + +.. [#] neutron/db/l3_db.py (_get_sync_floating_ips) + Model ~~~~~ diff --git a/neutron/agent/l3/agent.py b/neutron/agent/l3/agent.py index e71e51d87..94289dcdb 100644 --- a/neutron/agent/l3/agent.py +++ b/neutron/agent/l3/agent.py @@ -82,6 +82,7 @@ class L3PluginApi(object): 1.6 - Added process_prefix_update 1.7 - DVR support: new L3 plugin methods added. - delete_agent_gateway_port + 1.8 - Added address scope information """ def __init__(self, topic, host): diff --git a/neutron/api/rpc/handlers/l3_rpc.py b/neutron/api/rpc/handlers/l3_rpc.py index ac87c3d71..e0ec3e1c6 100644 --- a/neutron/api/rpc/handlers/l3_rpc.py +++ b/neutron/api/rpc/handlers/l3_rpc.py @@ -45,7 +45,8 @@ class L3RpcCallback(object): # 1.5 Added update_ha_routers_states # 1.6 Added process_prefix_update to support IPv6 Prefix Delegation # 1.7 Added method delete_agent_gateway_port for DVR Routers - target = oslo_messaging.Target(version='1.7') + # 1.8 Added address scope information + target = oslo_messaging.Target(version='1.8') @property def plugin(self): diff --git a/neutron/db/l3_db.py b/neutron/db/l3_db.py index d27a62dc5..c01d8d588 100644 --- a/neutron/db/l3_db.py +++ b/neutron/db/l3_db.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import itertools import netaddr from oslo_log import log as logging from oslo_utils import uuidutils @@ -1180,11 +1181,52 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase): if r.get('gw_port')) return self._build_routers_list(context, router_dicts, gw_ports) + @staticmethod + def _unique_floatingip_iterator(query): + """Iterates over only one row per floating ip. Ignores others.""" + # Group rows by fip id. They must be sorted by same. + q = query.order_by(FloatingIP.id) + keyfunc = lambda row: row[0]['id'] + group_iterator = itertools.groupby(q, keyfunc) + + # Just hit the first row of each group + for key, value in group_iterator: + yield six.next(value) + + def _make_floatingip_dict_with_scope(self, floatingip_db, scope_id): + d = self._make_floatingip_dict(floatingip_db) + d['fixed_ip_address_scope'] = scope_id + return d + def _get_sync_floating_ips(self, context, router_ids): - """Query floating_ips that relate to list of router_ids.""" + """Query floating_ips that relate to list of router_ids with scope. + + This is different than the regular get_floatingips in that it finds the + address scope of the fixed IP. The router needs to know this to + distinguish it from other scopes. + + There are a few redirections to go through to discover the address + scope from the floating ip. + """ if not router_ids: return [] - return self.get_floatingips(context, {'router_id': router_ids}) + + query = context.session.query(FloatingIP, + models_v2.SubnetPool.address_scope_id) + query = query.join(models_v2.Port, + FloatingIP.fixed_port_id == models_v2.Port.id) + # Outer join of Subnet can cause each ip to have more than one row. + query = query.outerjoin(models_v2.Subnet, + models_v2.Subnet.network_id == models_v2.Port.network_id) + query = query.filter(models_v2.Subnet.ip_version == 4) + query = query.outerjoin(models_v2.SubnetPool, + models_v2.Subnet.subnetpool_id == models_v2.SubnetPool.id) + + # Filter out on router_ids + query = query.filter(FloatingIP.router_id.in_(router_ids)) + + return [self._make_floatingip_dict_with_scope(*row) + for row in self._unique_floatingip_iterator(query)] def _get_sync_interfaces(self, context, router_ids, device_owners=None): """Query router interfaces that relate to list of router_ids.""" diff --git a/neutron/tests/unit/db/test_l3_db.py b/neutron/tests/unit/db/test_l3_db.py index cd71a2169..5694e6808 100644 --- a/neutron/tests/unit/db/test_l3_db.py +++ b/neutron/tests/unit/db/test_l3_db.py @@ -96,3 +96,37 @@ class TestL3_NAT_dbonly_mixin(base.BaseTestCase): 'network_id': 'net_id', 'subnets': [{k: subnet[k] for k in keys}], 'address_scopes': address_scopes}], ports) + + def test__get_sync_floating_ips_no_query(self): + """Basic test that no query is performed if no router ids are passed""" + db = l3_db.L3_NAT_dbonly_mixin() + context = mock.Mock() + db._get_sync_floating_ips(context, []) + self.assertFalse(context.session.query.called) + + @mock.patch.object(l3_db.L3_NAT_dbonly_mixin, '_make_floatingip_dict') + def test__make_floatingip_dict_with_scope(self, make_fip_dict): + db = l3_db.L3_NAT_dbonly_mixin() + make_fip_dict.return_value = {'id': mock.sentinel.fip_ip} + result = db._make_floatingip_dict_with_scope( + mock.sentinel.floating_ip_db, mock.sentinel.address_scope_id) + self.assertEqual({ + 'fixed_ip_address_scope': mock.sentinel.address_scope_id, + 'id': mock.sentinel.fip_ip}, result) + + def test__unique_floatingip_iterator(self): + query = mock.MagicMock() + query.order_by().__iter__.return_value = [ + ({'id': 'id1'}, 'scope1'), + ({'id': 'id1'}, 'scope1'), + ({'id': 'id2'}, 'scope2'), + ({'id': 'id2'}, 'scope2'), + ({'id': 'id2'}, 'scope2'), + ({'id': 'id3'}, 'scope3')] + query.reset_mock() + result = list( + l3_db.L3_NAT_dbonly_mixin._unique_floatingip_iterator(query)) + query.order_by.assert_called_once_with(l3_db.FloatingIP.id) + self.assertEqual([({'id': 'id1'}, 'scope1'), + ({'id': 'id2'}, 'scope2'), + ({'id': 'id3'}, 'scope3')], result) -- 2.45.2