-
Notifications
You must be signed in to change notification settings - Fork 1.5k
/
driver.py
456 lines (405 loc) · 20.4 KB
/
driver.py
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
# Copyright 2015 OpenStack LLC.
# 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 oslo_db import exception as db_exc
from oslo_log import log
from oslo_utils import uuidutils
from neutron._i18n import _, _LE
from neutron.common import exceptions as n_exc
from neutron.common import ipv6_utils
from neutron.db import api as db_api
from neutron.ipam import driver as ipam_base
from neutron.ipam.drivers.neutrondb_ipam import db_api as ipam_db_api
from neutron.ipam import exceptions as ipam_exc
from neutron.ipam import requests as ipam_req
from neutron.ipam import subnet_alloc
from neutron.ipam import utils as ipam_utils
from neutron import manager
LOG = log.getLogger(__name__)
class NeutronDbSubnet(ipam_base.Subnet):
"""Manage IP addresses for Neutron DB IPAM driver.
This class implements the strategy for IP address allocation and
deallocation for the Neutron DB IPAM driver.
Allocation for IP addresses is based on the concept of availability
ranges, which were already used in Neutron's DB base class for handling
IPAM operations.
"""
@classmethod
def create_allocation_pools(cls, subnet_manager, session, pools, cidr):
for pool in pools:
# IPv6 addresses that start '::1', '::2', etc cause IP version
# ambiguity when converted to integers by pool.first and pool.last.
# Infer the IP version from the subnet cidr.
ip_version = cidr.version
subnet_manager.create_pool(
session,
netaddr.IPAddress(pool.first, ip_version).format(),
netaddr.IPAddress(pool.last, ip_version).format())
@classmethod
def create_from_subnet_request(cls, subnet_request, ctx):
ipam_subnet_id = uuidutils.generate_uuid()
subnet_manager = ipam_db_api.IpamSubnetManager(
ipam_subnet_id,
subnet_request.subnet_id)
# Create subnet resource
session = ctx.session
subnet_manager.create(session)
# If allocation pools are not specified, define them around
# the subnet's gateway IP
if not subnet_request.allocation_pools:
pools = ipam_utils.generate_pools(subnet_request.subnet_cidr,
subnet_request.gateway_ip)
else:
pools = subnet_request.allocation_pools
# Create IPAM allocation pools and availability ranges
cls.create_allocation_pools(subnet_manager, session, pools,
subnet_request.subnet_cidr)
return cls(ipam_subnet_id,
ctx,
cidr=subnet_request.subnet_cidr,
allocation_pools=pools,
gateway_ip=subnet_request.gateway_ip,
tenant_id=subnet_request.tenant_id,
subnet_id=subnet_request.subnet_id)
@classmethod
def load(cls, neutron_subnet_id, ctx):
"""Load an IPAM subnet from the database given its neutron ID.
:param neutron_subnet_id: neutron subnet identifier.
"""
ipam_subnet = ipam_db_api.IpamSubnetManager.load_by_neutron_subnet_id(
ctx.session, neutron_subnet_id)
if not ipam_subnet:
LOG.error(_LE("IPAM subnet referenced to "
"Neutron subnet %s does not exist"),
neutron_subnet_id)
raise n_exc.SubnetNotFound(subnet_id=neutron_subnet_id)
pools = []
for pool in ipam_subnet.allocation_pools:
pools.append(netaddr.IPRange(pool['first_ip'], pool['last_ip']))
neutron_subnet = cls._fetch_subnet(ctx, neutron_subnet_id)
return cls(ipam_subnet['id'],
ctx,
cidr=neutron_subnet['cidr'],
allocation_pools=pools,
gateway_ip=neutron_subnet['gateway_ip'],
tenant_id=neutron_subnet['tenant_id'],
subnet_id=neutron_subnet_id)
@classmethod
def _fetch_subnet(cls, context, id):
plugin = manager.NeutronManager.get_plugin()
return plugin._get_subnet(context, id)
def __init__(self, internal_id, ctx, cidr=None,
allocation_pools=None, gateway_ip=None, tenant_id=None,
subnet_id=None):
# NOTE: In theory it could have been possible to grant the IPAM
# driver direct access to the database. While this is possible,
# it would have led to duplicate code and/or non-trivial
# refactorings in neutron.db.db_base_plugin_v2.
# This is because in the Neutron V2 plugin logic DB management is
# encapsulated within the plugin.
self._cidr = cidr
self._pools = allocation_pools
self._gateway_ip = gateway_ip
self._tenant_id = tenant_id
self._subnet_id = subnet_id
self.subnet_manager = ipam_db_api.IpamSubnetManager(internal_id,
self._subnet_id)
self._context = ctx
def _verify_ip(self, session, ip_address):
"""Verify whether IP address can be allocated on subnet.
:param session: database session
:param ip_address: String representing the IP address to verify
:raises: InvalidInput, IpAddressAlreadyAllocated
"""
# Ensure that the IP's are unique
if not self.subnet_manager.check_unique_allocation(session,
ip_address):
raise ipam_exc.IpAddressAlreadyAllocated(
subnet_id=self.subnet_manager.neutron_id,
ip=ip_address)
# Ensure that the IP is valid on the subnet
if not ipam_utils.check_subnet_ip(self._cidr, ip_address):
raise ipam_exc.InvalidIpForSubnet(
subnet_id=self.subnet_manager.neutron_id,
ip=ip_address)
def _allocate_specific_ip(self, session, ip_address,
allocation_pool_id=None,
auto_generated=False):
"""Remove an IP address from subnet's availability ranges.
This method is supposed to be called from within a database
transaction, otherwise atomicity and integrity might not be
enforced and the operation might result in incosistent availability
ranges for the subnet.
:param session: database session
:param ip_address: ip address to mark as allocated
:param allocation_pool_id: identifier of the allocation pool from
which the ip address has been extracted. If not specified this
routine will scan all allocation pools.
:param auto_generated: indicates whether ip was auto generated
:returns: list of IP ranges as instances of IPAvailabilityRange
"""
# Return immediately for EUI-64 addresses. For this
# class of subnets availability ranges do not apply
if ipv6_utils.is_eui64_address(ip_address):
return
LOG.debug("Removing %(ip_address)s from availability ranges for "
"subnet id:%(subnet_id)s",
{'ip_address': ip_address,
'subnet_id': self.subnet_manager.neutron_id})
# Netaddr's IPRange and IPSet objects work very well even with very
# large subnets, including IPv6 ones.
final_ranges = []
ip_in_pools = False
if allocation_pool_id:
av_ranges = self.subnet_manager.list_ranges_by_allocation_pool(
session, allocation_pool_id)
else:
av_ranges = self.subnet_manager.list_ranges_by_subnet_id(session)
for db_range in av_ranges:
initial_ip_set = netaddr.IPSet(netaddr.IPRange(
db_range['first_ip'], db_range['last_ip']))
final_ip_set = initial_ip_set - netaddr.IPSet([ip_address])
if not final_ip_set:
ip_in_pools = True
# Range exhausted - bye bye
if not self.subnet_manager.delete_range(session, db_range):
raise db_exc.RetryRequest(ipam_exc.IPAllocationFailed())
continue
if initial_ip_set == final_ip_set:
# IP address does not fall within the current range, move
# to the next one
final_ranges.append(db_range)
continue
ip_in_pools = True
for new_range in final_ip_set.iter_ipranges():
# store new range in database
# use netaddr.IPAddress format() method which is equivalent
# to str(...) but also enables us to use different
# representation formats (if needed) for IPv6.
first_ip = netaddr.IPAddress(new_range.first)
last_ip = netaddr.IPAddress(new_range.last)
if (db_range['first_ip'] == first_ip.format() or
db_range['last_ip'] == last_ip.format()):
rows = self.subnet_manager.update_range(
session, db_range, first_ip=first_ip, last_ip=last_ip)
if not rows:
raise db_exc.RetryRequest(
ipam_exc.IPAllocationFailed())
LOG.debug("Adjusted availability range for pool %s",
db_range['allocation_pool_id'])
final_ranges.append(db_range)
else:
new_ip_range = self.subnet_manager.create_range(
session,
db_range['allocation_pool_id'],
first_ip.format(),
last_ip.format())
LOG.debug("Created availability range for pool %s",
new_ip_range['allocation_pool_id'])
final_ranges.append(new_ip_range)
# If ip is autogenerated it should be present in allocation pools,
# so retry if it is not there
if auto_generated and not ip_in_pools:
raise db_exc.RetryRequest(ipam_exc.IPAllocationFailed())
# Most callers might ignore this return value, which is however
# useful for testing purposes
LOG.debug("Availability ranges for subnet id %(subnet_id)s "
"modified: %(new_ranges)s",
{'subnet_id': self.subnet_manager.neutron_id,
'new_ranges': ", ".join(["[%s; %s]" %
(r['first_ip'], r['last_ip']) for
r in final_ranges])})
return final_ranges
def _rebuild_availability_ranges(self, session):
"""Rebuild availability ranges.
This method should be called only when the availability ranges are
exhausted or when the subnet's allocation pools are updated,
which may trigger a deletion of the availability ranges.
For this operation to complete successfully, this method uses a
locking query to ensure that no IP is allocated while the regeneration
of availability ranges is in progress.
:param session: database session
"""
# List all currently allocated addresses, and prevent further
# allocations with a write-intent lock.
# NOTE: because of this driver's logic the write intent lock is
# probably unnecessary as this routine is called when the availability
# ranges for a subnet are exhausted and no further address can be
# allocated.
# TODO(salv-orlando): devise, if possible, a more efficient solution
# for building the IPSet to ensure decent performances even with very
# large subnets.
allocations = netaddr.IPSet(
[netaddr.IPAddress(allocation['ip_address']) for
allocation in self.subnet_manager.list_allocations(
session)])
# MEH MEH
# There should be no need to set a write intent lock on the allocation
# pool table. Indeed it is not important for the correctness of this
# operation if the allocation pools are updated by another operation,
# which will result in the generation of new availability ranges.
# NOTE: it might be argued that an allocation pool update should in
# theory preempt rebuilding the availability range. This is an option
# to consider for future developments.
LOG.debug("Rebuilding availability ranges for subnet %s",
self.subnet_manager.neutron_id)
for pool in self.subnet_manager.list_pools(session):
# 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
# Write the ranges to the db
for ip_range in available.iter_ipranges():
av_range = self.subnet_manager.create_range(
session,
pool['id'],
netaddr.IPAddress(ip_range.first).format(),
netaddr.IPAddress(ip_range.last).format())
session.add(av_range)
def _generate_ip(self, session):
try:
return self._try_generate_ip(session)
except ipam_exc.IpAddressGenerationFailure:
self._rebuild_availability_ranges(session)
return self._try_generate_ip(session)
def _try_generate_ip(self, session):
"""Generate an IP address from availability ranges."""
ip_range = self.subnet_manager.get_first_range(session)
if not ip_range:
LOG.debug("All IPs from subnet %(subnet_id)s allocated",
{'subnet_id': self.subnet_manager.neutron_id})
raise ipam_exc.IpAddressGenerationFailure(
subnet_id=self.subnet_manager.neutron_id)
# A suitable range was found. Return IP address.
ip_address = ip_range['first_ip']
LOG.debug("Allocated IP - %(ip_address)s from range "
"[%(first_ip)s; %(last_ip)s]",
{'ip_address': ip_address,
'first_ip': ip_address,
'last_ip': ip_range['last_ip']})
return ip_address, ip_range['allocation_pool_id']
def allocate(self, address_request):
# NOTE(salv-orlando): Creating a new db session might be a rather
# dangerous thing to do, if executed from within another database
# transaction. Therefore the IPAM driver should never be
# called from within a database transaction, which is also good
# practice since in the general case these drivers may interact
# with remote backends
session = self._context.session
all_pool_id = None
auto_generated = False
with db_api.autonested_transaction(session):
# NOTE(salv-orlando): It would probably better to have a simpler
# model for address requests and just check whether there is a
# specific IP address specified in address_request
if isinstance(address_request, ipam_req.SpecificAddressRequest):
# This handles both specific and automatic address requests
# Check availability of requested IP
ip_address = str(address_request.address)
self._verify_ip(session, ip_address)
else:
ip_address, all_pool_id = self._generate_ip(session)
auto_generated = True
self._allocate_specific_ip(session, ip_address, all_pool_id,
auto_generated)
# Create IP allocation request object
# The only defined status at this stage is 'ALLOCATED'.
# More states will be available in the future - e.g.: RECYCLABLE
self.subnet_manager.create_allocation(session, ip_address)
return ip_address
def deallocate(self, address):
# This is almost a no-op because the Neutron DB IPAM driver does not
# delete IPAllocation objects, neither rebuilds availability ranges
# at every deallocation. The only operation it performs is to delete
# an IPRequest entry.
session = self._context.session
count = self.subnet_manager.delete_allocation(
session, address)
# count can hardly be greater than 1, but it can be 0...
if not count:
raise ipam_exc.IpAddressAllocationNotFound(
subnet_id=self.subnet_manager.neutron_id,
ip_address=address)
def update_allocation_pools(self, pools, cidr):
# Pools have already been validated in the subnet request object which
# was sent to the subnet pool driver. Further validation should not be
# required.
session = db_api.get_session()
self.subnet_manager.delete_allocation_pools(session)
self.create_allocation_pools(self.subnet_manager, session, pools, cidr)
self._pools = pools
def get_details(self):
"""Return subnet data as a SpecificSubnetRequest"""
return ipam_req.SpecificSubnetRequest(
self._tenant_id, self.subnet_manager.neutron_id,
self._cidr, self._gateway_ip, self._pools)
class NeutronDbPool(subnet_alloc.SubnetAllocator):
"""Subnet pools backed by Neutron Database.
As this driver does not implement yet the subnet pool concept, most
operations are either trivial or no-ops.
"""
def get_subnet(self, subnet_id):
"""Retrieve an IPAM subnet.
:param subnet_id: Neutron subnet identifier
:returns: a NeutronDbSubnet instance
"""
return NeutronDbSubnet.load(subnet_id, self._context)
def allocate_subnet(self, subnet_request):
"""Create an IPAMSubnet object for the provided cidr.
This method does not actually do any operation in the driver, given
its simplified nature.
:param cidr: subnet's CIDR
:returns: a NeutronDbSubnet instance
"""
if self._subnetpool:
subnet = super(NeutronDbPool, self).allocate_subnet(subnet_request)
subnet_request = subnet.get_details()
# SubnetRequest must be an instance of SpecificSubnet
if not isinstance(subnet_request, ipam_req.SpecificSubnetRequest):
raise ipam_exc.InvalidSubnetRequestType(
subnet_type=type(subnet_request))
return NeutronDbSubnet.create_from_subnet_request(subnet_request,
self._context)
def update_subnet(self, subnet_request):
"""Update subnet info the in the IPAM driver.
The only update subnet information the driver needs to be aware of
are allocation pools.
"""
if not subnet_request.subnet_id:
raise ipam_exc.InvalidSubnetRequest(
reason=_("An identifier must be specified when updating "
"a subnet"))
if not subnet_request.allocation_pools:
LOG.debug("Update subnet request for subnet %s did not specify "
"new allocation pools, there is nothing to do",
subnet_request.subnet_id)
return
subnet = NeutronDbSubnet.load(subnet_request.subnet_id, self._context)
cidr = netaddr.IPNetwork(subnet._cidr)
subnet.update_allocation_pools(subnet_request.allocation_pools, cidr)
return subnet
def remove_subnet(self, subnet_id):
"""Remove data structures for a given subnet.
IPAM-related data has no foreign key relationships to neutron subnet,
so removing ipam subnet manually
"""
count = ipam_db_api.IpamSubnetManager.delete(self._context.session,
subnet_id)
if count < 1:
LOG.error(_LE("IPAM subnet referenced to "
"Neutron subnet %s does not exist"),
subnet_id)
raise n_exc.SubnetNotFound(subnet_id=subnet_id)