summaryrefslogtreecommitdiff
path: root/neutron/db/ipam_non_pluggable_backend.py
blob: abc0098c9724c456eaaa8d9dbbe7da25cbfa96a5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# Copyright (c) 2015 OpenStack Foundation.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import netaddr
from neutron_lib import constants
from neutron_lib import exceptions as n_exc
from oslo_db import exception as db_exc
from oslo_log import log as logging
from sqlalchemy import and_
from sqlalchemy import orm
from sqlalchemy.orm import exc

from neutron._i18n import _
from neutron.common import constants as n_const
from neutron.common import ipv6_utils
from neutron.db import ipam_backend_mixin
from neutron.db import models_v2
from neutron.ipam import requests as ipam_req
from neutron.ipam import subnet_alloc

LOG = logging.getLogger(__name__)


class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):

    @staticmethod
    def _generate_ip(context, subnets):
        try:
            return IpamNonPluggableBackend._try_generate_ip(context, subnets)
        except n_exc.IpAddressGenerationFailure:
            IpamNonPluggableBackend._rebuild_availability_ranges(context,
                                                                 subnets)

        return IpamNonPluggableBackend._try_generate_ip(context, subnets)

    @staticmethod
    def _try_generate_ip(context, subnets):
        """Generate an IP address.

        The IP address will be generated from one of the subnets defined on
        the network.
        """
        range_qry = context.session.query(
            models_v2.IPAvailabilityRange).join(
                models_v2.IPAllocationPool).with_lockmode('update')
        for subnet in subnets:
            ip_range = range_qry.filter_by(subnet_id=subnet['id']).first()
            if not ip_range:
                LOG.debug("All IPs from subnet %(subnet_id)s (%(cidr)s) "
                          "allocated",
                          {'subnet_id': subnet['id'],
                           'cidr': subnet['cidr']})
                continue
            ip_address = ip_range['first_ip']
            if ip_range['first_ip'] == ip_range['last_ip']:
                # No more free indices on subnet => delete
                LOG.debug("No more free IP's in slice. Deleting "
                          "allocation pool.")
                context.session.delete(ip_range)
            else:
                # increment the first free
                new_first_ip = str(netaddr.IPAddress(ip_address) + 1)
                ip_range['first_ip'] = new_first_ip
            LOG.debug("Allocated IP - %(ip_address)s from %(first_ip)s "
                      "to %(last_ip)s",
                      {'ip_address': ip_address,
                       'first_ip': ip_range['first_ip'],
                       'last_ip': ip_range['last_ip']})
            return {'ip_address': ip_address,
                    'subnet_id': subnet['id']}
        raise n_exc.IpAddressGenerationFailure(net_id=subnets[0]['network_id'])

    @staticmethod
    def _rebuild_availability_ranges(context, subnets):
        """Rebuild availability ranges.

        This method is called only when there's no more IP available or by
        _update_subnet_allocation_pools. Calling
        _update_subnet_allocation_pools before calling this function deletes
        the IPAllocationPools associated with the subnet that is updating,
        which will result in deleting the IPAvailabilityRange too.
        """
        ip_qry = context.session.query(
            models_v2.IPAllocation).with_lockmode('update')
        # PostgreSQL does not support select...for update with an outer join.
        # No join is needed here.
        pool_qry = context.session.query(
            models_v2.IPAllocationPool).options(
                orm.noload('available_ranges')).with_lockmode('update')
        for subnet in sorted(subnets):
            LOG.debug("Rebuilding availability ranges for subnet %s",
                      subnet)

            # Create a set of all currently allocated addresses
            ip_qry_results = ip_qry.filter_by(subnet_id=subnet['id'])
            allocations = netaddr.IPSet([netaddr.IPAddress(i['ip_address'])
                                        for i in ip_qry_results])

            for pool in pool_qry.filter_by(subnet_id=subnet['id']):
                # Create a set of all addresses in the pool
                poolset = netaddr.IPSet(netaddr.IPRange(pool['first_ip'],
                                                        pool['last_ip']))

                # Use set difference to find free addresses in the pool
                available = poolset - allocations

                # Generator compacts an ip set into contiguous ranges
                def ipset_to_ranges(ipset):
                    first, last = None, None
                    for cidr in ipset.iter_cidrs():
                        if last and last + 1 != cidr.first:
                            yield netaddr.IPRange(first, last)
                            first = None
                        first, last = first if first else cidr.first, cidr.last
                    if first:
                        yield netaddr.IPRange(first, last)

                # Write the ranges to the db
                for ip_range in ipset_to_ranges(available):
                    available_range = models_v2.IPAvailabilityRange(
                        allocation_pool_id=pool['id'],
                        first_ip=str(netaddr.IPAddress(ip_range.first)),
                        last_ip=str(netaddr.IPAddress(ip_range.last)))
                    context.session.add(available_range)

    @staticmethod
    def _allocate_specific_ip(context, subnet_id, ip_address):
        """Allocate a specific IP address on the subnet."""
        ip = int(netaddr.IPAddress(ip_address))
        range_qry = context.session.query(
            models_v2.IPAvailabilityRange).join(
                models_v2.IPAllocationPool).with_lockmode('update')
        results = range_qry.filter_by(subnet_id=subnet_id)
        for ip_range in results:
            first = int(netaddr.IPAddress(ip_range['first_ip']))
            last = int(netaddr.IPAddress(ip_range['last_ip']))
            if first <= ip <= last:
                if first == last:
                    context.session.delete(ip_range)
                    return
                elif first == ip:
                    new_first_ip = str(netaddr.IPAddress(ip_address) + 1)
                    ip_range['first_ip'] = new_first_ip
                    return
                elif last == ip:
                    new_last_ip = str(netaddr.IPAddress(ip_address) - 1)
                    ip_range['last_ip'] = new_last_ip
                    return
                else:
                    # Adjust the original range to end before ip_address
                    old_last_ip = ip_range['last_ip']
                    new_last_ip = str(netaddr.IPAddress(ip_address) - 1)
                    ip_range['last_ip'] = new_last_ip

                    # Create a new second range for after ip_address
                    new_first_ip = str(netaddr.IPAddress(ip_address) + 1)
                    new_ip_range = models_v2.IPAvailabilityRange(
                        allocation_pool_id=ip_range['allocation_pool_id'],
                        first_ip=new_first_ip,
                        last_ip=old_last_ip)
                    context.session.add(new_ip_range)
                    return

    @staticmethod
    def _check_unique_ip(context, network_id, subnet_id, ip_address):
        """Validate that the IP address on the subnet is not in use."""
        ip_qry = context.session.query(models_v2.IPAllocation)
        try:
            ip_qry.filter_by(network_id=network_id,
                             subnet_id=subnet_id,
                             ip_address=ip_address).one()
        except exc.NoResultFound:
            return True
        return False

    def save_allocation_pools(self, context, subnet, allocation_pools):
        for pool in allocation_pools:
            first_ip = str(netaddr.IPAddress(pool.first, pool.version))
            last_ip = str(netaddr.IPAddress(pool.last, pool.version))
            ip_pool = models_v2.IPAllocationPool(subnet=subnet,
                                                 first_ip=first_ip,
                                                 last_ip=last_ip)
            context.session.add(ip_pool)
            ip_range = models_v2.IPAvailabilityRange(
                ipallocationpool=ip_pool,
                first_ip=first_ip,
                last_ip=last_ip)
            context.session.add(ip_range)

    def allocate_ips_for_port_and_store(self, context, port, port_id):
        network_id = port['port']['network_id']
        ips = self._allocate_ips_for_port(context, port)
        if ips:
            for ip in ips:
                ip_address = ip['ip_address']
                subnet_id = ip['subnet_id']
                self._store_ip_allocation(context, ip_address, network_id,
                                          subnet_id, port_id)
        return ips

    def update_port_with_ips(self, context, db_port, new_port, new_mac):
        changes = self.Changes(add=[], original=[], remove=[])
        # Check if the IPs need to be updated
        network_id = db_port['network_id']
        if 'fixed_ips' in new_port:
            original = self._make_port_dict(db_port, process_extensions=False)
            changes = self._update_ips_for_port(
                context, network_id,
                original["fixed_ips"], new_port['fixed_ips'],
                original['mac_address'], db_port['device_owner'])

            # Update ips if necessary
            for ip in changes.add:
                IpamNonPluggableBackend._store_ip_allocation(
                    context, ip['ip_address'], network_id,
                    ip['subnet_id'], db_port.id)
        self._update_db_port(context, db_port, new_port, network_id, new_mac)
        return changes

    def _test_fixed_ips_for_port(self, context, network_id, fixed_ips,
                                 device_owner, subnets):
        """Test fixed IPs for port.

        Check that configured subnets are valid prior to allocating any
        IPs. Include the subnet_id in the result if only an IP address is
        configured.

        :raises: InvalidInput, IpAddressInUse, InvalidIpForNetwork,
                 InvalidIpForSubnet
        """
        fixed_ip_set = []
        for fixed in fixed_ips:
            subnet = self._get_subnet_for_fixed_ip(context, fixed, subnets)

            is_auto_addr_subnet = ipv6_utils.is_auto_address_subnet(subnet)
            if ('ip_address' in fixed and
                subnet['cidr'] != n_const.PROVISIONAL_IPV6_PD_PREFIX):
                # Ensure that the IP's are unique
                if not IpamNonPluggableBackend._check_unique_ip(
                        context, network_id,
                        subnet['id'], fixed['ip_address']):
                    raise n_exc.IpAddressInUse(net_id=network_id,
                                               ip_address=fixed['ip_address'])

                if (is_auto_addr_subnet and
                    device_owner not in
                        constants.ROUTER_INTERFACE_OWNERS):
                    msg = (_("IPv6 address %(address)s can not be directly "
                            "assigned to a port on subnet %(id)s since the "
                            "subnet is configured for automatic addresses") %
                           {'address': fixed['ip_address'],
                            'id': subnet['id']})
                    raise n_exc.InvalidInput(error_message=msg)
                fixed_ip_set.append({'subnet_id': subnet['id'],
                                     'ip_address': fixed['ip_address']})
            else:
                # A scan for auto-address subnets on the network is done
                # separately so that all such subnets (not just those
                # listed explicitly here by subnet ID) are associated
                # with the port.
                if (device_owner in constants.ROUTER_INTERFACE_OWNERS_SNAT or
                    not is_auto_addr_subnet):
                    fixed_ip_set.append({'subnet_id': subnet['id']})

        self._validate_max_ips_per_port(fixed_ip_set, device_owner)
        return fixed_ip_set

    def _allocate_fixed_ips(self, context, fixed_ips, mac_address):
        """Allocate IP addresses according to the configured fixed_ips."""
        ips = []

        # we need to start with entries that asked for a specific IP in case
        # those IPs happen to be next in the line for allocation for ones that
        # didn't ask for a specific IP
        fixed_ips.sort(key=lambda x: 'ip_address' not in x)
        for fixed in fixed_ips:
            subnet = self._get_subnet(context, fixed['subnet_id'])
            is_auto_addr = ipv6_utils.is_auto_address_subnet(subnet)
            if 'ip_address' in fixed:
                if not is_auto_addr:
                    # Remove the IP address from the allocation pool
                    IpamNonPluggableBackend._allocate_specific_ip(
                        context, fixed['subnet_id'], fixed['ip_address'])
                ips.append({'ip_address': fixed['ip_address'],
                            'subnet_id': fixed['subnet_id']})
            # Only subnet ID is specified => need to generate IP
            # from subnet
            else:
                if is_auto_addr:
                    ip_address = self._calculate_ipv6_eui64_addr(context,
                                                                 subnet,
                                                                 mac_address)
                    ips.append({'ip_address': ip_address.format(),
                                'subnet_id': subnet['id']})
                else:
                    subnets = [subnet]
                    # IP address allocation
                    result = self._generate_ip(context, subnets)
                    ips.append({'ip_address': result['ip_address'],
                                'subnet_id': result['subnet_id']})
        return ips

    def _update_ips_for_port(self, context, network_id, original_ips,
                             new_ips, mac_address, device_owner):
        """Add or remove IPs from the port."""
        added = []
        changes = self._get_changed_ips_for_port(context, original_ips,
                                                 new_ips, device_owner)
        subnets = self._ipam_get_subnets(
            context, network_id=network_id, segment_id=None)
        # Check if the IP's to add are OK
        to_add = self._test_fixed_ips_for_port(context, network_id,
                                               changes.add, device_owner,
                                               subnets)

        if device_owner not in constants.ROUTER_INTERFACE_OWNERS:
            to_add += self._update_ips_for_pd_subnet(
                context, subnets, changes.add)

        for ip in changes.remove:
            LOG.debug("Port update. Hold %s", ip)
            IpamNonPluggableBackend._delete_ip_allocation(context,
                                                          network_id,
                                                          ip['subnet_id'],
                                                          ip['ip_address'])

        if to_add:
            LOG.debug("Port update. Adding %s", to_add)
            added = self._allocate_fixed_ips(context, to_add, mac_address)
        return self.Changes(add=added,
                            original=changes.original,
                            remove=changes.remove)

    def _allocate_ips_for_port(self, context, port):
        """Allocate IP addresses for the port.

        If port['fixed_ips'] is set to 'ATTR_NOT_SPECIFIED', allocate IP
        addresses for the port. If port['fixed_ips'] contains an IP address or
        a subnet_id then allocate an IP address accordingly.
        """
        p = port['port']
        subnets = self._ipam_get_subnets(
            context, network_id=p['network_id'], segment_id=None)

        v4, v6_stateful, v6_stateless = self._classify_subnets(
            context, subnets)

        fixed_configured = p['fixed_ips'] is not constants.ATTR_NOT_SPECIFIED
        if fixed_configured:
            configured_ips = self._test_fixed_ips_for_port(context,
                                                           p["network_id"],
                                                           p['fixed_ips'],
                                                           p['device_owner'],
                                                           subnets)
            ips = self._allocate_fixed_ips(context,
                                           configured_ips,
                                           p['mac_address'])

        else:
            ips = []
            version_subnets = [v4, v6_stateful]
            for subnets in version_subnets:
                if subnets:
                    result = IpamNonPluggableBackend._generate_ip(context,
                                                                  subnets)
                    ips.append({'ip_address': result['ip_address'],
                                'subnet_id': result['subnet_id']})

        is_router_port = (
            p['device_owner'] in constants.ROUTER_INTERFACE_OWNERS_SNAT)
        if not is_router_port:
            # IP addresses for IPv6 SLAAC and DHCPv6-stateless subnets
            # are generated and implicitly included.
            for subnet in v6_stateless:
                ip_address = self._calculate_ipv6_eui64_addr(
                    context, subnet, p['mac_address'])
                ips.append({'ip_address': ip_address.format(),
                            'subnet_id': subnet['id']})

        return ips

    def add_auto_addrs_on_network_ports(self, context, subnet, ipam_subnet):
        """For an auto-address subnet, add addrs for ports on the net."""
        with context.session.begin(subtransactions=True):
            network_id = subnet['network_id']
            port_qry = context.session.query(models_v2.Port)
            ports = port_qry.filter(
                and_(models_v2.Port.network_id == network_id,
                     ~models_v2.Port.device_owner.in_(
                         constants.ROUTER_INTERFACE_OWNERS_SNAT)))
            updated_ports = []
            for port in ports:
                ip_address = self._calculate_ipv6_eui64_addr(
                    context, subnet, port['mac_address'])
                allocated = models_v2.IPAllocation(network_id=network_id,
                                                   port_id=port['id'],
                                                   ip_address=ip_address,
                                                   subnet_id=subnet['id'])
                try:
                    # Do the insertion of each IP allocation entry within
                    # the context of a nested transaction, so that the entry
                    # is rolled back independently of other entries whenever
                    # the corresponding port has been deleted.
                    with context.session.begin_nested():
                        context.session.add(allocated)
                    updated_ports.append(port['id'])
                except db_exc.DBReferenceError:
                    LOG.debug("Port %s was deleted while updating it with an "
                              "IPv6 auto-address. Ignoring.", port['id'])
            return updated_ports

    def _calculate_ipv6_eui64_addr(self, context, subnet, mac_addr):
        prefix = subnet['cidr']
        network_id = subnet['network_id']
        ip_address = ipv6_utils.get_ipv6_addr_by_EUI64(
            prefix, mac_addr).format()
        if not self._check_unique_ip(context, network_id,
                                     subnet['id'], ip_address):
            raise n_exc.IpAddressInUse(net_id=network_id,
                                       ip_address=ip_address)
        return ip_address

    def allocate_subnet(self, context, network, subnet, subnetpool_id):
        subnetpool = None
        if subnetpool_id and not subnetpool_id == constants.IPV6_PD_POOL_ID:
            subnetpool = self._get_subnetpool(context, subnetpool_id)
            self._validate_ip_version_with_subnetpool(subnet, subnetpool)

        # gateway_ip and allocation pools should be validated or generated
        # only for specific request
        if subnet['cidr'] is not constants.ATTR_NOT_SPECIFIED:
            subnet['gateway_ip'] = self._gateway_ip_str(subnet,
                                                        subnet['cidr'])
            # allocation_pools are converted to list of IPRanges
            subnet['allocation_pools'] = self._prepare_allocation_pools(
                subnet['allocation_pools'],
                subnet['cidr'],
                subnet['gateway_ip'])

        subnet_request = ipam_req.SubnetRequestFactory.get_request(context,
                                                                   subnet,
                                                                   subnetpool)

        if subnetpool_id and not subnetpool_id == constants.IPV6_PD_POOL_ID:
            driver = subnet_alloc.SubnetAllocator(subnetpool, context)
            ipam_subnet = driver.allocate_subnet(subnet_request)
            subnet_request = ipam_subnet.get_details()

        subnet = self._save_subnet(context,
                                   network,
                                   self._make_subnet_args(
                                       subnet_request,
                                       subnet,
                                       subnetpool_id),
                                   subnet['dns_nameservers'],
                                   subnet['host_routes'],
                                   subnet_request)
        # ipam_subnet is not expected to be allocated for non pluggable ipam,
        # so just return None for it (second element in returned tuple)
        return subnet, None