Merge "Use compare-and-swap for IpamAvailabilityRange"

This commit is contained in:
Jenkins 2015-11-20 22:05:59 +00:00 committed by Gerrit Code Review
commit 01dc4e913e
5 changed files with 168 additions and 62 deletions

View File

@ -13,10 +13,13 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from oslo_db import exception as db_exc
from oslo_log import log from oslo_log import log
from oslo_utils import uuidutils from oslo_utils import uuidutils
from sqlalchemy.orm import exc as orm_exc
from neutron.ipam.drivers.neutrondb_ipam import db_models from neutron.ipam.drivers.neutrondb_ipam import db_models
from neutron.ipam import exceptions as ipam_exc
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
# Database operations for Neutron's DB-backed IPAM driver # Database operations for Neutron's DB-backed IPAM driver
@ -103,45 +106,35 @@ class IpamSubnetManager(object):
db_models.IpamAllocationPool).filter_by( db_models.IpamAllocationPool).filter_by(
ipam_subnet_id=self._ipam_subnet_id) ipam_subnet_id=self._ipam_subnet_id)
def _range_query(self, session, locking): def _range_query(self, session):
range_qry = session.query( return session.query(
db_models.IpamAvailabilityRange).join( db_models.IpamAvailabilityRange).join(
db_models.IpamAllocationPool).filter_by( db_models.IpamAllocationPool).filter_by(
ipam_subnet_id=self._ipam_subnet_id) ipam_subnet_id=self._ipam_subnet_id)
if locking:
range_qry = range_qry.with_lockmode('update')
return range_qry
def get_first_range(self, session, locking=False): def get_first_range(self, session):
"""Return the first availability range for the subnet """Return the first availability range for the subnet
:param session: database session :param session: database session
:param locking: specifies whether a write-intent lock should be
performed on the database operation
:return: first available range as instance of :return: first available range as instance of
neutron.ipam.drivers.neutrondb_ipam.db_models.IpamAvailabilityRange neutron.ipam.drivers.neutrondb_ipam.db_models.IpamAvailabilityRange
""" """
return self._range_query(session, locking).first() return self._range_query(session).first()
def list_ranges_by_subnet_id(self, session, locking=False): def list_ranges_by_subnet_id(self, session):
"""Return availability ranges for a given ipam subnet """Return availability ranges for a given ipam subnet
:param session: database session :param session: database session
:param locking: specifies whether a write-intent lock should be
acquired with this database operation.
:return: list of availability ranges as instances of :return: list of availability ranges as instances of
neutron.ipam.drivers.neutrondb_ipam.db_models.IpamAvailabilityRange neutron.ipam.drivers.neutrondb_ipam.db_models.IpamAvailabilityRange
""" """
return self._range_query(session, locking) return self._range_query(session)
def list_ranges_by_allocation_pool(self, session, allocation_pool_id, def list_ranges_by_allocation_pool(self, session, allocation_pool_id):
locking=False):
"""Return availability ranges for a given pool. """Return availability ranges for a given pool.
:param session: database session :param session: database session
:param allocation_pool_id: allocation pool identifier :param allocation_pool_id: allocation pool identifier
:param locking: specifies whether a write-intent lock should be
acquired with this database operation.
:return: list of availability ranges as instances of :return: list of availability ranges as instances of
neutron.ipam.drivers.neutrondb_ipam.db_models.IpamAvailabilityRange neutron.ipam.drivers.neutrondb_ipam.db_models.IpamAvailabilityRange
""" """
@ -150,6 +143,46 @@ class IpamSubnetManager(object):
db_models.IpamAllocationPool).filter_by( db_models.IpamAllocationPool).filter_by(
id=allocation_pool_id) id=allocation_pool_id)
def update_range(self, session, db_range, first_ip=None, last_ip=None):
"""Updates db_range to have new first_ip and last_ip.
:param session: database session
:param db_range: IpamAvailabilityRange db object
:param first_ip: first ip address in range
:param last_ip: last ip address in range
:return: count of updated rows
"""
opts = {}
if first_ip:
opts['first_ip'] = str(first_ip)
if last_ip:
opts['last_ip'] = str(last_ip)
if not opts:
raise ipam_exc.IpamAvailabilityRangeNoChanges()
try:
return session.query(
db_models.IpamAvailabilityRange).filter_by(
allocation_pool_id=db_range.allocation_pool_id).filter_by(
first_ip=db_range.first_ip).filter_by(
last_ip=db_range.last_ip).update(opts)
except orm_exc.ObjectDeletedError:
raise db_exc.RetryRequest(ipam_exc.IPAllocationFailed)
def delete_range(self, session, db_range):
"""Return count of deleted ranges
:param session: database session
:param db_range: IpamAvailabilityRange db object
"""
try:
return session.query(
db_models.IpamAvailabilityRange).filter_by(
allocation_pool_id=db_range.allocation_pool_id).filter_by(
first_ip=db_range.first_ip).filter_by(
last_ip=db_range.last_ip).delete()
except orm_exc.ObjectDeletedError:
raise db_exc.RetryRequest(ipam_exc.IPAllocationFailed)
def create_range(self, session, allocation_pool_id, def create_range(self, session, allocation_pool_id,
range_start, range_end): range_start, range_end):
"""Create an availability range for a given pool. """Create an availability range for a given pool.
@ -180,23 +213,18 @@ class IpamSubnetManager(object):
return False return False
return True return True
def list_allocations(self, session, status='ALLOCATED', locking=False): def list_allocations(self, session, status='ALLOCATED'):
"""Return current allocations for the subnet. """Return current allocations for the subnet.
:param session: database session :param session: database session
:param status: IP allocation status :param status: IP allocation status
:param locking: specifies whether a write-intent lock should be
performed on the database operation
:returns: a list of IP allocation as instance of :returns: a list of IP allocation as instance of
neutron.ipam.drivers.neutrondb_ipam.db_models.IpamAllocation neutron.ipam.drivers.neutrondb_ipam.db_models.IpamAllocation
""" """
ip_qry = session.query( return session.query(
db_models.IpamAllocation).filter_by( db_models.IpamAllocation).filter_by(
ipam_subnet_id=self._ipam_subnet_id, ipam_subnet_id=self._ipam_subnet_id,
status=status) status=status)
if locking:
ip_qry = ip_qry.with_lockmode('update')
return ip_qry
def create_allocation(self, session, ip_address, def create_allocation(self, session, ip_address,
status='ALLOCATED'): status='ALLOCATED'):

