diff options
author | Johannes Erdfelt <johannes.erdfelt@rackspace.com> | 2012-11-01 15:53:27 +0000 |
---|---|---|
committer | Johannes Erdfelt <johannes.erdfelt@rackspace.com> | 2012-11-06 19:54:44 +0000 |
commit | b84b7daaf83b8280d4a80ca00c4d5e3783a162db (patch) | |
tree | 8038d26d28dae83f06757daa92573f104dfd262f | |
parent | 66a6fdd73f8fd095fa212f515407fd64fdbf805f (diff) | |
download | nova-b84b7daaf83b8280d4a80ca00c4d5e3783a162db.tar.gz nova-b84b7daaf83b8280d4a80ca00c4d5e3783a162db.tar.xz nova-b84b7daaf83b8280d4a80ca00c4d5e3783a162db.zip |
Fix quota updating during soft delete and restore
Fixes bug 1075716
Quotas were not properly updated when soft deletes were enabled. This
patch fixes quotas for soft deletes and restores.
Change-Id: I77fd3ff76caa9eba3e2180c1abcfb390ea7857d6
-rw-r--r-- | nova/api/openstack/compute/contrib/deferred_delete.py | 3 | ||||
-rw-r--r-- | nova/compute/api.py | 126 | ||||
-rw-r--r-- | nova/tests/compute/test_compute.py | 42 |
3 files changed, 104 insertions, 67 deletions
diff --git a/nova/api/openstack/compute/contrib/deferred_delete.py b/nova/api/openstack/compute/contrib/deferred_delete.py index 8eaea04bb..ea7ac00f9 100644 --- a/nova/api/openstack/compute/contrib/deferred_delete.py +++ b/nova/api/openstack/compute/contrib/deferred_delete.py @@ -42,6 +42,9 @@ class DeferredDeleteController(wsgi.Controller): instance = self.compute_api.get(context, id) try: self.compute_api.restore(context, instance) + except exception.QuotaError as error: + raise exc.HTTPRequestEntityTooLarge(explanation=unicode(error), + headers={'Retry-After': 0}) except exception.InstanceInvalidState as state_error: common.raise_http_conflict_for_instance_invalid_state(state_error, 'restore') diff --git a/nova/compute/api.py b/nova/compute/api.py index ba4f2b4ff..121de6a47 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -823,50 +823,29 @@ class API(base.Base): return dict(old_ref.iteritems()), dict(instance_ref.iteritems()) - @wrap_check_policy - @check_instance_lock - @check_instance_state(vm_state=None, task_state=None) - def soft_delete(self, context, instance): - """Terminate an instance.""" - LOG.debug(_('Going to try to soft delete instance'), - instance=instance) - + def _delete(self, context, instance, cb, **instance_attrs): if instance['disable_terminate']: + LOG.info(_('instance termination disabled'), + instance=instance) return - # NOTE(jerdfelt): The compute daemon handles reclaiming instances - # that are in soft delete. If there is no host assigned, there is - # no daemon to reclaim, so delete it immediately. - if instance['host']: - instance = self.update(context, instance, - task_state=task_states.SOFT_DELETING, - expected_task_state=None, - deleted_at=timeutils.utcnow()) - - self.compute_rpcapi.soft_delete_instance(context, instance) - else: - LOG.warning(_('No host for instance, deleting immediately'), - instance=instance) - try: - self.db.instance_destroy(context, instance['uuid']) - except exception.InstanceNotFound: - # NOTE(comstud): Race condition. Instance already gone. - pass - - def _delete(self, context, instance): host = instance['host'] + bdms = self.db.block_device_mapping_get_all_by_instance( + context, instance['uuid']) reservations = None try: - - #Note(maoy): no expected_task_state needs to be set + # NOTE(maoy): no expected_task_state needs to be set + attrs = {'progress': 0} + attrs.update(instance_attrs) old, updated = self._update(context, instance, - task_state=task_states.DELETING, - progress=0) + **attrs) # Avoid double-counting the quota usage reduction # where delete is already in progress - if old['task_state'] != task_states.DELETING: + if (old['vm_state'] != vm_states.SOFT_DELETED and + old['task_state'] not in (task_states.DELETING, + task_states.SOFT_DELETING)): reservations = QUOTAS.reserve(context, instances=-1, cores=-instance['vcpus'], @@ -876,12 +855,11 @@ class API(base.Base): # Just update database, nothing else we can do constraint = self.db.constraint(host=self.db.equal_any(host)) try: - result = self.db.instance_destroy(context, - instance['uuid'], - constraint) + self.db.instance_destroy(context, instance['uuid'], + constraint) if reservations: QUOTAS.commit(context, reservations) - return result + return except exception.ConstraintNotMet: # Refresh to get new host information instance = self.get(context, instance['uuid']) @@ -916,9 +894,7 @@ class API(base.Base): reservations=downsize_reservations) is_up = False - bdms = self.db.block_device_mapping_get_all_by_instance( - context, instance["uuid"]) - #Note(jogo): db allows for multiple compute services per host + # NOTE(jogo): db allows for multiple compute services per host try: services = self.db.service_get_all_compute_by_host( context.elevated(), instance['host']) @@ -927,8 +903,7 @@ class API(base.Base): for service in services: if utils.service_is_up(service): is_up = True - self.compute_rpcapi.terminate_instance(context, instance, - bdms) + cb(context, instance, bdms) break if not is_up: # If compute node isn't up, just delete from DB @@ -987,40 +962,69 @@ class API(base.Base): @wrap_check_policy @check_instance_lock @check_instance_state(vm_state=None, task_state=None) - def delete(self, context, instance): + def soft_delete(self, context, instance): """Terminate an instance.""" - LOG.debug(_("Going to try to terminate instance"), instance=instance) + LOG.debug(_('Going to try to soft delete instance'), + instance=instance) - if instance['disable_terminate']: - return + def soft_delete(context, instance, bdms): + self.compute_rpcapi.soft_delete_instance(context, instance) + + self._delete(context, instance, soft_delete, + task_state=task_states.SOFT_DELETING, + deleted_at=timeutils.utcnow()) - self._delete(context, instance) + def _delete_instance(self, context, instance): + def terminate(context, instance, bdms): + self.compute_rpcapi.terminate_instance(context, instance, bdms) + + self._delete(context, instance, terminate, + task_state=task_states.DELETING) + + @wrap_check_policy + @check_instance_lock + @check_instance_state(vm_state=None, task_state=None) + def delete(self, context, instance): + """Terminate an instance.""" + LOG.debug(_("Going to try to terminate instance"), instance=instance) + self._delete_instance(context, instance) @wrap_check_policy @check_instance_lock @check_instance_state(vm_state=[vm_states.SOFT_DELETED]) def restore(self, context, instance): """Restore a previously deleted (but not reclaimed) instance.""" - if instance['host']: - instance = self.update(context, instance, - task_state=task_states.RESTORING, - expected_task_state=None, - deleted_at=None) - self.compute_rpcapi.restore_instance(context, instance) - else: - self.update(context, - instance, - vm_state=vm_states.ACTIVE, - task_state=None, - expected_task_state=None, - deleted_at=None) + # Reserve quotas + instance_type = instance['instance_type'] + num_instances, quota_reservations = self._check_num_instances_quota( + context, instance_type, 1, 1) + + try: + if instance['host']: + instance = self.update(context, instance, + task_state=task_states.RESTORING, + expected_task_state=None, + deleted_at=None) + self.compute_rpcapi.restore_instance(context, instance) + else: + self.update(context, + instance, + vm_state=vm_states.ACTIVE, + task_state=None, + expected_task_state=None, + deleted_at=None) + + QUOTAS.commit(context, quota_reservations) + except Exception: + with excutils.save_and_reraise_exception(): + QUOTAS.rollback(context, quota_reservations) @wrap_check_policy @check_instance_lock @check_instance_state(vm_state=[vm_states.SOFT_DELETED]) def force_delete(self, context, instance): """Force delete a previously deleted (but not reclaimed) instance.""" - self._delete(context, instance) + self._delete_instance(context, instance) @wrap_check_policy @check_instance_lock diff --git a/nova/tests/compute/test_compute.py b/nova/tests/compute/test_compute.py index be3eeb601..00f2ceb7a 100644 --- a/nova/tests/compute/test_compute.py +++ b/nova/tests/compute/test_compute.py @@ -3229,7 +3229,12 @@ class ComputeAPITestCase(BaseTestCase): db.instance_destroy(self.context, instance['uuid']) def test_delete_soft(self): - instance, instance_uuid = self._run_instance() + instance, instance_uuid = self._run_instance(params={ + 'host': FLAGS.host}) + + self.mox.StubOutWithMock(nova.quota.QUOTAS, 'commit') + nova.quota.QUOTAS.commit(mox.IgnoreArg(), mox.IgnoreArg()) + self.mox.ReplayAll() self.compute_api.soft_delete(self.context, instance) @@ -3239,10 +3244,11 @@ class ComputeAPITestCase(BaseTestCase): db.instance_destroy(self.context, instance['uuid']) def test_delete_soft_fail(self): - instance, instance_uuid = self._run_instance() - + instance, instance_uuid = self._run_instance(params={ + 'host': FLAGS.host}) instance = db.instance_update(self.context, instance_uuid, {'disable_terminate': True}) + self.compute_api.soft_delete(self.context, instance) instance = db.instance_get_by_uuid(self.context, instance_uuid) @@ -3250,6 +3256,27 @@ class ComputeAPITestCase(BaseTestCase): db.instance_destroy(self.context, instance['uuid']) + def test_delete_soft_rollback(self): + instance, instance_uuid = self._run_instance(params={ + 'host': FLAGS.host}) + + self.mox.StubOutWithMock(nova.quota.QUOTAS, 'rollback') + nova.quota.QUOTAS.rollback(mox.IgnoreArg(), mox.IgnoreArg()) + self.mox.ReplayAll() + + def fail(*args, **kwargs): + raise test.TestingException() + self.stubs.Set(self.compute_api.compute_rpcapi, 'soft_delete_instance', + fail) + + self.assertRaises(test.TestingException, self.compute_api.soft_delete, + self.context, instance) + + instance = db.instance_get_by_uuid(self.context, instance_uuid) + self.assertEqual(instance['task_state'], task_states.SOFT_DELETING) + + db.instance_destroy(self.context, instance['uuid']) + def test_force_delete(self): """Ensure instance can be deleted after a soft delete""" instance = jsonutils.to_primitive(self._create_fake_instance(params={ @@ -3344,9 +3371,8 @@ class ComputeAPITestCase(BaseTestCase): def test_restore(self): """Ensure instance can be restored from a soft delete""" - instance = jsonutils.to_primitive(self._create_fake_instance()) - instance_uuid = instance['uuid'] - self.compute.run_instance(self.context, instance=instance) + instance, instance_uuid = self._run_instance(params={ + 'host': FLAGS.host}) instance = db.instance_get_by_uuid(self.context, instance_uuid) self.compute_api.soft_delete(self.context, instance) @@ -3359,6 +3385,10 @@ class ComputeAPITestCase(BaseTestCase): {'vm_state': vm_states.SOFT_DELETED, 'task_state': None}) + self.mox.StubOutWithMock(nova.quota.QUOTAS, 'commit') + nova.quota.QUOTAS.commit(mox.IgnoreArg(), mox.IgnoreArg()) + self.mox.ReplayAll() + self.compute_api.restore(self.context, instance) instance = db.instance_get_by_uuid(self.context, instance_uuid) |