--- /dev/null
+Subnet Pools and Address Scopes
+===============================
+
+This page discusses subnet pools and address scopes
+
+Subnet Pools
+------------
+
+Learn about subnet pools by watching the summit talk given in Vancouver [#]_.
+
+.. [#] http://www.youtube.com/watch?v=QqP8yBUUXBM&t=6m12s
+
+Subnet pools were added in Kilo. They are relatively simple. A SubnetPool has
+any number of SubnetPoolPrefix objects associated to it. These prefixes are in
+CIDR format. Each CIDR is a piece of the address space that is available for
+allocation.
+
+Subnet Pools support IPv6 just as well as IPv4.
+
+The Subnet model object now has a subnetpool_id attribute whose default is null
+for backward compatibility. The subnetpool_id attribute stores the UUID of the
+subnet pool that acted as the source for the address range of a particular
+subnet.
+
+When creating a subnet, the subnetpool_id can be optionally specified. If it
+is, the 'cidr' field is not required. If 'cidr' is specified, it will be
+allocated from the pool assuming the pool includes it and hasn't already
+allocated any part of it. If 'cidr' is left out, then the prefixlen attribute
+can be specified. If it is not, the default prefix length will be taken from
+the subnet pool. Think of it this way, the allocation logic always needs to
+know the size of the subnet desired. It can pull it from a specific CIDR,
+prefixlen, or default. A specific CIDR is optional and the allocation will try
+to honor it if provided. The request will fail if it can't honor it.
+
+Subnet pools do not allow overlap of subnets.
+
+Subnet Pool Quotas
+~~~~~~~~~~~~~~~~~~
+
+A quota mechanism was provided for subnet pools. It is different than other
+quota mechanisms in Neutron because it doesn't count instances of first class
+objects. Instead it counts how much of the address space is used.
+
+For IPv4, it made reasonable sense to count quota in terms of individual
+addresses. So, if you're allowed exactly one /24, your quota should be set to
+256. Three /26s would be 192. This mechanism encourages more efficient use of
+the IPv4 space which will be increasingly important when working with globally
+routable addresses.
+
+For IPv6, the smallest viable subnet in Neutron is a /64. There is no reason
+to allocate a subnet of any other size for use on a Neutron network. It would
+look pretty funny to set a quota of 4611686018427387904 to allow one /64
+subnet. To avoid this, we count IPv6 quota in terms of /64s. So, a quota of 3
+allows three /64 subnets. When we need to allocate something smaller in the
+future, we will need to ensure that the code can handle non-integer quota
+consumption.
+
+Allocation
+~~~~~~~~~~
+
+Allocation is done in a way that aims to minimize fragmentation of the pool.
+The relevant code is here [#]_. First, the available prefixes are computed
+using a set difference: pool - allocations. The result is compacted [#]_ and
+then sorted by size. The subnet is then allocated from the smallest available
+prefix that is large enough to accommodate the request.
+
+.. [#] neutron/ipam/subnet_alloc.py (_allocate_any_subnet)
+.. [#] http://pythonhosted.org/netaddr/api.html#netaddr.IPSet.compact
+
+Address Scopes
+--------------
+
+Before subnet pools or address scopes, it was impossible to tell if a network
+address was routable in a certain context because the address was given
+explicitly on subnet create and wasn't validated against any other addresses.
+Address scopes are meant to solve this by putting control over the address
+space in the hands of an authority: the address scope owner. It makes use of
+the already existing SubnetPool concept for allocation.
+
+Address scopes are "the thing within which address overlap is not allowed" and
+thus provide more flexible control as well as decoupling of address overlap
+from tenancy.
+
+Prior to the Mitaka release, there was implicitly only a single 'shared'
+address scope. Arbitrary address overlap was allowed making it pretty much a
+"free for all". To make things seem somewhat sane, normal tenants are not able
+to use routers to cross-plug networks from different tenants and NAT was used
+between internal networks and external networks. It was almost as if each
+tenant had a private address scope.
+
+The problem is that this model cannot support use cases where NAT is not
+desired or supported (e.g. IPv6) or we want to allow different tenants to
+cross-plug their networks.
+
+An AddressScope covers only one address family. But, they work equally well
+for IPv4 and IPv6.
+
+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.
+
+.. TODO (Carl) Implement NAT for floating ips crossing scopes
+.. TODO (Carl) Implement SNAT for crossing scopes
+
+RPC
+~~~
+
+The L3 agent in the reference implementation needs to know the address scope
+for each port on each router in order to map ingress traffic correctly.
+
+Each subnet from the same address family on a network is required to be from
+the same subnet pool. Therefore, the address scope will also be the same. If
+this were not the case, it would be more difficult to match ingress traffic on
+a port with the appropriate scope. It may be counter-intuitive but L3 address
+scopes need to be anchored to some sort of non-L3 thing (e.g. an L2 interface)
+in the topology in order to determine the scope of ingress traffic. For now,
+we use ports/networks. In the future, we may be able to distinguish by
+something else like the remote MAC address or something.
+
+The address scope id is set on each port in a dict under the 'address_scopes'
+attribute. The scope is distinct per address family. If the attribute does
+not appear, it is assumed to be null for both families. A value of null means
+that the addresses are in the "implicit" address scope which holds all
+addresses that don't have an explicit one. All subnets that existed in Neutron
+before address scopes existed fall here.
+
+Here is an example of how the json will look in the context of a router port::
+
+ "address_scopes": {
+ "4": "d010a0ea-660e-4df4-86ca-ae2ed96da5c1",
+ "6": null
+ },
+
+Model
+~~~~~
+
+The model for subnet pools and address scopes can be found in
+neutron/db/models_v2.py and neutron/db/address_scope_db.py. This document
+won't go over all of the details. It is worth noting how they relate to
+existing Neutron objects. The existing Neutron subnet now optionally
+references a single subnet pool::
+
+ +----------------+ +------------------+ +--------------+
+ | Subnet | | SubnetPool | | AddressScope |
+ +----------------+ +------------------+ +--------------+
+ | subnet_pool_id +------> | address_scope_id +------> | |
+ | | | | | |
+ | | | | | |
+ | | | | | |
+ +----------------+ +------------------+ +--------------+
if not network_ids:
return {}
- filters = {'network_id': [id for id in network_ids]}
+ query = context.session.query(models_v2.Subnet,
+ models_v2.SubnetPool.address_scope_id)
+ query = query.outerjoin(
+ models_v2.SubnetPool,
+ models_v2.Subnet.subnetpool_id == models_v2.SubnetPool.id)
+ query = query.filter(models_v2.Subnet.network_id.in_(network_ids))
+
fields = ['id', 'cidr', 'gateway_ip', 'dns_nameservers',
'network_id', 'ipv6_ra_mode', 'subnetpool_id']
+ def make_subnet_dict_with_scope(row):
+ subnet_db, address_scope_id = row
+ subnet = self._core_plugin._make_subnet_dict(
+ subnet_db, fields, context=context)
+ subnet['address_scope_id'] = address_scope_id
+ return subnet
+
subnets_by_network = dict((id, []) for id in network_ids)
- for subnet in self._core_plugin.get_subnets(context, filters, fields):
+ for subnet in (make_subnet_dict_with_scope(row) for row in query):
subnets_by_network[subnet['network_id']].append(subnet)
return subnets_by_network
port['subnets'] = []
port['extra_subnets'] = []
+ port['address_scopes'] = {l3_constants.IP_VERSION_4: None,
+ l3_constants.IP_VERSION_6: None}
+
+ scopes = {}
for subnet in subnets_by_network[port['network_id']]:
+ scope = subnet['address_scope_id']
+ cidr = netaddr.IPNetwork(subnet['cidr'])
+ scopes[cidr.version] = scope
+
# If this subnet is used by the port (has a matching entry
# in the port's fixed_ips), then add this subnet to the
# port's subnets list, and populate the fixed_ips entry
for fixed_ip in port['fixed_ips']:
if fixed_ip['subnet_id'] == subnet['id']:
port['subnets'].append(subnet_info)
- prefixlen = netaddr.IPNetwork(
- subnet['cidr']).prefixlen
+ prefixlen = cidr.prefixlen
fixed_ip['prefixlen'] = prefixlen
break
else:
# This subnet is not used by the port.
port['extra_subnets'].append(subnet_info)
+ port['address_scopes'].update(scopes)
+
def _process_floating_ips(self, context, routers_dict, floating_ips):
for floating_ip in floating_ips:
router = routers_dict.get(floating_ip['router_id'])
def test__get_subnets_by_network_no_query(self):
"""Basic test that no query is performed if no Ports are passed"""
+ context = mock.Mock()
with mock.patch.object(manager.NeutronManager, 'get_plugin') as get_p:
- self.db._get_subnets_by_network_list(mock.sentinel.context, [])
- self.assertFalse(get_p().get_subnets.called)
+ self.db._get_subnets_by_network_list(context, [])
+ self.assertFalse(context.session.query.called)
+ self.assertFalse(get_p.called)
def test__get_subnets_by_network(self):
"""Basic test that the right query is called"""
- network_ids = ['a', 'b']
+ context = mock.MagicMock()
+ query = context.session.query().outerjoin().filter()
+ query.__iter__.return_value = [(mock.sentinel.subnet_db,
+ mock.sentinel.address_scope_id)]
+
with mock.patch.object(manager.NeutronManager, 'get_plugin') as get_p:
- self.db._get_subnets_by_network_list(
- mock.sentinel.context, network_ids)
- get_p().get_subnets.assert_called_once_with(
- mock.sentinel.context,
- {'network_id': network_ids},
- mock.ANY)
+ get_p()._make_subnet_dict.return_value = {
+ 'network_id': mock.sentinel.network_id}
+ subnets = self.db._get_subnets_by_network_list(
+ context, [mock.sentinel.network_id])
+ self.assertEqual({
+ mock.sentinel.network_id: [{
+ 'address_scope_id': mock.sentinel.address_scope_id,
+ 'network_id': mock.sentinel.network_id}]}, subnets)
def test__populate_ports_for_subnets_none(self):
"""Basic test that the method runs correctly with no ports"""
'gateway_ip': mock.sentinel.gateway_ip,
'dns_nameservers': mock.sentinel.dns_nameservers,
'ipv6_ra_mode': mock.sentinel.ipv6_ra_mode,
- 'subnetpool_id': mock.sentinel.subnetpool_id}
+ 'subnetpool_id': mock.sentinel.subnetpool_id,
+ 'address_scope_id': mock.sentinel.address_scope_id}
get_subnets_by_network.return_value = {'net_id': [subnet]}
ports = [{'network_id': 'net_id',
'id': 'port_id',
'fixed_ips': [{'subnet_id': mock.sentinel.subnet_id}]}]
self.db._populate_subnets_for_ports(mock.sentinel.context, ports)
+ keys = ('id', 'cidr', 'gateway_ip', 'ipv6_ra_mode', 'subnetpool_id',
+ 'dns_nameservers')
+ address_scopes = {4: None, 6: mock.sentinel.address_scope_id}
self.assertEqual([{'extra_subnets': [],
'fixed_ips': [{'subnet_id': mock.sentinel.subnet_id,
'prefixlen': 64}],
'id': 'port_id',
'network_id': 'net_id',
- 'subnets': [subnet]}], ports)
+ 'subnets': [{k: subnet[k] for k in keys}],
+ 'address_scopes': address_scopes}], ports)