View File

@ -14,6 +14,7 @@
# under the License. # under the License.
import netaddr import netaddr
from oslo_db import exception as db_exc
from oslo_log import log from oslo_log import log
from oslo_utils import uuidutils from oslo_utils import uuidutils
@ -154,7 +155,8 @@ class NeutronDbSubnet(ipam_base.Subnet):
ip=ip_address) ip=ip_address)
def _allocate_specific_ip(self, session, ip_address, def _allocate_specific_ip(self, session, ip_address,
allocation_pool_id=None): allocation_pool_id=None,
auto_generated=False):
"""Remove an IP address from subnet's availability ranges. """Remove an IP address from subnet's availability ranges.
This method is supposed to be called from within a database This method is supposed to be called from within a database
@ -167,6 +169,7 @@ class NeutronDbSubnet(ipam_base.Subnet):
:param allocation_pool_id: identifier of the allocation pool from :param allocation_pool_id: identifier of the allocation pool from
which the ip address has been extracted. If not specified this which the ip address has been extracted. If not specified this
routine will scan all allocation pools. routine will scan all allocation pools.
:param auto_generated: indicates whether ip was auto generated
:returns: list of IP ranges as instances of IPAvailabilityRange :returns: list of IP ranges as instances of IPAvailabilityRange
""" """
# Return immediately for EUI-64 addresses. For this # Return immediately for EUI-64 addresses. For this
@ -181,25 +184,28 @@ class NeutronDbSubnet(ipam_base.Subnet):
# Netaddr's IPRange and IPSet objects work very well even with very # Netaddr's IPRange and IPSet objects work very well even with very
# large subnets, including IPv6 ones. # large subnets, including IPv6 ones.
final_ranges = [] final_ranges = []
ip_in_pools = False
if allocation_pool_id: if allocation_pool_id:
av_ranges = self.subnet_manager.list_ranges_by_allocation_pool( av_ranges = self.subnet_manager.list_ranges_by_allocation_pool(
session, allocation_pool_id, locking=True) session, allocation_pool_id)
else: else:
av_ranges = self.subnet_manager.list_ranges_by_subnet_id( av_ranges = self.subnet_manager.list_ranges_by_subnet_id(session)
session, locking=True)
for db_range in av_ranges: for db_range in av_ranges:
initial_ip_set = netaddr.IPSet(netaddr.IPRange( initial_ip_set = netaddr.IPSet(netaddr.IPRange(
db_range['first_ip'], db_range['last_ip'])) db_range['first_ip'], db_range['last_ip']))
final_ip_set = initial_ip_set - netaddr.IPSet([ip_address]) final_ip_set = initial_ip_set - netaddr.IPSet([ip_address])
if not final_ip_set: if not final_ip_set:
ip_in_pools = True
# Range exhausted - bye bye # Range exhausted - bye bye
session.delete(db_range) if not self.subnet_manager.delete_range(session, db_range):
raise db_exc.RetryRequest(ipam_exc.IPAllocationFailed)
continue continue
if initial_ip_set == final_ip_set: if initial_ip_set == final_ip_set:
# IP address does not fall within the current range, move # IP address does not fall within the current range, move
# to the next one # to the next one
final_ranges.append(db_range) final_ranges.append(db_range)
continue continue
ip_in_pools = True
for new_range in final_ip_set.iter_ipranges(): for new_range in final_ip_set.iter_ipranges():
# store new range in database # store new range in database
# use netaddr.IPAddress format() method which is equivalent # use netaddr.IPAddress format() method which is equivalent
@ -208,9 +214,11 @@ class NeutronDbSubnet(ipam_base.Subnet):
first_ip = netaddr.IPAddress(new_range.first) first_ip = netaddr.IPAddress(new_range.first)
last_ip = netaddr.IPAddress(new_range.last) last_ip = netaddr.IPAddress(new_range.last)
if (db_range['first_ip'] == first_ip.format() or if (db_range['first_ip'] == first_ip.format() or
db_range['last_ip'] == last_ip.format()): db_range['last_ip'] == last_ip.format()):
db_range['first_ip'] = first_ip.format() rows = self.subnet_manager.update_range(
db_range['last_ip'] = last_ip.format() 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", LOG.debug("Adjusted availability range for pool %s",
db_range['allocation_pool_id']) db_range['allocation_pool_id'])
final_ranges.append(db_range) final_ranges.append(db_range)
@ -223,6 +231,11 @@ class NeutronDbSubnet(ipam_base.Subnet):
LOG.debug("Created availability range for pool %s", LOG.debug("Created availability range for pool %s",
new_ip_range['allocation_pool_id']) new_ip_range['allocation_pool_id'])
final_ranges.append(new_ip_range) 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 # Most callers might ignore this return value, which is however
# useful for testing purposes # useful for testing purposes
LOG.debug("Availability ranges for subnet id %(subnet_id)s " LOG.debug("Availability ranges for subnet id %(subnet_id)s "
@ -258,7 +271,7 @@ class NeutronDbSubnet(ipam_base.Subnet):
allocations = netaddr.IPSet( allocations = netaddr.IPSet(
[netaddr.IPAddress(allocation['ip_address']) for [netaddr.IPAddress(allocation['ip_address']) for
allocation in self.subnet_manager.list_allocations( allocation in self.subnet_manager.list_allocations(
session, locking=True)]) session)])
# MEH MEH # MEH MEH
# There should be no need to set a write intent lock on the allocation # There should be no need to set a write intent lock on the allocation
@ -296,7 +309,7 @@ class NeutronDbSubnet(ipam_base.Subnet):
def _try_generate_ip(self, session): def _try_generate_ip(self, session):
"""Generate an IP address from availability ranges.""" """Generate an IP address from availability ranges."""
ip_range = self.subnet_manager.get_first_range(session, locking=True) ip_range = self.subnet_manager.get_first_range(session)
if not ip_range: if not ip_range:
LOG.debug("All IPs from subnet %(subnet_id)s allocated", LOG.debug("All IPs from subnet %(subnet_id)s allocated",
{'subnet_id': self.subnet_manager.neutron_id}) {'subnet_id': self.subnet_manager.neutron_id})
@ -320,22 +333,26 @@ class NeutronDbSubnet(ipam_base.Subnet):
# with remote backends # with remote backends
session = self._context.session session = self._context.session
all_pool_id = None all_pool_id = None
# NOTE(salv-orlando): It would probably better to have a simpler auto_generated = False
# model for address requests and just check whether there is a with db_api.autonested_transaction(session):
# specific IP address specified in address_request # NOTE(salv-orlando): It would probably better to have a simpler
if isinstance(address_request, ipam_req.SpecificAddressRequest): # model for address requests and just check whether there is a
# This handles both specific and automatic address requests # specific IP address specified in address_request
# Check availability of requested IP if isinstance(address_request, ipam_req.SpecificAddressRequest):
ip_address = str(address_request.address) # This handles both specific and automatic address requests
self._verify_ip(session, ip_address) # Check availability of requested IP
else: ip_address = str(address_request.address)
ip_address, all_pool_id = self._generate_ip(session) self._verify_ip(session, ip_address)
self._allocate_specific_ip(session, ip_address, all_pool_id) else:
# Create IP allocation request object ip_address, all_pool_id = self._generate_ip(session)
# The only defined status at this stage is 'ALLOCATED'. auto_generated = True
# More states will be available in the future - e.g.: RECYCLABLE self._allocate_specific_ip(session, ip_address, all_pool_id,
self.subnet_manager.create_allocation(session, ip_address) auto_generated)
return ip_address # 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): def deallocate(self, address):
# This is almost a no-op because the Neutron DB IPAM driver does not # This is almost a no-op because the Neutron DB IPAM driver does not

View File

@ -60,3 +60,11 @@ class AllocationOnAutoAddressSubnet(exceptions.NeutronException):
class IpAddressGenerationFailure(exceptions.Conflict): class IpAddressGenerationFailure(exceptions.Conflict):
message = _("No more IP addresses available for subnet %(subnet_id)s.") message = _("No more IP addresses available for subnet %(subnet_id)s.")
class IPAllocationFailed(exceptions.NeutronException):
message = _("IP allocation failed. Try again later.")
class IpamAvailabilityRangeNoChanges(exceptions.NeutronException):
message = _("New value for first_ip or last_ip has to be specified.")

View File

@ -13,11 +13,16 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import mock
from oslo_db import exception as db_exc
from oslo_utils import uuidutils from oslo_utils import uuidutils
from sqlalchemy.orm import exc as orm_exc
from neutron import context from neutron import context
from neutron.ipam.drivers.neutrondb_ipam import db_api from neutron.ipam.drivers.neutrondb_ipam import db_api
from neutron.ipam.drivers.neutrondb_ipam import db_models from neutron.ipam.drivers.neutrondb_ipam import db_models
from neutron.ipam import exceptions as ipam_exc
from neutron.tests.unit import testlib_api from neutron.tests.unit import testlib_api
@ -80,24 +85,16 @@ class TestIpamSubnetManager(testlib_api.SqlTestCase):
filter_by(allocation_pool_id=db_pools[0].id).first() filter_by(allocation_pool_id=db_pools[0].id).first()
self._validate_ips([self.single_pool], range) self._validate_ips([self.single_pool], range)
def _test_get_first_range(self, locking):
self._create_pools(self.multi_pool)
range = self.subnet_manager.get_first_range(self.ctx.session,
locking=locking)
self._validate_ips(self.multi_pool, range)
def test_get_first_range(self): def test_get_first_range(self):
self._test_get_first_range(False) self._create_pools(self.multi_pool)
range = self.subnet_manager.get_first_range(self.ctx.session)
def test_get_first_range_locking(self): self._validate_ips(self.multi_pool, range)
self._test_get_first_range(True)
def test_list_ranges_by_subnet_id(self): def test_list_ranges_by_subnet_id(self):
self._create_pools(self.multi_pool) self._create_pools(self.multi_pool)
db_ranges = self.subnet_manager.list_ranges_by_subnet_id( db_ranges = self.subnet_manager.list_ranges_by_subnet_id(
self.ctx.session, self.ctx.session).all()
self.ipam_subnet_id).all()
self.assertEqual(2, len(db_ranges)) self.assertEqual(2, len(db_ranges))
self.assertEqual(db_models.IpamAvailabilityRange, type(db_ranges[0])) self.assertEqual(db_models.IpamAvailabilityRange, type(db_ranges[0]))
@ -136,6 +133,46 @@ class TestIpamSubnetManager(testlib_api.SqlTestCase):
self.assertEqual(range_start, new_range.first_ip) self.assertEqual(range_start, new_range.first_ip)
self.assertEqual(range_end, new_range.last_ip) self.assertEqual(range_end, new_range.last_ip)
def test_update_range(self):
self._create_pools([self.single_pool])
db_range = self.subnet_manager.get_first_range(self.ctx.session)
updated_count = self.subnet_manager.update_range(self.ctx.session,
db_range,
first_ip='1.2.3.6',
last_ip='1.2.3.8')
self.assertEqual(1, updated_count)
def test_update_range_no_new_values(self):
self._create_pools([self.single_pool])
db_range = self.subnet_manager.get_first_range(self.ctx.session)
self.assertRaises(ipam_exc.IpamAvailabilityRangeNoChanges,
self.subnet_manager.update_range,
self.ctx.session, db_range)
def test_update_range_reraise_error(self):
session = mock.Mock()
session.query.side_effect = orm_exc.ObjectDeletedError(None, None)
self.assertRaises(db_exc.RetryRequest,
self.subnet_manager.update_range,
session,
mock.Mock(),
first_ip='1.2.3.5')
def test_delete_range(self):
self._create_pools([self.single_pool])
db_range = self.subnet_manager.get_first_range(self.ctx.session)
deleted_count = self.subnet_manager.delete_range(self.ctx.session,
db_range)
self.assertEqual(1, deleted_count)
def test_delete_range_reraise_error(self):
session = mock.Mock()
session.query.side_effect = orm_exc.ObjectDeletedError(None, None)
self.assertRaises(db_exc.RetryRequest,
self.subnet_manager.delete_range,
session,
mock.Mock())
def test_check_unique_allocation(self): def test_check_unique_allocation(self):
self.assertTrue(self.subnet_manager.check_unique_allocation( self.assertTrue(self.subnet_manager.check_unique_allocation(
self.ctx.session, self.subnet_ip)) self.ctx.session, self.subnet_ip))

View File

@ -13,8 +13,11 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import mock
import netaddr import netaddr
from oslo_db import exception as db_exc
from neutron.api.v2 import attributes from neutron.api.v2 import attributes
from neutron.common import constants from neutron.common import constants
from neutron.common import exceptions as n_exc from neutron.common import exceptions as n_exc
@ -444,3 +447,16 @@ class TestNeutronDbIpamSubnet(testlib_api.SqlTestCase,
subnet_req = ipam_req.SpecificSubnetRequest( subnet_req = ipam_req.SpecificSubnetRequest(
'tenant_id', 'meh', '192.168.0.0/24') 'tenant_id', 'meh', '192.168.0.0/24')
self.ipam_pool.allocate_subnet(subnet_req) self.ipam_pool.allocate_subnet(subnet_req)
def test__allocate_specific_ip_raises_exception(self):
cidr = '10.0.0.0/24'
ip = '10.0.0.15'
ipam_subnet = self._create_and_allocate_ipam_subnet(cidr)[0]
ipam_subnet.subnet_manager = mock.Mock()
ipam_subnet.subnet_manager.list_ranges_by_subnet_id.return_value = [{
'first_ip': '10.0.0.15', 'last_ip': '10.0.0.15'}]
ipam_subnet.subnet_manager.delete_range.return_value = 0
self.assertRaises(db_exc.RetryRequest,
ipam_subnet._allocate_specific_ip,
self.ctx.session, ip)