backups: implement async backup/restore cancel method (#793)
The backup_cancel() method kills processes registered by main thread and set "running_backup_operation.canceled" to True. Then main thread get an error because of killed processes and check if that was because of cancel request. Introduce BackupCanceledError, which can report temporary dir to remove.
This commit is contained in:
parent
b52d1a4379
commit
f4194c9d08
101
core/backup.py
101
core/backup.py
@ -47,6 +47,20 @@ MAX_STDERR_BYTES = 1024
|
|||||||
# header + qubes.xml max size
|
# header + qubes.xml max size
|
||||||
HEADER_QUBES_XML_MAX_SIZE = 1024 * 1024
|
HEADER_QUBES_XML_MAX_SIZE = 1024 * 1024
|
||||||
|
|
||||||
|
# global state for backup_cancel()
|
||||||
|
running_backup_operation = None
|
||||||
|
|
||||||
|
class BackupOperationInfo:
|
||||||
|
def __init__(self):
|
||||||
|
self.canceled = False
|
||||||
|
self.processes_to_kill_on_cancel = []
|
||||||
|
self.tmpdir_to_remove = None
|
||||||
|
|
||||||
|
class BackupCanceledError(QubesException):
|
||||||
|
def __init__(self, msg, tmpdir=None):
|
||||||
|
super(BackupCanceledError, self).__init__(msg)
|
||||||
|
self.tmpdir = tmpdir
|
||||||
|
|
||||||
class BackupHeader:
|
class BackupHeader:
|
||||||
encrypted = 'encrypted'
|
encrypted = 'encrypted'
|
||||||
compressed = 'compressed'
|
compressed = 'compressed'
|
||||||
@ -81,6 +95,23 @@ def file_to_backup (file_path, subdir = None):
|
|||||||
subdir += '/'
|
subdir += '/'
|
||||||
return [ { "path" : file_path, "size": sz, "subdir": subdir} ]
|
return [ { "path" : file_path, "size": sz, "subdir": subdir} ]
|
||||||
|
|
||||||
|
def backup_cancel():
|
||||||
|
"""
|
||||||
|
Cancel currently running backup/restore operation
|
||||||
|
|
||||||
|
@return: True if any operation was signaled
|
||||||
|
"""
|
||||||
|
if running_backup_operation is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
running_backup_operation.canceled = True
|
||||||
|
for proc in running_backup_operation.processes_to_kill_on_cancel:
|
||||||
|
try:
|
||||||
|
proc.terminate()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
|
||||||
def backup_prepare(vms_list = None, exclude_list = None,
|
def backup_prepare(vms_list = None, exclude_list = None,
|
||||||
print_callback = print_stdout, hide_vm_names=True):
|
print_callback = print_stdout, hide_vm_names=True):
|
||||||
"""If vms = None, include all (sensible) VMs; exclude_list is always applied"""
|
"""If vms = None, include all (sensible) VMs; exclude_list is always applied"""
|
||||||
@ -333,7 +364,7 @@ class SendWorker(Process):
|
|||||||
os.chdir(self.base_dir)
|
os.chdir(self.base_dir)
|
||||||
|
|
||||||
for filename in iter(self.queue.get,None):
|
for filename in iter(self.queue.get,None):
|
||||||
if filename == "FINISHED":
|
if filename == "FINISHED" or filename == "ERROR":
|
||||||
break
|
break
|
||||||
|
|
||||||
if BACKUP_DEBUG:
|
if BACKUP_DEBUG:
|
||||||
@ -381,6 +412,8 @@ def backup_do(base_backup_dir, files_to_backup, passphrase,
|
|||||||
progress_callback = None, encrypted=False, appvm=None,
|
progress_callback = None, encrypted=False, appvm=None,
|
||||||
compressed=False, hmac_algorithm=DEFAULT_HMAC_ALGORITHM,
|
compressed=False, hmac_algorithm=DEFAULT_HMAC_ALGORITHM,
|
||||||
crypto_algorithm=DEFAULT_CRYPTO_ALGORITHM):
|
crypto_algorithm=DEFAULT_CRYPTO_ALGORITHM):
|
||||||
|
global running_backup_operation
|
||||||
|
|
||||||
total_backup_sz = 0
|
total_backup_sz = 0
|
||||||
passphrase = passphrase.encode('utf-8')
|
passphrase = passphrase.encode('utf-8')
|
||||||
for file in files_to_backup:
|
for file in files_to_backup:
|
||||||
@ -390,6 +423,7 @@ def backup_do(base_backup_dir, files_to_backup, passphrase,
|
|||||||
raise QubesException("Compressed and encrypted backups are not "
|
raise QubesException("Compressed and encrypted backups are not "
|
||||||
"supported (yet).")
|
"supported (yet).")
|
||||||
|
|
||||||
|
running_backup_operation = BackupOperationInfo()
|
||||||
vmproc = None
|
vmproc = None
|
||||||
if appvm != None:
|
if appvm != None:
|
||||||
# Prepare the backup target (Qubes service call)
|
# Prepare the backup target (Qubes service call)
|
||||||
@ -401,6 +435,7 @@ def backup_do(base_backup_dir, files_to_backup, passphrase,
|
|||||||
vmproc.stdin.write(base_backup_dir.
|
vmproc.stdin.write(base_backup_dir.
|
||||||
replace("\r", "").replace("\n", "")+"\n")
|
replace("\r", "").replace("\n", "")+"\n")
|
||||||
backup_stdout = vmproc.stdin
|
backup_stdout = vmproc.stdin
|
||||||
|
running_backup_operation.processes_to_kill_on_cancel.append(vmproc)
|
||||||
else:
|
else:
|
||||||
# Prepare the backup target (local file)
|
# Prepare the backup target (local file)
|
||||||
backup_target = base_backup_dir + "/qubes-{0}". \
|
backup_target = base_backup_dir + "/qubes-{0}". \
|
||||||
@ -421,6 +456,7 @@ def backup_do(base_backup_dir, files_to_backup, passphrase,
|
|||||||
progress_callback(progress)
|
progress_callback(progress)
|
||||||
|
|
||||||
backup_tmpdir = tempfile.mkdtemp(prefix="/var/tmp/backup_")
|
backup_tmpdir = tempfile.mkdtemp(prefix="/var/tmp/backup_")
|
||||||
|
running_backup_operation.tmpdir_to_remove = backup_tmpdir
|
||||||
|
|
||||||
# Tar with tapelength does not deals well with stdout (close stdout between
|
# Tar with tapelength does not deals well with stdout (close stdout between
|
||||||
# two tapes)
|
# two tapes)
|
||||||
@ -489,6 +525,7 @@ def backup_do(base_backup_dir, files_to_backup, passphrase,
|
|||||||
# Pipe: tar-sparse [| hmac] | tar | backup_target
|
# Pipe: tar-sparse [| hmac] | tar | backup_target
|
||||||
tar_sparse = subprocess.Popen (tar_cmdline, stdin=subprocess.PIPE,
|
tar_sparse = subprocess.Popen (tar_cmdline, stdin=subprocess.PIPE,
|
||||||
stderr=(open(os.devnull, 'w') if not BACKUP_DEBUG else None))
|
stderr=(open(os.devnull, 'w') if not BACKUP_DEBUG else None))
|
||||||
|
running_backup_operation.processes_to_kill_on_cancel.append(tar_sparse)
|
||||||
|
|
||||||
# Wait for compressor (tar) process to finish or for any error of other
|
# Wait for compressor (tar) process to finish or for any error of other
|
||||||
# subprocesses
|
# subprocesses
|
||||||
@ -542,6 +579,22 @@ def backup_do(base_backup_dir, files_to_backup, passphrase,
|
|||||||
if BACKUP_DEBUG:
|
if BACKUP_DEBUG:
|
||||||
print "Wait_backup_feedback returned:", run_error
|
print "Wait_backup_feedback returned:", run_error
|
||||||
|
|
||||||
|
if running_backup_operation.canceled:
|
||||||
|
try:
|
||||||
|
tar_sparse.terminate()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
hmac.terminate()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
tar_sparse.wait()
|
||||||
|
hmac.wait()
|
||||||
|
to_send.put("ERROR")
|
||||||
|
send_proc.join()
|
||||||
|
shutil.rmtree(backup_tmpdir)
|
||||||
|
running_backup_operation = None
|
||||||
|
raise BackupCanceledError("Backup canceled")
|
||||||
if len(run_error) > 0:
|
if len(run_error) > 0:
|
||||||
send_proc.terminate()
|
send_proc.terminate()
|
||||||
if run_error == "VM" and vmproc:
|
if run_error == "VM" and vmproc:
|
||||||
@ -583,11 +636,20 @@ def backup_do(base_backup_dir, files_to_backup, passphrase,
|
|||||||
tar_sparse.stdin.flush()
|
tar_sparse.stdin.flush()
|
||||||
run_error="paused"
|
run_error="paused"
|
||||||
else:
|
else:
|
||||||
|
running_backup_operation.processes_to_kill_on_cancel.remove(
|
||||||
|
tar_sparse)
|
||||||
if BACKUP_DEBUG:
|
if BACKUP_DEBUG:
|
||||||
print "Finished tar sparse with error", tar_sparse.poll()
|
print "Finished tar sparse with error", tar_sparse.poll()
|
||||||
|
|
||||||
to_send.put("FINISHED")
|
to_send.put("FINISHED")
|
||||||
send_proc.join()
|
send_proc.join()
|
||||||
|
shutil.rmtree(backup_tmpdir)
|
||||||
|
|
||||||
|
if running_backup_operation.canceled:
|
||||||
|
running_backup_operation = None
|
||||||
|
raise BackupCanceledError("Backup canceled")
|
||||||
|
|
||||||
|
running_backup_operation = None
|
||||||
|
|
||||||
if send_proc.exitcode != 0:
|
if send_proc.exitcode != 0:
|
||||||
raise QubesException("Failed to send backup: error in the sending process")
|
raise QubesException("Failed to send backup: error in the sending process")
|
||||||
@ -598,7 +660,6 @@ def backup_do(base_backup_dir, files_to_backup, passphrase,
|
|||||||
print "Sparse1 proc return code:", tar_sparse.poll()
|
print "Sparse1 proc return code:", tar_sparse.poll()
|
||||||
vmproc.stdin.close()
|
vmproc.stdin.close()
|
||||||
|
|
||||||
shutil.rmtree(backup_tmpdir)
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
' Wait for backup chunk to finish
|
' Wait for backup chunk to finish
|
||||||
@ -920,6 +981,8 @@ def restore_vm_dirs (backup_source, restore_tmpdir, passphrase, vms_dirs, vms,
|
|||||||
compressed = False, hmac_algorithm=DEFAULT_HMAC_ALGORITHM,
|
compressed = False, hmac_algorithm=DEFAULT_HMAC_ALGORITHM,
|
||||||
crypto_algorithm=DEFAULT_CRYPTO_ALGORITHM):
|
crypto_algorithm=DEFAULT_CRYPTO_ALGORITHM):
|
||||||
|
|
||||||
|
global running_backup_operation
|
||||||
|
|
||||||
if BACKUP_DEBUG:
|
if BACKUP_DEBUG:
|
||||||
print_callback("Working in temporary dir:"+restore_tmpdir)
|
print_callback("Working in temporary dir:"+restore_tmpdir)
|
||||||
print_callback("Extracting data: " + size_to_human(vms_size)+" to restore")
|
print_callback("Extracting data: " + size_to_human(vms_size)+" to restore")
|
||||||
@ -937,6 +1000,8 @@ def restore_vm_dirs (backup_source, restore_tmpdir, passphrase, vms_dirs, vms,
|
|||||||
|
|
||||||
# Send to tar2qfile the VMs that should be extracted
|
# Send to tar2qfile the VMs that should be extracted
|
||||||
vmproc.stdin.write(" ".join(vms_dirs)+"\n")
|
vmproc.stdin.write(" ".join(vms_dirs)+"\n")
|
||||||
|
if running_backup_operation:
|
||||||
|
running_backup_operation.processes_to_kill_on_cancel.append(vmproc)
|
||||||
|
|
||||||
backup_stdin = vmproc.stdout
|
backup_stdin = vmproc.stdout
|
||||||
tar1_command = ['/usr/libexec/qubes/qfile-dom0-unpacker',
|
tar1_command = ['/usr/libexec/qubes/qfile-dom0-unpacker',
|
||||||
@ -970,6 +1035,8 @@ def restore_vm_dirs (backup_source, restore_tmpdir, passphrase, vms_dirs, vms,
|
|||||||
stdout=vmproc.stdin if vmproc else subprocess.PIPE,
|
stdout=vmproc.stdin if vmproc else subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
env=tar1_env)
|
env=tar1_env)
|
||||||
|
if running_backup_operation:
|
||||||
|
running_backup_operation.processes_to_kill_on_cancel.append(command)
|
||||||
|
|
||||||
# qfile-dom0-unpacker output filelist on stderr (and have stdout connected
|
# qfile-dom0-unpacker output filelist on stderr (and have stdout connected
|
||||||
# to the VM), while tar output filelist on stdout
|
# to the VM), while tar output filelist on stdout
|
||||||
@ -1059,6 +1126,8 @@ def restore_vm_dirs (backup_source, restore_tmpdir, passphrase, vms_dirs, vms,
|
|||||||
try:
|
try:
|
||||||
filename = None
|
filename = None
|
||||||
while True:
|
while True:
|
||||||
|
if running_backup_operation and running_backup_operation.canceled:
|
||||||
|
break
|
||||||
if nextfile is not None:
|
if nextfile is not None:
|
||||||
filename = nextfile
|
filename = nextfile
|
||||||
else:
|
else:
|
||||||
@ -1071,6 +1140,9 @@ def restore_vm_dirs (backup_source, restore_tmpdir, passphrase, vms_dirs, vms,
|
|||||||
break
|
break
|
||||||
|
|
||||||
hmacfile = filelist_pipe.readline().strip()
|
hmacfile = filelist_pipe.readline().strip()
|
||||||
|
|
||||||
|
if running_backup_operation and running_backup_operation.canceled:
|
||||||
|
break
|
||||||
# if reading archive directly with tar, wait for next filename -
|
# if reading archive directly with tar, wait for next filename -
|
||||||
# tar prints filename before processing it, so wait for the next one to be
|
# tar prints filename before processing it, so wait for the next one to be
|
||||||
# sure that whole file was extracted
|
# sure that whole file was extracted
|
||||||
@ -1095,6 +1167,10 @@ def restore_vm_dirs (backup_source, restore_tmpdir, passphrase, vms_dirs, vms,
|
|||||||
passphrase, hmac_algorithm):
|
passphrase, hmac_algorithm):
|
||||||
to_extract.put(os.path.join(restore_tmpdir, filename))
|
to_extract.put(os.path.join(restore_tmpdir, filename))
|
||||||
|
|
||||||
|
if running_backup_operation and running_backup_operation.canceled:
|
||||||
|
raise BackupCanceledError("Restore canceled",
|
||||||
|
tmpdir=restore_tmpdir)
|
||||||
|
|
||||||
if command.wait() != 0 and not expect_tar_error:
|
if command.wait() != 0 and not expect_tar_error:
|
||||||
raise QubesException(
|
raise QubesException(
|
||||||
"ERROR: unable to read the qubes backup file {0} ({1}). " \
|
"ERROR: unable to read the qubes backup file {0} ({1}). " \
|
||||||
@ -1111,6 +1187,7 @@ def restore_vm_dirs (backup_source, restore_tmpdir, passphrase, vms_dirs, vms,
|
|||||||
except:
|
except:
|
||||||
to_extract.put("ERROR")
|
to_extract.put("ERROR")
|
||||||
extract_proc.join()
|
extract_proc.join()
|
||||||
|
running_backup_operation = None
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
to_extract.put("FINISHED")
|
to_extract.put("FINISHED")
|
||||||
@ -1163,7 +1240,9 @@ def backup_restore_header(source, passphrase,
|
|||||||
hmac_algorithm = DEFAULT_HMAC_ALGORITHM,
|
hmac_algorithm = DEFAULT_HMAC_ALGORITHM,
|
||||||
crypto_algorithm = DEFAULT_CRYPTO_ALGORITHM):
|
crypto_algorithm = DEFAULT_CRYPTO_ALGORITHM):
|
||||||
|
|
||||||
|
global running_backup_operation
|
||||||
vmproc = None
|
vmproc = None
|
||||||
|
running_backup_operation = None
|
||||||
|
|
||||||
restore_tmpdir = tempfile.mkdtemp(prefix="/var/tmp/restore_")
|
restore_tmpdir = tempfile.mkdtemp(prefix="/var/tmp/restore_")
|
||||||
|
|
||||||
@ -1531,6 +1610,8 @@ def backup_restore_do(restore_info,
|
|||||||
error_callback = print_stderr, progress_callback = None,
|
error_callback = print_stderr, progress_callback = None,
|
||||||
):
|
):
|
||||||
|
|
||||||
|
global running_backup_operation
|
||||||
|
|
||||||
### Private functions begin
|
### Private functions begin
|
||||||
def restore_vm_dir_v1 (backup_dir, src_dir, dst_dir):
|
def restore_vm_dir_v1 (backup_dir, src_dir, dst_dir):
|
||||||
|
|
||||||
@ -1580,6 +1661,8 @@ def backup_restore_do(restore_info,
|
|||||||
vms_dirs.append(vm.backup_path)
|
vms_dirs.append(vm.backup_path)
|
||||||
vms[vm.name] = vm
|
vms[vm.name] = vm
|
||||||
|
|
||||||
|
running_backup_operation = BackupOperationInfo()
|
||||||
|
|
||||||
if format_version == 2:
|
if format_version == 2:
|
||||||
if 'dom0' in restore_info.keys() and restore_info['dom0']['good-to-go']:
|
if 'dom0' in restore_info.keys() and restore_info['dom0']['good-to-go']:
|
||||||
vms_dirs.append('dom0-home')
|
vms_dirs.append('dom0-home')
|
||||||
@ -1603,7 +1686,13 @@ def backup_restore_do(restore_info,
|
|||||||
# Add VM in right order
|
# Add VM in right order
|
||||||
for (vm_class_name, vm_class) in sorted(QubesVmClasses.items(),
|
for (vm_class_name, vm_class) in sorted(QubesVmClasses.items(),
|
||||||
key=lambda _x: _x[1].load_order):
|
key=lambda _x: _x[1].load_order):
|
||||||
|
if running_backup_operation.canceled:
|
||||||
|
break
|
||||||
for vm in vms.values():
|
for vm in vms.values():
|
||||||
|
if running_backup_operation.canceled:
|
||||||
|
# only break the loop to save qubes.xml with already restored
|
||||||
|
# VMs
|
||||||
|
break
|
||||||
if not vm.__class__ == vm_class:
|
if not vm.__class__ == vm_class:
|
||||||
continue
|
continue
|
||||||
print_callback("-> Restoring {type} {0}...".format(vm.name, type=vm_class_name))
|
print_callback("-> Restoring {type} {0}...".format(vm.name, type=vm_class_name))
|
||||||
@ -1674,6 +1763,14 @@ def backup_restore_do(restore_info,
|
|||||||
if lock_obtained:
|
if lock_obtained:
|
||||||
host_collection.unlock_db()
|
host_collection.unlock_db()
|
||||||
|
|
||||||
|
if running_backup_operation.canceled:
|
||||||
|
if format_version == 2:
|
||||||
|
raise BackupCanceledError("Restore canceled",
|
||||||
|
tmpdir=restore_tmpdir)
|
||||||
|
else:
|
||||||
|
raise BackupCanceledError("Restore canceled")
|
||||||
|
|
||||||
|
|
||||||
# ... and dom0 home as last step
|
# ... and dom0 home as last step
|
||||||
if 'dom0' in restore_info.keys() and restore_info['dom0']['good-to-go']:
|
if 'dom0' in restore_info.keys() and restore_info['dom0']['good-to-go']:
|
||||||
backup_path = restore_info['dom0']['subdir']
|
backup_path = restore_info['dom0']['subdir']
|
||||||
|
Loading…
Reference in New Issue
Block a user