1 # Copyright (c) 2015 OpenStack Foundation.
4 # Licensed under the Apache License, Version 2.0 (the "License"); you may
5 # not use this file except in compliance with the License. You may obtain
6 # a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 # License for the specific language governing permissions and limitations
17 from oslo_db import exception as db_exc
18 from oslo_log import log as logging
19 from sqlalchemy import and_
20 from sqlalchemy import orm
21 from sqlalchemy.orm import exc
23 from neutron._i18n import _
24 from neutron.api.v2 import attributes
25 from neutron.common import constants
26 from neutron.common import exceptions as n_exc
27 from neutron.common import ipv6_utils
28 from neutron.db import ipam_backend_mixin
29 from neutron.db import models_v2
30 from neutron.ipam import requests as ipam_req
31 from neutron.ipam import subnet_alloc
33 LOG = logging.getLogger(__name__)
36 class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
39 def _generate_ip(context, subnets):
41 return IpamNonPluggableBackend._try_generate_ip(context, subnets)
42 except n_exc.IpAddressGenerationFailure:
43 IpamNonPluggableBackend._rebuild_availability_ranges(context,
46 return IpamNonPluggableBackend._try_generate_ip(context, subnets)
49 def _try_generate_ip(context, subnets):
50 """Generate an IP address.
52 The IP address will be generated from one of the subnets defined on
55 range_qry = context.session.query(
56 models_v2.IPAvailabilityRange).join(
57 models_v2.IPAllocationPool).with_lockmode('update')
58 for subnet in subnets:
59 ip_range = range_qry.filter_by(subnet_id=subnet['id']).first()
61 LOG.debug("All IPs from subnet %(subnet_id)s (%(cidr)s) "
63 {'subnet_id': subnet['id'],
64 'cidr': subnet['cidr']})
66 ip_address = ip_range['first_ip']
67 if ip_range['first_ip'] == ip_range['last_ip']:
68 # No more free indices on subnet => delete
69 LOG.debug("No more free IP's in slice. Deleting "
71 context.session.delete(ip_range)
73 # increment the first free
74 new_first_ip = str(netaddr.IPAddress(ip_address) + 1)
75 ip_range['first_ip'] = new_first_ip
76 LOG.debug("Allocated IP - %(ip_address)s from %(first_ip)s "
78 {'ip_address': ip_address,
79 'first_ip': ip_range['first_ip'],
80 'last_ip': ip_range['last_ip']})
81 return {'ip_address': ip_address,
82 'subnet_id': subnet['id']}
83 raise n_exc.IpAddressGenerationFailure(net_id=subnets[0]['network_id'])
86 def _rebuild_availability_ranges(context, subnets):
87 """Rebuild availability ranges.
89 This method is called only when there's no more IP available or by
90 _update_subnet_allocation_pools. Calling
91 _update_subnet_allocation_pools before calling this function deletes
92 the IPAllocationPools associated with the subnet that is updating,
93 which will result in deleting the IPAvailabilityRange too.
95 ip_qry = context.session.query(
96 models_v2.IPAllocation).with_lockmode('update')
97 # PostgreSQL does not support select...for update with an outer join.
98 # No join is needed here.
99 pool_qry = context.session.query(
100 models_v2.IPAllocationPool).options(
101 orm.noload('available_ranges')).with_lockmode('update')
102 for subnet in sorted(subnets):
103 LOG.debug("Rebuilding availability ranges for subnet %s",
106 # Create a set of all currently allocated addresses
107 ip_qry_results = ip_qry.filter_by(subnet_id=subnet['id'])
108 allocations = netaddr.IPSet([netaddr.IPAddress(i['ip_address'])
109 for i in ip_qry_results])
111 for pool in pool_qry.filter_by(subnet_id=subnet['id']):
112 # Create a set of all addresses in the pool
113 poolset = netaddr.IPSet(netaddr.IPRange(pool['first_ip'],
116 # Use set difference to find free addresses in the pool
117 available = poolset - allocations
119 # Generator compacts an ip set into contiguous ranges
120 def ipset_to_ranges(ipset):
121 first, last = None, None
122 for cidr in ipset.iter_cidrs():
123 if last and last + 1 != cidr.first:
124 yield netaddr.IPRange(first, last)
126 first, last = first if first else cidr.first, cidr.last
128 yield netaddr.IPRange(first, last)
130 # Write the ranges to the db
131 for ip_range in ipset_to_ranges(available):
132 available_range = models_v2.IPAvailabilityRange(
133 allocation_pool_id=pool['id'],
134 first_ip=str(netaddr.IPAddress(ip_range.first)),
135 last_ip=str(netaddr.IPAddress(ip_range.last)))
136 context.session.add(available_range)
139 def _allocate_specific_ip(context, subnet_id, ip_address):
140 """Allocate a specific IP address on the subnet."""
141 ip = int(netaddr.IPAddress(ip_address))
142 range_qry = context.session.query(
143 models_v2.IPAvailabilityRange).join(
144 models_v2.IPAllocationPool).with_lockmode('update')
145 results = range_qry.filter_by(subnet_id=subnet_id)
146 for ip_range in results:
147 first = int(netaddr.IPAddress(ip_range['first_ip']))
148 last = int(netaddr.IPAddress(ip_range['last_ip']))
149 if first <= ip <= last:
151 context.session.delete(ip_range)
154 new_first_ip = str(netaddr.IPAddress(ip_address) + 1)
155 ip_range['first_ip'] = new_first_ip
158 new_last_ip = str(netaddr.IPAddress(ip_address) - 1)
159 ip_range['last_ip'] = new_last_ip
162 # Adjust the original range to end before ip_address
163 old_last_ip = ip_range['last_ip']
164 new_last_ip = str(netaddr.IPAddress(ip_address) - 1)
165 ip_range['last_ip'] = new_last_ip
167 # Create a new second range for after ip_address
168 new_first_ip = str(netaddr.IPAddress(ip_address) + 1)
169 new_ip_range = models_v2.IPAvailabilityRange(
170 allocation_pool_id=ip_range['allocation_pool_id'],
171 first_ip=new_first_ip,
173 context.session.add(new_ip_range)
177 def _check_unique_ip(context, network_id, subnet_id, ip_address):
178 """Validate that the IP address on the subnet is not in use."""
179 ip_qry = context.session.query(models_v2.IPAllocation)
181 ip_qry.filter_by(network_id=network_id,
183 ip_address=ip_address).one()
184 except exc.NoResultFound:
188 def save_allocation_pools(self, context, subnet, allocation_pools):
189 for pool in allocation_pools:
190 first_ip = str(netaddr.IPAddress(pool.first, pool.version))
191 last_ip = str(netaddr.IPAddress(pool.last, pool.version))
192 ip_pool = models_v2.IPAllocationPool(subnet=subnet,
195 context.session.add(ip_pool)
196 ip_range = models_v2.IPAvailabilityRange(
197 ipallocationpool=ip_pool,
200 context.session.add(ip_range)
202 def allocate_ips_for_port_and_store(self, context, port, port_id):
203 network_id = port['port']['network_id']
204 ips = self._allocate_ips_for_port(context, port)
207 ip_address = ip['ip_address']
208 subnet_id = ip['subnet_id']
209 self._store_ip_allocation(context, ip_address, network_id,
213 def update_port_with_ips(self, context, db_port, new_port, new_mac):
214 changes = self.Changes(add=[], original=[], remove=[])
215 # Check if the IPs need to be updated
216 network_id = db_port['network_id']
217 if 'fixed_ips' in new_port:
218 original = self._make_port_dict(db_port, process_extensions=False)
219 changes = self._update_ips_for_port(
221 original["fixed_ips"], new_port['fixed_ips'],
222 original['mac_address'], db_port['device_owner'])
224 # Update ips if necessary
225 for ip in changes.add:
226 IpamNonPluggableBackend._store_ip_allocation(
227 context, ip['ip_address'], network_id,
228 ip['subnet_id'], db_port.id)
229 self._update_db_port(context, db_port, new_port, network_id, new_mac)
232 def _test_fixed_ips_for_port(self, context, network_id, fixed_ips,
234 """Test fixed IPs for port.
236 Check that configured subnets are valid prior to allocating any
237 IPs. Include the subnet_id in the result if only an IP address is
240 :raises: InvalidInput, IpAddressInUse, InvalidIpForNetwork,
244 for fixed in fixed_ips:
245 subnet = self._get_subnet_for_fixed_ip(context, fixed, network_id)
247 is_auto_addr_subnet = ipv6_utils.is_auto_address_subnet(subnet)
248 if ('ip_address' in fixed and
249 subnet['cidr'] != constants.PROVISIONAL_IPV6_PD_PREFIX):
250 # Ensure that the IP's are unique
251 if not IpamNonPluggableBackend._check_unique_ip(
253 subnet['id'], fixed['ip_address']):
254 raise n_exc.IpAddressInUse(net_id=network_id,
255 ip_address=fixed['ip_address'])
257 if (is_auto_addr_subnet and
259 constants.ROUTER_INTERFACE_OWNERS):
260 msg = (_("IPv6 address %(address)s can not be directly "
261 "assigned to a port on subnet %(id)s since the "
262 "subnet is configured for automatic addresses") %
263 {'address': fixed['ip_address'],
265 raise n_exc.InvalidInput(error_message=msg)
266 fixed_ip_set.append({'subnet_id': subnet['id'],
267 'ip_address': fixed['ip_address']})
269 # A scan for auto-address subnets on the network is done
270 # separately so that all such subnets (not just those
271 # listed explicitly here by subnet ID) are associated
273 if (device_owner in constants.ROUTER_INTERFACE_OWNERS_SNAT or
274 not is_auto_addr_subnet):
275 fixed_ip_set.append({'subnet_id': subnet['id']})
277 self._validate_max_ips_per_port(fixed_ip_set, device_owner)
280 def _allocate_fixed_ips(self, context, fixed_ips, mac_address):
281 """Allocate IP addresses according to the configured fixed_ips."""
284 # we need to start with entries that asked for a specific IP in case
285 # those IPs happen to be next in the line for allocation for ones that
286 # didn't ask for a specific IP
287 fixed_ips.sort(key=lambda x: 'ip_address' not in x)
288 for fixed in fixed_ips:
289 subnet = self._get_subnet(context, fixed['subnet_id'])
290 is_auto_addr = ipv6_utils.is_auto_address_subnet(subnet)
291 if 'ip_address' in fixed:
293 # Remove the IP address from the allocation pool
294 IpamNonPluggableBackend._allocate_specific_ip(
295 context, fixed['subnet_id'], fixed['ip_address'])
296 ips.append({'ip_address': fixed['ip_address'],
297 'subnet_id': fixed['subnet_id']})
298 # Only subnet ID is specified => need to generate IP
302 ip_address = self._calculate_ipv6_eui64_addr(context,
305 ips.append({'ip_address': ip_address.format(),
306 'subnet_id': subnet['id']})
309 # IP address allocation
310 result = self._generate_ip(context, subnets)
311 ips.append({'ip_address': result['ip_address'],
312 'subnet_id': result['subnet_id']})
315 def _update_ips_for_port(self, context, network_id, original_ips,
316 new_ips, mac_address, device_owner):
317 """Add or remove IPs from the port."""
319 changes = self._get_changed_ips_for_port(context, original_ips,
320 new_ips, device_owner)
321 # Check if the IP's to add are OK
322 to_add = self._test_fixed_ips_for_port(context, network_id,
323 changes.add, device_owner)
324 for ip in changes.remove:
325 LOG.debug("Port update. Hold %s", ip)
326 IpamNonPluggableBackend._delete_ip_allocation(context,
332 LOG.debug("Port update. Adding %s", to_add)
333 added = self._allocate_fixed_ips(context, to_add, mac_address)
334 return self.Changes(add=added,
335 original=changes.original,
336 remove=changes.remove)
338 def _allocate_ips_for_port(self, context, port):
339 """Allocate IP addresses for the port.
341 If port['fixed_ips'] is set to 'ATTR_NOT_SPECIFIED', allocate IP
342 addresses for the port. If port['fixed_ips'] contains an IP address or
343 a subnet_id then allocate an IP address accordingly.
348 net_id_filter = {'network_id': [p['network_id']]}
349 subnets = self._get_subnets(context, filters=net_id_filter)
351 p['device_owner'] in constants.ROUTER_INTERFACE_OWNERS_SNAT)
353 fixed_configured = p['fixed_ips'] is not attributes.ATTR_NOT_SPECIFIED
355 configured_ips = self._test_fixed_ips_for_port(context,
359 ips = self._allocate_fixed_ips(context,
363 # For ports that are not router ports, implicitly include all
364 # auto-address subnets for address association.
365 if not is_router_port:
366 v6_stateless += [subnet for subnet in subnets
367 if ipv6_utils.is_auto_address_subnet(subnet)]
369 # Split into v4, v6 stateless and v6 stateful subnets
372 for subnet in subnets:
373 if subnet['ip_version'] == 4:
375 elif ipv6_utils.is_auto_address_subnet(subnet):
376 if not is_router_port:
377 v6_stateless.append(subnet)
379 v6_stateful.append(subnet)
381 version_subnets = [v4, v6_stateful]
382 for subnets in version_subnets:
384 result = IpamNonPluggableBackend._generate_ip(context,
386 ips.append({'ip_address': result['ip_address'],
387 'subnet_id': result['subnet_id']})
389 for subnet in v6_stateless:
390 # IP addresses for IPv6 SLAAC and DHCPv6-stateless subnets
391 # are implicitly included.
392 ip_address = self._calculate_ipv6_eui64_addr(context, subnet,
394 ips.append({'ip_address': ip_address.format(),
395 'subnet_id': subnet['id']})
399 def add_auto_addrs_on_network_ports(self, context, subnet, ipam_subnet):
400 """For an auto-address subnet, add addrs for ports on the net."""
401 with context.session.begin(subtransactions=True):
402 network_id = subnet['network_id']
403 port_qry = context.session.query(models_v2.Port)
404 ports = port_qry.filter(
405 and_(models_v2.Port.network_id == network_id,
406 ~models_v2.Port.device_owner.in_(
407 constants.ROUTER_INTERFACE_OWNERS_SNAT)))
409 ip_address = self._calculate_ipv6_eui64_addr(
410 context, subnet, port['mac_address'])
411 allocated = models_v2.IPAllocation(network_id=network_id,
413 ip_address=ip_address,
414 subnet_id=subnet['id'])
416 # Do the insertion of each IP allocation entry within
417 # the context of a nested transaction, so that the entry
418 # is rolled back independently of other entries whenever
419 # the corresponding port has been deleted.
420 with context.session.begin_nested():
421 context.session.add(allocated)
422 except db_exc.DBReferenceError:
423 LOG.debug("Port %s was deleted while updating it with an "
424 "IPv6 auto-address. Ignoring.", port['id'])
426 def _calculate_ipv6_eui64_addr(self, context, subnet, mac_addr):
427 prefix = subnet['cidr']
428 network_id = subnet['network_id']
429 ip_address = ipv6_utils.get_ipv6_addr_by_EUI64(
430 prefix, mac_addr).format()
431 if not self._check_unique_ip(context, network_id,
432 subnet['id'], ip_address):
433 raise n_exc.IpAddressInUse(net_id=network_id,
434 ip_address=ip_address)
437 def allocate_subnet(self, context, network, subnet, subnetpool_id):
439 if subnetpool_id and not subnetpool_id == constants.IPV6_PD_POOL_ID:
440 subnetpool = self._get_subnetpool(context, subnetpool_id)
441 self._validate_ip_version_with_subnetpool(subnet, subnetpool)
443 # gateway_ip and allocation pools should be validated or generated
444 # only for specific request
445 if subnet['cidr'] is not attributes.ATTR_NOT_SPECIFIED:
446 subnet['gateway_ip'] = self._gateway_ip_str(subnet,
448 # allocation_pools are converted to list of IPRanges
449 subnet['allocation_pools'] = self._prepare_allocation_pools(
450 subnet['allocation_pools'],
452 subnet['gateway_ip'])
454 subnet_request = ipam_req.SubnetRequestFactory.get_request(context,
458 if subnetpool_id and not subnetpool_id == constants.IPV6_PD_POOL_ID:
459 driver = subnet_alloc.SubnetAllocator(subnetpool, context)
460 ipam_subnet = driver.allocate_subnet(subnet_request)
461 subnet_request = ipam_subnet.get_details()
463 subnet = self._save_subnet(context,
465 self._make_subnet_args(
469 subnet['dns_nameservers'],
470 subnet['host_routes'],
472 # ipam_subnet is not expected to be allocated for non pluggable ipam,
473 # so just return None for it (second element in returned tuple)