summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--neutron/agent/l3/dvr_fip_ns.py14
-rw-r--r--neutron/db/ipam_pluggable_backend.py33
-rw-r--r--neutron/ipam/drivers/neutrondb_ipam/driver.py9
-rw-r--r--neutron/plugins/ml2/managers.py2
-rw-r--r--neutron/plugins/ml2/plugin.py77
-rw-r--r--neutron/tests/functional/agent/test_l3_agent.py64
-rw-r--r--neutron/tests/unit/db/test_ipam_pluggable_backend.py61
-rw-r--r--neutron/tests/unit/db/test_l3_dvr_db.py2
-rw-r--r--neutron/tests/unit/ipam/drivers/neutrondb_ipam/test_driver.py42
-rw-r--r--neutron/tests/unit/plugins/ml2/test_plugin.py6
-rw-r--r--tox.ini2
11 files changed, 253 insertions, 59 deletions
diff --git a/neutron/agent/l3/dvr_fip_ns.py b/neutron/agent/l3/dvr_fip_ns.py
index 441005c89d..6557cc267b 100644
--- a/neutron/agent/l3/dvr_fip_ns.py
+++ b/neutron/agent/l3/dvr_fip_ns.py
@@ -108,6 +108,18 @@ class FipNamespace(namespaces.Namespace):
prefix=FIP_EXT_DEV_PREFIX,
mtu=ex_gw_port.get('mtu'))
+ # Remove stale fg devices
+ ip_wrapper = ip_lib.IPWrapper(namespace=ns_name)
+ devices = ip_wrapper.get_devices()
+ for device in devices:
+ name = device.name
+ if name.startswith(FIP_EXT_DEV_PREFIX) and name != interface_name:
+ ext_net_bridge = self.agent_conf.external_network_bridge
+ self.driver.unplug(name,
+ bridge=ext_net_bridge,
+ namespace=ns_name,
+ prefix=FIP_EXT_DEV_PREFIX)
+
ip_cidrs = common_utils.fixed_ip_cidrs(ex_gw_port['fixed_ips'])
self.driver.init_l3(interface_name, ip_cidrs, namespace=ns_name,
clean_connections=True)
@@ -115,8 +127,6 @@ class FipNamespace(namespaces.Namespace):
self.update_gateway_port(ex_gw_port)
cmd = ['sysctl', '-w', 'net.ipv4.conf.%s.proxy_arp=1' % interface_name]
- # TODO(Carl) mlavelle's work has self.ip_wrapper
- ip_wrapper = ip_lib.IPWrapper(namespace=ns_name)
ip_wrapper.netns.execute(cmd, check_exit_code=False)
def create(self):
diff --git a/neutron/db/ipam_pluggable_backend.py b/neutron/db/ipam_pluggable_backend.py
index 4a47f797d5..6c845e0232 100644
--- a/neutron/db/ipam_pluggable_backend.py
+++ b/neutron/db/ipam_pluggable_backend.py
@@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import copy
+
import netaddr
from oslo_db import exception as db_exc
from oslo_log import log as logging
@@ -137,9 +139,6 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
return allocated
def _ipam_update_allocation_pools(self, context, ipam_driver, subnet):
- self.validate_allocation_pools(subnet['allocation_pools'],
- subnet['cidr'])
-
factory = ipam_driver.get_subnet_request_factory()
subnet_request = factory.get_request(context, subnet, None)
@@ -358,22 +357,22 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
port['fixed_ips'])
def update_db_subnet(self, context, id, s, old_pools):
+ # 'allocation_pools' is removed from 's' in
+ # _update_subnet_allocation_pools (ipam_backend_mixin),
+ # so create unchanged copy for ipam driver
+ subnet_copy = copy.deepcopy(s)
+ subnet, changes = super(IpamPluggableBackend, self).update_db_subnet(
+ context, id, s, old_pools)
ipam_driver = driver.Pool.get_instance(None, context)
- if "allocation_pools" in s:
- self._ipam_update_allocation_pools(context, ipam_driver, s)
- try:
- subnet, changes = super(IpamPluggableBackend,
- self).update_db_subnet(context, id,
- s, old_pools)
- except Exception:
- with excutils.save_and_reraise_exception():
- if "allocation_pools" in s and old_pools:
- LOG.error(
- _LE("An exception occurred during subnet update."
- "Reverting allocation pool changes"))
- s['allocation_pools'] = old_pools
- self._ipam_update_allocation_pools(context, ipam_driver, s)
+ # Set old allocation pools if no new pools are provided by user.
+ # Passing old pools allows to call ipam driver on each subnet update
+ # even if allocation pools are not changed. So custom ipam drivers
+ # are able to track other fields changes on subnet update.
+ if 'allocation_pools' not in subnet_copy:
+ subnet_copy['allocation_pools'] = old_pools
+ self._ipam_update_allocation_pools(context, ipam_driver, subnet_copy)
+
return subnet, changes
def add_auto_addrs_on_network_ports(self, context, subnet, ipam_subnet):
diff --git a/neutron/ipam/drivers/neutrondb_ipam/driver.py b/neutron/ipam/drivers/neutrondb_ipam/driver.py
index 31d30c460e..92f53bb8a9 100644
--- a/neutron/ipam/drivers/neutrondb_ipam/driver.py
+++ b/neutron/ipam/drivers/neutrondb_ipam/driver.py
@@ -351,11 +351,20 @@ class NeutronDbSubnet(ipam_base.Subnet):
subnet_id=self.subnet_manager.neutron_id,
ip_address=address)
+ def _no_pool_changes(self, session, pools):
+ """Check if pool updates in db are required."""
+ db_pools = self.subnet_manager.list_pools(session)
+ iprange_pools = [netaddr.IPRange(pool.first_ip, pool.last_ip)
+ for pool in db_pools]
+ return pools == iprange_pools
+
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 = self._context.session
+ if self._no_pool_changes(session, pools):
+ return
self.subnet_manager.delete_allocation_pools(session)
self.create_allocation_pools(self.subnet_manager, session, pools, cidr)
self._pools = pools
diff --git a/neutron/plugins/ml2/managers.py b/neutron/plugins/ml2/managers.py
index 3c9e1c2b75..d1e18f06fb 100644
--- a/neutron/plugins/ml2/managers.py
+++ b/neutron/plugins/ml2/managers.py
@@ -161,7 +161,7 @@ class TypeManager(stevedore.named.NamedExtensionManager):
def _extend_network_dict_provider(self, network, segments):
if not segments:
- LOG.error(_LE("Network %s has no segments"), network['id'])
+ LOG.debug("Network %s has no segments", network['id'])
for attr in provider.ATTRIBUTES:
network[attr] = None
elif len(segments) > 1:
diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py
index 56b4ec47a7..01fd1a6c6b 100644
--- a/neutron/plugins/ml2/plugin.py
+++ b/neutron/plugins/ml2/plugin.py
@@ -879,6 +879,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
LOG.debug("Deleting subnet %s", id)
session = context.session
+ deallocated = set()
while True:
with session.begin(subtransactions=True):
record = self._get_subnet(context, id)
@@ -897,43 +898,52 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
qry_allocated = (
qry_allocated.filter(models_v2.Port.device_owner.
in_(db_base_plugin_v2.AUTO_DELETE_PORT_OWNERS)))
- allocated = qry_allocated.all()
- # Delete all the IPAllocation that can be auto-deleted
- if allocated:
- for x in allocated:
- session.delete(x)
+ allocated = set(qry_allocated.all())
LOG.debug("Ports to auto-deallocate: %s", allocated)
- # Check if there are more IP allocations, unless
- # is_auto_address_subnet is True. In that case the check is
- # unnecessary. This additional check not only would be wasteful
- # for this class of subnet, but is also error-prone since when
- # the isolation level is set to READ COMMITTED allocations made
- # concurrently will be returned by this query
if not is_auto_addr_subnet:
- alloc = self._subnet_check_ip_allocations(context, id)
- if alloc:
- user_alloc = self._subnet_get_user_allocation(
- context, id)
- if user_alloc:
- LOG.info(_LI("Found port (%(port_id)s, %(ip)s) "
- "having IP allocation on subnet "
- "%(subnet)s, cannot delete"),
- {'ip': user_alloc.ip_address,
- 'port_id': user_alloc.port_id,
- 'subnet': id})
- raise exc.SubnetInUse(subnet_id=id)
- else:
- # allocation found and it was DHCP port
- # that appeared after autodelete ports were
- # removed - need to restart whole operation
- raise os_db_exception.RetryRequest(
- exc.SubnetInUse(subnet_id=id))
+ user_alloc = self._subnet_get_user_allocation(
+ context, id)
+ if user_alloc:
+ LOG.info(_LI("Found port (%(port_id)s, %(ip)s) "
+ "having IP allocation on subnet "
+ "%(subnet)s, cannot delete"),
+ {'ip': user_alloc.ip_address,
+ 'port_id': user_alloc.port_id,
+ 'subnet': id})
+ raise exc.SubnetInUse(subnet_id=id)
db_base_plugin_v2._check_subnet_not_used(context, id)
- # If allocated is None, then all the IPAllocation were
- # correctly deleted during the previous pass.
- if not allocated:
+ # SLAAC allocations currently can not be removed using
+ # update_port workflow, and will persist in 'allocated'.
+ # So for now just make sure update_port is called once for
+ # them so MechanismDrivers is aware of the change.
+ # This way SLAAC allocation is deleted by FK on subnet deletion
+ # TODO(pbondar): rework update_port workflow to allow deletion
+ # of SLAAC allocation via update_port.
+ to_deallocate = allocated - deallocated
+
+ # If to_deallocate is blank, then all known IPAllocations
+ # (except SLAAC allocations) were correctly deleted
+ # during the previous pass.
+ # Check if there are more IP allocations, unless
+ # is_auto_address_subnet is True. If transaction isolation
+ # level is set to READ COMMITTED allocations made
+ # concurrently will be returned by this query and transaction
+ # will be restarted. It works for REPEATABLE READ isolation
+ # level too because this query is executed only once during
+ # transaction, and if concurrent allocations are detected
+ # transaction gets restarted. Executing this query second time
+ # in transaction would result in not seeing allocations
+ # committed by concurrent transactions.
+ if not to_deallocate:
+ if (not is_auto_addr_subnet and
+ self._subnet_check_ip_allocations(context, id)):
+ # allocation found and it was DHCP port
+ # that appeared after autodelete ports were
+ # removed - need to restart whole operation
+ raise os_db_exception.RetryRequest(
+ exc.SubnetInUse(subnet_id=id))
network = self.get_network(context, subnet['network_id'])
mech_context = driver_context.SubnetContext(self, context,
subnet,
@@ -951,7 +961,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
LOG.debug("Committing transaction")
break
- for a in allocated:
+ for a in to_deallocate:
+ deallocated.add(a)
if a.port:
# calling update_port() for each allocation to remove the
# IP from the port and call the MechanismDrivers
diff --git a/neutron/tests/functional/agent/test_l3_agent.py b/neutron/tests/functional/agent/test_l3_agent.py
index fbf56385e5..f5b9185e86 100644
--- a/neutron/tests/functional/agent/test_l3_agent.py
+++ b/neutron/tests/functional/agent/test_l3_agent.py
@@ -32,6 +32,7 @@ import webob.exc
from neutron.agent.common import config as agent_config
from neutron.agent.common import ovs_lib
from neutron.agent.l3 import agent as neutron_l3_agent
+from neutron.agent.l3 import dvr_fip_ns
from neutron.agent.l3 import dvr_snat_ns
from neutron.agent.l3 import namespace_manager
from neutron.agent.l3 import namespaces
@@ -1135,6 +1136,69 @@ class TestDvrRouter(L3AgentTestFramework):
self._assert_dvr_floating_ips(router)
self._assert_snat_namespace_does_not_exist(router)
+ def test_dvr_router_fips_stale_gw_port(self):
+ self.agent.conf.agent_mode = 'dvr'
+
+ # Create the router with external net
+ dvr_router_kwargs = {'ip_address': '19.4.4.3',
+ 'subnet_cidr': '19.4.4.0/24',
+ 'gateway_ip': '19.4.4.1',
+ 'gateway_mac': 'ca:fe:de:ab:cd:ef'}
+ router_info = self.generate_dvr_router_info(**dvr_router_kwargs)
+ external_gw_port = router_info['gw_port']
+ ext_net_id = router_info['_floatingips'][0]['floating_network_id']
+ self.mock_plugin_api.get_external_network_id.return_value(ext_net_id)
+
+ # Create the fip namespace up front
+ stale_fip_ns = dvr_fip_ns.FipNamespace(ext_net_id,
+ self.agent.conf,
+ self.agent.driver,
+ self.agent.use_ipv6)
+ stale_fip_ns.create()
+
+ # Add a stale fg port to the namespace
+ fixed_ip = external_gw_port['fixed_ips'][0]
+ float_subnet = external_gw_port['subnets'][0]
+ fip_gw_port_ip = str(netaddr.IPAddress(fixed_ip['ip_address']) + 10)
+ prefixlen = netaddr.IPNetwork(float_subnet['cidr']).prefixlen
+ stale_agent_gw_port = {
+ 'subnets': [{'cidr': float_subnet['cidr'],
+ 'gateway_ip': float_subnet['gateway_ip'],
+ 'id': fixed_ip['subnet_id']}],
+ 'network_id': external_gw_port['network_id'],
+ 'device_owner': l3_constants.DEVICE_OWNER_AGENT_GW,
+ 'mac_address': 'fa:16:3e:80:8f:89',
+ 'binding:host_id': self.agent.conf.host,
+ 'fixed_ips': [{'subnet_id': fixed_ip['subnet_id'],
+ 'ip_address': fip_gw_port_ip,
+ 'prefixlen': prefixlen}],
+ 'id': _uuid(),
+ 'device_id': _uuid()}
+ stale_fip_ns.create_gateway_port(stale_agent_gw_port)
+
+ stale_dev_exists = self.device_exists_with_ips_and_mac(
+ stale_agent_gw_port,
+ stale_fip_ns.get_ext_device_name,
+ stale_fip_ns.get_name())
+ self.assertTrue(stale_dev_exists)
+
+ # Create the router, this shouldn't allow the duplicate port to stay
+ router = self.manage_router(self.agent, router_info)
+
+ # Assert the device no longer exists
+ stale_dev_exists = self.device_exists_with_ips_and_mac(
+ stale_agent_gw_port,
+ stale_fip_ns.get_ext_device_name,
+ stale_fip_ns.get_name())
+ self.assertFalse(stale_dev_exists)
+
+ # Validate things are looking good and clean up
+ self._validate_fips_for_external_network(
+ router, router.fip_ns.get_name())
+ ext_gateway_port = router_info['gw_port']
+ self._delete_router(self.agent, router.router_id)
+ self._assert_fip_namespace_deleted(ext_gateway_port)
+
def test_dvr_update_floatingip_statuses(self):
self.agent.conf.agent_mode = 'dvr'
self._test_update_floatingip_statuses(self.generate_dvr_router_info())
diff --git a/neutron/tests/unit/db/test_ipam_pluggable_backend.py b/neutron/tests/unit/db/test_ipam_pluggable_backend.py
index 4d17c2e776..a6aa505576 100644
--- a/neutron/tests/unit/db/test_ipam_pluggable_backend.py
+++ b/neutron/tests/unit/db/test_ipam_pluggable_backend.py
@@ -63,9 +63,11 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
self.tenant_id = uuidutils.generate_uuid()
self.subnet_id = uuidutils.generate_uuid()
- def _prepare_mocks(self, address_factory=None):
+ def _prepare_mocks(self, address_factory=None, subnet_factory=None):
if address_factory is None:
address_factory = ipam_req.AddressRequestFactory
+ if subnet_factory is None:
+ subnet_factory = ipam_req.SubnetRequestFactory
mocks = {
'driver': mock.Mock(),
@@ -80,7 +82,7 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
mocks['driver'].get_subnet.return_value = mocks['subnet']
mocks['driver'].allocate_subnet.return_value = mocks['subnet']
mocks['driver'].get_subnet_request_factory.return_value = (
- ipam_req.SubnetRequestFactory)
+ subnet_factory)
mocks['driver'].get_address_request_factory.return_value = (
address_factory)
mocks['subnet'].get_details.return_value = mocks['subnet_request']
@@ -91,8 +93,10 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
mocks['ipam'] = ipam_pluggable_backend.IpamPluggableBackend()
return mocks
- def _prepare_mocks_with_pool_mock(self, pool_mock, address_factory=None):
- mocks = self._prepare_mocks(address_factory=address_factory)
+ def _prepare_mocks_with_pool_mock(self, pool_mock, address_factory=None,
+ subnet_factory=None):
+ mocks = self._prepare_mocks(address_factory=address_factory,
+ subnet_factory=subnet_factory)
pool_mock.get_instance.return_value = mocks['driver']
return mocks
@@ -583,3 +587,52 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
ip_dict)
# Verify incoming port dict is not changed ('id' is not added to it)
self.assertIsNone(port_dict['port'].get('id'))
+
+ def _test_update_db_subnet(self, pool_mock, subnet, expected_subnet,
+ old_pools):
+ subnet_factory = mock.Mock()
+ context = mock.Mock()
+
+ mocks = self._prepare_mocks_with_pool_mock(
+ pool_mock, subnet_factory=subnet_factory)
+
+ mocks['ipam'] = ipam_pluggable_backend.IpamPluggableBackend()
+ mocks['ipam'].update_db_subnet(context, id, subnet, old_pools)
+
+ mocks['driver'].get_subnet_request_factory.assert_called_once_with()
+ subnet_factory.get_request.assert_called_once_with(context,
+ expected_subnet,
+ None)
+
+ @mock.patch('neutron.ipam.driver.Pool')
+ def test_update_db_subnet_unchanged_pools(self, pool_mock):
+ old_pools = [netaddr.IPRange('192.1.1.2', '192.1.1.254')]
+ subnet = {'id': uuidutils.generate_uuid(),
+ 'network_id': uuidutils.generate_uuid(),
+ 'cidr': '192.1.1.0/24',
+ 'ipv6_address_mode': None,
+ 'ipv6_ra_mode': None}
+ subnet_with_pools = subnet.copy()
+ subnet_with_pools['allocation_pools'] = old_pools
+ # if subnet has no allocation pools set, then old pools has to
+ # be added to subnet dict passed to request factory
+ self._test_update_db_subnet(pool_mock, subnet, subnet_with_pools,
+ old_pools)
+
+ @mock.patch('neutron.ipam.driver.Pool')
+ def test_update_db_subnet_new_pools(self, pool_mock):
+ old_pools = [netaddr.IPRange('192.1.1.2', '192.1.1.254')]
+ subnet = {'id': uuidutils.generate_uuid(),
+ 'network_id': uuidutils.generate_uuid(),
+ 'cidr': '192.1.1.0/24',
+ 'allocation_pools': [
+ netaddr.IPRange('192.1.1.10', '192.1.1.254')],
+ 'ipv6_address_mode': None,
+ 'ipv6_ra_mode': None}
+ # make a copy of subnet for validation, since update_subnet changes
+ # incoming subnet dict
+ expected_subnet = subnet.copy()
+ # validate that subnet passed to request factory is the same as
+ # incoming one, i.e. new pools in it are not overwritten by old pools
+ self._test_update_db_subnet(pool_mock, subnet, expected_subnet,
+ old_pools)
diff --git a/neutron/tests/unit/db/test_l3_dvr_db.py b/neutron/tests/unit/db/test_l3_dvr_db.py
index f9502f43d4..aedc496cf0 100644
--- a/neutron/tests/unit/db/test_l3_dvr_db.py
+++ b/neutron/tests/unit/db/test_l3_dvr_db.py
@@ -579,7 +579,7 @@ class L3DvrTestCase(test_db_base_plugin_v2.NeutronDbPluginV2TestCase):
elif action == 'del':
self.mixin.delete_arp_entry_for_dvr_service_port(
self.ctx, port)
- self.assertTrue(3, l3_notify.del_arp_entry.call_count)
+ self.assertEqual(3, l3_notify.del_arp_entry.call_count)
def test_update_arp_entry_for_dvr_service_port_added(self):
action = 'add'
diff --git a/neutron/tests/unit/ipam/drivers/neutrondb_ipam/test_driver.py b/neutron/tests/unit/ipam/drivers/neutrondb_ipam/test_driver.py
index 5a3f6d6e9c..f4889387ea 100644
--- a/neutron/tests/unit/ipam/drivers/neutrondb_ipam/test_driver.py
+++ b/neutron/tests/unit/ipam/drivers/neutrondb_ipam/test_driver.py
@@ -13,12 +13,14 @@
# License for the specific language governing permissions and limitations
# under the License.
+import mock
import netaddr
from neutron.api.v2 import attributes
from neutron.common import constants
from neutron.common import exceptions as n_exc
from neutron import context
+from neutron.ipam.drivers.neutrondb_ipam import db_models
from neutron.ipam.drivers.neutrondb_ipam import driver
from neutron.ipam import exceptions as ipam_exc
from neutron.ipam import requests as ipam_req
@@ -444,3 +446,43 @@ class TestNeutronDbIpamSubnet(testlib_api.SqlTestCase,
subnet_req = ipam_req.SpecificSubnetRequest(
'tenant_id', 'meh', '192.168.0.0/24')
self.ipam_pool.allocate_subnet(subnet_req)
+
+ def test_update_allocation_pools_with_no_pool_change(self):
+ cidr = '10.0.0.0/24'
+ ipam_subnet = self._create_and_allocate_ipam_subnet(
+ cidr)[0]
+ ipam_subnet.subnet_manager.delete_allocation_pools = mock.Mock()
+ ipam_subnet.create_allocation_pools = mock.Mock()
+ alloc_pools = [netaddr.IPRange('10.0.0.2', '10.0.0.254')]
+ # Make sure allocation pools recreation does not happen in case of
+ # unchanged allocation pools
+ ipam_subnet.update_allocation_pools(alloc_pools, cidr)
+ self.assertFalse(
+ ipam_subnet.subnet_manager.delete_allocation_pools.called)
+ self.assertFalse(ipam_subnet.create_allocation_pools.called)
+
+ def _test__no_pool_changes(self, new_pools):
+ id = 'some-id'
+ ipam_subnet = driver.NeutronDbSubnet(id, self.ctx)
+ pools = [db_models.IpamAllocationPool(ipam_subnet_id=id,
+ first_ip='192.168.10.20',
+ last_ip='192.168.10.41'),
+ db_models.IpamAllocationPool(ipam_subnet_id=id,
+ first_ip='192.168.10.50',
+ last_ip='192.168.10.60')]
+
+ ipam_subnet.subnet_manager.list_pools = mock.Mock(return_value=pools)
+ return ipam_subnet._no_pool_changes(self.ctx.session, new_pools)
+
+ def test__no_pool_changes_negative(self):
+ pool_list = [[netaddr.IPRange('192.168.10.2', '192.168.10.254')],
+ [netaddr.IPRange('192.168.10.20', '192.168.10.41')],
+ [netaddr.IPRange('192.168.10.20', '192.168.10.41'),
+ netaddr.IPRange('192.168.10.51', '192.168.10.60')]]
+ for pools in pool_list:
+ self.assertFalse(self._test__no_pool_changes(pools))
+
+ def test__no_pool_changes_positive(self):
+ pools = [netaddr.IPRange('192.168.10.20', '192.168.10.41'),
+ netaddr.IPRange('192.168.10.50', '192.168.10.60')]
+ self.assertTrue(self._test__no_pool_changes(pools))
diff --git a/neutron/tests/unit/plugins/ml2/test_plugin.py b/neutron/tests/unit/plugins/ml2/test_plugin.py
index 03c0b40936..619855597a 100644
--- a/neutron/tests/unit/plugins/ml2/test_plugin.py
+++ b/neutron/tests/unit/plugins/ml2/test_plugin.py
@@ -431,6 +431,12 @@ class TestMl2SubnetsV2(test_plugin.TestSubnetsV2,
req = self.new_delete_request('subnets', subnet_id)
res = req.get_response(self.api)
self.assertEqual(204, res.status_int)
+ # Validate chat check is called twice, i.e. after the
+ # first check transaction gets restarted.
+ calls = [mock.call(mock.ANY, subnet_id),
+ mock.call(mock.ANY, subnet_id)]
+ plugin._subnet_check_ip_allocations.assert_has_calls = (
+ calls)
class TestMl2DbOperationBounds(test_plugin.DbOperationBoundMixin,
diff --git a/tox.ini b/tox.ini
index 89a593e79c..4e9c33fe02 100644
--- a/tox.ini
+++ b/tox.ini
@@ -166,7 +166,7 @@ commands = {[testenv:docs]commands}
# E126 continuation line over-indented for hanging indent
# E128 continuation line under-indented for visual indent
# E129 visually indented line with same indent as next logical line
-# E265 block comment should start with ‘# ‘
+# E265 block comment should start with '# '
# H404 multi line docstring should start with a summary
# H405 multi line docstring summary not separated with an empty line
ignore = E125,E126,E128,E129,E265,H404,H405