Эх сурвалжийг харах

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
Marek Marczykowski-Górecki 3 жил өмнө
parent
commit
f30eebc40e

+ 4 - 0
doc/qubes-storage.rst

@@ -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)

+ 46 - 18
qubes/backup.py

@@ -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
-            self.path = file_path
+            if name is None:
+                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:

+ 32 - 4
qubes/storage/__init__.py

@@ -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):
-            return volume.export()
+        if not isinstance(volume, Volume):
+            volume = self.vm.volumes[volume]
+        return (yield from qubes.utils.coro_maybe(volume.export()))
+
+    @asyncio.coroutine
+    def export_end(self, volume, export_path):
+        """ Cleanup after exporting data from the volume
 
 
-        return self.vm.volumes[volume].export()
+        :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):

+ 8 - 2
qubes/storage/file.py

@@ -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):

+ 13 - 9
qubes/storage/lvm.py

@@ -518,15 +518,19 @@ 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()
-            cmd = ['dd', 'if=' + src_path, 'of=/dev/' + self._vid_import,
-                'conv=sparse', 'status=none', 'bs=128K']
-            if not os.access('/dev/' + self._vid_import, os.W_OK) or \
-                    not os.access(src_path, os.R_OK):
-                cmd.insert(0, 'sudo')
-
-            p = yield from asyncio.create_subprocess_exec(*cmd)
-            yield from p.wait()
+            src_path = yield from qubes.utils.coro_maybe(src_volume.export())
+            try:
+                cmd = ['dd', 'if=' + src_path, 'of=/dev/' + self._vid_import,
+                    'conv=sparse', 'status=none', 'bs=128K']
+                if not os.access('/dev/' + self._vid_import, os.W_OK) or \
+                        not os.access(src_path, os.R_OK):
+                    cmd.insert(0, 'sudo')
+
+                p = yield from asyncio.create_subprocess_exec(*cmd)
+                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)

+ 11 - 3
qubes/storage/reflink.py

@@ -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):

+ 1 - 0
qubes/tests/__init__.py

@@ -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

+ 18 - 8
qubes/tests/integ/backup.py

@@ -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, \
-            True)
+        self.fill_image_vm(testvm2, 'root', 1024 * 1024 * 1024, 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()

+ 8 - 3
qubes/tests/integ/basic.py

@@ -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(
-            ['sha1sum', self.test_template.volumes['root'].export()]).\
-            decode().split(' ')[0]
+        path = self.loop.run_until_complete(
+            self.test_template.storage.export('root'))
+        try:
+            return subprocess.check_output(['sha1sum', path]).\
+                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()

+ 9 - 0
qubes/tests/integ/qrexec.py

@@ -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',

+ 2 - 0
qubes/tests/integ/vm_qrexec_gui.py

@@ -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))

+ 1 - 1
qubes/tests/storage_lvm.py

@@ -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):

+ 14 - 1
qubes/tests/vm/appvm.py

@@ -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,

+ 25 - 0
qubes/tests/vm/dispvm.py

@@ -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)

+ 6 - 0
qubes/vm/dispvm.py

@@ -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)