Merge branch 'devel20200705'
* devel20200705: tests: skip gnome-terminal on xfce template flavor tests: fix FD leak in qrexec test tests: switch default LVM pool to qubes_dom0/vm-pool backup: fix error handler for scrypt errors Adjust code for possibly coroutine Volume.export() and Volume.export_end() storage: add Volume.export_end() function backup: add support for calling a function after backing up a file/volume backup: call volume.export() just before actually extracting it vm/dispvm: place all volumes in the same pool as DispVM's template tests: extend TestPool storage driver to make create_on_disk working storage: pass a copy of volume_config to pool.init_volume tests: cleanup properly in wait_on_fail decorator
This commit is contained in:
commit
f30eebc40e
@ -99,6 +99,10 @@ Methods and properties required to be implemented by the volume class:
|
|||||||
- :py:meth:`~qubes.storage.Volume.export` - return a path to be read to extract
|
- :py:meth:`~qubes.storage.Volume.export` - return a path to be read to extract
|
||||||
volume data; for complex formats, this can be a pipe (connected to some
|
volume data; for complex formats, this can be a pipe (connected to some
|
||||||
data-extracting process)
|
data-extracting process)
|
||||||
|
- :py:meth:`~qubes.storage.Volume.export_end` - cleanup after exporting the
|
||||||
|
data; this function is called when the path returned by
|
||||||
|
:py:meth:`~qubes.storage.Volume.export` is not used anymore. This method
|
||||||
|
optional - some storage drivers may not implement it if not needed.
|
||||||
- :py:meth:`~qubes.storage.Volume.import_data` - return a path the data should
|
- :py:meth:`~qubes.storage.Volume.import_data` - return a path the data should
|
||||||
be written to, to import volume data; for complex formats, this can be pipe
|
be written to, to import volume data; for complex formats, this can be pipe
|
||||||
(connected to some data-importing process)
|
(connected to some data-importing process)
|
||||||
|
@ -252,12 +252,30 @@ class Backup:
|
|||||||
# pylint: disable=too-many-instance-attributes
|
# pylint: disable=too-many-instance-attributes
|
||||||
class FileToBackup:
|
class FileToBackup:
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
def __init__(self, file_path, subdir=None, name=None, size=None):
|
def __init__(self, file_path_or_func, subdir=None, name=None, size=None,
|
||||||
|
cleanup_func=None):
|
||||||
|
"""Store a single file to backup
|
||||||
|
|
||||||
|
:param file_path_or_func: path to the file or a function
|
||||||
|
returning one; in case of function, it can be a coroutine;
|
||||||
|
if a function is given, *name*, *subdir* and *size* needs to be
|
||||||
|
given too
|
||||||
|
:param subdir: directory in a backup archive to place file in
|
||||||
|
:param name: name of the file in the backup archive
|
||||||
|
:param size: size
|
||||||
|
:param cleanup_func: function to call after processing the file;
|
||||||
|
the function will get the file path as an argument
|
||||||
|
"""
|
||||||
|
if callable(file_path_or_func):
|
||||||
|
assert subdir is not None \
|
||||||
|
and name is not None \
|
||||||
|
and size is not None
|
||||||
|
|
||||||
if size is None:
|
if size is None:
|
||||||
size = qubes.storage.file.get_disk_usage(file_path)
|
size = qubes.storage.file.get_disk_usage(file_path_or_func)
|
||||||
|
|
||||||
if subdir is None:
|
if subdir is None:
|
||||||
abs_file_path = os.path.abspath(file_path)
|
abs_file_path = os.path.abspath(file_path_or_func)
|
||||||
abs_base_dir = os.path.abspath(
|
abs_base_dir = os.path.abspath(
|
||||||
qubes.config.system_path["qubes_base_dir"]) + '/'
|
qubes.config.system_path["qubes_base_dir"]) + '/'
|
||||||
abs_file_dir = os.path.dirname(abs_file_path) + '/'
|
abs_file_dir = os.path.dirname(abs_file_path) + '/'
|
||||||
@ -269,16 +287,19 @@ class Backup:
|
|||||||
if subdir and not subdir.endswith('/'):
|
if subdir and not subdir.endswith('/'):
|
||||||
subdir += '/'
|
subdir += '/'
|
||||||
|
|
||||||
#: real path to the file
|
if name is None:
|
||||||
self.path = file_path
|
name = os.path.basename(file_path_or_func)
|
||||||
|
|
||||||
|
#: real path to the file (or callable to get one)
|
||||||
|
self.path = file_path_or_func
|
||||||
#: size of the file
|
#: size of the file
|
||||||
self.size = size
|
self.size = size
|
||||||
#: directory in backup archive where file should be placed
|
#: directory in backup archive where file should be placed
|
||||||
self.subdir = subdir
|
self.subdir = subdir
|
||||||
#: use this name in the archive (aka rename)
|
#: use this name in the archive (aka rename)
|
||||||
self.name = os.path.basename(file_path)
|
|
||||||
if name is not None:
|
|
||||||
self.name = name
|
self.name = name
|
||||||
|
#: function to call after processing the file
|
||||||
|
self.cleanup_func = cleanup_func
|
||||||
|
|
||||||
class VMToBackup:
|
class VMToBackup:
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
@ -370,10 +391,11 @@ class Backup:
|
|||||||
if not volume.save_on_stop:
|
if not volume.save_on_stop:
|
||||||
continue
|
continue
|
||||||
vm_files.append(self.FileToBackup(
|
vm_files.append(self.FileToBackup(
|
||||||
volume.export(),
|
volume.export,
|
||||||
subdir,
|
subdir,
|
||||||
name + '.img',
|
name + '.img',
|
||||||
volume.usage))
|
volume.usage,
|
||||||
|
cleanup_func=volume.export_end))
|
||||||
|
|
||||||
vm_files.extend(self.FileToBackup(i, subdir)
|
vm_files.extend(self.FileToBackup(i, subdir)
|
||||||
for i in vm.fire_event('backup-get-files'))
|
for i in vm.fire_event('backup-get-files'))
|
||||||
@ -514,10 +536,9 @@ class Backup:
|
|||||||
if retcode:
|
if retcode:
|
||||||
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())
|
+ (yield from scrypt.stderr.read()).decode())
|
||||||
return HEADER_FILENAME, HEADER_FILENAME + ".hmac"
|
return HEADER_FILENAME, HEADER_FILENAME + ".hmac"
|
||||||
|
|
||||||
|
|
||||||
def _send_progress_update(self):
|
def _send_progress_update(self):
|
||||||
if not self.total_backup_bytes:
|
if not self.total_backup_bytes:
|
||||||
return
|
return
|
||||||
@ -610,18 +631,21 @@ class Backup:
|
|||||||
# Files will be verified before untaring this.
|
# Files will be verified before untaring this.
|
||||||
# Prefix the path in archive with filename["subdir"] to have it
|
# Prefix the path in archive with filename["subdir"] to have it
|
||||||
# verified during untar
|
# verified during untar
|
||||||
|
path = file_info.path
|
||||||
|
if callable(path):
|
||||||
|
path = yield from qubes.utils.coro_maybe(path())
|
||||||
tar_cmdline = (["tar", "-Pc", '--sparse',
|
tar_cmdline = (["tar", "-Pc", '--sparse',
|
||||||
'-C', os.path.dirname(file_info.path)] +
|
'-C', os.path.dirname(path)] +
|
||||||
(['--dereference'] if
|
(['--dereference'] if
|
||||||
file_info.subdir != "dom0-home/" else []) +
|
file_info.subdir != "dom0-home/" else []) +
|
||||||
['--xform=s:^%s:%s\\0:' % (
|
['--xform=s:^%s:%s\\0:' % (
|
||||||
os.path.basename(file_info.path),
|
os.path.basename(path),
|
||||||
file_info.subdir),
|
file_info.subdir),
|
||||||
os.path.basename(file_info.path)
|
os.path.basename(path)
|
||||||
])
|
])
|
||||||
file_stat = os.stat(file_info.path)
|
file_stat = os.stat(path)
|
||||||
if stat.S_ISBLK(file_stat.st_mode) or \
|
if stat.S_ISBLK(file_stat.st_mode) or \
|
||||||
file_info.name != os.path.basename(file_info.path):
|
file_info.name != os.path.basename(path):
|
||||||
# tar doesn't handle content of block device, use our
|
# tar doesn't handle content of block device, use our
|
||||||
# writer
|
# writer
|
||||||
# also use our tar writer when renaming file
|
# also use our tar writer when renaming file
|
||||||
@ -631,7 +655,7 @@ class Backup:
|
|||||||
'--override-name=%s' % (
|
'--override-name=%s' % (
|
||||||
os.path.join(file_info.subdir, os.path.basename(
|
os.path.join(file_info.subdir, os.path.basename(
|
||||||
file_info.name))),
|
file_info.name))),
|
||||||
file_info.path]
|
path]
|
||||||
if self.compressed:
|
if self.compressed:
|
||||||
tar_cmdline.insert(-2,
|
tar_cmdline.insert(-2,
|
||||||
"--use-compress-program=%s" % self.compression_filter)
|
"--use-compress-program=%s" % self.compression_filter)
|
||||||
@ -655,6 +679,10 @@ class Backup:
|
|||||||
except ProcessLookupError:
|
except ProcessLookupError:
|
||||||
pass
|
pass
|
||||||
raise
|
raise
|
||||||
|
finally:
|
||||||
|
if file_info.cleanup_func is not None:
|
||||||
|
yield from qubes.utils.coro_maybe(
|
||||||
|
file_info.cleanup_func(path))
|
||||||
|
|
||||||
yield from tar_sparse.wait()
|
yield from tar_sparse.wait()
|
||||||
if tar_sparse.returncode:
|
if tar_sparse.returncode:
|
||||||
|
@ -203,9 +203,24 @@ class Volume:
|
|||||||
volume data. If extracting volume data require something more
|
volume data. If extracting volume data require something more
|
||||||
than just reading from file (for example connecting to some other
|
than just reading from file (for example connecting to some other
|
||||||
domain, or decompressing the data), the returned path may be a pipe.
|
domain, or decompressing the data), the returned path may be a pipe.
|
||||||
|
|
||||||
|
This can be implemented as a coroutine.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
raise self._not_implemented("export")
|
raise self._not_implemented("export")
|
||||||
|
|
||||||
|
def export_end(self, path):
|
||||||
|
""" Cleanup after exporting data.
|
||||||
|
|
||||||
|
This method is called after exporting the volume data (using
|
||||||
|
:py:meth:`export`), when the *path* is not needed anymore.
|
||||||
|
|
||||||
|
This can be implemented as a coroutine.
|
||||||
|
|
||||||
|
:param path: path to cleanup, returned by :py:meth:`export`
|
||||||
|
"""
|
||||||
|
# do nothing by default (optional method)
|
||||||
|
|
||||||
def import_data(self, size):
|
def import_data(self, size):
|
||||||
''' Returns a path to overwrite volume data.
|
''' Returns a path to overwrite volume data.
|
||||||
|
|
||||||
@ -423,7 +438,7 @@ class Storage:
|
|||||||
if 'internal' in volume_config:
|
if 'internal' in volume_config:
|
||||||
# migrate old config
|
# migrate old config
|
||||||
del volume_config['internal']
|
del volume_config['internal']
|
||||||
volume = pool.init_volume(self.vm, volume_config)
|
volume = pool.init_volume(self.vm, volume_config.copy())
|
||||||
self.vm.volumes[name] = volume
|
self.vm.volumes[name] = volume
|
||||||
return volume
|
return volume
|
||||||
|
|
||||||
@ -634,14 +649,27 @@ class Storage:
|
|||||||
for target in parsed_xml.xpath(
|
for target in parsed_xml.xpath(
|
||||||
"//domain/devices/disk/target")}
|
"//domain/devices/disk/target")}
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def export(self, volume):
|
def export(self, volume):
|
||||||
''' Helper function to export volume (pool.export(volume))'''
|
''' Helper function to export volume (pool.export(volume))'''
|
||||||
assert isinstance(volume, (Volume, str)), \
|
assert isinstance(volume, (Volume, str)), \
|
||||||
"You need to pass a Volume or pool name as str"
|
"You need to pass a Volume or pool name as str"
|
||||||
if isinstance(volume, Volume):
|
if not isinstance(volume, Volume):
|
||||||
return volume.export()
|
volume = self.vm.volumes[volume]
|
||||||
|
return (yield from qubes.utils.coro_maybe(volume.export()))
|
||||||
|
|
||||||
return self.vm.volumes[volume].export()
|
@asyncio.coroutine
|
||||||
|
def export_end(self, volume, export_path):
|
||||||
|
""" Cleanup after exporting data from the volume
|
||||||
|
|
||||||
|
:param volume: volume that was exported
|
||||||
|
:param export_path: path returned by the export() call
|
||||||
|
"""
|
||||||
|
assert isinstance(volume, (Volume, str)), \
|
||||||
|
"You need to pass a Volume or pool name as str"
|
||||||
|
if not isinstance(volume, Volume):
|
||||||
|
volume = self.vm.volumes[volume]
|
||||||
|
yield from qubes.utils.coro_maybe(volume.export_end(export_path))
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def import_data(self, volume, size):
|
def import_data(self, volume, size):
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
''' This module contains pool implementations backed by file images'''
|
''' This module contains pool implementations backed by file images'''
|
||||||
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import re
|
import re
|
||||||
@ -29,6 +29,7 @@ import subprocess
|
|||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
|
||||||
import qubes.storage
|
import qubes.storage
|
||||||
|
import qubes.utils
|
||||||
|
|
||||||
BLKSIZE = 512
|
BLKSIZE = 512
|
||||||
|
|
||||||
@ -268,6 +269,7 @@ class FileVolume(qubes.storage.Volume):
|
|||||||
# if domain is running
|
# if domain is running
|
||||||
return self.path
|
return self.path
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def import_volume(self, src_volume):
|
def import_volume(self, src_volume):
|
||||||
if src_volume.snap_on_start:
|
if src_volume.snap_on_start:
|
||||||
raise qubes.storage.StoragePoolException(
|
raise qubes.storage.StoragePoolException(
|
||||||
@ -275,7 +277,11 @@ class FileVolume(qubes.storage.Volume):
|
|||||||
src_volume, self))
|
src_volume, self))
|
||||||
if self.save_on_stop:
|
if self.save_on_stop:
|
||||||
_remove_if_exists(self.path)
|
_remove_if_exists(self.path)
|
||||||
copy_file(src_volume.export(), self.path)
|
path = yield from qubes.utils.coro_maybe(src_volume.export())
|
||||||
|
try:
|
||||||
|
copy_file(path, self.path)
|
||||||
|
finally:
|
||||||
|
yield from qubes.utils.coro_maybe(src_volume.export_end(path))
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def import_data(self, size):
|
def import_data(self, size):
|
||||||
|
@ -518,7 +518,8 @@ class ThinVolume(qubes.storage.Volume):
|
|||||||
self._vid_import.split('/')[1],
|
self._vid_import.split('/')[1],
|
||||||
str(src_volume.size)]
|
str(src_volume.size)]
|
||||||
yield from qubes_lvm_coro(cmd, self.log)
|
yield from qubes_lvm_coro(cmd, self.log)
|
||||||
src_path = src_volume.export()
|
src_path = yield from qubes.utils.coro_maybe(src_volume.export())
|
||||||
|
try:
|
||||||
cmd = ['dd', 'if=' + src_path, 'of=/dev/' + self._vid_import,
|
cmd = ['dd', 'if=' + src_path, 'of=/dev/' + self._vid_import,
|
||||||
'conv=sparse', 'status=none', 'bs=128K']
|
'conv=sparse', 'status=none', 'bs=128K']
|
||||||
if not os.access('/dev/' + self._vid_import, os.W_OK) or \
|
if not os.access('/dev/' + self._vid_import, os.W_OK) or \
|
||||||
@ -527,6 +528,9 @@ class ThinVolume(qubes.storage.Volume):
|
|||||||
|
|
||||||
p = yield from asyncio.create_subprocess_exec(*cmd)
|
p = yield from asyncio.create_subprocess_exec(*cmd)
|
||||||
yield from p.wait()
|
yield from p.wait()
|
||||||
|
finally:
|
||||||
|
yield from qubes.utils.coro_maybe(
|
||||||
|
src_volume.export_end(src_path))
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
cmd = ['remove', self._vid_import]
|
cmd = ['remove', self._vid_import]
|
||||||
yield from qubes_lvm_coro(cmd, self.log)
|
yield from qubes_lvm_coro(cmd, self.log)
|
||||||
|
@ -35,6 +35,7 @@ import tempfile
|
|||||||
from contextlib import contextmanager, suppress
|
from contextlib import contextmanager, suppress
|
||||||
|
|
||||||
import qubes.storage
|
import qubes.storage
|
||||||
|
import qubes.utils
|
||||||
|
|
||||||
FICLONE = 1074041865 # defined in <linux/fs.h>, assuming sizeof(int)==4
|
FICLONE = 1074041865 # defined in <linux/fs.h>, assuming sizeof(int)==4
|
||||||
LOOP_SET_CAPACITY = 0x4C07 # defined in <linux/loop.h>
|
LOOP_SET_CAPACITY = 0x4C07 # defined in <linux/loop.h>
|
||||||
@ -297,15 +298,22 @@ class ReflinkVolume(qubes.storage.Volume):
|
|||||||
_import_data_end))
|
_import_data_end))
|
||||||
|
|
||||||
@qubes.storage.Volume.locked
|
@qubes.storage.Volume.locked
|
||||||
@_coroutinized
|
@asyncio.coroutine
|
||||||
def import_volume(self, src_volume):
|
def import_volume(self, src_volume):
|
||||||
if self.save_on_stop:
|
if self.save_on_stop:
|
||||||
try:
|
try:
|
||||||
success = False
|
success = False
|
||||||
_copy_file(src_volume.export(), self._path_import)
|
src_path = yield from qubes.utils.coro_maybe(
|
||||||
|
src_volume.export())
|
||||||
|
try:
|
||||||
|
yield from _coroutinized(_copy_file)(
|
||||||
|
src_path, self._path_import)
|
||||||
|
finally:
|
||||||
|
yield from qubes.utils.coro_maybe(
|
||||||
|
src_volume.export_end(src_path))
|
||||||
success = True
|
success = True
|
||||||
finally:
|
finally:
|
||||||
self._import_data_end(success)
|
yield from _coroutinized(self._import_data_end)(success)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def _path_revision(self, number, timestamp=None):
|
def _path_revision(self, number, timestamp=None):
|
||||||
|
@ -251,6 +251,7 @@ def wait_on_fail(func):
|
|||||||
lambda: asyncio.StreamReaderProtocol(reader),
|
lambda: asyncio.StreamReaderProtocol(reader),
|
||||||
sys.stdin))
|
sys.stdin))
|
||||||
self.loop.run_until_complete(reader.readline())
|
self.loop.run_until_complete(reader.readline())
|
||||||
|
transport.close()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
@ -36,6 +36,7 @@ import qubes.exc
|
|||||||
import qubes.storage.lvm
|
import qubes.storage.lvm
|
||||||
import qubes.tests
|
import qubes.tests
|
||||||
import qubes.tests.storage_lvm
|
import qubes.tests.storage_lvm
|
||||||
|
import qubes.utils
|
||||||
import qubes.vm
|
import qubes.vm
|
||||||
import qubes.vm.appvm
|
import qubes.vm.appvm
|
||||||
import qubes.vm.templatevm
|
import qubes.vm.templatevm
|
||||||
@ -106,6 +107,13 @@ class BackupTestsMixin(object):
|
|||||||
|
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
|
def fill_image_vm(self, vm, volume, size=None, sparse=False):
|
||||||
|
path = self.loop.run_until_complete(vm.storage.export(volume))
|
||||||
|
try:
|
||||||
|
self.fill_image(path, size=size, sparse=sparse)
|
||||||
|
finally:
|
||||||
|
self.loop.run_until_complete(vm.storage.export_end(volume, path))
|
||||||
|
|
||||||
# NOTE: this was create_basic_vms
|
# NOTE: this was create_basic_vms
|
||||||
def create_backup_vms(self, pool=None):
|
def create_backup_vms(self, pool=None):
|
||||||
template = self.app.default_template
|
template = self.app.default_template
|
||||||
@ -120,7 +128,7 @@ class BackupTestsMixin(object):
|
|||||||
testnet.create_on_disk(pool=pool))
|
testnet.create_on_disk(pool=pool))
|
||||||
testnet.features['service.ntpd'] = True
|
testnet.features['service.ntpd'] = True
|
||||||
vms.append(testnet)
|
vms.append(testnet)
|
||||||
self.fill_image(testnet.storage.export('private'), 20*1024*1024)
|
self.fill_image_vm(testnet, 'private', 20*1024*1024)
|
||||||
|
|
||||||
vmname = self.make_vm_name('test1')
|
vmname = self.make_vm_name('test1')
|
||||||
self.log.debug("Creating %s" % vmname)
|
self.log.debug("Creating %s" % vmname)
|
||||||
@ -130,7 +138,7 @@ class BackupTestsMixin(object):
|
|||||||
self.loop.run_until_complete(
|
self.loop.run_until_complete(
|
||||||
testvm1.create_on_disk(pool=pool))
|
testvm1.create_on_disk(pool=pool))
|
||||||
vms.append(testvm1)
|
vms.append(testvm1)
|
||||||
self.fill_image(testvm1.storage.export('private'), 100 * 1024 * 1024)
|
self.fill_image_vm(testvm1, 'private', 100 * 1024 * 1024)
|
||||||
|
|
||||||
vmname = self.make_vm_name('testhvm1')
|
vmname = self.make_vm_name('testhvm1')
|
||||||
self.log.debug("Creating %s" % vmname)
|
self.log.debug("Creating %s" % vmname)
|
||||||
@ -140,8 +148,7 @@ class BackupTestsMixin(object):
|
|||||||
label='red')
|
label='red')
|
||||||
self.loop.run_until_complete(
|
self.loop.run_until_complete(
|
||||||
testvm2.create_on_disk(pool=pool))
|
testvm2.create_on_disk(pool=pool))
|
||||||
self.fill_image(testvm2.storage.export('root'), 1024 * 1024 * 1024, \
|
self.fill_image_vm(testvm2, 'root', 1024 * 1024 * 1024, True)
|
||||||
True)
|
|
||||||
vms.append(testvm2)
|
vms.append(testvm2)
|
||||||
|
|
||||||
vmname = self.make_vm_name('template')
|
vmname = self.make_vm_name('template')
|
||||||
@ -150,7 +157,7 @@ class BackupTestsMixin(object):
|
|||||||
name=vmname, label='red')
|
name=vmname, label='red')
|
||||||
self.loop.run_until_complete(
|
self.loop.run_until_complete(
|
||||||
testvm3.create_on_disk(pool=pool))
|
testvm3.create_on_disk(pool=pool))
|
||||||
self.fill_image(testvm3.storage.export('root'), 100 * 1024 * 1024, True)
|
self.fill_image_vm(testvm3, 'root', 100 * 1024 * 1024, True)
|
||||||
vms.append(testvm3)
|
vms.append(testvm3)
|
||||||
|
|
||||||
vmname = self.make_vm_name('custom')
|
vmname = self.make_vm_name('custom')
|
||||||
@ -262,11 +269,14 @@ class BackupTestsMixin(object):
|
|||||||
for name, volume in vm.volumes.items():
|
for name, volume in vm.volumes.items():
|
||||||
if not volume.rw or not volume.save_on_stop:
|
if not volume.rw or not volume.save_on_stop:
|
||||||
continue
|
continue
|
||||||
vol_path = volume.export()
|
vol_path = self.loop.run_until_complete(
|
||||||
|
qubes.utils.coro_maybe(volume.export()))
|
||||||
hasher = hashlib.sha1()
|
hasher = hashlib.sha1()
|
||||||
with open(vol_path, 'rb') as afile:
|
with open(vol_path, 'rb') as afile:
|
||||||
for buf in iter(lambda: afile.read(4096000), b''):
|
for buf in iter(lambda: afile.read(4096000), b''):
|
||||||
hasher.update(buf)
|
hasher.update(buf)
|
||||||
|
self.loop.run_until_complete(
|
||||||
|
qubes.utils.coro_maybe(volume.export_end(vol_path)))
|
||||||
hashes[vm.name][name] = hasher.hexdigest()
|
hashes[vm.name][name] = hasher.hexdigest()
|
||||||
return hashes
|
return hashes
|
||||||
|
|
||||||
@ -382,9 +392,9 @@ class TC_00_Backup(BackupTestsMixin, qubes.tests.SystemTestCase):
|
|||||||
self.fill_image(
|
self.fill_image(
|
||||||
os.path.join(self.hvmtemplate.dir_path, '00file'),
|
os.path.join(self.hvmtemplate.dir_path, '00file'),
|
||||||
195 * 1024 * 1024 - 4096 * 3)
|
195 * 1024 * 1024 - 4096 * 3)
|
||||||
self.fill_image(self.hvmtemplate.storage.export('private'),
|
self.fill_image_vm(self.hvmtemplate, 'private',
|
||||||
195 * 1024 * 1024 - 4096 * 3)
|
195 * 1024 * 1024 - 4096 * 3)
|
||||||
self.fill_image(self.hvmtemplate.storage.export('root'), 1024 * 1024 * 1024,
|
self.fill_image_vm(self.hvmtemplate, 'root', 1024 * 1024 * 1024,
|
||||||
sparse=True)
|
sparse=True)
|
||||||
vms.append(self.hvmtemplate)
|
vms.append(self.hvmtemplate)
|
||||||
self.app.save()
|
self.app.save()
|
||||||
|
@ -518,9 +518,14 @@ class TC_03_QvmRevertTemplateChanges(qubes.tests.SystemTestCase):
|
|||||||
self.app.save()
|
self.app.save()
|
||||||
|
|
||||||
def get_rootimg_checksum(self):
|
def get_rootimg_checksum(self):
|
||||||
return subprocess.check_output(
|
path = self.loop.run_until_complete(
|
||||||
['sha1sum', self.test_template.volumes['root'].export()]).\
|
self.test_template.storage.export('root'))
|
||||||
|
try:
|
||||||
|
return subprocess.check_output(['sha1sum', path]).\
|
||||||
decode().split(' ')[0]
|
decode().split(' ')[0]
|
||||||
|
finally:
|
||||||
|
self.loop.run_until_complete(
|
||||||
|
self.test_template.storage.export_end('root', path))
|
||||||
|
|
||||||
def _do_test(self):
|
def _do_test(self):
|
||||||
checksum_before = self.get_rootimg_checksum()
|
checksum_before = self.get_rootimg_checksum()
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import contextlib
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@ -585,6 +586,10 @@ class TC_00_QrexecMixin(object):
|
|||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
self.fail(
|
self.fail(
|
||||||
"service timeout, probably EOF wasn't transferred from the VM process")
|
"service timeout, probably EOF wasn't transferred from the VM process")
|
||||||
|
finally:
|
||||||
|
with contextlib.suppress(ProcessLookupError):
|
||||||
|
p.terminate()
|
||||||
|
self.loop.run_until_complete(p.wait())
|
||||||
|
|
||||||
self.assertEqual(stdout, b'test\n',
|
self.assertEqual(stdout, b'test\n',
|
||||||
'Received data differs from what was expected')
|
'Received data differs from what was expected')
|
||||||
@ -633,6 +638,10 @@ class TC_00_QrexecMixin(object):
|
|||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
self.fail(
|
self.fail(
|
||||||
"service timeout, probably EOF wasn't transferred from the VM process")
|
"service timeout, probably EOF wasn't transferred from the VM process")
|
||||||
|
finally:
|
||||||
|
with contextlib.suppress(ProcessLookupError):
|
||||||
|
p.terminate()
|
||||||
|
self.loop.run_until_complete(p.wait())
|
||||||
|
|
||||||
service_descriptor = b'test.Socket+ test-inst-vm1 keyword adminvm\0'
|
service_descriptor = b'test.Socket+ test-inst-vm1 keyword adminvm\0'
|
||||||
self.assertEqual(service_stdout, service_descriptor + b'test1test2',
|
self.assertEqual(service_stdout, service_descriptor + b'test1test2',
|
||||||
|
@ -101,6 +101,8 @@ class TC_00_AppVMMixin(object):
|
|||||||
self.skipTest("Minimal template doesn't have 'gnome-terminal'")
|
self.skipTest("Minimal template doesn't have 'gnome-terminal'")
|
||||||
if 'whonix' in self.template:
|
if 'whonix' in self.template:
|
||||||
self.skipTest("Whonix template doesn't have 'gnome-terminal'")
|
self.skipTest("Whonix template doesn't have 'gnome-terminal'")
|
||||||
|
if 'xfce' in self.template:
|
||||||
|
self.skipTest("Xfce template doesn't have 'gnome-terminal'")
|
||||||
self.loop.run_until_complete(self.testvm1.start())
|
self.loop.run_until_complete(self.testvm1.start())
|
||||||
self.assertEqual(self.testvm1.get_power_state(), "Running")
|
self.assertEqual(self.testvm1.get_power_state(), "Running")
|
||||||
self.loop.run_until_complete(self.wait_for_session(self.testvm1))
|
self.loop.run_until_complete(self.wait_for_session(self.testvm1))
|
||||||
|
@ -40,7 +40,7 @@ from qubes.storage.lvm import ThinPool, ThinVolume, qubes_lvm
|
|||||||
if 'DEFAULT_LVM_POOL' in os.environ.keys():
|
if 'DEFAULT_LVM_POOL' in os.environ.keys():
|
||||||
DEFAULT_LVM_POOL = os.environ['DEFAULT_LVM_POOL']
|
DEFAULT_LVM_POOL = os.environ['DEFAULT_LVM_POOL']
|
||||||
else:
|
else:
|
||||||
DEFAULT_LVM_POOL = 'qubes_dom0/pool00'
|
DEFAULT_LVM_POOL = 'qubes_dom0/vm-pool'
|
||||||
|
|
||||||
|
|
||||||
def lvm_pool_exists(volume_group, thin_pool):
|
def lvm_pool_exists(volume_group, thin_pool):
|
||||||
|
@ -51,11 +51,24 @@ class TestVM(object):
|
|||||||
def is_running(self):
|
def is_running(self):
|
||||||
return self.running
|
return self.running
|
||||||
|
|
||||||
|
class TestVolume(qubes.storage.Volume):
|
||||||
|
def create(self):
|
||||||
|
pass
|
||||||
|
|
||||||
class TestPool(qubes.storage.Pool):
|
class TestPool(qubes.storage.Pool):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(TestPool, self).__init__(*args, **kwargs)
|
||||||
|
self._volumes = {}
|
||||||
|
|
||||||
def init_volume(self, vm, volume_config):
|
def init_volume(self, vm, volume_config):
|
||||||
vid = '{}/{}'.format(vm.name, volume_config['name'])
|
vid = '{}/{}'.format(vm.name, volume_config['name'])
|
||||||
assert volume_config.pop('pool', None) == self
|
assert volume_config.pop('pool', None) == self
|
||||||
return qubes.storage.Volume(vid=vid, pool=self, **volume_config)
|
vol = TestVolume(vid=vid, pool=self, **volume_config)
|
||||||
|
self._volumes[vid] = vol
|
||||||
|
return vol
|
||||||
|
|
||||||
|
def get_volume(self, vid):
|
||||||
|
return self._volumes[vid]
|
||||||
|
|
||||||
|
|
||||||
class TC_90_AppVM(qubes.tests.vm.qubesvm.QubesVMTestsMixin,
|
class TC_90_AppVM(qubes.tests.vm.qubesvm.QubesVMTestsMixin,
|
||||||
|
@ -166,3 +166,28 @@ class TC_00_DispVM(qubes.tests.QubesTestCase):
|
|||||||
self.app.add_new_vm(qubes.vm.dispvm.DispVM,
|
self.app.add_new_vm(qubes.vm.dispvm.DispVM,
|
||||||
name='test-dispvm', template=self.appvm)
|
name='test-dispvm', template=self.appvm)
|
||||||
self.assertFalse(mock_domains.get_new_unused_dispid.called)
|
self.assertFalse(mock_domains.get_new_unused_dispid.called)
|
||||||
|
|
||||||
|
@mock.patch('os.symlink')
|
||||||
|
@mock.patch('os.makedirs')
|
||||||
|
def test_020_copy_storage_pool(self, mock_makedirs, mock_symlink):
|
||||||
|
self.app.pools['alternative'] = qubes.tests.vm.appvm.TestPool(name='alternative')
|
||||||
|
self.appvm.template_for_dispvms = True
|
||||||
|
self.loop.run_until_complete(self.template.create_on_disk())
|
||||||
|
self.loop.run_until_complete(self.appvm.create_on_disk(pool='alternative'))
|
||||||
|
orig_getitem = self.app.domains.__getitem__
|
||||||
|
with mock.patch.object(self.app, 'domains', wraps=self.app.domains) \
|
||||||
|
as mock_domains:
|
||||||
|
mock_domains.configure_mock(**{
|
||||||
|
'get_new_unused_dispid': mock.Mock(return_value=42),
|
||||||
|
'__getitem__.side_effect': orig_getitem
|
||||||
|
})
|
||||||
|
dispvm = self.app.add_new_vm(qubes.vm.dispvm.DispVM,
|
||||||
|
name='test-dispvm', template=self.appvm)
|
||||||
|
self.loop.run_until_complete(dispvm.create_on_disk())
|
||||||
|
self.assertEqual(dispvm.template, self.appvm)
|
||||||
|
self.assertEqual(dispvm.volumes['private'].pool,
|
||||||
|
self.appvm.volumes['private'].pool)
|
||||||
|
self.assertEqual(dispvm.volumes['root'].pool,
|
||||||
|
self.appvm.volumes['root'].pool)
|
||||||
|
self.assertEqual(dispvm.volumes['volatile'].pool,
|
||||||
|
self.appvm.volumes['volatile'].pool)
|
||||||
|
@ -109,6 +109,12 @@ class DispVM(qubes.vm.qubesvm.QubesVM):
|
|||||||
self.volume_config[name] = config.copy()
|
self.volume_config[name] = config.copy()
|
||||||
if 'vid' in self.volume_config[name]:
|
if 'vid' in self.volume_config[name]:
|
||||||
del self.volume_config[name]['vid']
|
del self.volume_config[name]['vid']
|
||||||
|
# copy pool setting from base AppVM; root and private would be
|
||||||
|
# in the same pool anyway (because of snap_on_start),
|
||||||
|
# but not volatile, which could be surprising
|
||||||
|
elif 'pool' not in self.volume_config[name] \
|
||||||
|
and 'pool' in config:
|
||||||
|
self.volume_config[name]['pool'] = config['pool']
|
||||||
|
|
||||||
super(DispVM, self).__init__(app, xml, *args, **kwargs)
|
super(DispVM, self).__init__(app, xml, *args, **kwargs)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user