diff --git a/quantum/db/db_base_plugin_v2.py b/quantum/db/db_base_plugin_v2.py index 9388ba7c07..4cdfe9b4e4 100644 --- a/quantum/db/db_base_plugin_v2.py +++ b/quantum/db/db_base_plugin_v2.py @@ -191,7 +191,49 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): return False @staticmethod - def _recycle_ip(context, network_id, subnet_id, port_id, ip_address): + def _hold_ip(context, network_id, subnet_id, port_id, ip_address): + alloc_qry = context.session.query(models_v2.IPAllocation) + allocated = alloc_qry.filter_by(network_id=network_id, + port_id=port_id, + ip_address=ip_address, + subnet_id=subnet_id).one() + + if not allocated: + return + if allocated.expiration < timeutils.utcnow(): + # immediately delete expired allocations + QuantumDbPluginV2._recycle_ip( + context, network_id, subnet_id, ip_address) + else: + LOG.debug("Hold allocated IP %s (%s/%s/%s)", ip_address, + network_id, subnet_id, port_id) + allocated.port_id = None + + @staticmethod + def _recycle_expired_ip_allocations(context, network_id): + """Return held ip allocations with expired leases back to the pool.""" + if network_id in getattr(context, '_recycled_networks', set()): + return + + expired_qry = context.session.query(models_v2.IPAllocation) + expired_qry = expired_qry.filter_by(network_id=network_id, + port_id=None) + expired_qry = expired_qry.filter( + models_v2.IPAllocation.expiration <= timeutils.utcnow()) + + for expired in expired_qry.all(): + QuantumDbPluginV2._recycle_ip(context, + network_id, + expired['subnet_id'], + expired['ip_address']) + + if hasattr(context, '_recycled_networks'): + context._recycled_networks.add(network_id) + else: + context._recycled_networks = set([network_id]) + + @staticmethod + def _recycle_ip(context, network_id, subnet_id, ip_address): """Return an IP address to the pool of free IP's on the network subnet. """ @@ -266,7 +308,7 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): context.session.add(ip_range) LOG.debug("Recycle: created new %s-%s", ip_address, ip_address) QuantumDbPluginV2._delete_ip_allocation(context, network_id, subnet_id, - port_id, ip_address) + ip_address) @staticmethod def _default_allocation_expiration(): @@ -289,15 +331,13 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): "ip address %s.", network_id, ip_address) @staticmethod - def _delete_ip_allocation(context, network_id, subnet_id, port_id, - ip_address): + def _delete_ip_allocation(context, network_id, subnet_id, ip_address): # Delete the IP address from the IPAllocate table - LOG.debug("Delete allocated IP %s (%s/%s/%s)", ip_address, - network_id, subnet_id, port_id) + LOG.debug("Delete allocated IP %s (%s/%s)", ip_address, + network_id, subnet_id) alloc_qry = context.session.query(models_v2.IPAllocation) allocated = alloc_qry.filter_by(network_id=network_id, - port_id=port_id, ip_address=ip_address, subnet_id=subnet_id).delete() @@ -474,6 +514,7 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): new_ips): """Add or remove IPs from the port.""" ips = [] + # Remove all of the intersecting elements for original_ip in original_ips[:]: for new_ip in new_ips[:]: @@ -487,12 +528,12 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): # Check if the IP's to add are OK to_add = self._test_fixed_ips_for_port(context, network_id, new_ips) for ip in original_ips: - LOG.debug("Port update. Deleting %s", ip) - QuantumDbPluginV2._recycle_ip(context, - network_id=network_id, - subnet_id=ip['subnet_id'], - ip_address=ip['ip_address'], - port_id=port_id) + LOG.debug("Port update. Hold %s", ip) + QuantumDbPluginV2._hold_ip(context, + network_id, + ip['subnet_id'], + port_id, + ip['ip_address']) if to_add: LOG.debug("Port update. Adding %s", to_add) @@ -968,7 +1009,8 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): allocated_qry = allocated_qry.options(orm.joinedload('ports')) allocated = allocated_qry.filter_by(subnet_id=id).all() - only_svc = all(a.ports.device_owner.startswith(AGENT_OWNER_PREFIX) + only_svc = all(not a.port_id or + a.ports.device_owner.startswith(AGENT_OWNER_PREFIX) for a in allocated) if not only_svc: raise q_exc.NetworkInUse(subnet_id=id) @@ -998,6 +1040,7 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): tenant_id = self._get_tenant_id_for_create(context, p) with context.session.begin(subtransactions=True): + self._recycle_expired_ip_allocations(context, p['network_id']) network = self._get_network(context, p["network_id"]) # Ensure that a MAC address is defined and it is unique on the @@ -1051,6 +1094,8 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): port = self._get_port(context, id) # Check if the IPs need to be updated if 'fixed_ips' in p: + self._recycle_expired_ip_allocations(context, + port['network_id']) original = self._make_port_dict(port) ips = self._update_ips_for_port(context, port["network_id"], @@ -1091,18 +1136,17 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): # Gateway address will not be recycled, but we do # need to delete the allocation from the DB QuantumDbPluginV2._delete_ip_allocation( - context, - a['network_id'], a['subnet_id'], - id, a['ip_address']) + context, a['network_id'], + a['subnet_id'], a['ip_address']) LOG.debug("Gateway address (%s/%s) is not recycled", a['ip_address'], a['subnet_id']) continue - QuantumDbPluginV2._recycle_ip(context, - network_id=a['network_id'], - subnet_id=a['subnet_id'], - ip_address=a['ip_address'], - port_id=id) + QuantumDbPluginV2._hold_ip(context, + a['network_id'], + a['subnet_id'], + id, + a['ip_address']) context.session.delete(port) def get_port(self, context, id, fields=None): diff --git a/quantum/db/models_v2.py b/quantum/db/models_v2.py index 41654c9d5c..2239573ac5 100644 --- a/quantum/db/models_v2.py +++ b/quantum/db/models_v2.py @@ -76,7 +76,7 @@ class IPAllocation(model_base.BASEV2): """ port_id = sa.Column(sa.String(36), sa.ForeignKey('ports.id', ondelete="CASCADE"), - nullable=False, primary_key=True) + nullable=True) ip_address = sa.Column(sa.String(64), nullable=False, primary_key=True) subnet_id = sa.Column(sa.String(36), sa.ForeignKey('subnets.id', ondelete="CASCADE"), diff --git a/quantum/plugins/cisco/network_plugin.py b/quantum/plugins/cisco/network_plugin.py index 59fc235d5c..64ce6796b2 100644 --- a/quantum/plugins/cisco/network_plugin.py +++ b/quantum/plugins/cisco/network_plugin.py @@ -256,8 +256,8 @@ class PluginV2(db_base_plugin_v2.QuantumDbPluginV2): allocated = allocated_qry.filter_by(subnet_id=id).all() prefix = db_base_plugin_v2.AGENT_OWNER_PREFIX - if not all(a.ports.device_owner.startswith(prefix) for a in - allocated): + if not all(not a.port_id or a.ports.device_owner.startswith(prefix) + for a in allocated): raise exc.SubnetInUse(subnet_id=id) context.session.close() try: diff --git a/quantum/tests/unit/test_db_plugin.py b/quantum/tests/unit/test_db_plugin.py index 73184f2363..bb0fc54c82 100644 --- a/quantum/tests/unit/test_db_plugin.py +++ b/quantum/tests/unit/test_db_plugin.py @@ -856,7 +856,7 @@ class TestPortsV2(QuantumDbPluginV2TestCase): data['port']['admin_state_up']) ips = res['port']['fixed_ips'] self.assertEquals(len(ips), 1) - self.assertEquals(ips[0]['ip_address'], '10.0.0.2') + self.assertEquals(ips[0]['ip_address'], '10.0.0.3') self.assertEquals(ips[0]['subnet_id'], subnet['subnet']['id']) def test_update_port_add_additional_ip(self): @@ -875,9 +875,9 @@ class TestPortsV2(QuantumDbPluginV2TestCase): data['port']['admin_state_up']) ips = res['port']['fixed_ips'] self.assertEquals(len(ips), 2) - self.assertEquals(ips[0]['ip_address'], '10.0.0.2') + self.assertEquals(ips[0]['ip_address'], '10.0.0.3') self.assertEquals(ips[0]['subnet_id'], subnet['subnet']['id']) - self.assertEquals(ips[1]['ip_address'], '10.0.0.3') + self.assertEquals(ips[1]['ip_address'], '10.0.0.4') self.assertEquals(ips[1]['subnet_id'], subnet['subnet']['id']) def test_requested_duplicate_mac(self): @@ -1192,30 +1192,38 @@ class TestPortsV2(QuantumDbPluginV2TestCase): self._delete('ports', p['port']['id']) def test_recycling(self): + # set expirations to past so that recycling is checked + reference = datetime.datetime(2012, 8, 13, 23, 11, 0) + cfg.CONF.set_override('dhcp_lease_duration', 0) fmt = 'json' + with self.subnet(cidr='10.0.1.0/24') as subnet: with self.port(subnet=subnet) as port: - ips = port['port']['fixed_ips'] - self.assertEquals(len(ips), 1) - self.assertEquals(ips[0]['ip_address'], '10.0.1.2') - self.assertEquals(ips[0]['subnet_id'], subnet['subnet']['id']) - net_id = port['port']['network_id'] - ports = [] - for i in range(16 - 3): + with mock.patch.object(timeutils, 'utcnow') as mock_utcnow: + mock_utcnow.return_value = reference + ips = port['port']['fixed_ips'] + self.assertEquals(len(ips), 1) + self.assertEquals(ips[0]['ip_address'], '10.0.1.2') + self.assertEquals(ips[0]['subnet_id'], + subnet['subnet']['id']) + net_id = port['port']['network_id'] + ports = [] + for i in range(16 - 3): + res = self._create_port(fmt, net_id=net_id) + p = self.deserialize(fmt, res) + ports.append(p) + for i in range(16 - 3): + x = random.randrange(0, len(ports), 1) + p = ports.pop(x) + self._delete('ports', p['port']['id']) res = self._create_port(fmt, net_id=net_id) - p = self.deserialize(fmt, res) - ports.append(p) - for i in range(16 - 3): - x = random.randrange(0, len(ports), 1) - p = ports.pop(x) - self._delete('ports', p['port']['id']) - res = self._create_port(fmt, net_id=net_id) - port = self.deserialize(fmt, res) - ips = port['port']['fixed_ips'] - self.assertEquals(len(ips), 1) - self.assertEquals(ips[0]['ip_address'], '10.0.1.3') - self.assertEquals(ips[0]['subnet_id'], subnet['subnet']['id']) - self._delete('ports', port['port']['id']) + port = self.deserialize(fmt, res) + ips = port['port']['fixed_ips'] + self.assertEquals(len(ips), 1) + self.assertEquals(ips[0]['ip_address'], '10.0.1.3') + self.assertEquals(ips[0]['subnet_id'], + subnet['subnet']['id']) + self._delete('ports', port['port']['id']) def test_invalid_admin_state(self): with self.network() as network: @@ -1239,15 +1247,16 @@ class TestPortsV2(QuantumDbPluginV2TestCase): self.assertEquals(res.status_int, 422) def test_default_allocation_expiration(self): - reference = datetime.datetime(2012, 8, 13, 23, 11, 0) - timeutils.utcnow.override_time = reference - cfg.CONF.set_override('dhcp_lease_duration', 120) - expires = QuantumManager.get_plugin()._default_allocation_expiration() - timeutils.utcnow - cfg.CONF.reset() - timeutils.utcnow.override_time = None - self.assertEqual(expires, reference + datetime.timedelta(seconds=120)) + reference = datetime.datetime(2012, 8, 13, 23, 11, 0) + + with mock.patch.object(timeutils, 'utcnow') as mock_utcnow: + mock_utcnow.return_value = reference + + plugin = QuantumManager.get_plugin() + expires = plugin._default_allocation_expiration() + self.assertEqual(expires, + reference + datetime.timedelta(seconds=120)) def test_update_fixed_ip_lease_expiration(self): cfg.CONF.set_override('dhcp_lease_duration', 10) @@ -1272,7 +1281,22 @@ class TestPortsV2(QuantumDbPluginV2TestCase): ip_allocation.expiration - timeutils.utcnow(), datetime.timedelta(seconds=10)) - cfg.CONF.reset() + def test_port_delete_holds_ip(self): + plugin = QuantumManager.get_plugin() + base_class = db_base_plugin_v2.QuantumDbPluginV2 + with mock.patch.object(base_class, '_hold_ip') as hold_ip: + with self.subnet() as subnet: + with self.port(subnet=subnet, no_delete=True) as port: + req = self.new_delete_request('ports', port['port']['id']) + res = req.get_response(self.api) + self.assertEquals(res.status_int, 204) + + hold_ip.assert_called_once_with( + mock.ANY, + port['port']['network_id'], + port['port']['fixed_ips'][0]['subnet_id'], + port['port']['id'], + port['port']['fixed_ips'][0]['ip_address']) def test_update_fixed_ip_lease_expiration_invalid_address(self): cfg.CONF.set_override('dhcp_lease_duration', 10) @@ -1287,7 +1311,68 @@ class TestPortsV2(QuantumDbPluginV2TestCase): '255.255.255.0', 120) self.assertTrue(log.mock_calls) - cfg.CONF.reset() + + def test_hold_ip_address(self): + plugin = QuantumManager.get_plugin() + with self.subnet() as subnet: + with self.port(subnet=subnet) as port: + update_context = context.Context('', port['port']['tenant_id']) + port_id = port['port']['id'] + with mock.patch.object(db_base_plugin_v2, 'LOG') as log: + ip_address = port['port']['fixed_ips'][0]['ip_address'] + plugin._hold_ip( + update_context, + subnet['subnet']['network_id'], + subnet['subnet']['id'], + port_id, + ip_address) + self.assertTrue(log.mock_calls) + + q = update_context.session.query(models_v2.IPAllocation) + q = q.filter_by(port_id=None, ip_address=ip_address) + + self.assertEquals(len(q.all()), 1) + + def test_recycle_held_ip_address(self): + plugin = QuantumManager.get_plugin() + with self.subnet() as subnet: + with self.port(subnet=subnet) as port: + update_context = context.Context('', port['port']['tenant_id']) + port_id = port['port']['id'] + port_obj = plugin._get_port(update_context, port_id) + + for fixed_ip in port_obj.fixed_ips: + fixed_ip.active = False + fixed_ip.expiration = datetime.datetime.utcnow() + + with mock.patch.object(plugin, '_recycle_ip') as rc: + plugin._recycle_expired_ip_allocations( + update_context, subnet['subnet']['network_id']) + rc.assertEquals(len(rc.mock_calls), 1) + self.assertEquals(update_context._recycled_networks, + set([subnet['subnet']['network_id']])) + + def test_recycle_expired_previously_run_within_context(self): + plugin = QuantumManager.get_plugin() + with self.subnet() as subnet: + with self.port(subnet=subnet) as port: + update_context = context.Context('', port['port']['tenant_id']) + port_id = port['port']['id'] + port_obj = plugin._get_port(update_context, port_id) + + update_context._recycled_networks = set( + [subnet['subnet']['network_id']]) + + for fixed_ip in port_obj.fixed_ips: + fixed_ip.active = False + fixed_ip.expiration = datetime.datetime.utcnow() + + with mock.patch.object(plugin, '_recycle_ip') as rc: + plugin._recycle_expired_ip_allocations( + update_context, subnet['subnet']['network_id']) + rc.assertFalse(rc.called) + self.assertEquals(update_context._recycled_networks, + set([subnet['subnet']['network_id']])) class TestNetworksV2(QuantumDbPluginV2TestCase):