Set lock_path correctly.
[openstack-build/neutron-build.git] / neutron / db / ipam_non_pluggable_backend.py
1 # Copyright (c) 2015 OpenStack Foundation.
2 # All Rights Reserved.
3 #
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
7 #
8 #         http://www.apache.org/licenses/LICENSE-2.0
9 #
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
14 #    under the License.
15
16 import netaddr
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
22
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
32
33 LOG = logging.getLogger(__name__)
34
35
36 class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
37
38     @staticmethod
39     def _generate_ip(context, subnets):
40         try:
41             return IpamNonPluggableBackend._try_generate_ip(context, subnets)
42         except n_exc.IpAddressGenerationFailure:
43             IpamNonPluggableBackend._rebuild_availability_ranges(context,
44                                                                  subnets)
45
46         return IpamNonPluggableBackend._try_generate_ip(context, subnets)
47
48     @staticmethod
49     def _try_generate_ip(context, subnets):
50         """Generate an IP address.
51
52         The IP address will be generated from one of the subnets defined on
53         the network.
54         """
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()
60             if not ip_range:
61                 LOG.debug("All IPs from subnet %(subnet_id)s (%(cidr)s) "
62                           "allocated",
63                           {'subnet_id': subnet['id'],
64                            'cidr': subnet['cidr']})
65                 continue
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 "
70                           "allocation pool.")
71                 context.session.delete(ip_range)
72             else:
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 "
77                       "to %(last_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'])
84
85     @staticmethod
86     def _rebuild_availability_ranges(context, subnets):
87         """Rebuild availability ranges.
88
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.
94         """
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",
104                       subnet)
105
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])
110
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'],
114                                                         pool['last_ip']))
115
116                 # Use set difference to find free addresses in the pool
117                 available = poolset - allocations
118
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)
125                             first = None
126                         first, last = first if first else cidr.first, cidr.last
127                     if first:
128                         yield netaddr.IPRange(first, last)
129
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)
137
138     @staticmethod
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:
150                 if first == last:
151                     context.session.delete(ip_range)
152                     return
153                 elif first == ip:
154                     new_first_ip = str(netaddr.IPAddress(ip_address) + 1)
155                     ip_range['first_ip'] = new_first_ip
156                     return
157                 elif last == ip:
158                     new_last_ip = str(netaddr.IPAddress(ip_address) - 1)
159                     ip_range['last_ip'] = new_last_ip
160                     return
161                 else:
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
166
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,
172                         last_ip=old_last_ip)
173                     context.session.add(new_ip_range)
174                     return
175
176     @staticmethod
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)
180         try:
181             ip_qry.filter_by(network_id=network_id,
182                              subnet_id=subnet_id,
183                              ip_address=ip_address).one()
184         except exc.NoResultFound:
185             return True
186         return False
187
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,
193                                                  first_ip=first_ip,
194                                                  last_ip=last_ip)
195             context.session.add(ip_pool)
196             ip_range = models_v2.IPAvailabilityRange(
197                 ipallocationpool=ip_pool,
198                 first_ip=first_ip,
199                 last_ip=last_ip)
200             context.session.add(ip_range)
201
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)
205         if ips:
206             for ip in ips:
207                 ip_address = ip['ip_address']
208                 subnet_id = ip['subnet_id']
209                 self._store_ip_allocation(context, ip_address, network_id,
210                                           subnet_id, port_id)
211         return ips
212
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(
220                 context, network_id,
221                 original["fixed_ips"], new_port['fixed_ips'],
222                 original['mac_address'], db_port['device_owner'])
223
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)
230         return changes
231
232     def _test_fixed_ips_for_port(self, context, network_id, fixed_ips,
233                                  device_owner):
234         """Test fixed IPs for port.
235
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
238         configured.
239
240         :raises: InvalidInput, IpAddressInUse, InvalidIpForNetwork,
241                  InvalidIpForSubnet
242         """
243         fixed_ip_set = []
244         for fixed in fixed_ips:
245             subnet = self._get_subnet_for_fixed_ip(context, fixed, network_id)
246
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(
252                         context, network_id,
253                         subnet['id'], fixed['ip_address']):
254                     raise n_exc.IpAddressInUse(net_id=network_id,
255                                                ip_address=fixed['ip_address'])
256
257                 if (is_auto_addr_subnet and
258                     device_owner not in
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'],
264                             'id': subnet['id']})
265                     raise n_exc.InvalidInput(error_message=msg)
266                 fixed_ip_set.append({'subnet_id': subnet['id'],
267                                      'ip_address': fixed['ip_address']})
268             else:
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
272                 # with the port.
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']})
276
277         self._validate_max_ips_per_port(fixed_ip_set, device_owner)
278         return fixed_ip_set
279
280     def _allocate_fixed_ips(self, context, fixed_ips, mac_address):
281         """Allocate IP addresses according to the configured fixed_ips."""
282         ips = []
283
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:
292                 if not is_auto_addr:
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
299             # from subnet
300             else:
301                 if is_auto_addr:
302                     ip_address = self._calculate_ipv6_eui64_addr(context,
303                                                                  subnet,
304                                                                  mac_address)
305                     ips.append({'ip_address': ip_address.format(),
306                                 'subnet_id': subnet['id']})
307                 else:
308                     subnets = [subnet]
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']})
313         return ips
314
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."""
318         added = []
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,
327                                                           network_id,
328                                                           ip['subnet_id'],
329                                                           ip['ip_address'])
330
331         if to_add:
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)
337
338     def _allocate_ips_for_port(self, context, port):
339         """Allocate IP addresses for the port.
340
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.
344         """
345         p = port['port']
346         ips = []
347         v6_stateless = []
348         net_id_filter = {'network_id': [p['network_id']]}
349         subnets = self._get_subnets(context, filters=net_id_filter)
350         is_router_port = (
351             p['device_owner'] in constants.ROUTER_INTERFACE_OWNERS_SNAT)
352
353         fixed_configured = p['fixed_ips'] is not attributes.ATTR_NOT_SPECIFIED
354         if fixed_configured:
355             configured_ips = self._test_fixed_ips_for_port(context,
356                                                            p["network_id"],
357                                                            p['fixed_ips'],
358                                                            p['device_owner'])
359             ips = self._allocate_fixed_ips(context,
360                                            configured_ips,
361                                            p['mac_address'])
362
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)]
368         else:
369             # Split into v4, v6 stateless and v6 stateful subnets
370             v4 = []
371             v6_stateful = []
372             for subnet in subnets:
373                 if subnet['ip_version'] == 4:
374                     v4.append(subnet)
375                 elif ipv6_utils.is_auto_address_subnet(subnet):
376                     if not is_router_port:
377                         v6_stateless.append(subnet)
378                 else:
379                     v6_stateful.append(subnet)
380
381             version_subnets = [v4, v6_stateful]
382             for subnets in version_subnets:
383                 if subnets:
384                     result = IpamNonPluggableBackend._generate_ip(context,
385                                                                   subnets)
386                     ips.append({'ip_address': result['ip_address'],
387                                 'subnet_id': result['subnet_id']})
388
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,
393                                                          p['mac_address'])
394             ips.append({'ip_address': ip_address.format(),
395                         'subnet_id': subnet['id']})
396
397         return ips
398
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)))
408             for port in ports:
409                 ip_address = self._calculate_ipv6_eui64_addr(
410                     context, subnet, port['mac_address'])
411                 allocated = models_v2.IPAllocation(network_id=network_id,
412                                                    port_id=port['id'],
413                                                    ip_address=ip_address,
414                                                    subnet_id=subnet['id'])
415                 try:
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'])
425
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)
435         return ip_address
436
437     def allocate_subnet(self, context, network, subnet, subnetpool_id):
438         subnetpool = None
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)
442
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,
447                                                         subnet['cidr'])
448             # allocation_pools are converted to list of IPRanges
449             subnet['allocation_pools'] = self._prepare_allocation_pools(
450                 subnet['allocation_pools'],
451                 subnet['cidr'],
452                 subnet['gateway_ip'])
453
454         subnet_request = ipam_req.SubnetRequestFactory.get_request(context,
455                                                                    subnet,
456                                                                    subnetpool)
457
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()
462
463         subnet = self._save_subnet(context,
464                                    network,
465                                    self._make_subnet_args(
466                                        subnet_request,
467                                        subnet,
468                                        subnetpool_id),
469                                    subnet['dns_nameservers'],
470                                    subnet['host_routes'],
471                                    subnet_request)
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)
474         return subnet, None