Merge remote-tracking branch 'marmarek/core3-backup2' into core3-devel
This commit is contained in:
commit
fd953f4f27
592
qubes/backup.py
592
qubes/backup.py
@ -24,6 +24,8 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
|
import termios
|
||||||
|
|
||||||
from qubes.utils import size_to_human
|
from qubes.utils import size_to_human
|
||||||
import sys
|
import sys
|
||||||
import stat
|
import stat
|
||||||
@ -50,7 +52,9 @@ QUEUE_FINISHED = "FINISHED"
|
|||||||
|
|
||||||
HEADER_FILENAME = 'backup-header'
|
HEADER_FILENAME = 'backup-header'
|
||||||
DEFAULT_CRYPTO_ALGORITHM = 'aes-256-cbc'
|
DEFAULT_CRYPTO_ALGORITHM = 'aes-256-cbc'
|
||||||
DEFAULT_HMAC_ALGORITHM = 'SHA512'
|
# 'scrypt' is not exactly HMAC algorithm, but a tool we use to
|
||||||
|
# integrity-protect the data
|
||||||
|
DEFAULT_HMAC_ALGORITHM = 'scrypt'
|
||||||
DEFAULT_COMPRESSION_FILTER = 'gzip'
|
DEFAULT_COMPRESSION_FILTER = 'gzip'
|
||||||
CURRENT_BACKUP_FORMAT_VERSION = '4'
|
CURRENT_BACKUP_FORMAT_VERSION = '4'
|
||||||
# Maximum size of error message get from process stderr (including VM process)
|
# Maximum size of error message get from process stderr (including VM process)
|
||||||
@ -76,6 +80,7 @@ class BackupHeader(object):
|
|||||||
'compression-filter': 'compression_filter',
|
'compression-filter': 'compression_filter',
|
||||||
'crypto-algorithm': 'crypto_algorithm',
|
'crypto-algorithm': 'crypto_algorithm',
|
||||||
'hmac-algorithm': 'hmac_algorithm',
|
'hmac-algorithm': 'hmac_algorithm',
|
||||||
|
'backup-id': 'backup_id'
|
||||||
}
|
}
|
||||||
bool_options = ['encrypted', 'compressed']
|
bool_options = ['encrypted', 'compressed']
|
||||||
int_options = ['version']
|
int_options = ['version']
|
||||||
@ -87,7 +92,8 @@ class BackupHeader(object):
|
|||||||
compressed=None,
|
compressed=None,
|
||||||
compression_filter=None,
|
compression_filter=None,
|
||||||
hmac_algorithm=None,
|
hmac_algorithm=None,
|
||||||
crypto_algorithm=None):
|
crypto_algorithm=None,
|
||||||
|
backup_id=None):
|
||||||
# repeat the list to help code completion...
|
# repeat the list to help code completion...
|
||||||
self.version = version
|
self.version = version
|
||||||
self.encrypted = encrypted
|
self.encrypted = encrypted
|
||||||
@ -97,6 +103,7 @@ class BackupHeader(object):
|
|||||||
self.compression_filter = compression_filter
|
self.compression_filter = compression_filter
|
||||||
self.hmac_algorithm = hmac_algorithm
|
self.hmac_algorithm = hmac_algorithm
|
||||||
self.crypto_algorithm = crypto_algorithm
|
self.crypto_algorithm = crypto_algorithm
|
||||||
|
self.backup_id = backup_id
|
||||||
|
|
||||||
if header_data is not None:
|
if header_data is not None:
|
||||||
self.load(header_data)
|
self.load(header_data)
|
||||||
@ -148,6 +155,8 @@ class BackupHeader(object):
|
|||||||
expected_attrs += ['crypto_algorithm']
|
expected_attrs += ['crypto_algorithm']
|
||||||
if self.version >= 3 and self.compressed:
|
if self.version >= 3 and self.compressed:
|
||||||
expected_attrs += ['compression_filter']
|
expected_attrs += ['compression_filter']
|
||||||
|
if self.version >= 4:
|
||||||
|
expected_attrs += ['backup_id']
|
||||||
for key in expected_attrs:
|
for key in expected_attrs:
|
||||||
if getattr(self, key) is None:
|
if getattr(self, key) is None:
|
||||||
raise qubes.exc.QubesException(
|
raise qubes.exc.QubesException(
|
||||||
@ -213,6 +222,63 @@ class SendWorker(Process):
|
|||||||
self.log.debug("Finished sending thread")
|
self.log.debug("Finished sending thread")
|
||||||
|
|
||||||
|
|
||||||
|
def launch_proc_with_pty(args, stdin=None, stdout=None, stderr=None, echo=True):
|
||||||
|
"""Similar to pty.fork, but handle stdin/stdout according to parameters
|
||||||
|
instead of connecting to the pty
|
||||||
|
|
||||||
|
:return tuple (subprocess.Popen, pty_master)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def set_ctty(ctty_fd, master_fd):
|
||||||
|
os.setsid()
|
||||||
|
os.close(master_fd)
|
||||||
|
fcntl.ioctl(ctty_fd, termios.TIOCSCTTY, 0)
|
||||||
|
if not echo:
|
||||||
|
termios_p = termios.tcgetattr(ctty_fd)
|
||||||
|
# termios_p.c_lflags
|
||||||
|
termios_p[3] &= ~termios.ECHO
|
||||||
|
termios.tcsetattr(ctty_fd, termios.TCSANOW, termios_p)
|
||||||
|
(pty_master, pty_slave) = os.openpty()
|
||||||
|
p = subprocess.Popen(args, stdin=stdin, stdout=stdout, stderr=stderr,
|
||||||
|
preexec_fn=lambda: set_ctty(pty_slave, pty_master))
|
||||||
|
os.close(pty_slave)
|
||||||
|
return p, os.fdopen(pty_master, 'w+')
|
||||||
|
|
||||||
|
|
||||||
|
def launch_scrypt(action, input_name, output_name, passphrase):
|
||||||
|
'''
|
||||||
|
Launch 'scrypt' process, pass passphrase to it and return
|
||||||
|
subprocess.Popen object.
|
||||||
|
|
||||||
|
:param action: 'enc' or 'dec'
|
||||||
|
:param input_name: input path or '-' for stdin
|
||||||
|
:param output_name: output path or '-' for stdout
|
||||||
|
:param passphrase: passphrase
|
||||||
|
:return: subprocess.Popen object
|
||||||
|
'''
|
||||||
|
command_line = ['scrypt', action, input_name, output_name]
|
||||||
|
(p, pty) = launch_proc_with_pty(command_line,
|
||||||
|
stdin=subprocess.PIPE if input_name == '-' else None,
|
||||||
|
stdout=subprocess.PIPE if output_name == '-' else None,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
echo=False)
|
||||||
|
if action == 'enc':
|
||||||
|
prompts = ('Please enter passphrase: ', 'Please confirm passphrase: ')
|
||||||
|
else:
|
||||||
|
prompts = ('Please enter passphrase: ',)
|
||||||
|
for prompt in prompts:
|
||||||
|
actual_prompt = p.stderr.read(len(prompt))
|
||||||
|
if actual_prompt != prompt:
|
||||||
|
raise qubes.exc.QubesException(
|
||||||
|
'Unexpected prompt from scrypt: {}'.format(actual_prompt))
|
||||||
|
pty.write(passphrase.encode('utf-8') + b'\n')
|
||||||
|
pty.flush()
|
||||||
|
# save it here, so garbage collector would not close it (which would kill
|
||||||
|
# the child)
|
||||||
|
p.pty = pty
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
class Backup(object):
|
class Backup(object):
|
||||||
class FileToBackup(object):
|
class FileToBackup(object):
|
||||||
def __init__(self, file_path, subdir=None, name=None):
|
def __init__(self, file_path, subdir=None, name=None):
|
||||||
@ -292,6 +358,10 @@ class Backup(object):
|
|||||||
#: callback for progress reporting. Will be called with one argument
|
#: callback for progress reporting. Will be called with one argument
|
||||||
#: - progress in percents
|
#: - progress in percents
|
||||||
self.progress_callback = None
|
self.progress_callback = None
|
||||||
|
#: backup ID, needs to be unique (for a given user),
|
||||||
|
#: not necessary unpredictable; automatically generated
|
||||||
|
self.backup_id = datetime.datetime.now().strftime(
|
||||||
|
'%Y%m%dT%H%M%S-' + str(os.getpid()))
|
||||||
|
|
||||||
for key, value in kwargs.iteritems():
|
for key, value in kwargs.iteritems():
|
||||||
if hasattr(self, key):
|
if hasattr(self, key):
|
||||||
@ -306,7 +376,9 @@ class Backup(object):
|
|||||||
|
|
||||||
self.log = logging.getLogger('qubes.backup')
|
self.log = logging.getLogger('qubes.backup')
|
||||||
|
|
||||||
self.compression_filter = DEFAULT_COMPRESSION_FILTER
|
if not self.encrypted:
|
||||||
|
self.log.warning('\'encrypted\' option is ignored, backup is '
|
||||||
|
'always encrypted')
|
||||||
|
|
||||||
if exclude_list is None:
|
if exclude_list is None:
|
||||||
exclude_list = []
|
exclude_list = []
|
||||||
@ -474,17 +546,21 @@ class Backup(object):
|
|||||||
encrypted=self.encrypted,
|
encrypted=self.encrypted,
|
||||||
compressed=self.compressed,
|
compressed=self.compressed,
|
||||||
compression_filter=self.compression_filter,
|
compression_filter=self.compression_filter,
|
||||||
|
backup_id=self.backup_id,
|
||||||
)
|
)
|
||||||
backup_header.save(header_file_path)
|
backup_header.save(header_file_path)
|
||||||
|
# Start encrypt, scrypt will also handle integrity
|
||||||
|
# protection
|
||||||
|
scrypt_passphrase = u'{filename}!{passphrase}'.format(
|
||||||
|
filename=HEADER_FILENAME, passphrase=self.passphrase)
|
||||||
|
scrypt = launch_scrypt(
|
||||||
|
'enc', header_file_path, header_file_path + '.hmac',
|
||||||
|
scrypt_passphrase)
|
||||||
|
|
||||||
hmac = subprocess.Popen(
|
if scrypt.wait() != 0:
|
||||||
["openssl", "dgst", "-" + self.hmac_algorithm,
|
|
||||||
"-hmac", self.passphrase],
|
|
||||||
stdin=open(header_file_path, "r"),
|
|
||||||
stdout=open(header_file_path + ".hmac", "w"))
|
|
||||||
if hmac.wait() != 0:
|
|
||||||
raise qubes.exc.QubesException(
|
raise qubes.exc.QubesException(
|
||||||
"Failed to compute hmac of header file")
|
"Failed to compute hmac of header file: "
|
||||||
|
+ scrypt.stderr.read())
|
||||||
return HEADER_FILENAME, HEADER_FILENAME + ".hmac"
|
return HEADER_FILENAME, HEADER_FILENAME + ".hmac"
|
||||||
|
|
||||||
|
|
||||||
@ -534,8 +610,6 @@ class Backup(object):
|
|||||||
backup_app.domains[qid].features['backup-size'] = vm_info.size
|
backup_app.domains[qid].features['backup-size'] = vm_info.size
|
||||||
backup_app.save()
|
backup_app.save()
|
||||||
|
|
||||||
passphrase = self.passphrase.encode('utf-8')
|
|
||||||
|
|
||||||
vmproc = None
|
vmproc = None
|
||||||
tar_sparse = None
|
tar_sparse = None
|
||||||
if self.target_vm is not None:
|
if self.target_vm is not None:
|
||||||
@ -640,73 +714,53 @@ class Backup(object):
|
|||||||
|
|
||||||
self.log.debug(" ".join(tar_cmdline))
|
self.log.debug(" ".join(tar_cmdline))
|
||||||
|
|
||||||
# Tips: Popen(bufsize=0)
|
# Pipe: tar-sparse | scrypt | tar | backup_target
|
||||||
# Pipe: tar-sparse | encryptor [| hmac] | tar | backup_target
|
|
||||||
# Pipe: tar-sparse [| hmac] | tar | backup_target
|
|
||||||
# TODO: log handle stderr
|
# TODO: log handle stderr
|
||||||
tar_sparse = subprocess.Popen(
|
tar_sparse = subprocess.Popen(
|
||||||
tar_cmdline, stdin=subprocess.PIPE)
|
tar_cmdline)
|
||||||
self.processes_to_kill_on_cancel.append(tar_sparse)
|
self.processes_to_kill_on_cancel.append(tar_sparse)
|
||||||
|
|
||||||
# Wait for compressor (tar) process to finish or for any
|
# Wait for compressor (tar) process to finish or for any
|
||||||
# error of other subprocesses
|
# error of other subprocesses
|
||||||
i = 0
|
i = 0
|
||||||
run_error = "paused"
|
|
||||||
encryptor = None
|
|
||||||
if self.encrypted:
|
|
||||||
# Start encrypt
|
|
||||||
# If no cipher is provided,
|
|
||||||
# the data is forwarded unencrypted !!!
|
|
||||||
encryptor = subprocess.Popen([
|
|
||||||
"openssl", "enc",
|
|
||||||
"-e", "-" + self.crypto_algorithm,
|
|
||||||
"-pass", "pass:" + passphrase],
|
|
||||||
stdin=open(backup_pipe, 'rb'),
|
|
||||||
stdout=subprocess.PIPE)
|
|
||||||
pipe = encryptor.stdout
|
|
||||||
else:
|
|
||||||
pipe = open(backup_pipe, 'rb')
|
pipe = open(backup_pipe, 'rb')
|
||||||
|
run_error = "paused"
|
||||||
while run_error == "paused":
|
while run_error == "paused":
|
||||||
|
|
||||||
# Start HMAC
|
|
||||||
hmac = subprocess.Popen([
|
|
||||||
"openssl", "dgst", "-" + self.hmac_algorithm,
|
|
||||||
"-hmac", passphrase],
|
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE)
|
|
||||||
|
|
||||||
# Prepare a first chunk
|
# Prepare a first chunk
|
||||||
chunkfile = backup_tempfile + "." + "%03d" % i
|
chunkfile = backup_tempfile + ".%03d.enc" % i
|
||||||
i += 1
|
i += 1
|
||||||
chunkfile_p = open(chunkfile, 'wb')
|
|
||||||
|
|
||||||
common_args = {
|
# Start encrypt, scrypt will also handle integrity
|
||||||
'backup_target': chunkfile_p,
|
# protection
|
||||||
'hmac': hmac,
|
scrypt_passphrase = \
|
||||||
'vmproc': vmproc,
|
u'{backup_id}!{filename}!{passphrase}'.format(
|
||||||
|
backup_id=self.backup_id,
|
||||||
|
filename=os.path.relpath(chunkfile[:-4],
|
||||||
|
self.tmpdir),
|
||||||
|
passphrase=self.passphrase)
|
||||||
|
scrypt = launch_scrypt(
|
||||||
|
"enc", "-", chunkfile, scrypt_passphrase)
|
||||||
|
|
||||||
|
run_error = handle_streams(
|
||||||
|
pipe,
|
||||||
|
{'backup_target': scrypt.stdin},
|
||||||
|
{'vmproc': vmproc,
|
||||||
'addproc': tar_sparse,
|
'addproc': tar_sparse,
|
||||||
'progress_callback': self._add_vm_progress,
|
'scrypt': scrypt,
|
||||||
'size_limit': self.chunk_size,
|
},
|
||||||
}
|
self.chunk_size,
|
||||||
run_error = wait_backup_feedback(
|
self._add_vm_progress
|
||||||
in_stream=pipe, streamproc=encryptor,
|
)
|
||||||
**common_args)
|
|
||||||
chunkfile_p.close()
|
|
||||||
|
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
"Wait_backup_feedback returned: {}".format(run_error))
|
"12 returned: {}".format(run_error))
|
||||||
|
|
||||||
if self.canceled:
|
if self.canceled:
|
||||||
try:
|
try:
|
||||||
tar_sparse.terminate()
|
tar_sparse.terminate()
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
try:
|
|
||||||
hmac.terminate()
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
tar_sparse.wait()
|
tar_sparse.wait()
|
||||||
hmac.wait()
|
|
||||||
to_send.put(QUEUE_ERROR)
|
to_send.put(QUEUE_ERROR)
|
||||||
send_proc.join()
|
send_proc.join()
|
||||||
shutil.rmtree(self.tmpdir)
|
shutil.rmtree(self.tmpdir)
|
||||||
@ -722,29 +776,16 @@ class Backup(object):
|
|||||||
"Failed to perform backup: error in " +
|
"Failed to perform backup: error in " +
|
||||||
run_error)
|
run_error)
|
||||||
|
|
||||||
|
scrypt.stdin.close()
|
||||||
|
scrypt.wait()
|
||||||
|
self.log.debug("scrypt return code: {}".format(
|
||||||
|
scrypt.poll()))
|
||||||
|
|
||||||
# Send the chunk to the backup target
|
# Send the chunk to the backup target
|
||||||
self._queue_put_with_check(
|
self._queue_put_with_check(
|
||||||
send_proc, vmproc, to_send,
|
send_proc, vmproc, to_send,
|
||||||
os.path.relpath(chunkfile, self.tmpdir))
|
os.path.relpath(chunkfile, self.tmpdir))
|
||||||
|
|
||||||
# Close HMAC
|
|
||||||
hmac.stdin.close()
|
|
||||||
hmac.wait()
|
|
||||||
self.log.debug("HMAC proc return code: {}".format(
|
|
||||||
hmac.poll()))
|
|
||||||
|
|
||||||
# Write HMAC data next to the chunk file
|
|
||||||
hmac_data = hmac.stdout.read()
|
|
||||||
self.log.debug(
|
|
||||||
"Writing hmac to {}.hmac".format(chunkfile))
|
|
||||||
with open(chunkfile + ".hmac", 'w') as hmac_file:
|
|
||||||
hmac_file.write(hmac_data)
|
|
||||||
|
|
||||||
# Send the HMAC to the backup target
|
|
||||||
self._queue_put_with_check(
|
|
||||||
send_proc, vmproc, to_send,
|
|
||||||
os.path.relpath(chunkfile, self.tmpdir) + ".hmac")
|
|
||||||
|
|
||||||
if tar_sparse.poll() is None or run_error == "size_limit":
|
if tar_sparse.poll() is None or run_error == "size_limit":
|
||||||
run_error = "paused"
|
run_error = "paused"
|
||||||
else:
|
else:
|
||||||
@ -783,96 +824,52 @@ class Backup(object):
|
|||||||
self.app.save()
|
self.app.save()
|
||||||
|
|
||||||
|
|
||||||
|
def handle_streams(stream_in, streams_out, processes, size_limit=None,
|
||||||
|
progress_callback=None):
|
||||||
def wait_backup_feedback(progress_callback, in_stream, streamproc,
|
|
||||||
backup_target, hmac=None, vmproc=None,
|
|
||||||
addproc=None,
|
|
||||||
size_limit=None):
|
|
||||||
'''
|
|
||||||
Wait for backup chunk to finish
|
|
||||||
- Monitor all the processes (streamproc, hmac, vmproc, addproc) for errors
|
|
||||||
- Copy stdout of streamproc to backup_target and hmac stdin if available
|
|
||||||
- Compute progress based on total_backup_sz and send progress to
|
|
||||||
progress_callback function
|
|
||||||
- Returns if
|
|
||||||
- one of the monitored processes error out (streamproc, hmac, vmproc,
|
|
||||||
addproc), along with the processe that failed
|
|
||||||
- all of the monitored processes except vmproc finished successfully
|
|
||||||
(vmproc termination is controlled by the python script)
|
|
||||||
- streamproc does not delivers any data anymore (return with the error
|
|
||||||
"")
|
|
||||||
- size_limit is provided and is about to be exceeded
|
|
||||||
'''
|
'''
|
||||||
|
Copy stream_in to all streams_out and monitor all mentioned processes.
|
||||||
|
If any of them terminate with non-zero code, interrupt the process. Copy
|
||||||
|
at most `size_limit` data (if given).
|
||||||
|
|
||||||
|
:param stream_in: file-like object to read data from
|
||||||
|
:param streams_out: dict of file-like objects to write data to
|
||||||
|
:param processes: dict of subprocess.Popen objects to monitor
|
||||||
|
:param size_limit: int maximum data amount to process
|
||||||
|
:param progress_callback: callable function to report progress, will be
|
||||||
|
given copied data size (it should accumulate internally)
|
||||||
|
:return: failed process name, failed stream name, "size_limit" or None (
|
||||||
|
no error)
|
||||||
|
'''
|
||||||
buffer_size = 409600
|
buffer_size = 409600
|
||||||
run_error = None
|
|
||||||
run_count = 1
|
|
||||||
bytes_copied = 0
|
bytes_copied = 0
|
||||||
log = logging.getLogger('qubes.backup')
|
while True:
|
||||||
|
if size_limit:
|
||||||
while run_count > 0 and run_error is None:
|
to_copy = min(buffer_size, size_limit - bytes_copied)
|
||||||
if size_limit and bytes_copied + buffer_size > size_limit:
|
if to_copy <= 0:
|
||||||
return "size_limit"
|
return "size_limit"
|
||||||
|
else:
|
||||||
|
to_copy = buffer_size
|
||||||
|
buf = stream_in.read(to_copy)
|
||||||
|
if not len(buf):
|
||||||
|
# done
|
||||||
|
return None
|
||||||
|
|
||||||
buf = in_stream.read(buffer_size)
|
|
||||||
if callable(progress_callback):
|
if callable(progress_callback):
|
||||||
progress_callback(len(buf))
|
progress_callback(len(buf))
|
||||||
|
for name, stream in streams_out.items():
|
||||||
|
if stream is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
stream.write(buf)
|
||||||
|
except IOError:
|
||||||
|
return name
|
||||||
bytes_copied += len(buf)
|
bytes_copied += len(buf)
|
||||||
|
|
||||||
run_count = 0
|
for name, proc in processes.items():
|
||||||
if hmac:
|
if proc is None:
|
||||||
retcode = hmac.poll()
|
continue
|
||||||
if retcode is not None:
|
if proc.poll():
|
||||||
if retcode != 0:
|
return name
|
||||||
run_error = "hmac"
|
|
||||||
else:
|
|
||||||
run_count += 1
|
|
||||||
|
|
||||||
if addproc:
|
|
||||||
retcode = addproc.poll()
|
|
||||||
if retcode is not None:
|
|
||||||
if retcode != 0:
|
|
||||||
run_error = "addproc"
|
|
||||||
else:
|
|
||||||
run_count += 1
|
|
||||||
|
|
||||||
if vmproc:
|
|
||||||
retcode = vmproc.poll()
|
|
||||||
if retcode is not None:
|
|
||||||
if retcode != 0:
|
|
||||||
run_error = "VM"
|
|
||||||
log.debug(vmproc.stdout.read())
|
|
||||||
else:
|
|
||||||
# VM should run until the end
|
|
||||||
pass
|
|
||||||
|
|
||||||
if streamproc:
|
|
||||||
retcode = streamproc.poll()
|
|
||||||
if retcode is not None:
|
|
||||||
if retcode != 0:
|
|
||||||
run_error = "streamproc"
|
|
||||||
break
|
|
||||||
elif retcode == 0 and len(buf) <= 0:
|
|
||||||
return ""
|
|
||||||
run_count += 1
|
|
||||||
|
|
||||||
else:
|
|
||||||
if len(buf) <= 0:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
try:
|
|
||||||
backup_target.write(buf)
|
|
||||||
except IOError as e:
|
|
||||||
if e.errno == errno.EPIPE:
|
|
||||||
run_error = "target"
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
if hmac:
|
|
||||||
hmac.stdin.write(buf)
|
|
||||||
|
|
||||||
return run_error
|
|
||||||
|
|
||||||
|
|
||||||
class ExtractWorker2(Process):
|
class ExtractWorker2(Process):
|
||||||
@ -1127,6 +1124,10 @@ class ExtractWorker2(Process):
|
|||||||
self.tar2_current_file = filename
|
self.tar2_current_file = filename
|
||||||
|
|
||||||
pipe = open(self.restore_pipe, 'wb')
|
pipe = open(self.restore_pipe, 'wb')
|
||||||
|
monitor_processes = {
|
||||||
|
'vmproc': self.vmproc,
|
||||||
|
'addproc': self.tar2_process,
|
||||||
|
}
|
||||||
common_args = {
|
common_args = {
|
||||||
'backup_target': pipe,
|
'backup_target': pipe,
|
||||||
'hmac': None,
|
'hmac': None,
|
||||||
@ -1144,28 +1145,23 @@ class ExtractWorker2(Process):
|
|||||||
(["-z"] if self.compressed else []),
|
(["-z"] if self.compressed else []),
|
||||||
stdin=open(filename, 'rb'),
|
stdin=open(filename, 'rb'),
|
||||||
stdout=subprocess.PIPE)
|
stdout=subprocess.PIPE)
|
||||||
|
in_stream = self.decryptor_process.stdout
|
||||||
run_error = wait_backup_feedback(
|
monitor_processes['decryptor'] = self.decryptor_process
|
||||||
progress_callback=self.progress_callback,
|
|
||||||
in_stream=self.decryptor_process.stdout,
|
|
||||||
streamproc=self.decryptor_process,
|
|
||||||
**common_args)
|
|
||||||
elif self.compressed:
|
elif self.compressed:
|
||||||
self.decompressor_process = subprocess.Popen(
|
self.decompressor_process = subprocess.Popen(
|
||||||
["gzip", "-d"],
|
["gzip", "-d"],
|
||||||
stdin=open(filename, 'rb'),
|
stdin=open(filename, 'rb'),
|
||||||
stdout=subprocess.PIPE)
|
stdout=subprocess.PIPE)
|
||||||
|
in_stream = self.decompressor_process.stdout
|
||||||
run_error = wait_backup_feedback(
|
monitor_processes['decompresor'] = self.decompressor_process
|
||||||
progress_callback=self.progress_callback,
|
|
||||||
in_stream=self.decompressor_process.stdout,
|
|
||||||
streamproc=self.decompressor_process,
|
|
||||||
**common_args)
|
|
||||||
else:
|
else:
|
||||||
run_error = wait_backup_feedback(
|
in_stream = open(filename, 'rb')
|
||||||
progress_callback=self.progress_callback,
|
|
||||||
in_stream=open(filename, "rb"), streamproc=None,
|
run_error = handle_streams(
|
||||||
**common_args)
|
in_stream,
|
||||||
|
{'target': pipe},
|
||||||
|
monitor_processes,
|
||||||
|
progress_callback=self.progress_callback)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pipe.close()
|
pipe.close()
|
||||||
@ -1177,7 +1173,7 @@ class ExtractWorker2(Process):
|
|||||||
# ignore the error
|
# ignore the error
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
if len(run_error):
|
if run_error:
|
||||||
if run_error == "target":
|
if run_error == "target":
|
||||||
self.collect_tar_output()
|
self.collect_tar_output()
|
||||||
details = "\n".join(self.tar2_stderr)
|
details = "\n".join(self.tar2_stderr)
|
||||||
@ -1307,22 +1303,31 @@ class ExtractWorker3(ExtractWorker2):
|
|||||||
os.remove(filename)
|
os.remove(filename)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
|
(basename, ext) = os.path.splitext(self.tar2_current_file)
|
||||||
|
previous_chunk_number = int(ext[1:])
|
||||||
|
expected_filename = basename + '.%03d' % (
|
||||||
|
previous_chunk_number+1)
|
||||||
|
if expected_filename != filename:
|
||||||
|
self.cleanup_tar2(wait=True, terminate=True)
|
||||||
|
self.log.error(
|
||||||
|
'Unexpected file in archive: {}, expected {}'.format(
|
||||||
|
filename, expected_filename))
|
||||||
|
os.remove(filename)
|
||||||
|
continue
|
||||||
self.log.debug("Releasing next chunck")
|
self.log.debug("Releasing next chunck")
|
||||||
|
|
||||||
self.tar2_current_file = filename
|
self.tar2_current_file = filename
|
||||||
|
|
||||||
common_args = {
|
run_error = handle_streams(
|
||||||
'backup_target': input_pipe,
|
open(filename, 'rb'),
|
||||||
'hmac': None,
|
{'target': input_pipe},
|
||||||
'vmproc': self.vmproc,
|
{'vmproc': self.vmproc,
|
||||||
'addproc': self.tar2_process
|
'addproc': self.tar2_process,
|
||||||
}
|
'decryptor': self.decryptor_process,
|
||||||
|
},
|
||||||
|
progress_callback=self.progress_callback)
|
||||||
|
|
||||||
run_error = wait_backup_feedback(
|
if run_error:
|
||||||
progress_callback=self.progress_callback,
|
|
||||||
in_stream=open(filename, "rb"), streamproc=None,
|
|
||||||
**common_args)
|
|
||||||
|
|
||||||
if len(run_error):
|
|
||||||
if run_error == "target":
|
if run_error == "target":
|
||||||
self.collect_tar_output()
|
self.collect_tar_output()
|
||||||
details = "\n".join(self.tar2_stderr)
|
details = "\n".join(self.tar2_stderr)
|
||||||
@ -1356,6 +1361,8 @@ def get_supported_hmac_algo(hmac_algorithm=None):
|
|||||||
# Start with provided default
|
# Start with provided default
|
||||||
if hmac_algorithm:
|
if hmac_algorithm:
|
||||||
yield hmac_algorithm
|
yield hmac_algorithm
|
||||||
|
if hmac_algorithm != 'scrypt':
|
||||||
|
yield 'scrypt'
|
||||||
proc = subprocess.Popen(['openssl', 'list-message-digest-algorithms'],
|
proc = subprocess.Popen(['openssl', 'list-message-digest-algorithms'],
|
||||||
stdout=subprocess.PIPE)
|
stdout=subprocess.PIPE)
|
||||||
for algo in proc.stdout.readlines():
|
for algo in proc.stdout.readlines():
|
||||||
@ -1575,6 +1582,10 @@ class BackupRestore(object):
|
|||||||
|
|
||||||
def _verify_hmac(self, filename, hmacfile, algorithm=None):
|
def _verify_hmac(self, filename, hmacfile, algorithm=None):
|
||||||
def load_hmac(hmac_text):
|
def load_hmac(hmac_text):
|
||||||
|
if filter(lambda x: ord(x) not in range(128),
|
||||||
|
hmac_text):
|
||||||
|
raise qubes.exc.QubesException(
|
||||||
|
"Invalid content of {}".format(hmacfile))
|
||||||
hmac_text = hmac_text.strip().split("=")
|
hmac_text = hmac_text.strip().split("=")
|
||||||
if len(hmac_text) > 1:
|
if len(hmac_text) > 1:
|
||||||
hmac_text = hmac_text[1].strip()
|
hmac_text = hmac_text[1].strip()
|
||||||
@ -1593,6 +1604,17 @@ class BackupRestore(object):
|
|||||||
"ERROR: expected hmac for {}, but got {}".
|
"ERROR: expected hmac for {}, but got {}".
|
||||||
format(filename, hmacfile))
|
format(filename, hmacfile))
|
||||||
|
|
||||||
|
if algorithm == 'scrypt':
|
||||||
|
# in case of 'scrypt' _verify_hmac is only used for backup header
|
||||||
|
assert filename == HEADER_FILENAME
|
||||||
|
self._verify_and_decrypt(hmacfile, HEADER_FILENAME + '.dec')
|
||||||
|
if open(os.path.join(self.tmpdir, filename)).read() != \
|
||||||
|
open(os.path.join(self.tmpdir, filename + '.dec')).read():
|
||||||
|
raise qubes.exc.QubesException(
|
||||||
|
'Invalid hmac on {}'.format(filename))
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
hmac_proc = subprocess.Popen(
|
hmac_proc = subprocess.Popen(
|
||||||
["openssl", "dgst", "-" + algorithm, "-hmac", passphrase],
|
["openssl", "dgst", "-" + algorithm, "-hmac", passphrase],
|
||||||
stdin=open(os.path.join(self.tmpdir, filename), 'rb'),
|
stdin=open(os.path.join(self.tmpdir, filename), 'rb'),
|
||||||
@ -1618,6 +1640,80 @@ class BackupRestore(object):
|
|||||||
"Is the passphrase correct?".
|
"Is the passphrase correct?".
|
||||||
format(filename, load_hmac(hmac_stdout)))
|
format(filename, load_hmac(hmac_stdout)))
|
||||||
|
|
||||||
|
def _verify_and_decrypt(self, filename, output=None):
|
||||||
|
assert filename.endswith('.enc') or filename.endswith('.hmac')
|
||||||
|
fullname = os.path.join(self.tmpdir, filename)
|
||||||
|
(origname, _) = os.path.splitext(filename)
|
||||||
|
if output:
|
||||||
|
fulloutput = os.path.join(self.tmpdir, output)
|
||||||
|
else:
|
||||||
|
fulloutput = os.path.join(self.tmpdir, origname)
|
||||||
|
if origname == HEADER_FILENAME:
|
||||||
|
passphrase = u'{filename}!{passphrase}'.format(
|
||||||
|
filename=origname,
|
||||||
|
passphrase=self.passphrase)
|
||||||
|
else:
|
||||||
|
passphrase = u'{backup_id}!{filename}!{passphrase}'.format(
|
||||||
|
backup_id=self.header_data.backup_id,
|
||||||
|
filename=origname,
|
||||||
|
passphrase=self.passphrase)
|
||||||
|
p = launch_scrypt('dec', fullname, fulloutput, passphrase)
|
||||||
|
(_, stderr) = p.communicate()
|
||||||
|
if p.returncode != 0:
|
||||||
|
os.unlink(fulloutput)
|
||||||
|
raise qubes.exc.QubesException('failed to decrypt {}: {}'.format(
|
||||||
|
fullname, stderr))
|
||||||
|
# encrypted file is no longer needed
|
||||||
|
os.unlink(fullname)
|
||||||
|
return origname
|
||||||
|
|
||||||
|
def _retrieve_backup_header_files(self, files, allow_none=False):
|
||||||
|
(retrieve_proc, filelist_pipe, error_pipe) = \
|
||||||
|
self._start_retrieval_process(
|
||||||
|
files, len(files), 1024 * 1024)
|
||||||
|
filelist = filelist_pipe.read()
|
||||||
|
retrieve_proc_returncode = retrieve_proc.wait()
|
||||||
|
if retrieve_proc in self.processes_to_kill_on_cancel:
|
||||||
|
self.processes_to_kill_on_cancel.remove(retrieve_proc)
|
||||||
|
extract_stderr = error_pipe.read(MAX_STDERR_BYTES)
|
||||||
|
|
||||||
|
# wait for other processes (if any)
|
||||||
|
for proc in self.processes_to_kill_on_cancel:
|
||||||
|
if proc.wait() != 0:
|
||||||
|
raise qubes.exc.QubesException(
|
||||||
|
"Backup header retrieval failed (exit code {})".format(
|
||||||
|
proc.wait())
|
||||||
|
)
|
||||||
|
|
||||||
|
if retrieve_proc_returncode != 0:
|
||||||
|
if not filelist and 'Not found in archive' in extract_stderr:
|
||||||
|
if allow_none:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
raise qubes.exc.QubesException(
|
||||||
|
"unable to read the qubes backup file {0} ({1}): {2}".format(
|
||||||
|
self.backup_location,
|
||||||
|
retrieve_proc.wait(),
|
||||||
|
extract_stderr
|
||||||
|
))
|
||||||
|
actual_files = filelist.splitlines()
|
||||||
|
if sorted(actual_files) != sorted(files):
|
||||||
|
raise qubes.exc.QubesException(
|
||||||
|
'unexpected files in archive: got {!r}, expeced {!r}'.format(
|
||||||
|
actual_files, files
|
||||||
|
))
|
||||||
|
for f in files:
|
||||||
|
if not os.path.exists(os.path.join(self.tmpdir, f)):
|
||||||
|
if allow_none:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
raise qubes.exc.QubesException(
|
||||||
|
'Unable to retrieve file {} from backup {}: {}'.format(
|
||||||
|
f, self.backup_location, extract_stderr
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return files
|
||||||
|
|
||||||
def _retrieve_backup_header(self):
|
def _retrieve_backup_header(self):
|
||||||
"""Retrieve backup header and qubes.xml. Only backup header is
|
"""Retrieve backup header and qubes.xml. Only backup header is
|
||||||
analyzed, qubes.xml is left as-is
|
analyzed, qubes.xml is left as-is
|
||||||
@ -1634,82 +1730,47 @@ class BackupRestore(object):
|
|||||||
header_data.version = 1
|
header_data.version = 1
|
||||||
return header_data
|
return header_data
|
||||||
|
|
||||||
(retrieve_proc, filelist_pipe, error_pipe) = \
|
header_files = self._retrieve_backup_header_files(
|
||||||
self._start_retrieval_process(
|
['backup-header', 'backup-header.hmac'], allow_none=True)
|
||||||
['backup-header', 'backup-header.hmac',
|
|
||||||
'qubes.xml.000', 'qubes.xml.000.hmac'], 4, 1024 * 1024)
|
|
||||||
|
|
||||||
expect_tar_error = False
|
|
||||||
|
|
||||||
filename = filelist_pipe.readline().strip()
|
|
||||||
hmacfile = filelist_pipe.readline().strip()
|
|
||||||
# tar output filename before actually extracting it, so wait for the
|
|
||||||
# next one before trying to access it
|
|
||||||
if not self.backup_vm:
|
|
||||||
filelist_pipe.readline().strip()
|
|
||||||
|
|
||||||
|
if not header_files:
|
||||||
|
# R2-Beta3 didn't have backup header, so if none is found,
|
||||||
|
# assume it's version=2 and use values present at that time
|
||||||
|
header_data = BackupHeader(
|
||||||
|
version=2,
|
||||||
|
# place explicitly this value, because it is what format_version
|
||||||
|
# 2 have
|
||||||
|
hmac_algorithm='SHA1',
|
||||||
|
crypto_algorithm='aes-256-cbc',
|
||||||
|
# TODO: set encrypted to something...
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
filename = HEADER_FILENAME
|
||||||
|
hmacfile = HEADER_FILENAME + '.hmac'
|
||||||
self.log.debug("Got backup header and hmac: {}, {}".format(
|
self.log.debug("Got backup header and hmac: {}, {}".format(
|
||||||
filename, hmacfile))
|
filename, hmacfile))
|
||||||
|
|
||||||
if not filename or filename == "EOF" or \
|
|
||||||
not hmacfile or hmacfile == "EOF":
|
|
||||||
retrieve_proc.wait()
|
|
||||||
proc_error_msg = error_pipe.read(MAX_STDERR_BYTES)
|
|
||||||
raise qubes.exc.QubesException(
|
|
||||||
"Premature end of archive while receiving "
|
|
||||||
"backup header. Process output:\n" + proc_error_msg)
|
|
||||||
file_ok = False
|
file_ok = False
|
||||||
hmac_algorithm = DEFAULT_HMAC_ALGORITHM
|
hmac_algorithm = DEFAULT_HMAC_ALGORITHM
|
||||||
for hmac_algo in get_supported_hmac_algo(hmac_algorithm):
|
for hmac_algo in get_supported_hmac_algo(hmac_algorithm):
|
||||||
try:
|
try:
|
||||||
if self._verify_hmac(filename, hmacfile, hmac_algo):
|
if self._verify_hmac(filename, hmacfile, hmac_algo):
|
||||||
file_ok = True
|
file_ok = True
|
||||||
hmac_algorithm = hmac_algo
|
|
||||||
break
|
break
|
||||||
except qubes.exc.QubesException:
|
except qubes.exc.QubesException as e:
|
||||||
|
self.log.debug(
|
||||||
|
'Failed to verify {} using {}: {}'.format(
|
||||||
|
hmacfile, hmac_algo, str(e)))
|
||||||
# Ignore exception here, try the next algo
|
# Ignore exception here, try the next algo
|
||||||
pass
|
pass
|
||||||
if not file_ok:
|
if not file_ok:
|
||||||
raise qubes.exc.QubesException(
|
raise qubes.exc.QubesException(
|
||||||
"Corrupted backup header (hmac verification "
|
"Corrupted backup header (hmac verification "
|
||||||
"failed). Is the password correct?")
|
"failed). Is the password correct?")
|
||||||
if os.path.basename(filename) == HEADER_FILENAME:
|
|
||||||
filename = os.path.join(self.tmpdir, filename)
|
filename = os.path.join(self.tmpdir, filename)
|
||||||
header_data = BackupHeader(open(filename, 'r').read())
|
header_data = BackupHeader(open(filename, 'r').read())
|
||||||
os.unlink(filename)
|
os.unlink(filename)
|
||||||
else:
|
|
||||||
# if no header found, create one with guessed HMAC algo
|
|
||||||
header_data = BackupHeader(
|
|
||||||
version=2,
|
|
||||||
hmac_algorithm=hmac_algorithm,
|
|
||||||
# place explicitly this value, because it is what format_version
|
|
||||||
# 2 have
|
|
||||||
crypto_algorithm='aes-256-cbc',
|
|
||||||
# TODO: set encrypted to something...
|
|
||||||
)
|
|
||||||
# when tar do not find expected file in archive, it exit with
|
|
||||||
# code 2. This will happen because we've requested backup-header
|
|
||||||
# file, but the archive do not contain it. Ignore this particular
|
|
||||||
# error.
|
|
||||||
if not self.backup_vm:
|
|
||||||
expect_tar_error = True
|
|
||||||
|
|
||||||
if retrieve_proc.wait() != 0 and not expect_tar_error:
|
|
||||||
raise qubes.exc.QubesException(
|
|
||||||
"unable to read the qubes backup file {0} ({1}): {2}".format(
|
|
||||||
self.backup_location,
|
|
||||||
retrieve_proc.wait(),
|
|
||||||
error_pipe.read(MAX_STDERR_BYTES)
|
|
||||||
))
|
|
||||||
if retrieve_proc in self.processes_to_kill_on_cancel:
|
|
||||||
self.processes_to_kill_on_cancel.remove(retrieve_proc)
|
|
||||||
# wait for other processes (if any)
|
|
||||||
for proc in self.processes_to_kill_on_cancel:
|
|
||||||
if proc.wait() != 0:
|
|
||||||
raise qubes.exc.QubesException(
|
|
||||||
"Backup header retrieval failed (exit code {})".format(
|
|
||||||
proc.wait())
|
|
||||||
)
|
|
||||||
return header_data
|
return header_data
|
||||||
|
|
||||||
def _start_inner_extraction_worker(self, queue, relocate):
|
def _start_inner_extraction_worker(self, queue, relocate):
|
||||||
@ -1742,6 +1803,9 @@ class BackupRestore(object):
|
|||||||
elif format_version in [3, 4]:
|
elif format_version in [3, 4]:
|
||||||
extractor_params['compression_filter'] = \
|
extractor_params['compression_filter'] = \
|
||||||
self.header_data.compression_filter
|
self.header_data.compression_filter
|
||||||
|
if format_version == 4:
|
||||||
|
# encryption already handled
|
||||||
|
extractor_params['encrypted'] = False
|
||||||
extract_proc = ExtractWorker3(**extractor_params)
|
extract_proc = ExtractWorker3(**extractor_params)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
@ -1760,7 +1824,14 @@ class BackupRestore(object):
|
|||||||
offline_mode=True)
|
offline_mode=True)
|
||||||
return backup_app
|
return backup_app
|
||||||
else:
|
else:
|
||||||
|
if self.header_data.version in [2, 3]:
|
||||||
|
self._retrieve_backup_header_files(
|
||||||
|
['qubes.xml.000', 'qubes.xml.000.hmac'])
|
||||||
self._verify_hmac("qubes.xml.000", "qubes.xml.000.hmac")
|
self._verify_hmac("qubes.xml.000", "qubes.xml.000.hmac")
|
||||||
|
else:
|
||||||
|
self._retrieve_backup_header_files(['qubes.xml.000.enc'])
|
||||||
|
self._verify_and_decrypt('qubes.xml.000.enc')
|
||||||
|
|
||||||
queue = Queue()
|
queue = Queue()
|
||||||
queue.put("qubes.xml.000")
|
queue.put("qubes.xml.000")
|
||||||
queue.put(QUEUE_FINISHED)
|
queue.put(QUEUE_FINISHED)
|
||||||
@ -1808,6 +1879,7 @@ class BackupRestore(object):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
filename = None
|
filename = None
|
||||||
|
hmacfile = None
|
||||||
nextfile = None
|
nextfile = None
|
||||||
while True:
|
while True:
|
||||||
if self.canceled:
|
if self.canceled:
|
||||||
@ -1831,29 +1903,57 @@ class BackupRestore(object):
|
|||||||
if not filename or filename == "EOF":
|
if not filename or filename == "EOF":
|
||||||
break
|
break
|
||||||
|
|
||||||
hmacfile = filelist_pipe.readline().strip()
|
|
||||||
|
|
||||||
if self.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
|
# tar prints filename before processing it, so wait for
|
||||||
# the next one to be sure that whole file was extracted
|
# the next one to be sure that whole file was extracted
|
||||||
if not self.backup_vm:
|
if not self.backup_vm:
|
||||||
nextfile = filelist_pipe.readline().strip()
|
nextfile = filelist_pipe.readline().strip()
|
||||||
|
|
||||||
|
if self.header_data.version in [2, 3]:
|
||||||
|
if not self.backup_vm:
|
||||||
|
hmacfile = nextfile
|
||||||
|
nextfile = filelist_pipe.readline().strip()
|
||||||
|
else:
|
||||||
|
hmacfile = filelist_pipe.readline().strip()
|
||||||
|
|
||||||
|
if self.canceled:
|
||||||
|
break
|
||||||
|
|
||||||
self.log.debug("Getting hmac:" + hmacfile)
|
self.log.debug("Getting hmac:" + hmacfile)
|
||||||
if not hmacfile or hmacfile == "EOF":
|
if not hmacfile or hmacfile == "EOF":
|
||||||
# Premature end of archive, either of tar1_command or
|
# Premature end of archive, either of tar1_command or
|
||||||
# vmproc exited with error
|
# vmproc exited with error
|
||||||
break
|
break
|
||||||
|
else: # self.header_data.version == 4
|
||||||
|
if not filename.endswith('.enc'):
|
||||||
|
raise qubes.exc.QubesException(
|
||||||
|
'Invalid file extension found in archive: {}'.
|
||||||
|
format(filename))
|
||||||
|
|
||||||
if not any(map(lambda x: filename.startswith(x), vms_dirs)):
|
if not any(map(lambda x: filename.startswith(x), vms_dirs)):
|
||||||
self.log.debug("Ignoring VM not selected for restore")
|
self.log.debug("Ignoring VM not selected for restore")
|
||||||
os.unlink(os.path.join(self.tmpdir, filename))
|
os.unlink(os.path.join(self.tmpdir, filename))
|
||||||
|
if hmacfile:
|
||||||
os.unlink(os.path.join(self.tmpdir, hmacfile))
|
os.unlink(os.path.join(self.tmpdir, hmacfile))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self._verify_hmac(filename, hmacfile):
|
if self.header_data.version in [2, 3]:
|
||||||
|
self._verify_hmac(filename, hmacfile)
|
||||||
|
else:
|
||||||
|
# _verify_and_decrypt will write output to a file with
|
||||||
|
# '.enc' extension cut off. This is safe because:
|
||||||
|
# - `scrypt` tool will override output, so if the file was
|
||||||
|
# already there (received from the VM), it will be removed
|
||||||
|
# - incoming archive extraction will refuse to override
|
||||||
|
# existing file, so if `scrypt` already created one,
|
||||||
|
# it can not be manipulated by the VM
|
||||||
|
# - when the file is retrieved from the VM, it appears at
|
||||||
|
# the final form - if it's visible, VM have no longer
|
||||||
|
# influence over its content
|
||||||
|
#
|
||||||
|
# This all means that if the file was correctly verified
|
||||||
|
# + decrypted, we will surely access the right file
|
||||||
|
filename = self._verify_and_decrypt(filename)
|
||||||
to_extract.put(os.path.join(self.tmpdir, filename))
|
to_extract.put(os.path.join(self.tmpdir, filename))
|
||||||
|
|
||||||
if self.canceled:
|
if self.canceled:
|
||||||
@ -1921,7 +2021,7 @@ class BackupRestore(object):
|
|||||||
vm_info.problems.add(self.VMToRestore.EXCLUDED)
|
vm_info.problems.add(self.VMToRestore.EXCLUDED)
|
||||||
|
|
||||||
if not self.options.verify_only and \
|
if not self.options.verify_only and \
|
||||||
vm in self.app.domains:
|
vm_info.name in self.app.domains:
|
||||||
if self.options.rename_conflicting:
|
if self.options.rename_conflicting:
|
||||||
new_name = self.generate_new_name_for_conflicting_vm(
|
new_name = self.generate_new_name_for_conflicting_vm(
|
||||||
vm, restore_info
|
vm, restore_info
|
||||||
@ -2243,6 +2343,8 @@ class BackupRestore(object):
|
|||||||
|
|
||||||
# FIXME handle locking
|
# FIXME handle locking
|
||||||
|
|
||||||
|
restore_info = self.restore_info_verify(restore_info)
|
||||||
|
|
||||||
self._restore_vms_metadata(restore_info)
|
self._restore_vms_metadata(restore_info)
|
||||||
|
|
||||||
# Perform VM restoration in backup order
|
# Perform VM restoration in backup order
|
||||||
|
@ -163,12 +163,23 @@ class Core2Qubes(qubes.Qubes):
|
|||||||
'template_qid'))]
|
'template_qid'))]
|
||||||
vm_class = AppVM
|
vm_class = AppVM
|
||||||
# simple attributes
|
# simple attributes
|
||||||
for attr in ['installed_by_rpm', 'include_in_backups',
|
for attr, default in {
|
||||||
'qrexec_timeout', 'internal', 'label', 'name',
|
'installed_by_rpm': 'False',
|
||||||
'vcpus', 'memory', 'maxmem', 'default_user',
|
'include_in_backups': 'True',
|
||||||
'debug', 'pci_strictreset', 'mac', 'autostart']:
|
'qrexec_timeout': '60',
|
||||||
|
'internal': 'False',
|
||||||
|
'label': None,
|
||||||
|
'name': None,
|
||||||
|
'vcpus': '2',
|
||||||
|
'memory': '400',
|
||||||
|
'maxmem': '4000',
|
||||||
|
'default_user': 'user',
|
||||||
|
'debug': 'False',
|
||||||
|
'pci_strictreset': 'True',
|
||||||
|
'mac': None,
|
||||||
|
'autostart': 'False'}.items():
|
||||||
value = element.get(attr)
|
value = element.get(attr)
|
||||||
if value:
|
if value and value != default:
|
||||||
kwargs[attr] = value
|
kwargs[attr] = value
|
||||||
# attributes with default value
|
# attributes with default value
|
||||||
for attr in ["kernel", "kernelopts"]:
|
for attr in ["kernel", "kernelopts"]:
|
||||||
|
@ -63,6 +63,17 @@ VMPREFIX = 'test-inst-'
|
|||||||
CLSVMPREFIX = 'test-cls-'
|
CLSVMPREFIX = 'test-cls-'
|
||||||
|
|
||||||
|
|
||||||
|
if 'DEFAULT_LVM_POOL' in os.environ.keys():
|
||||||
|
DEFAULT_LVM_POOL = os.environ['DEFAULT_LVM_POOL']
|
||||||
|
else:
|
||||||
|
DEFAULT_LVM_POOL = 'qubes_dom0/pool00'
|
||||||
|
|
||||||
|
|
||||||
|
POOL_CONF = {'name': 'test-lvm',
|
||||||
|
'driver': 'lvm_thin',
|
||||||
|
'volume_group': DEFAULT_LVM_POOL.split('/')[0],
|
||||||
|
'thin_pool': DEFAULT_LVM_POOL.split('/')[1]}
|
||||||
|
|
||||||
#: :py:obj:`True` if running in dom0, :py:obj:`False` otherwise
|
#: :py:obj:`True` if running in dom0, :py:obj:`False` otherwise
|
||||||
in_dom0 = False
|
in_dom0 = False
|
||||||
|
|
||||||
@ -528,6 +539,30 @@ class SystemTestsMixin(object):
|
|||||||
)
|
)
|
||||||
self.app.default_netvm = netvm_clone
|
self.app.default_netvm = netvm_clone
|
||||||
|
|
||||||
|
|
||||||
|
def _find_pool(self, volume_group, thin_pool):
|
||||||
|
''' Returns the pool matching the specified ``volume_group`` &
|
||||||
|
``thin_pool``, or None.
|
||||||
|
'''
|
||||||
|
pools = [p for p in self.app.pools
|
||||||
|
if issubclass(p.__class__, qubes.storage.lvm.ThinPool)]
|
||||||
|
for pool in pools:
|
||||||
|
if pool.volume_group == volume_group \
|
||||||
|
and pool.thin_pool == thin_pool:
|
||||||
|
return pool
|
||||||
|
return None
|
||||||
|
|
||||||
|
def init_lvm_pool(self):
|
||||||
|
volume_group, thin_pool = DEFAULT_LVM_POOL.split('/', 1)
|
||||||
|
path = "/dev/mapper/{!s}-{!s}".format(volume_group, thin_pool)
|
||||||
|
if not os.path.exists(path):
|
||||||
|
self.skipTest('LVM thin pool {!r} does not exist'.
|
||||||
|
format(DEFAULT_LVM_POOL))
|
||||||
|
self.pool = self._find_pool(volume_group, thin_pool)
|
||||||
|
if not self.pool:
|
||||||
|
self.pool = self.app.add_pool(**POOL_CONF)
|
||||||
|
self.created_pool = True
|
||||||
|
|
||||||
def reload_db(self):
|
def reload_db(self):
|
||||||
self.app = qubes.Qubes(qubes.tests.XMLPATH)
|
self.app = qubes.Qubes(qubes.tests.XMLPATH)
|
||||||
|
|
||||||
|
@ -161,6 +161,7 @@ class BackupTestsMixin(qubes.tests.SystemTestsMixin):
|
|||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
if 'passphrase' not in kwargs:
|
||||||
backup.passphrase = 'qubes'
|
backup.passphrase = 'qubes'
|
||||||
backup.target_dir = target
|
backup.target_dir = target
|
||||||
|
|
||||||
@ -176,7 +177,8 @@ class BackupTestsMixin(qubes.tests.SystemTestsMixin):
|
|||||||
#self.reload_db()
|
#self.reload_db()
|
||||||
|
|
||||||
def restore_backup(self, source=None, appvm=None, options=None,
|
def restore_backup(self, source=None, appvm=None, options=None,
|
||||||
expect_errors=None):
|
expect_errors=None, manipulate_restore_info=None,
|
||||||
|
passphrase='qubes'):
|
||||||
if source is None:
|
if source is None:
|
||||||
backupfile = os.path.join(self.backupdir,
|
backupfile = os.path.join(self.backupdir,
|
||||||
sorted(os.listdir(self.backupdir))[-1])
|
sorted(os.listdir(self.backupdir))[-1])
|
||||||
@ -185,11 +187,13 @@ class BackupTestsMixin(qubes.tests.SystemTestsMixin):
|
|||||||
|
|
||||||
with self.assertNotRaises(qubes.exc.QubesException):
|
with self.assertNotRaises(qubes.exc.QubesException):
|
||||||
restore_op = qubes.backup.BackupRestore(
|
restore_op = qubes.backup.BackupRestore(
|
||||||
self.app, backupfile, appvm, "qubes")
|
self.app, backupfile, appvm, passphrase)
|
||||||
if options:
|
if options:
|
||||||
for key, value in options.items():
|
for key, value in options.items():
|
||||||
setattr(restore_op.options, key, value)
|
setattr(restore_op.options, key, value)
|
||||||
restore_info = restore_op.get_restore_info()
|
restore_info = restore_op.get_restore_info()
|
||||||
|
if callable(manipulate_restore_info):
|
||||||
|
restore_info = manipulate_restore_info(restore_info)
|
||||||
self.log.debug(restore_op.get_restore_summary(restore_info))
|
self.log.debug(restore_op.get_restore_summary(restore_info))
|
||||||
|
|
||||||
with self.assertNotRaises(qubes.exc.QubesException):
|
with self.assertNotRaises(qubes.exc.QubesException):
|
||||||
@ -357,7 +361,36 @@ class TC_00_Backup(BackupTestsMixin, qubes.tests.QubesTestCase):
|
|||||||
# create backup with internal dependencies (template, netvm etc)
|
# create backup with internal dependencies (template, netvm etc)
|
||||||
# try restoring only AppVMs (but not templates, netvms) - should
|
# try restoring only AppVMs (but not templates, netvms) - should
|
||||||
# handle according to options set
|
# handle according to options set
|
||||||
self.skipTest('test not implemented')
|
exclude = [
|
||||||
|
self.make_vm_name('test-net'),
|
||||||
|
self.make_vm_name('template')
|
||||||
|
]
|
||||||
|
def exclude_some(restore_info):
|
||||||
|
for name in exclude:
|
||||||
|
restore_info.pop(name)
|
||||||
|
return restore_info
|
||||||
|
vms = self.create_backup_vms()
|
||||||
|
orig_hashes = self.vm_checksum(vms)
|
||||||
|
self.make_backup(vms, compression_filter="bzip2")
|
||||||
|
self.remove_vms(reversed(vms))
|
||||||
|
self.restore_backup(manipulate_restore_info=exclude_some)
|
||||||
|
for vm in vms:
|
||||||
|
if vm.name == self.make_vm_name('test1'):
|
||||||
|
# netvm was set to 'test-inst-test-net' - excluded
|
||||||
|
vm.netvm = qubes.property.DEFAULT
|
||||||
|
elif vm.name == self.make_vm_name('custom'):
|
||||||
|
# template was set to 'test-inst-template' - excluded
|
||||||
|
vm.template = self.app.default_template
|
||||||
|
vms = [vm for vm in vms if vm.name not in exclude]
|
||||||
|
self.assertCorrectlyRestored(vms, orig_hashes)
|
||||||
|
|
||||||
|
def test_020_encrypted_backup_non_ascii(self):
|
||||||
|
vms = self.create_backup_vms()
|
||||||
|
orig_hashes = self.vm_checksum(vms)
|
||||||
|
self.make_backup(vms, encrypted=True, passphrase=u'zażółć gęślą jaźń')
|
||||||
|
self.remove_vms(reversed(vms))
|
||||||
|
self.restore_backup(passphrase=u'zażółć gęślą jaźń')
|
||||||
|
self.assertCorrectlyRestored(vms, orig_hashes)
|
||||||
|
|
||||||
def test_100_backup_dom0_no_restore(self):
|
def test_100_backup_dom0_no_restore(self):
|
||||||
# do not write it into dom0 home itself...
|
# do not write it into dom0 home itself...
|
||||||
|
@ -405,6 +405,28 @@ class TC_00_BackupCompatibility(
|
|||||||
|
|
||||||
output.close()
|
output.close()
|
||||||
|
|
||||||
|
def assertRestored(self, name, **kwargs):
|
||||||
|
with self.assertNotRaises((KeyError, qubes.exc.QubesException)):
|
||||||
|
vm = self.app.domains[name]
|
||||||
|
vm.storage.verify()
|
||||||
|
for prop, value in kwargs.items():
|
||||||
|
if prop == 'klass':
|
||||||
|
self.assertIsInstance(vm, value)
|
||||||
|
elif value is qubes.property.DEFAULT:
|
||||||
|
self.assertTrue(vm.property_is_default(prop),
|
||||||
|
'VM {} - property {} not default'.format(vm.name, prop))
|
||||||
|
else:
|
||||||
|
actual_value = getattr(vm, prop)
|
||||||
|
if isinstance(actual_value, qubes.vm.BaseVM):
|
||||||
|
self.assertEqual(value, actual_value.name,
|
||||||
|
'VM {} - property {}'.format(vm.name, prop))
|
||||||
|
elif isinstance(actual_value, qubes.Label):
|
||||||
|
self.assertEqual(value, actual_value.name,
|
||||||
|
'VM {} - property {}'.format(vm.name, prop))
|
||||||
|
else:
|
||||||
|
self.assertEqual(value, actual_value,
|
||||||
|
'VM {} - property {}'.format(vm.name, prop))
|
||||||
|
|
||||||
def test_100_r1(self):
|
def test_100_r1(self):
|
||||||
self.create_v1_files(r2b2=False)
|
self.create_v1_files(r2b2=False)
|
||||||
|
|
||||||
@ -418,15 +440,49 @@ class TC_00_BackupCompatibility(
|
|||||||
'use-default-netvm': True,
|
'use-default-netvm': True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
with self.assertNotRaises(KeyError):
|
common_props = {
|
||||||
vm = self.app.domains["test-template-clone"]
|
'installed_by_rpm': False,
|
||||||
vm = self.app.domains["test-testproxy"]
|
'kernel': qubes.property.DEFAULT,
|
||||||
vm = self.app.domains["test-work"]
|
'kernelopts': qubes.property.DEFAULT,
|
||||||
vm = self.app.domains["test-standalonevm"]
|
'qrexec_timeout': qubes.property.DEFAULT,
|
||||||
vm = self.app.domains["test-custom-template-appvm"]
|
'netvm': qubes.property.DEFAULT,
|
||||||
self.assertEqual(self.app.domains["test-custom-template-appvm"]
|
'default_user': qubes.property.DEFAULT,
|
||||||
.template,
|
'internal': qubes.property.DEFAULT,
|
||||||
self.app.domains["test-template-clone"])
|
'include_in_backups': True,
|
||||||
|
'debug': False,
|
||||||
|
'maxmem': 4000, # 4063 caped by 10*400
|
||||||
|
'memory': 400,
|
||||||
|
}
|
||||||
|
self.assertRestored("test-template-clone",
|
||||||
|
klass=qubes.vm.templatevm.TemplateVM,
|
||||||
|
label='gray',
|
||||||
|
provides_network=False,
|
||||||
|
**common_props)
|
||||||
|
testproxy_props = common_props.copy()
|
||||||
|
testproxy_props.update(
|
||||||
|
label='yellow',
|
||||||
|
provides_network=True,
|
||||||
|
memory=200,
|
||||||
|
maxmem=2000,
|
||||||
|
template=self.app.default_template.name,
|
||||||
|
)
|
||||||
|
self.assertRestored("test-testproxy",
|
||||||
|
klass=qubes.vm.appvm.AppVM,
|
||||||
|
**testproxy_props)
|
||||||
|
self.assertRestored("test-work",
|
||||||
|
klass=qubes.vm.appvm.AppVM,
|
||||||
|
template=self.app.default_template.name,
|
||||||
|
label='green',
|
||||||
|
**common_props)
|
||||||
|
self.assertRestored("test-standalonevm",
|
||||||
|
klass=qubes.vm.standalonevm.StandaloneVM,
|
||||||
|
label='red',
|
||||||
|
**common_props)
|
||||||
|
self.assertRestored("test-custom-template-appvm",
|
||||||
|
klass=qubes.vm.appvm.AppVM,
|
||||||
|
template='test-template-clone',
|
||||||
|
label='yellow',
|
||||||
|
**common_props)
|
||||||
|
|
||||||
def test_200_r2b2(self):
|
def test_200_r2b2(self):
|
||||||
self.create_v1_files(r2b2=True)
|
self.create_v1_files(r2b2=True)
|
||||||
@ -439,16 +495,51 @@ class TC_00_BackupCompatibility(
|
|||||||
'use-default-template': True,
|
'use-default-template': True,
|
||||||
'use-default-netvm': True,
|
'use-default-netvm': True,
|
||||||
})
|
})
|
||||||
with self.assertNotRaises(KeyError):
|
common_props = {
|
||||||
vm = self.app.domains["test-template-clone"]
|
'installed_by_rpm': False,
|
||||||
vm = self.app.domains["test-testproxy"]
|
'kernel': qubes.property.DEFAULT,
|
||||||
vm = self.app.domains["test-work"]
|
'kernelopts': qubes.property.DEFAULT,
|
||||||
vm = self.app.domains["test-testhvm"]
|
'qrexec_timeout': qubes.property.DEFAULT,
|
||||||
vm = self.app.domains["test-standalonevm"]
|
'netvm': qubes.property.DEFAULT,
|
||||||
vm = self.app.domains["test-custom-template-appvm"]
|
'default_user': qubes.property.DEFAULT,
|
||||||
self.assertEqual(self.app.domains["test-custom-template-appvm"]
|
'internal': qubes.property.DEFAULT,
|
||||||
.template,
|
'include_in_backups': True,
|
||||||
self.app.domains["test-template-clone"])
|
'debug': False,
|
||||||
|
'maxmem': 1535,
|
||||||
|
'memory': 400,
|
||||||
|
}
|
||||||
|
template_clone_props = common_props.copy()
|
||||||
|
template_clone_props.update(
|
||||||
|
label='green',
|
||||||
|
provides_network=False,
|
||||||
|
)
|
||||||
|
self.assertRestored("test-template-clone",
|
||||||
|
klass=qubes.vm.templatevm.TemplateVM,
|
||||||
|
**template_clone_props)
|
||||||
|
testproxy_props = common_props.copy()
|
||||||
|
testproxy_props.update(
|
||||||
|
label='red',
|
||||||
|
provides_network=True,
|
||||||
|
memory=200,
|
||||||
|
template=self.app.default_template.name,
|
||||||
|
)
|
||||||
|
self.assertRestored("test-testproxy",
|
||||||
|
klass=qubes.vm.appvm.AppVM,
|
||||||
|
**testproxy_props)
|
||||||
|
self.assertRestored("test-work",
|
||||||
|
klass=qubes.vm.appvm.AppVM,
|
||||||
|
template=self.app.default_template.name,
|
||||||
|
label='green',
|
||||||
|
**common_props)
|
||||||
|
self.assertRestored("test-standalonevm",
|
||||||
|
klass=qubes.vm.standalonevm.StandaloneVM,
|
||||||
|
label='blue',
|
||||||
|
**common_props)
|
||||||
|
self.assertRestored("test-custom-template-appvm",
|
||||||
|
klass=qubes.vm.appvm.AppVM,
|
||||||
|
template='test-template-clone',
|
||||||
|
label='yellow',
|
||||||
|
**common_props)
|
||||||
|
|
||||||
def test_210_r2(self):
|
def test_210_r2(self):
|
||||||
self.create_v3_backup(False)
|
self.create_v3_backup(False)
|
||||||
@ -457,16 +548,48 @@ class TC_00_BackupCompatibility(
|
|||||||
'use-default-template': True,
|
'use-default-template': True,
|
||||||
'use-default-netvm': True,
|
'use-default-netvm': True,
|
||||||
})
|
})
|
||||||
with self.assertNotRaises(KeyError):
|
common_props = {
|
||||||
vm = self.app.domains["test-template-clone"]
|
'installed_by_rpm': False,
|
||||||
vm = self.app.domains["test-testproxy"]
|
'kernel': qubes.property.DEFAULT,
|
||||||
vm = self.app.domains["test-work"]
|
'kernelopts': qubes.property.DEFAULT,
|
||||||
vm = self.app.domains["test-testhvm"]
|
'qrexec_timeout': qubes.property.DEFAULT,
|
||||||
vm = self.app.domains["test-standalonevm"]
|
'netvm': qubes.property.DEFAULT,
|
||||||
vm = self.app.domains["test-custom-template-appvm"]
|
'default_user': qubes.property.DEFAULT,
|
||||||
self.assertEqual(self.app.domains["test-custom-template-appvm"]
|
'internal': qubes.property.DEFAULT,
|
||||||
.template,
|
'include_in_backups': True,
|
||||||
self.app.domains["test-template-clone"])
|
'debug': False,
|
||||||
|
'maxmem': 1535,
|
||||||
|
'memory': 400,
|
||||||
|
}
|
||||||
|
self.assertRestored("test-template-clone",
|
||||||
|
klass=qubes.vm.templatevm.TemplateVM,
|
||||||
|
label='green',
|
||||||
|
provides_network=False,
|
||||||
|
**common_props)
|
||||||
|
testproxy_props = common_props.copy()
|
||||||
|
testproxy_props.update(
|
||||||
|
label='red',
|
||||||
|
provides_network=True,
|
||||||
|
memory=200,
|
||||||
|
template=self.app.default_template.name,
|
||||||
|
)
|
||||||
|
self.assertRestored("test-testproxy",
|
||||||
|
klass=qubes.vm.appvm.AppVM,
|
||||||
|
**testproxy_props)
|
||||||
|
self.assertRestored("test-work",
|
||||||
|
klass=qubes.vm.appvm.AppVM,
|
||||||
|
template=self.app.default_template.name,
|
||||||
|
label='green',
|
||||||
|
**common_props)
|
||||||
|
self.assertRestored("test-standalonevm",
|
||||||
|
klass=qubes.vm.standalonevm.StandaloneVM,
|
||||||
|
label='blue',
|
||||||
|
**common_props)
|
||||||
|
self.assertRestored("test-custom-template-appvm",
|
||||||
|
klass=qubes.vm.appvm.AppVM,
|
||||||
|
template='test-template-clone',
|
||||||
|
label='yellow',
|
||||||
|
**common_props)
|
||||||
|
|
||||||
def test_220_r2_encrypted(self):
|
def test_220_r2_encrypted(self):
|
||||||
self.create_v3_backup(True)
|
self.create_v3_backup(True)
|
||||||
@ -475,13 +598,59 @@ class TC_00_BackupCompatibility(
|
|||||||
'use-default-template': True,
|
'use-default-template': True,
|
||||||
'use-default-netvm': True,
|
'use-default-netvm': True,
|
||||||
})
|
})
|
||||||
with self.assertNotRaises(KeyError):
|
common_props = {
|
||||||
vm = self.app.domains["test-template-clone"]
|
'installed_by_rpm': False,
|
||||||
vm = self.app.domains["test-testproxy"]
|
'kernel': qubes.property.DEFAULT,
|
||||||
vm = self.app.domains["test-work"]
|
'kernelopts': qubes.property.DEFAULT,
|
||||||
vm = self.app.domains["test-testhvm"]
|
'qrexec_timeout': qubes.property.DEFAULT,
|
||||||
vm = self.app.domains["test-standalonevm"]
|
'netvm': qubes.property.DEFAULT,
|
||||||
vm = self.app.domains["test-custom-template-appvm"]
|
'default_user': qubes.property.DEFAULT,
|
||||||
self.assertEqual(self.app.domains["test-custom-template-appvm"]
|
'internal': qubes.property.DEFAULT,
|
||||||
.template,
|
'include_in_backups': True,
|
||||||
self.app.domains["test-template-clone"])
|
'debug': False,
|
||||||
|
'maxmem': 1535, # 4063 caped by 10*400
|
||||||
|
'memory': 400,
|
||||||
|
}
|
||||||
|
self.assertRestored("test-template-clone",
|
||||||
|
klass=qubes.vm.templatevm.TemplateVM,
|
||||||
|
label='green',
|
||||||
|
provides_network=False,
|
||||||
|
**common_props)
|
||||||
|
testproxy_props = common_props.copy()
|
||||||
|
testproxy_props.update(
|
||||||
|
label='red',
|
||||||
|
provides_network=True,
|
||||||
|
memory=200,
|
||||||
|
template=self.app.default_template.name,
|
||||||
|
)
|
||||||
|
self.assertRestored("test-testproxy",
|
||||||
|
klass=qubes.vm.appvm.AppVM,
|
||||||
|
**testproxy_props)
|
||||||
|
self.assertRestored("test-work",
|
||||||
|
klass=qubes.vm.appvm.AppVM,
|
||||||
|
template=self.app.default_template.name,
|
||||||
|
label='green',
|
||||||
|
**common_props)
|
||||||
|
self.assertRestored("test-standalonevm",
|
||||||
|
klass=qubes.vm.standalonevm.StandaloneVM,
|
||||||
|
label='blue',
|
||||||
|
**common_props)
|
||||||
|
self.assertRestored("test-custom-template-appvm",
|
||||||
|
klass=qubes.vm.appvm.AppVM,
|
||||||
|
template='test-template-clone',
|
||||||
|
label='yellow',
|
||||||
|
**common_props)
|
||||||
|
|
||||||
|
|
||||||
|
class TC_01_BackupCompatibilityIntoLVM(TC_00_BackupCompatibility):
|
||||||
|
def setUp(self):
|
||||||
|
super(TC_01_BackupCompatibilityIntoLVM, self).setUp()
|
||||||
|
self.init_lvm_pool()
|
||||||
|
|
||||||
|
def restore_backup(self, source=None, appvm=None, options=None,
|
||||||
|
expect_errors=None, manipulate_restore_info=None):
|
||||||
|
if options is None:
|
||||||
|
options = {}
|
||||||
|
options['override_pool'] = self.pool.name
|
||||||
|
super(TC_01_BackupCompatibilityIntoLVM, self).restore_backup(source,
|
||||||
|
appvm, options, expect_errors, manipulate_restore_info)
|
||||||
|
@ -83,6 +83,7 @@ Requires: createrepo
|
|||||||
Requires: gnome-packagekit
|
Requires: gnome-packagekit
|
||||||
Requires: cronie
|
Requires: cronie
|
||||||
Requires: bsdtar
|
Requires: bsdtar
|
||||||
|
Requires: scrypt
|
||||||
# for qubes-hcl-report
|
# for qubes-hcl-report
|
||||||
Requires: dmidecode
|
Requires: dmidecode
|
||||||
Requires: PyQt4
|
Requires: PyQt4
|
||||||
|
Loading…
Reference in New Issue
Block a user