Skip to content

Commit

Permalink
Fix racey snapshots.
Browse files Browse the repository at this point in the history
Fixes bug 949475

Atomically tests and sets the instance task_state before allowing a
snapshot or backup to be initiated.

Change-Id: I40671a80f5e75337e176a715837f62d400cc21b6
  • Loading branch information
rconradharris committed Mar 8, 2012
1 parent 70f0ea5 commit 08b4e6c
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 14 deletions.
17 changes: 12 additions & 5 deletions nova/compute/api.py
Expand Up @@ -1083,8 +1083,7 @@ def _get_instances_by_filters(self, context, filters):
return self.db.instance_get_all_by_filters(context, filters)

@wrap_check_policy
@check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.SHUTOFF],
task_state=[None, task_states.RESIZE_VERIFY])
@check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.SHUTOFF])
def backup(self, context, instance, name, backup_type, rotation,
extra_properties=None):
"""Backup the given instance
Expand All @@ -1102,8 +1101,7 @@ def backup(self, context, instance, name, backup_type, rotation,
return recv_meta

@wrap_check_policy
@check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.SHUTOFF],
task_state=[None, task_states.RESIZE_VERIFY])
@check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.SHUTOFF])
def snapshot(self, context, instance, name, extra_properties=None):
"""Snapshot the given instance.
Expand All @@ -1130,9 +1128,18 @@ def _create_image(self, context, instance, name, image_type,
:param extra_properties: dict of extra image properties to include
"""
task_state = instance["task_state"]
instance_uuid = instance['uuid']

if image_type == "snapshot":
task_state = task_states.IMAGE_SNAPSHOT
elif image_type == "backup":
task_state = task_states.IMAGE_BACKUP
else:
raise Exception(_('Image type not recognized %s') % image_type)

self.db.instance_test_and_set(
context, instance_uuid, 'task_state', [None], task_state)

properties = {
'instance_uuid': instance_uuid,
'user_id': str(context.user_id),
Expand Down
10 changes: 1 addition & 9 deletions nova/compute/manager.py
Expand Up @@ -912,22 +912,14 @@ def snapshot_instance(self, context, instance_uuid, image_id,
:param rotation: int representing how many backups to keep around;
None if rotation shouldn't be used (as in the case of snapshots)
"""
if image_type == "snapshot":
task_state = task_states.IMAGE_SNAPSHOT
elif image_type == "backup":
task_state = task_states.IMAGE_BACKUP
else:
raise Exception(_('Image type not recognized %s') % image_type)

context = context.elevated()
instance_ref = self.db.instance_get_by_uuid(context, instance_uuid)

current_power_state = self._get_power_state(context, instance_ref)
self._instance_update(context,
instance_ref['id'],
power_state=current_power_state,
vm_state=vm_states.ACTIVE,
task_state=task_state)
vm_state=vm_states.ACTIVE)

LOG.audit(_('instance %s: snapshotting'), instance_uuid,
context=context)
Expand Down
9 changes: 9 additions & 0 deletions nova/db/api.py
Expand Up @@ -647,6 +647,15 @@ def instance_get_all_hung_in_rebooting(context, reboot_window):
return IMPL.instance_get_all_hung_in_rebooting(context, reboot_window)


def instance_test_and_set(context, instance_id, attr, ok_states,
new_state):
"""Atomically check if an instance is in a valid state, and if it is, set
the instance into a new state.
"""
return IMPL.instance_test_and_set(
context, instance_id, attr, ok_states, new_state)


def instance_update(context, instance_id, values):
"""Set the given properties on an instance and update it.
Expand Down
34 changes: 34 additions & 0 deletions nova/db/sqlalchemy/api.py
Expand Up @@ -1690,6 +1690,40 @@ def instance_get_all_hung_in_rebooting(context, reboot_window, session=None):
return results


@require_context
def instance_test_and_set(context, instance_id, attr, ok_states,
new_state, session=None):
"""Atomically check if an instance is in a valid state, and if it is, set
the instance into a new state.
"""
if not session:
session = get_session()

with session.begin():
query = model_query(context, models.Instance, session=session,
project_only=True)

if utils.is_uuid_like(instance_id):
query = query.filter_by(uuid=instance_id)
else:
query = query.filter_by(id=instance_id)

# NOTE(vish): if with_lockmode isn't supported, as in sqlite,
# then this has concurrency issues
instance = query.with_lockmode('update').first()

state = instance[attr]
if state not in ok_states:
raise exception.InstanceInvalidState(
attr=attr,
instance_uuid=instance['uuid'],
state=state,
method='instance_test_and_set')

instance[attr] = new_state
instance.save(session=session)


@require_context
def instance_update(context, instance_id, values):
session = get_session()
Expand Down

0 comments on commit 08b4e6c

Please sign in to comment.