]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Add address scope to floating IPs in RPC response to L3 agent
authorCarl Baldwin <carl.baldwin@hpe.com>
Wed, 23 Dec 2015 17:16:15 +0000 (10:16 -0700)
committerCarl Baldwin <carl.baldwin@hpe.com>
Wed, 23 Dec 2015 17:16:15 +0000 (10:16 -0700)
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
neutron/agent/l3/agent.py
neutron/api/rpc/handlers/l3_rpc.py
neutron/db/l3_db.py
neutron/tests/unit/db/test_l3_db.py

index 71186419134779cb7e96c9a52e948992a9d7fa9f..c3c39cc7f63508186a2356dc5cd782d0cd3f2574 100644 (file)
@@ -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
 ~~~~~
 
index e71e51d87201135984c9bf979875e198596ec7cd..94289dcdb62cccb37fe10aa15e99dc025364de31 100644 (file)
@@ -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):
index ac87c3d71583fe7defe2353533626a567cd2811f..e0ec3e1c61e4fad74f4192532afbb4385c5f1109 100644 (file)
@@ -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):
index d27a62dc54332fc1af0df1416d06fdb8dc78ac9e..c01d8d588d97cdf17bb1d81b95bb2072bd301dcf 100644 (file)
@@ -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."""
index cd71a2169201196a84d5530622e97cfb8a39ee07..5694e6808e22a29f5e0bf218fa75b5e077e46fa2 100644 (file)
@@ -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)