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
"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
~~~~~
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):
# 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):
# 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
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."""
'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)