Merge branch 'master' into devel-no-assert

This commit is contained in:
Marek Marczykowski-Górecki 2018-10-29 20:29:53 +01:00
commit f621e8792c
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
77 changed files with 3427 additions and 1484 deletions

View File

@ -141,7 +141,6 @@ rpms-dom0:
all:
$(PYTHON) setup.py build
$(MAKE) -C qubes-rpc all
# make all -C tests
# Currently supported only on xen
install:
@ -158,7 +157,6 @@ endif
ln -s qvm-device.1.gz $(DESTDIR)/usr/share/man/man1/qvm-block.1.gz
ln -s qvm-device.1.gz $(DESTDIR)/usr/share/man/man1/qvm-pci.1.gz
ln -s qvm-device.1.gz $(DESTDIR)/usr/share/man/man1/qvm-usb.1.gz
# $(MAKE) install -C tests
$(MAKE) install -C relaxng
mkdir -p $(DESTDIR)/etc/qubes
ifeq ($(BACKEND_VMM),xen)

View File

@ -6,6 +6,8 @@ ignore=tests
# abstract-class-little-used: see http://www.logilab.org/ticket/111138
# deprecated-method:
# enable again after disabling py-3.4.3 asyncio.ensure_future compat hack
# not-an-iterable:
# a lot of false possitives for asyncio (yield from (some coroutine))
disable=
abstract-class-little-used,
bad-continuation,
@ -18,6 +20,7 @@ disable=
locally-enabled,
logging-format-interpolation,
missing-docstring,
not-an-iterable,
star-args,
wrong-import-order

View File

@ -16,6 +16,7 @@ manpages and API documentation. For primary user documentation, see
qubes
qubes-vm/index
qubes-events
qubes-features
qubes-storage
qubes-exc
qubes-ext

View File

@ -22,14 +22,14 @@ The distributor may put a file at
:file:`/usr/share/qubes/template/xen-dist.xml`) to override this file.
User may put a file at either
:file:`/etc/qubes/templates/libvirt/xen-user.xml` or
:file:`/etc/qubes/templates/libvirt/by-name/<name>.xml`, where ``<name>`` is
:file:`/etc/qubes/templates/libvirt/xen/by-name/<name>.xml`, where ``<name>`` is
full name of the domain. Wildcards are not supported but symlinks are.
Jinja has a concept of template names, which basically is the path below some
load point, which in Qubes' case is :file:`/etc/qubes/templates` and
:file:`/usr/share/qubes/templates`. Thus names of those templates are
respectively ``'libvirt/xen.xml'``, ``'libvirt/xen-dist.xml'``,
``'libvirt/xen-user.xml'`` and ``'libvirt/by-name/<name>.xml'``.
``'libvirt/xen-user.xml'`` and ``'libvirt/xen/by-name/<name>.xml'``.
This will be important later.
.. note::
@ -95,6 +95,9 @@ basic
Contains ``<name>``, ``<uuid>``, ``<memory>``, ``<currentMemory>`` and
``<vcpu>`` nodes.
cpu
``<cpu>`` node.
os
Contents of ``<os>`` node.

3
doc/loading.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 37 KiB

193
doc/qubes-features.rst Normal file
View File

@ -0,0 +1,193 @@
:py:class:`qubes.vm.Features` - Qubes VM features, services
============================================================
Features are generic mechanism for storing key-value pairs attached to a
VM. The primary use case for them is data storage for extensions (you can think
of them as more flexible properties, defined by extensions), but some are also
used in the qubes core itself. There is no definite list of supported features,
each extension can set their own and there is no requirement of registration,
but :program:`qvm-features` man page contains well known ones.
In addition, there is a mechanism for VM request setting a feature. This is
useful for extensions to discover if its VM part is present.
Features can have three distinct values: no value (not present in mapping,
which is closest thing to :py:obj:`None`), empty string (which is
interpreted as :py:obj:`False`) and non-empty string, which is
:py:obj:`True`. Anything assigned to the mapping is coerced to strings,
however if you assign instances of :py:class:`bool`, they are converted as
described above. Be aware that assigning the number `0` (which is considered
false in Python) will result in string `'0'`, which is considered true.
:py:class:`qubes.vm.Features` inherits from :py:class:`dict`, so provide all the
standard functions to get, list and set values. Additionally provide helper
functions to check if given feature is set on the VM and default to the value
on the VM's template or netvm. This is useful for features which nature is
inherited from other VMs, like "is package X is installed" or "is VM behind a
VPN".
Example usage of features in extension:
.. code-block:: python
import qubes.exc
import qubes.ext
class ExampleExtension(qubes.ext.Extension):
@qubes.ext.handler('domain-pre-start')
def on_domain_start(self, vm, event, **kwargs):
if vm.features.get('do-not-start', False):
raise qubes.exc.QubesVMError(vm,
'Start prohibited because of do-not-start feature')
if vm.features.check_with_template('something-installed', False):
# do something
The above extension does two things:
- prevent starting a qube with ``do-not-start`` feature set
- do something when ``something-installed`` feature is set on the qube, or its template
qvm-features-request, qubes.PostInstall service
------------------------------------------------
When some package in the VM want to request feature to be set (aka advertise
support for it), it should place a shell script in ``/etc/qubes/post-install.d``.
This script should call :program:`qvm-features-request` with ``FEATURE=VALUE`` pair(s) as
arguments to request those features. It is recommended to use very simple
values here (for example ``1``). The script should be named in form
``XX-package-name.sh`` where ``XX`` is two-digits number below 90 and
``package-name`` is unique name specific to this package (preferably actual
package name). The script needs executable bit set.
``qubes.PostInstall`` service will call all those scripts after any package
installation and also after initial template installation.
This way package have a chance to report to dom0 if any feature is
added/removed.
The features flow to dom0 according to the diagram below. Important part is
that qubes core :py:class:`qubes.ext.Extension` is responsible for handling such request in
``features-request`` event handler. If no extension handles given feature request,
it will be ignored. The extension should carefuly validate requested
features (ignoring those not recognized - may be for another extension) and
only then set appropriate value on VM object
(:py:attr:`qubes.vm.BaseVM.features`). It is recommended to make the
verification code as bulletproof as possible (for example allow only specific
simple values, instead of complex structures), because feature requests
come from untrusted sources. The features actually set on the VM in some cases
may not be necessary those requested. Similar for values.
.. graphviz::
digraph {
"qubes.PostInstall";
"/etc/qubes/post-install.d/ scripts";
"qvm-features-request";
"qubes.FeaturesRequest";
"qubes core extensions";
"VM features";
"qubes.PostInstall" -> "/etc/qubes/post-install.d/ scripts";
"/etc/qubes/post-install.d/ scripts" -> "qvm-features-request"
[xlabel="each script calls"];
"qvm-features-request" -> "qubes.FeaturesRequest"
[xlabel="last script call the service to dom0"];
"qubes.FeaturesRequest" -> "qubes core extensions"
[xlabel="features-request event"];
"qubes core extensions" -> "VM features"
[xlabel="verification"];
}
Example ``/etc/qubes/post-install.d/20-example.sh`` file:
.. code-block:: shell
#!/bin/sh
qvm-features-request example-feature=1
Example extension handling the above:
.. code-block:: python
import qubes.ext
class ExampleExtension(qubes.ext.Extension):
# the last argument must be named untrusted_features
@qubes.ext.handler('features-request')
def on_features_request(self, vm, event, untrusted_features):
# don't allow TemplateBasedVMs to request the feature - should be
# requested by the template instead
if hasattr(vm, 'template'):
return
untrusted_value = untrusted_features.get('example-feature', None)
# check if feature is advertised and verify its value
if untrusted_value != '1':
return
value = untrusted_value
# and finally set the value
vm.features['example-feature'] = value
Services
---------
`Qubes services <https://www.qubes-os.org/doc/qubes-service/>`_ are implemented
as features with ``service.`` prefix. The
:py:class:`qubes.ext.services.ServicesExtension` enumerate all the features
in form of ``service.<service-name>`` prefix and write them to QubesDB as
``/qubes-service/<service-name>`` and value either ``0`` or ``1``.
VM startup scripts list those entries for for each with value of ``1``, create
``/var/run/qubes-service/<service-name>`` file. Then, it can be conveniently
used by other scripts to check whether dom0 wishes service to be enabled or
disabled.
VM package can advertise what services are supported. For that, it needs to
request ``supported-service.<service-name>`` feature with value ``1`` according
to description above. The :py:class:`qubes.ext.services.ServicesExtension` will
handle such request and set this feature on VM object. ``supported-service.``
features that stop being advertised with ``qvm-features-request`` call are
removed. This way, it's enough to remove the file from
``/etc/qubes/post-install.d`` (for example by uninstalling package providing
the service) to tell dom0 the service is no longer supported. Services
advertised by TemplateBasedVMs are currently ignored (related
``supported-service.`` features are not set), but retrieving them may be added
in the future. Applications checking for specific service support should use
``vm.features.check_with_template('supported-service.<service-name>', False)``
call on desired VM object. When enumerating all supported services, application
should consider both the vm and its template (if any).
Various tools will use this information to discover if given service is
supported. The API does not enforce service being first advertised before being
enabled (means: there can be service which is enabled, but without matching
``supported-service.`` feature). The list of well known services is in
:program:`qvm-service` man page.
Example ``/etc/qubes/post-install.d/20-my-service.sh``:
.. code-block:: shell
#!/bin/sh
qvm-features-request supported-service.my-service=1
Services and features can be then inspected from dom0 using
:program:`qvm-features` tool, for example:
.. code-block:: shell
$ qvm-features my-qube
supported-service.my-service 1
Module contents
---------------
.. autoclass:: qubes.vm.Features
:members:
:show-inheritance:
.. vim: ts=3 sw=3 et

View File

@ -8,10 +8,20 @@ Because all objects in Qubes' world are interconnected, there is no possibility
to instantiate them separately. They are all loaded together and contained in
the one ``app`` object, an instance of :py:class:`qubes.Qubes` class.
Loading
^^^^^^^
The objects may come to existence in two ways: by explicit instantiation or by
loading from XML file.
The loading from XML is done in stages, because Qubes domains are dependent on
each other in what can be even a circular dependency. Therefore some properties
(especcialy those that refer to another domains) are loaded later. Refer to
:py:class:`qubes.Qubes` class documentation to get description of every stage.
(especcialy those that refer to another domains) are loaded later.
.. image:: loading.svg
Refer to :py:class:`qubes.Qubes` class documentation to get description of every
stage.
Properties

View File

@ -2,6 +2,7 @@
Description=Start Qubes VM %i
Before=systemd-user-sessions.service
After=qubesd.service qubes-meminfo-writer-dom0.service
ConditionKernelCommandLine=!qubes.skip_autostart
[Service]
Type=oneshot

View File

@ -1,5 +1,6 @@
[Unit]
Description=Qubes OS daemon
Before=systemd-user-sessions.service
[Service]
Type=notify

View File

@ -3,4 +3,5 @@
## Please use a single # to start your custom comments
$tag:anon-vm $anyvm deny
$anyvm $anyvm allow,target=dom0

View File

@ -3,8 +3,5 @@
## Please use a single # to start your custom comments
sys-whonix anon-whonix allow
whonix-gw anon-whonix allow
whonix-ws anon-whonix allow
$anyvm $dispvm allow
$anyvm $anyvm ask

View File

@ -3,8 +3,5 @@
## Please use a single # to start your custom comments
sys-whonix anon-whonix allow
whonix-gw anon-whonix allow
whonix-ws anon-whonix allow
$anyvm $dispvm allow
$anyvm $anyvm ask

View File

@ -3,6 +3,15 @@
## Please use a single # to start your custom comments
# Upgrade all TemplateVMs through sys-whonix.
#$type:TemplateVM $default allow,target=sys-whonix
# Upgrade Whonix TemplateVMs through sys-whonix.
$tag:whonix-updatevm $default allow,target=sys-whonix
# Deny Whonix TemplateVMs using UpdatesProxy of any other VM.
$tag:whonix-updatevm $anyvm deny
# Default rule for all TemplateVMs - direct the connection to sys-net
$type:TemplateVM $default allow,target=sys-net

View File

@ -42,7 +42,7 @@ __license__ = 'GPLv2 or later'
__version__ = 'R3'
class Label(object):
class Label:
'''Label definition for virtual machines
Label specifies colour of the padlock displayed next to VM's name.
@ -134,7 +134,7 @@ class Label(object):
self.icon_dispvm) + ".png"
class property(object): # pylint: disable=redefined-builtin,invalid-name
class property: # pylint: disable=redefined-builtin,invalid-name
'''Qubes property.
This class holds one property that can be saved to and loaded from
@ -350,11 +350,10 @@ class property(object): # pylint: disable=redefined-builtin,invalid-name
raise qubes.exc.QubesValueError
if self.type is bool:
return self.bool(None, None, untrusted_newvalue)
else:
try:
return self.type(untrusted_newvalue)
except ValueError:
raise qubes.exc.QubesValueError
try:
return self.type(untrusted_newvalue)
except ValueError:
raise qubes.exc.QubesValueError
else:
# 'str' or not specified type
try:

View File

@ -97,7 +97,7 @@ def apply_filters(iterable, filters):
return iterable
class AbstractQubesAPI(object):
class AbstractQubesAPI:
'''Common code for Qubes Management Protocol handling
Different interfaces can expose different API call sets, however they share

View File

@ -44,7 +44,7 @@ import qubes.vm.adminvm
import qubes.vm.qubesvm
class QubesMgmtEventsDispatcher(object):
class QubesMgmtEventsDispatcher:
def __init__(self, filters, send_event):
self.filters = filters
self.send_event = send_event
@ -335,7 +335,7 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
volume = self.dest.volumes[self.arg]
# properties defined in API
volume_properties = [
'pool', 'vid', 'size', 'usage', 'rw', 'source',
'pool', 'vid', 'size', 'usage', 'rw', 'source', 'path',
'save_on_stop', 'snap_on_start', 'revisions_to_keep']
def _serialize(value):
@ -479,7 +479,7 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
if not self.dest.is_halted():
raise qubes.exc.QubesVMNotHaltedError(self.dest)
path = self.dest.storage.import_data(self.arg)
path = yield from self.dest.storage.import_data(self.arg)
self.enforce(' ' not in path)
size = self.dest.volumes[self.arg].size
@ -1101,7 +1101,7 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
try:
yield from self.dest.remove_from_disk()
except: # pylint: disable=bare-except
self.app.log.exception('Error wile removing VM \'%s\' files',
self.app.log.exception('Error while removing VM \'%s\' files',
self.dest.name)
self.app.save()

View File

@ -68,7 +68,8 @@ class QubesInternalAPI(qubes.api.AbstractQubesAPI):
success = untrusted_payload == b'ok'
try:
self.dest.storage.import_data_end(self.arg, success=success)
yield from self.dest.storage.import_data_end(self.arg,
success=success)
except:
self.dest.fire_event('domain-volume-import-end', volume=self.arg,
success=False)

View File

@ -76,7 +76,8 @@ class QubesMiscAPI(qubes.api.AbstractQubesAPI):
untrusted_features = {}
safe_set = string.ascii_letters + string.digits
expected_features = ('qrexec', 'gui', 'default-user')
expected_features = ('qrexec', 'gui', 'gui-emulated', 'default-user',
'os')
for feature in expected_features:
untrusted_value = self.src.untrusted_qdb.read(
'/qubes-tools/' + feature)

View File

@ -21,7 +21,7 @@
#
import collections
import errno
import copy
import functools
import grp
import itertools
@ -60,13 +60,14 @@ import qubes
import qubes.ext
import qubes.utils
import qubes.storage
import qubes.storage.reflink
import qubes.vm
import qubes.vm.adminvm
import qubes.vm.qubesvm
import qubes.vm.templatevm
# pylint: enable=wrong-import-position
class VirDomainWrapper(object):
class VirDomainWrapper:
# pylint: disable=too-few-public-methods
def __init__(self, connection, vm):
@ -97,7 +98,7 @@ class VirDomainWrapper(object):
return wrapper
class VirConnectWrapper(object):
class VirConnectWrapper:
# pylint: disable=too-few-public-methods
def __init__(self, uri):
@ -134,7 +135,7 @@ class VirConnectWrapper(object):
return wrapper
class VMMConnection(object):
class VMMConnection:
'''Connection to Virtual Machine Manager (libvirt)'''
def __init__(self, offline_mode=None):
@ -229,7 +230,7 @@ class VMMConnection(object):
self._xc = None # and pray it will get garbage-collected
class QubesHost(object):
class QubesHost:
'''Basic information about host machine
:param qubes.Qubes app: Qubes application context (must have \
@ -363,7 +364,7 @@ class QubesHost(object):
return (current_time, current)
class VMCollection(object):
class VMCollection:
'''A collection of Qubes VMs
VMCollection supports ``in`` operator. You may test for ``qid``, ``name``
@ -493,7 +494,7 @@ class VMCollection(object):
self.app.fire_event('domain-delete', vm=vm)
def __contains__(self, key):
return any((key == vm or key == vm.qid or key == vm.name)
return any((key in (vm, vm.qid, vm.name))
for vm in self)
@ -552,37 +553,37 @@ def _default_pool(app):
1. If there is one named 'default', use it.
2. Check if root fs is on LVM thin - use that
3. Look for file-based pool pointing /var/lib/qubes
3. Look for file(-reflink)-based pool pointing to /var/lib/qubes
4. Fail
'''
if 'default' in app.pools:
return app.pools['default']
else:
if 'DEFAULT_LVM_POOL' in os.environ:
thin_pool = os.environ['DEFAULT_LVM_POOL']
for pool in app.pools.values():
if pool.config.get('driver', None) != 'lvm_thin':
continue
if pool.config['thin_pool'] == thin_pool:
return pool
# no DEFAULT_LVM_POOL, or pool not defined
root_volume_group, root_thin_pool = \
qubes.storage.DirectoryThinPool.thin_pool('/')
if root_thin_pool:
for pool in app.pools.values():
if pool.config.get('driver', None) != 'lvm_thin':
continue
if (pool.config['volume_group'] == root_volume_group and
pool.config['thin_pool'] == root_thin_pool):
return pool
# not a thin volume? look for file pools
if 'DEFAULT_LVM_POOL' in os.environ:
thin_pool = os.environ['DEFAULT_LVM_POOL']
for pool in app.pools.values():
if pool.config.get('driver', None) not in ('file', 'file-reflink'):
if pool.config.get('driver', None) != 'lvm_thin':
continue
if pool.config['dir_path'] == qubes.config.qubes_base_dir:
if pool.config['thin_pool'] == thin_pool:
return pool
raise AttributeError('Cannot determine default storage pool')
# no DEFAULT_LVM_POOL, or pool not defined
root_volume_group, root_thin_pool = \
qubes.storage.DirectoryThinPool.thin_pool('/')
if root_thin_pool:
for pool in app.pools.values():
if pool.config.get('driver', None) != 'lvm_thin':
continue
if (pool.config['volume_group'] == root_volume_group and
pool.config['thin_pool'] == root_thin_pool):
return pool
# not a thin volume? look for file pools
for pool in app.pools.values():
if pool.config.get('driver', None) not in ('file', 'file-reflink'):
continue
if pool.config['dir_path'] == qubes.config.qubes_base_dir:
return pool
raise AttributeError('Cannot determine default storage pool')
def _setter_pool(app, prop, value):
if isinstance(value, qubes.storage.Pool):
@ -717,6 +718,19 @@ class Qubes(qubes.PropertyHolder):
setter=_setter_pool,
doc='Default storage pool for kernel volumes')
default_qrexec_timeout = qubes.property('default_qrexec_timeout',
load_stage=3,
default=60,
type=int,
doc='''Default time in seconds after which qrexec connection attempt is
deemed failed''')
default_shutdown_timeout = qubes.property('default_shutdown_timeout',
load_stage=3,
default=60,
type=int,
doc='''Default time in seconds for VM shutdown to complete''')
stats_interval = qubes.property('stats_interval',
default=3,
type=int,
@ -1006,8 +1020,8 @@ class Qubes(qubes.PropertyHolder):
try:
fd = os.open(self._store,
os.O_RDWR | (os.O_CREAT * int(for_save)))
except OSError as e:
if not for_save and e.errno == errno.ENOENT:
except FileNotFoundError:
if not for_save:
raise qubes.exc.QubesException(
'Qubes XML store {!r} is missing; '
'use qubes-create tool'.format(self._store))
@ -1064,15 +1078,29 @@ class Qubes(qubes.PropertyHolder):
}
assert max(self.labels.keys()) == qubes.config.max_default_label
pool_configs = copy.deepcopy(qubes.config.defaults['pool_configs'])
root_volume_group, root_thin_pool = \
qubes.storage.DirectoryThinPool.thin_pool('/')
if root_thin_pool:
self.add_pool(
volume_group=root_volume_group, thin_pool=root_thin_pool,
name='lvm', driver='lvm_thin')
# pool based on /var/lib/qubes will be created here:
for name, config in qubes.config.defaults['pool_configs'].items():
lvm_config = {
'name': 'lvm',
'driver': 'lvm_thin',
'volume_group': root_volume_group,
'thin_pool': root_thin_pool
}
pool_configs[lvm_config['name']] = lvm_config
for name, config in pool_configs.items():
if 'driver' not in config and 'dir_path' in config:
config['driver'] = 'file'
try:
os.makedirs(config['dir_path'], exist_ok=True)
if qubes.storage.reflink.is_supported(config['dir_path']):
config['driver'] = 'file-reflink'
config['setup_check'] = 'no' # don't check twice
except PermissionError: # looks like a testing environment
pass # stay with 'file'
self.pools[name] = self._get_pool(**config)
self.default_pool_kernel = 'linux-kernel'
@ -1170,6 +1198,11 @@ class Qubes(qubes.PropertyHolder):
raise KeyError(label)
def setup_pools(self):
""" Run implementation specific setup for each storage pool. """
for pool in self.pools.values():
pool.setup()
def add_pool(self, name, **kwargs):
""" Add a storage pool to config."""
@ -1251,6 +1284,20 @@ class Qubes(qubes.PropertyHolder):
if event == libvirt.VIR_DOMAIN_EVENT_STOPPED:
vm.on_libvirt_domain_stopped()
elif event == libvirt.VIR_DOMAIN_EVENT_SUSPENDED:
try:
vm.fire_event('domain-paused')
except Exception: # pylint: disable=broad-except
self.log.exception(
'Uncaught exception from domain-paused handler '
'for domain %s', vm.name)
elif event == libvirt.VIR_DOMAIN_EVENT_RESUMED:
try:
vm.fire_event('domain-unpaused')
except Exception: # pylint: disable=broad-except
self.log.exception(
'Uncaught exception from domain-unpaused handler '
'for domain %s', vm.name)
@qubes.events.handler('domain-pre-delete')
def on_domain_pre_deleted(self, event, vm):
@ -1263,9 +1310,9 @@ class Qubes(qubes.PropertyHolder):
self.log.error(
'Cannot remove %s, used by %s.%s',
vm, obj, prop.__name__)
raise qubes.exc.QubesVMInUseError(vm,
'Domain is in use: {!r}; details in system log'
.format(vm.name))
raise qubes.exc.QubesVMInUseError(vm, 'Domain is in '
'use: {!r}; see /var/log/qubes/qubes.log in dom0 for '
'details'.format(vm.name))
except AttributeError:
pass

View File

@ -76,7 +76,7 @@ class BackupCanceledError(qubes.exc.QubesException):
self.tmpdir = tmpdir
class BackupHeader(object):
class BackupHeader:
'''Structure describing backup-header file included as the first file in
backup archive
'''
@ -124,7 +124,7 @@ class BackupHeader(object):
f_header.write("{!s}={!s}\n".format(key, getattr(self, attr)))
class SendWorker(object):
class SendWorker:
# pylint: disable=too-few-public-methods
def __init__(self, queue, base_dir, backup_stdout):
super(SendWorker, self).__init__()
@ -148,6 +148,7 @@ class SendWorker(object):
# verified before untaring.
tar_final_cmd = ["tar", "-cO", "--posix",
"-C", self.base_dir, filename]
# pylint: disable=not-an-iterable
final_proc = yield from asyncio.create_subprocess_exec(
*tar_final_cmd,
stdout=self.backup_stdout)
@ -183,6 +184,7 @@ def launch_proc_with_pty(args, stdin=None, stdout=None, stderr=None, echo=True):
termios_p[3] &= ~termios.ECHO
termios.tcsetattr(ctty_fd, termios.TCSANOW, termios_p)
(pty_master, pty_slave) = os.openpty()
# pylint: disable=not-an-iterable
p = yield from asyncio.create_subprocess_exec(*args,
stdin=stdin,
stdout=stdout,
@ -228,7 +230,7 @@ def launch_scrypt(action, input_name, output_name, passphrase):
return p
class Backup(object):
class Backup:
'''Backup operation manager. Usage:
>>> app = qubes.Qubes()
@ -251,7 +253,7 @@ class Backup(object):
'''
# pylint: disable=too-many-instance-attributes
class FileToBackup(object):
class FileToBackup:
# pylint: disable=too-few-public-methods
def __init__(self, file_path, subdir=None, name=None, size=None):
if size is None:
@ -278,7 +280,7 @@ class Backup(object):
if name is not None:
self.name = name
class VMToBackup(object):
class VMToBackup:
# pylint: disable=too-few-public-methods
def __init__(self, vm, files, subdir):
self.vm = vm
@ -635,6 +637,7 @@ class Backup(object):
# Pipe: tar-sparse | scrypt | tar | backup_target
# TODO: log handle stderr
# pylint: disable=not-an-iterable
tar_sparse = yield from asyncio.create_subprocess_exec(
*tar_cmdline, stdout=subprocess.PIPE)

View File

@ -76,9 +76,8 @@ defaults = {
'root_img_size': 10*1024*1024*1024,
'pool_configs': {
# create file pool even when the default one is LVM
# create file(-reflink) pool even when the default one is LVM
'varlibqubes': {'dir_path': qubes_base_dir,
'driver': 'file',
'name': 'varlibqubes'},
'linux-kernel': {
'dir_path': os.path.join(qubes_base_dir,

View File

@ -67,7 +67,7 @@ class DeviceAlreadyAttached(qubes.exc.QubesException, KeyError):
'''Trying to attach already attached device'''
pass
class DeviceInfo(object):
class DeviceInfo:
''' Holds all information about a device '''
# pylint: disable=too-few-public-methods
def __init__(self, backend_domain, ident, description=None,
@ -117,7 +117,7 @@ class DeviceInfo(object):
return '{!s}:{!s}'.format(self.backend_domain, self.ident)
class DeviceAssignment(object): # pylint: disable=too-few-public-methods
class DeviceAssignment: # pylint: disable=too-few-public-methods
''' Maps a device to a frontend_domain. '''
def __init__(self, backend_domain, ident, options=None, persistent=False,
@ -158,7 +158,7 @@ class DeviceAssignment(object): # pylint: disable=too-few-public-methods
return self.backend_domain.devices[self.bus][self.ident]
class DeviceCollection(object):
class DeviceCollection:
'''Bag for devices.
Used as default value for :py:meth:`DeviceManager.__missing__` factory.
@ -168,13 +168,14 @@ class DeviceCollection(object):
This class emits following events on VM object:
.. event:: device-attach:<class> (device)
.. event:: device-attach:<class> (device, options)
Fired when device is attached to a VM.
Handler for this event can be asynchronous (a coroutine).
:param device: :py:class:`DeviceInfo` object to be attached
:param options: :py:class:`dict` of attachment options
.. event:: device-pre-attach:<class> (device)
@ -357,8 +358,7 @@ class DeviceCollection(object):
if persistent is True:
# don't break app.save()
return self._set
else:
raise
raise
result = set()
for dev, options in devices:
if dev in self._set and not persistent:
@ -433,7 +433,7 @@ class UnknownDevice(DeviceInfo):
frontend_domain)
class PersistentCollection(object):
class PersistentCollection:
''' Helper object managing persistent `DeviceAssignment`s.
'''

View File

@ -48,7 +48,7 @@ SUBCOMMANDS_TITLE = 'COMMANDS'
OPTIONS_TITLE = 'OPTIONS'
class GithubTicket(object):
class GithubTicket:
# pylint: disable=too-few-public-methods
def __init__(self, data):
self.number = data['number']
@ -418,7 +418,7 @@ def parse_event(env, sig, signode):
#
def break_to_pdb(app, *dummy):
def break_to_pdb(app, *_dummy):
if not app.config.break_to_pdb:
return
import pdb

View File

@ -94,7 +94,7 @@ class EmitterMeta(type):
cls.__handlers__[event].add(attr)
class Emitter(object, metaclass=EmitterMeta):
class Emitter(metaclass=EmitterMeta):
'''Subject that can emit events.
By default all events are disabled not to interfere with loading from XML.

View File

@ -101,6 +101,14 @@ class QubesVMNotHaltedError(QubesVMError):
super(QubesVMNotHaltedError, self).__init__(vm,
msg or 'Domain is not powered off: {!r}'.format(vm.name))
class QubesVMShutdownTimeoutError(QubesVMError):
'''Domain shutdown timed out.
'''
def __init__(self, vm, msg=None):
super(QubesVMShutdownTimeoutError, self).__init__(vm,
msg or 'Domain shutdown timed out: {!r}'.format(vm.name))
class QubesNoTemplateError(QubesVMError):
'''Cannot start domain, because there is no template'''
@ -151,7 +159,7 @@ class BackupCancelledError(QubesException):
msg or 'Backup cancelled')
class QubesMemoryError(QubesException, MemoryError):
class QubesMemoryError(QubesVMError, MemoryError):
'''Cannot start domain, because not enough memory is available'''
def __init__(self, vm, msg=None):
super(QubesMemoryError, self).__init__(

View File

@ -29,7 +29,7 @@ import pkg_resources
import qubes.events
class Extension(object):
class Extension:
'''Base class for all extensions
''' # pylint: disable=too-few-public-methods

View File

@ -32,7 +32,7 @@ class CoreFeatures(qubes.ext.Extension):
return
requested_features = {}
for feature in ('qrexec', 'gui', 'qubes-firewall'):
for feature in ('qrexec', 'gui', 'gui-emulated', 'qubes-firewall'):
untrusted_value = untrusted_features.get(feature, None)
if untrusted_value in ('1', '0'):
requested_features[feature] = bool(int(untrusted_value))
@ -44,7 +44,7 @@ class CoreFeatures(qubes.ext.Extension):
# gui agent presence (0 or 1)
qrexec_before = vm.features.get('qrexec', False)
for feature in ('qrexec', 'gui'):
for feature in ('qrexec', 'gui', 'gui-emulated'):
# do not allow (Template)VM to override setting if already set
# some other way
if feature in requested_features and feature not in vm.features:

View File

@ -42,7 +42,8 @@ def load_pci_classes():
# subclass subclass_name <-- single tab
# prog-if prog-if_name <-- two tabs
result = {}
with open('/usr/share/hwdata/pci.ids') as pciids:
with open('/usr/share/hwdata/pci.ids',
encoding='utf-8', errors='ignore') as pciids:
class_id = None
subclass_id = None
for line in pciids.readlines():

View File

@ -80,6 +80,9 @@ class R3Compatibility(qubes.ext.Extension):
def write_iptables_qubesdb_entry(self, firewallvm):
# pylint: disable=no-self-use
# skip compatibility rules if new format support is advertised
if firewallvm.features.check_with_template('qubes-firewall', False):
return
firewallvm.untrusted_qdb.rm("/qubes-iptables-domainrules/")
iptables = "# Generated by Qubes Core on {0}\n".format(
datetime.datetime.now().ctime())

View File

@ -62,3 +62,40 @@ class ServicesExtension(qubes.ext.Extension):
return
service = feature[len('service.'):]
vm.untrusted_qdb.rm('/qubes-service/{}'.format(service))
@qubes.ext.handler('features-request')
def supported_services(self, vm, event, untrusted_features):
'''Handle advertisement of supported services'''
# pylint: disable=no-self-use,unused-argument
if getattr(vm, 'template', None):
vm.log.warning(
'Ignoring qubes.FeaturesRequest from template-based VM')
return
new_supported_services = set()
for requested_service in untrusted_features:
if not requested_service.startswith('supported-service.'):
continue
if untrusted_features[requested_service] == '1':
# only allow to advertise service as supported, lack of entry
# means service is not supported
new_supported_services.add(requested_service)
del untrusted_features
# if no service is supported, ignore the whole thing - do not clear
# all services in case of empty request (manual or such)
if not new_supported_services:
return
old_supported_services = set(
feat for feat in vm.features
if feat.startswith('supported-service.') and vm.features[feat])
for feature in new_supported_services.difference(
old_supported_services):
vm.features[feature] = True
for feature in old_supported_services.difference(
new_supported_services):
del vm.features[feature]

73
qubes/ext/windows.py Normal file
View File

@ -0,0 +1,73 @@
# -*- encoding: utf-8 -*-
#
# The Qubes OS Project, http://www.qubes-os.org
#
# Copyright (C) 2017 Marek Marczykowski-Górecki
# <marmarek@invisiblethingslab.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, see <http://www.gnu.org/licenses/>.
import asyncio
import qubes.ext
class WindowsFeatures(qubes.ext.Extension):
# pylint: disable=too-few-public-methods
@qubes.ext.handler('features-request')
def qubes_features_request(self, vm, event, untrusted_features):
'''Handle features provided requested by Qubes Windows Tools'''
# pylint: disable=no-self-use,unused-argument
if getattr(vm, 'template', None):
vm.log.warning(
'Ignoring qubes.NotifyTools for template-based VM')
return
guest_os = None
if 'os' in untrusted_features:
if untrusted_features['os'] in ['Windows', 'Linux']:
guest_os = untrusted_features['os']
qrexec = None
if 'qrexec' in untrusted_features:
if untrusted_features['qrexec'] == '1':
# qrexec feature is set by CoreFeatures extension
qrexec = True
del untrusted_features
if guest_os:
vm.features['os'] = guest_os
if guest_os == 'Windows' and qrexec:
vm.features['rpc-clipboard'] = True
@qubes.ext.handler('domain-create-on-disk')
@asyncio.coroutine
def on_domain_create_on_disk(self, vm, _event, **kwargs):
# pylint: disable=no-self-use,unused-argument
if getattr(vm, 'template', None) is None:
# handle only template-based vms
return
template = vm.template
if template.features.check_with_template('os', None) != 'Windows':
# ignore non-windows templates
return
if vm.volumes['private'].save_on_stop:
# until windows tools get ability to prepare private.img on its own,
# copy one from the template
vm.log.info('Windows template - cloning private volume')
import_op = vm.volumes['private'].import_volume(
template.volumes['private'])
if asyncio.iscoroutine(import_op):
yield from import_op

View File

@ -34,7 +34,7 @@ import qubes
import qubes.vm.qubesvm
class RuleOption(object):
class RuleOption:
def __init__(self, untrusted_value):
# subset of string.punctuation
safe_set = string.ascii_letters + string.digits + \
@ -209,7 +209,7 @@ class Expire(RuleOption):
@property
def rule(self):
return None
pass
@property
def api_rule(self):
@ -234,7 +234,7 @@ class Comment(RuleOption):
@property
def rule(self):
return None
pass
@property
def api_rule(self):
@ -451,7 +451,7 @@ class Rule(qubes.PropertyHolder):
return hash(self.api_rule)
class Firewall(object):
class Firewall:
def __init__(self, vm, load=True):
assert hasattr(vm, 'firewall_conf')
self.vm = vm

View File

@ -26,7 +26,7 @@ import textwrap
import lxml.etree
class Element(object):
class Element:
def __init__(self, schema, xml):
self.schema = schema
self.xml = xml
@ -157,7 +157,7 @@ class Element(object):
write_rst_table(stream, childtable, ('element', 'number'))
class Schema(object):
class Schema:
# pylint: disable=too-few-public-methods
nsmap = {
'rng': 'http://relaxng.org/ns/structure/1.0',

View File

@ -47,7 +47,7 @@ class StoragePoolException(qubes.exc.QubesException):
pass
class BlockDevice(object):
class BlockDevice:
''' Represents a storage block device. '''
# pylint: disable=too-few-public-methods
def __init__(self, path, name, script=None, rw=True, domain=None,
@ -62,7 +62,7 @@ class BlockDevice(object):
self.devtype = devtype
class Volume(object):
class Volume:
''' Encapsulates all data about a volume for serialization to qubes.xml and
libvirt config.
@ -198,6 +198,8 @@ class Volume(object):
volume data require something more than just writing to a file (
for example connecting to some other domain, or converting data
on the fly), the returned path may be a pipe.
This can be implemented as a coroutine.
'''
raise self._not_implemented("import")
@ -207,6 +209,8 @@ class Volume(object):
This method is called regardless the operation was successful or not.
This can be implemented as a coroutine.
:param success: True if data import was successful, otherwise False
'''
# by default do nothing
@ -252,6 +256,8 @@ class Volume(object):
def revert(self, revision=None):
''' Revert volume to previous revision
This can be implemented as a coroutine.
:param revision: revision to revert volume to, see :py:attr:`revisions`
'''
# pylint: disable=unused-argument
@ -272,6 +278,7 @@ class Volume(object):
This include committing data if :py:attr:`save_on_stop` is set.
This can be implemented as a coroutine.'''
raise self._not_implemented("stop")
def verify(self):
''' Verifies the volume.
@ -334,14 +341,14 @@ class Volume(object):
msg = msg.format(str(self.__class__.__name__), method_name)
return NotImplementedError(msg)
class Storage(object):
class Storage:
''' Class for handling VM virtual disks.
This is base class for all other implementations, mostly with Xen on Linux
in mind.
'''
AVAILABLE_FRONTENDS = set(['xvd' + c for c in string.ascii_lowercase])
AVAILABLE_FRONTENDS = {'xvd' + c for c in string.ascii_lowercase}
def __init__(self, vm):
#: Domain for which we manage storage
@ -506,8 +513,7 @@ class Storage(object):
ret = volume.create()
if asyncio.iscoroutine(ret):
coros.append(ret)
if coros:
yield from asyncio.wait(coros)
yield from _wait_and_reraise(coros)
os.umask(old_umask)
@ -549,7 +555,7 @@ class Storage(object):
self.vm.volumes = {}
with VmCreationManager(self.vm):
yield from asyncio.wait([self.clone_volume(src_vm, vol_name)
yield from _wait_and_reraise([self.clone_volume(src_vm, vol_name)
for vol_name in self.vm.volume_config.keys()])
@property
@ -581,11 +587,7 @@ class Storage(object):
ret = volume.verify()
if asyncio.iscoroutine(ret):
futures.append(ret)
if futures:
done, _ = yield from asyncio.wait(futures)
for task in done:
# re-raise any exception from async task
task.result()
yield from _wait_and_reraise(futures)
self.vm.fire_event('domain-verify-files')
return True
@ -605,44 +607,32 @@ class Storage(object):
except (IOError, OSError) as e:
self.vm.log.exception("Failed to remove volume %s", name, e)
if futures:
try:
done, _ = yield from asyncio.wait(futures)
for task in done:
# re-raise any exception from async task
task.result()
except (IOError, OSError) as e:
self.vm.log.exception("Failed to remove some volume", e)
try:
yield from _wait_and_reraise(futures)
except (IOError, OSError) as e:
self.vm.log.exception("Failed to remove some volume", e)
@asyncio.coroutine
def start(self):
''' Execute the start method on each pool '''
''' Execute the start method on each volume '''
futures = []
for volume in self.vm.volumes.values():
ret = volume.start()
if asyncio.iscoroutine(ret):
futures.append(ret)
if futures:
done, _ = yield from asyncio.wait(futures)
for task in done:
# re-raise any exception from async task
task.result()
yield from _wait_and_reraise(futures)
@asyncio.coroutine
def stop(self):
''' Execute the start method on each pool '''
''' Execute the stop method on each volume '''
futures = []
for volume in self.vm.volumes.values():
ret = volume.stop()
if asyncio.iscoroutine(ret):
futures.append(ret)
if futures:
done, _ = yield from asyncio.wait(futures)
for task in done:
# re-raise any exception from async task
task.result()
yield from _wait_and_reraise(futures)
def unused_frontend(self):
''' Find an unused device name '''
@ -655,9 +645,9 @@ class Storage(object):
''' Used device names '''
xml = self.vm.libvirt_domain.XMLDesc()
parsed_xml = lxml.etree.fromstring(xml)
return set([target.get('dev', None)
return {target.get('dev', None)
for target in parsed_xml.xpath(
"//domain/devices/disk/target")])
"//domain/devices/disk/target")}
def export(self, volume):
''' Helper function to export volume (pool.export(volume))'''
@ -668,27 +658,37 @@ class Storage(object):
return self.vm.volumes[volume].export()
@asyncio.coroutine
def import_data(self, volume):
''' Helper function to import volume data (pool.import_data(volume))'''
assert isinstance(volume, (Volume, str)), \
"You need to pass a Volume or pool name as str"
if isinstance(volume, Volume):
return volume.import_data()
ret = volume.import_data()
else:
ret = self.vm.volumes[volume].import_data()
return self.vm.volumes[volume].import_data()
if asyncio.iscoroutine(ret):
ret = yield from ret
return ret
@asyncio.coroutine
def import_data_end(self, volume, success):
''' Helper function to finish/cleanup data import
(pool.import_data_end( volume))'''
assert isinstance(volume, (Volume, str)), \
"You need to pass a Volume or pool name as str"
if isinstance(volume, Volume):
return volume.import_data_end(success=success)
ret = volume.import_data_end(success=success)
else:
ret = self.vm.volumes[volume].import_data_end(success=success)
return self.vm.volumes[volume].import_data_end(success=success)
if asyncio.iscoroutine(ret):
ret = yield from ret
return ret
class VolumesCollection(object):
class VolumesCollection:
'''Convenient collection wrapper for pool.get_volume and
pool.list_volumes
'''
@ -706,8 +706,7 @@ class VolumesCollection(object):
if isinstance(item, Volume):
if item.pool == self._pool:
return self[item.vid]
else:
raise KeyError(item)
raise KeyError(item)
try:
return self._pool.get_volume(item)
except NotImplementedError:
@ -740,7 +739,7 @@ class VolumesCollection(object):
return [vol for vol in self]
class Pool(object):
class Pool:
''' A Pool is used to manage different kind of volumes (File
based/LVM/Btrfs/...).
@ -760,7 +759,7 @@ class Pool(object):
def __eq__(self, other):
if isinstance(other, Pool):
return self.name == other.name
elif isinstance(other, str):
if isinstance(other, str):
return self.name == other
return NotImplemented
@ -829,12 +828,12 @@ class Pool(object):
@property
def size(self):
''' Storage pool size in bytes, or None if unknown '''
return None
pass
@property
def usage(self):
''' Space used in the pool in bytes, or None if unknown '''
return None
pass
def _not_implemented(self, method_name):
''' Helper for emitting helpful `NotImplementedError` exceptions '''
@ -843,6 +842,14 @@ class Pool(object):
return NotImplementedError(msg)
@asyncio.coroutine
def _wait_and_reraise(futures):
if futures:
done, _ = yield from asyncio.wait(futures)
for task in done: # (re-)raise first exception in line
task.result()
def _sanitize_config(config):
''' Helper function to convert types to appropriate strings
''' # FIXME: find another solution for serializing basic types
@ -872,7 +879,7 @@ def driver_parameters(name):
return [p for p in params if p not in ignored_params]
def isodate(seconds=time.time()):
def isodate(seconds):
''' Helper method which returns an iso date '''
return datetime.utcfromtimestamp(seconds).isoformat("T")
@ -882,23 +889,27 @@ def search_pool_containing_dir(pools, dir_path):
This is useful for implementing Pool.included_in method
'''
real_dir_path = os.path.realpath(dir_path)
# prefer filesystem pools
for pool in pools:
if hasattr(pool, 'dir_path'):
if dir_path.startswith(pool.dir_path):
pool_real_dir_path = os.path.realpath(pool.dir_path)
if os.path.commonpath([pool_real_dir_path, real_dir_path]) == \
pool_real_dir_path:
return pool
# then look for lvm
for pool in pools:
if hasattr(pool, 'thin_pool') and hasattr(pool, 'volume_group'):
if (pool.volume_group, pool.thin_pool) == \
DirectoryThinPool.thin_pool(dir_path):
DirectoryThinPool.thin_pool(real_dir_path):
return pool
return None
class VmCreationManager(object):
class VmCreationManager:
''' A `ContextManager` which cleans up if volume creation fails.
''' # pylint: disable=too-few-public-methods
def __init__(self, vm):

View File

@ -356,9 +356,9 @@ class FileVolume(qubes.storage.Volume):
def script(self):
if not self.snap_on_start and not self.save_on_stop:
return None
elif not self.snap_on_start and self.save_on_stop:
if not self.snap_on_start and self.save_on_stop:
return 'block-origin'
elif self.snap_on_start:
if self.snap_on_start:
return 'block-snapshot'
return None

View File

@ -18,9 +18,8 @@
#
''' Driver for storing vm images in a LVM thin pool '''
import functools
import logging
import operator
import os
import subprocess
@ -44,8 +43,37 @@ def check_lvm_version():
lvm_is_very_old = check_lvm_version()
class ThinPool(qubes.storage.Pool):
''' LVM Thin based pool implementation
Volumes are stored as LVM thin volumes, in thin pool specified by
*volume_group*/*thin_pool* arguments. LVM volume naming scheme:
vm-{vm_name}-{volume_name}[-suffix]
Where suffix can be one of:
"-snap" - snapshot for currently running VM, at VM shutdown will be
either discarded (if save_on_stop=False), or committed
(if save_on_stop=True)
"-{revision_id}" - volume revision - new revision is automatically
created at each VM shutdown, *revisions_to_keep* control how many
old revisions (in addition to the current one) should be stored
"" (no suffix) - the most recent committed volume state; also volatile
volume (snap_on_start=False, save_on_stop=False)
On VM startup, new volume is created, depending on volume type,
according to the table below:
snap_on_start, save_on_stop
False, False, - no suffix, fresh empty volume
False, True, - "-snap", snapshot of last committed revision
True , False, - "-snap", snapshot of last committed revision
of source volume (from VM's template)
True, True, - unsupported configuration
Volume's revision_id format is "{timestamp}-back", where timestamp is in
'%s' format (seconds since unix epoch)
''' # pylint: disable=protected-access
size_cache = None
@ -62,13 +90,20 @@ class ThinPool(qubes.storage.Pool):
self._volume_objects_cache = {}
def __repr__(self):
return '<{} at {:#x} name={!r} volume_group={!r} thin_pool={!r}>'.\
format(
type(self).__name__, id(self),
self.name, self.volume_group, self.thin_pool)
@property
def config(self):
return {
'name': self.name,
'volume_group': self.volume_group,
'thin_pool': self.thin_pool,
'driver': ThinPool.driver
'driver': ThinPool.driver,
'revisions_to_keep': self.revisions_to_keep,
}
def destroy(self):
@ -131,20 +166,16 @@ class ThinPool(qubes.storage.Pool):
continue
if vol_info['pool_lv'] != self.thin_pool:
continue
if vid.endswith('-snap'):
if vid.endswith('-snap') or vid.endswith('-import'):
# implementation detail volume
continue
if vid.endswith('-back'):
# old revisions
continue
config = {
'pool': self,
'vid': vid,
'name': vid,
'volume_group': self.volume_group,
'rw': vol_info['attr'][1] == 'w',
}
volumes += [ThinVolume(**config)]
volume = self.get_volume(vid)
if volume in volumes:
continue
volumes.append(volume)
return volumes
@property
@ -164,26 +195,14 @@ class ThinPool(qubes.storage.Pool):
return 0
def init_cache(log=logging.getLogger('qubes.storage.lvm')):
cmd = ['lvs', '--noheadings', '-o',
'vg_name,pool_lv,name,lv_size,data_percent,lv_attr,origin',
'--units', 'b', '--separator', ';']
if os.getuid() != 0:
cmd.insert(0, 'sudo')
environ = os.environ.copy()
environ['LC_ALL'] = 'C.utf8'
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
close_fds=True, env=environ)
out, err = p.communicate()
return_code = p.returncode
if return_code == 0 and err:
log.warning(err)
elif return_code != 0:
raise qubes.storage.StoragePoolException(err)
_init_cache_cmd = ['lvs', '--noheadings', '-o',
'vg_name,pool_lv,name,lv_size,data_percent,lv_attr,origin',
'--units', 'b', '--separator', ';']
def _parse_lvm_cache(lvm_output):
result = {}
for line in out.splitlines():
for line in lvm_output.splitlines():
line = line.decode().strip()
pool_name, pool_lv, name, size, usage_percent, attr, \
origin = line.split(';', 6)
@ -197,9 +216,73 @@ def init_cache(log=logging.getLogger('qubes.storage.lvm')):
return result
def init_cache(log=logging.getLogger('qubes.storage.lvm')):
cmd = _init_cache_cmd
if os.getuid() != 0:
cmd = ['sudo'] + cmd
environ = os.environ.copy()
environ['LC_ALL'] = 'C.utf8'
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
close_fds=True, env=environ)
out, err = p.communicate()
return_code = p.returncode
if return_code == 0 and err:
log.warning(err)
elif return_code != 0:
raise qubes.storage.StoragePoolException(err)
return _parse_lvm_cache(out)
@asyncio.coroutine
def init_cache_coro(log=logging.getLogger('qubes.storage.lvm')):
cmd = _init_cache_cmd
if os.getuid() != 0:
cmd = ['sudo'] + cmd
environ = os.environ.copy()
environ['LC_ALL'] = 'C.utf8'
p = yield from asyncio.create_subprocess_exec(*cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
close_fds=True, env=environ)
out, err = yield from p.communicate()
return_code = p.returncode
if return_code == 0 and err:
log.warning(err)
elif return_code != 0:
raise qubes.storage.StoragePoolException(err)
return _parse_lvm_cache(out)
size_cache = init_cache()
def _revision_sort_key(revision):
'''Sort key for revisions. Sort them by time
:returns timestamp
'''
if isinstance(revision, tuple):
revision = revision[0]
if '-' in revision:
revision = revision.split('-')[0]
return int(revision)
def locked(method):
'''Decorator running given Volume's coroutine under a lock.
Needs to be added after wrapping with @asyncio.coroutine, for example:
>>>@locked
>>>@asyncio.coroutine
>>>def start(self):
>>> pass
'''
@asyncio.coroutine
@functools.wraps(method)
def wrapper(self, *args, **kwargs):
with (yield from self._lock): # pylint: disable=protected-access
return (yield from method(self, *args, **kwargs))
return wrapper
class ThinVolume(qubes.storage.Volume):
''' Default LVM thin volume implementation
''' # pylint: disable=too-few-public-methods
@ -212,12 +295,27 @@ class ThinVolume(qubes.storage.Volume):
if self.snap_on_start or self.save_on_stop:
self._vid_snap = self.vid + '-snap'
if self.save_on_stop:
self._vid_import = self.vid + '-import'
self._size = size
self._lock = asyncio.Lock()
@property
def path(self):
return '/dev/' + self.vid
return '/dev/' + self._vid_current
@property
def _vid_current(self):
if self.vid in size_cache:
return self.vid
vol_revisions = self.revisions
if vol_revisions:
last_revision = \
max(vol_revisions.items(), key=_revision_sort_key)[0]
return self.vid + '-' + last_revision
# detached pool? return expected path
return self.vid
@property
def revisions(self):
@ -229,7 +327,8 @@ class ThinVolume(qubes.storage.Volume):
if not revision_vid.endswith('-back'):
continue
revision_vid = revision_vid[len(name_prefix):]
seconds = int(revision_vid[:-len('-back')])
# get revision without suffix
seconds = int(revision_vid.split('-')[0])
iso_date = qubes.storage.isodate(seconds).split('.', 1)[0]
revisions[revision_vid] = iso_date
return revisions
@ -239,7 +338,7 @@ class ThinVolume(qubes.storage.Volume):
try:
if self.is_dirty():
return qubes.storage.lvm.size_cache[self._vid_snap]['size']
return qubes.storage.lvm.size_cache[self.vid]['size']
return qubes.storage.lvm.size_cache[self._vid_current]['size']
except KeyError:
return self._size
@ -248,6 +347,7 @@ class ThinVolume(qubes.storage.Volume):
raise qubes.storage.StoragePoolException(
"You shouldn't use lvm size setter")
@asyncio.coroutine
def _reset(self):
''' Resets a volatile volume '''
assert not self.snap_on_start and not self.save_on_stop, \
@ -255,14 +355,15 @@ class ThinVolume(qubes.storage.Volume):
self.log.debug('Resetting volatile %s', self.vid)
try:
cmd = ['remove', self.vid]
qubes_lvm(cmd, self.log)
yield from qubes_lvm_coro(cmd, self.log)
except qubes.storage.StoragePoolException:
pass
# pylint: disable=protected-access
cmd = ['create', self.pool._pool_id, self.vid.split('/')[1],
str(self.size)]
qubes_lvm(cmd, self.log)
yield from qubes_lvm_coro(cmd, self.log)
@asyncio.coroutine
def _remove_revisions(self, revisions=None):
'''Remove old volume revisions.
@ -273,19 +374,32 @@ class ThinVolume(qubes.storage.Volume):
'''
if revisions is None:
revisions = sorted(self.revisions.items(),
key=operator.itemgetter(1))
key=_revision_sort_key)
# pylint: disable=invalid-unary-operand-type
revisions = revisions[:(-self.revisions_to_keep) or None]
revisions = [rev_id for rev_id, _ in revisions]
for rev_id in revisions:
# safety check
assert rev_id != self._vid_current
try:
cmd = ['remove', self.vid + '-' + rev_id]
qubes_lvm(cmd, self.log)
yield from qubes_lvm_coro(cmd, self.log)
except qubes.storage.StoragePoolException:
pass
def _commit(self):
@asyncio.coroutine
def _commit(self, vid_to_commit=None, keep=False):
'''
Commit temporary volume into current one. By default
:py:attr:`_vid_snap` is used (which is created by :py:meth:`start()`),
but can be overriden by *vid_to_commit* argument.
:param vid_to_commit: LVM volume ID to commit into this one
:param keep: whether to keep or not *vid_to_commit*.
IOW use 'clone' or 'rename' methods.
:return: None
'''
msg = "Trying to commit {!s}, but it has save_on_stop == False"
msg = msg.format(self)
assert self.save_on_stop, msg
@ -293,39 +407,41 @@ class ThinVolume(qubes.storage.Volume):
msg = "Trying to commit {!s}, but it has rw == False"
msg = msg.format(self)
assert self.rw, msg
assert hasattr(self, '_vid_snap')
if vid_to_commit is None:
assert hasattr(self, '_vid_snap')
vid_to_commit = self._vid_snap
if self.revisions_to_keep > 0:
cmd = ['clone', self.vid,
'{}-{}-back'.format(self.vid, int(time.time()))]
qubes_lvm(cmd, self.log)
reset_cache()
self._remove_revisions()
assert self._lock.locked()
if not os.path.exists('/dev/' + vid_to_commit):
# nothing to commit
return
# TODO: when converting this function to coroutine, this _must_ be
# under a lock
# remove old volume only after _successful_ clone of the new one
cmd = ['rename', self.vid, self.vid + '-tmp']
qubes_lvm(cmd, self.log)
try:
cmd = ['clone', self._vid_snap, self.vid]
qubes_lvm(cmd, self.log)
except:
# restore original volume
cmd = ['rename', self.vid + '-tmp', self.vid]
qubes_lvm(cmd, self.log)
raise
else:
cmd = ['remove', self.vid + '-tmp']
qubes_lvm(cmd, self.log)
if self._vid_current == self.vid:
cmd = ['rename', self.vid,
'{}-{}-back'.format(self.vid, int(time.time()))]
yield from qubes_lvm_coro(cmd, self.log)
yield from reset_cache_coro()
cmd = ['clone' if keep else 'rename',
vid_to_commit,
self.vid]
yield from qubes_lvm_coro(cmd, self.log)
yield from reset_cache_coro()
# make sure the one we've committed right now is properly
# detected as the current one - before removing anything
assert self._vid_current == self.vid
# and remove old snapshots, if needed
yield from self._remove_revisions()
@locked
@asyncio.coroutine
def create(self):
assert self.vid
assert self.size
if self.save_on_stop:
if self.source:
cmd = ['clone', str(self.source), self.vid]
cmd = ['clone', self.source.path, self.vid]
else:
cmd = [
'create',
@ -333,70 +449,126 @@ class ThinVolume(qubes.storage.Volume):
self.vid.split('/', 1)[1],
str(self.size)
]
qubes_lvm(cmd, self.log)
reset_cache()
yield from qubes_lvm_coro(cmd, self.log)
yield from reset_cache_coro()
return self
@locked
@asyncio.coroutine
def remove(self):
assert self.vid
try:
if os.path.exists('/dev/' + self._vid_snap):
cmd = ['remove', self._vid_snap]
qubes_lvm(cmd, self.log)
yield from qubes_lvm_coro(cmd, self.log)
except AttributeError:
pass
self._remove_revisions(self.revisions.keys())
try:
if os.path.exists('/dev/' + self._vid_import):
cmd = ['remove', self._vid_import]
yield from qubes_lvm_coro(cmd, self.log)
except AttributeError:
pass
yield from self._remove_revisions(self.revisions.keys())
if not os.path.exists(self.path):
return
cmd = ['remove', self.vid]
qubes_lvm(cmd, self.log)
reset_cache()
cmd = ['remove', self.path]
yield from qubes_lvm_coro(cmd, self.log)
yield from reset_cache_coro()
# pylint: disable=protected-access
self.pool._volume_objects_cache.pop(self.vid, None)
def export(self):
''' Returns an object that can be `open()`. '''
# make sure the device node is available
qubes_lvm(['activate', self.vid], self.log)
devpath = '/dev/' + self.vid
qubes_lvm(['activate', self.path], self.log)
devpath = self.path
return devpath
@locked
@asyncio.coroutine
def import_volume(self, src_volume):
if not src_volume.save_on_stop:
return self
if self.is_dirty():
raise qubes.storage.StoragePoolException(
'Cannot import to dirty volume {} -'
' start and stop a qube to cleanup'.format(self.vid))
self.abort_if_import_in_progress()
# HACK: neat trick to speed up testing if you have same physical thin
# pool assigned to two qubes-pools i.e: qubes_dom0 and test-lvm
# pylint: disable=line-too-long
if isinstance(src_volume.pool, ThinPool) and \
src_volume.pool.thin_pool == self.pool.thin_pool: # NOQA
cmd = ['remove', self.vid]
qubes_lvm(cmd, self.log)
cmd = ['clone', str(src_volume), str(self)]
qubes_lvm(cmd, self.log)
yield from self._commit(src_volume.path[len('/dev/'):], keep=True)
else:
if src_volume.size != self.size:
self.resize(src_volume.size)
cmd = ['create',
self.pool._pool_id, # pylint: disable=protected-access
self._vid_import.split('/')[1],
str(src_volume.size)]
yield from qubes_lvm_coro(cmd, self.log)
src_path = src_volume.export()
cmd = ['dd', 'if=' + src_path, 'of=/dev/' + self.vid,
'conv=sparse']
cmd = ['dd', 'if=' + src_path, 'of=/dev/' + self._vid_import,
'conv=sparse', 'status=none']
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()
if p.returncode != 0:
cmd = ['remove', self._vid_import]
yield from qubes_lvm_coro(cmd, self.log)
raise qubes.storage.StoragePoolException(
'Failed to import volume {!r}, dd exit code: {}'.format(
src_volume, p.returncode))
reset_cache()
yield from self._commit(self._vid_import)
return self
@locked
@asyncio.coroutine
def import_data(self):
''' Returns an object that can be `open()`. '''
devpath = '/dev/' + self.vid
if self.is_dirty():
raise qubes.storage.StoragePoolException(
'Cannot import data to dirty volume {}, stop the qube first'.
format(self.vid))
self.abort_if_import_in_progress()
# pylint: disable=protected-access
cmd = ['create', self.pool._pool_id, self._vid_import.split('/')[1],
str(self.size)]
yield from qubes_lvm_coro(cmd, self.log)
yield from reset_cache_coro()
devpath = '/dev/' + self._vid_import
return devpath
@locked
@asyncio.coroutine
def import_data_end(self, success):
'''Either commit imported data, or discard temporary volume'''
if not os.path.exists('/dev/' + self._vid_import):
raise qubes.storage.StoragePoolException(
'No import operation in progress on {}'.format(self.vid))
if success:
yield from self._commit(self._vid_import)
else:
cmd = ['remove', self._vid_import]
yield from qubes_lvm_coro(cmd, self.log)
def abort_if_import_in_progress(self):
try:
devpath = '/dev/' + self._vid_import
if os.path.exists(devpath):
raise qubes.storage.StoragePoolException(
'Import operation in progress on {}'.format(self.vid))
except AttributeError: # self._vid_import
# no vid_import - import definitely not in progress
pass
def is_dirty(self):
if self.save_on_stop:
return os.path.exists('/dev/' + self._vid_snap)
@ -408,25 +580,34 @@ class ThinVolume(qubes.storage.Volume):
if self._vid_snap not in size_cache:
return False
return (size_cache[self._vid_snap]['origin'] !=
self.source.vid.split('/')[1])
self.source.path.split('/')[-1])
@locked
@asyncio.coroutine
def revert(self, revision=None):
if self.is_dirty():
raise qubes.storage.StoragePoolException(
'Cannot revert dirty volume {}, stop the qube first'.format(
self.vid))
self.abort_if_import_in_progress()
if revision is None:
revision = \
max(self.revisions.items(), key=operator.itemgetter(1))[0]
old_path = self.path + '-' + revision
max(self.revisions.items(), key=_revision_sort_key)[0]
old_path = '/dev/' + self.vid + '-' + revision
if not os.path.exists(old_path):
msg = "Volume {!s} has no {!s}".format(self, old_path)
raise qubes.storage.StoragePoolException(msg)
cmd = ['remove', self.vid]
qubes_lvm(cmd, self.log)
if self.vid in size_cache:
cmd = ['remove', self.vid]
yield from qubes_lvm_coro(cmd, self.log)
cmd = ['clone', self.vid + '-' + revision, self.vid]
qubes_lvm(cmd, self.log)
reset_cache()
yield from qubes_lvm_coro(cmd, self.log)
yield from reset_cache_coro()
return self
@locked
@asyncio.coroutine
def resize(self, size):
''' Expands volume, throws
:py:class:`qubst.storage.qubes.storage.StoragePoolException` if
@ -448,49 +629,58 @@ class ThinVolume(qubes.storage.Volume):
if self.is_dirty():
cmd = ['extend', self._vid_snap, str(size)]
qubes_lvm(cmd, self.log)
yield from qubes_lvm_coro(cmd, self.log)
elif hasattr(self, '_vid_import') and \
os.path.exists('/dev/' + self._vid_import):
cmd = ['extend', self._vid_import, str(size)]
yield from qubes_lvm_coro(cmd, self.log)
elif self.save_on_stop or not self.snap_on_start:
cmd = ['extend', self.vid, str(size)]
qubes_lvm(cmd, self.log)
reset_cache()
cmd = ['extend', self._vid_current, str(size)]
yield from qubes_lvm_coro(cmd, self.log)
yield from reset_cache_coro()
@asyncio.coroutine
def _snapshot(self):
try:
cmd = ['remove', self._vid_snap]
qubes_lvm(cmd, self.log)
yield from qubes_lvm_coro(cmd, self.log)
except: # pylint: disable=bare-except
pass
if self.source is None:
cmd = ['clone', self.vid, self._vid_snap]
cmd = ['clone', self._vid_current, self._vid_snap]
else:
cmd = ['clone', str(self.source), self._vid_snap]
qubes_lvm(cmd, self.log)
cmd = ['clone', self.source.path, self._vid_snap]
yield from qubes_lvm_coro(cmd, self.log)
@locked
@asyncio.coroutine
def start(self):
self.abort_if_import_in_progress()
try:
if self.snap_on_start or self.save_on_stop:
if not self.save_on_stop or not self.is_dirty():
self._snapshot()
yield from self._snapshot()
else:
self._reset()
yield from self._reset()
finally:
reset_cache()
yield from reset_cache_coro()
return self
@locked
@asyncio.coroutine
def stop(self):
try:
if self.save_on_stop:
self._commit()
if self.snap_on_start or self.save_on_stop:
yield from self._commit()
if self.snap_on_start and not self.save_on_stop:
cmd = ['remove', self._vid_snap]
qubes_lvm(cmd, self.log)
else:
yield from qubes_lvm_coro(cmd, self.log)
elif not self.snap_on_start and not self.save_on_stop:
cmd = ['remove', self.vid]
qubes_lvm(cmd, self.log)
yield from qubes_lvm_coro(cmd, self.log)
finally:
reset_cache()
yield from reset_cache_coro()
return self
def verify(self):
@ -499,9 +689,9 @@ class ThinVolume(qubes.storage.Volume):
# volatile volumes don't need any files
return True
if self.source is not None:
vid = str(self.source)
vid = self.source.path[len('/dev/'):]
else:
vid = self.vid
vid = self._vid_current
try:
vol_info = size_cache[vid]
if vol_info['attr'][4] != 'a':
@ -528,7 +718,7 @@ class ThinVolume(qubes.storage.Volume):
def usage(self): # lvm thin usage always returns at least the same usage as
# the parent
try:
return qubes.storage.lvm.size_cache[self.vid]['usage']
return qubes.storage.lvm.size_cache[self._vid_current]['usage']
except KeyError:
return 0
@ -541,9 +731,14 @@ def pool_exists(pool_id):
except KeyError:
return False
def _get_lvm_cmdline(cmd):
''' Build command line for :program:`lvm` call.
The purpose of this function is to keep all the detailed lvm options in
one place.
def qubes_lvm(cmd, log=logging.getLogger('qubes.storage.lvm')):
''' Call :program:`lvm` to execute an LVM operation '''
:param cmd: array of str, where cmd[0] is action and the rest are arguments
:return array of str appropriate for subprocess.Popen
'''
action = cmd[0]
if action == 'remove':
lvm_cmd = ['lvremove', '-f', cmd[1]]
@ -568,21 +763,57 @@ def qubes_lvm(cmd, log=logging.getLogger('qubes.storage.lvm')):
cmd = ['sudo', 'lvm'] + lvm_cmd
else:
cmd = ['lvm'] + lvm_cmd
return cmd
def _process_lvm_output(returncode, stdout, stderr, log):
'''Process output of LVM, determine if the call was successful and
possibly log warnings.'''
# Filter out warning about intended over-provisioning.
# Upstream discussion about missing option to silence it:
# https://bugzilla.redhat.com/1347008
err = '\n'.join(line for line in stderr.decode().splitlines()
if 'exceeds the size of thin pool' not in line)
if stdout:
log.debug(stdout)
if returncode == 0 and err:
log.warning(err)
elif returncode != 0:
assert err, "Command exited unsuccessful, but printed nothing to stderr"
err = err.replace('%', '%%')
raise qubes.storage.StoragePoolException(err)
return True
def qubes_lvm(cmd, log=logging.getLogger('qubes.storage.lvm')):
''' Call :program:`lvm` to execute an LVM operation '''
# the only caller for this non-coroutine version is ThinVolume.export()
cmd = _get_lvm_cmdline(cmd)
environ = os.environ.copy()
environ['LC_ALL'] = 'C.utf8'
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
close_fds=True, env=environ)
out, err = p.communicate()
return_code = p.returncode
if out:
log.debug(out)
if return_code == 0 and err:
log.warning(err)
elif return_code != 0:
assert err, "Command exited unsuccessful, but printed nothing to stderr"
raise qubes.storage.StoragePoolException(err)
return True
return _process_lvm_output(p.returncode, out, err, log)
@asyncio.coroutine
def qubes_lvm_coro(cmd, log=logging.getLogger('qubes.storage.lvm')):
''' Call :program:`lvm` to execute an LVM operation
Coroutine version of :py:func:`qubes_lvm`'''
cmd = _get_lvm_cmdline(cmd)
environ = os.environ.copy()
environ['LC_ALL'] = 'C.utf8'
p = yield from asyncio.create_subprocess_exec(*cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
close_fds=True, env=environ)
out, err = yield from p.communicate()
return _process_lvm_output(p.returncode, out, err, log)
def reset_cache():
qubes.storage.lvm.size_cache = init_cache()
@asyncio.coroutine
def reset_cache_coro():
qubes.storage.lvm.size_cache = yield from init_cache_coro()

View File

@ -22,13 +22,14 @@
but not required.
'''
import asyncio
import collections
import errno
import fcntl
import functools
import glob
import logging
import os
import re
import subprocess
import tempfile
from contextlib import contextmanager, suppress
@ -36,7 +37,8 @@ from contextlib import contextmanager, suppress
import qubes.storage
BLKSIZE = 512
FICLONE = 1074041865 # see ioctl_ficlone manpage
FICLONE = 1074041865 # defined in <linux/fs.h>
LOOP_SET_CAPACITY = 0x4C07 # defined in <linux/loop.h>
LOGGER = logging.getLogger('qubes.storage.reflink')
@ -53,7 +55,7 @@ class ReflinkPool(qubes.storage.Pool):
def setup(self):
created = _make_dir(self.dir_path)
if self.setup_check and not is_reflink_supported(self.dir_path):
if self.setup_check and not is_supported(self.dir_path):
if created:
_remove_empty_dir(self.dir_path)
raise qubes.storage.StoragePoolException(
@ -115,12 +117,37 @@ class ReflinkPool(qubes.storage.Pool):
[pool for pool in app.pools.values() if pool is not self],
self.dir_path)
def _unblock(method):
''' Decorator transforming a synchronous volume method into a
coroutine that runs the original method in the event loop's
thread-based default executor, under a per-volume lock.
'''
@asyncio.coroutine
@functools.wraps(method)
def wrapper(self, *args, **kwargs):
with (yield from self._lock): # pylint: disable=protected-access
return (yield from asyncio.get_event_loop().run_in_executor(
None, functools.partial(method, self, *args, **kwargs)))
return wrapper
class ReflinkVolume(qubes.storage.Volume):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._lock = asyncio.Lock()
self._path_vid = os.path.join(self.pool.dir_path, self.vid)
self._path_clean = self._path_vid + '.img'
self._path_dirty = self._path_vid + '-dirty.img'
self._path_import = self._path_vid + '-import.img'
self.path = self._path_dirty
@_unblock
def create(self):
if self.save_on_stop and not self.snap_on_start:
_create_sparse_file(self._path_clean, self.size)
return self
@_unblock
def verify(self):
if self.snap_on_start:
img = self.source._path_clean # pylint: disable=protected-access
@ -132,19 +159,26 @@ class ReflinkVolume(qubes.storage.Volume):
if img is None or os.path.exists(img):
return True
raise qubes.storage.StoragePoolException(
'Missing image file {!r} for volume {!s}'.format(img, self.vid))
'Missing image file {!r} for volume {}'.format(img, self.vid))
@_unblock
def remove(self):
''' Drop volume object from pool; remove volume images from
oldest to newest; remove empty VM directory.
'''
self.pool._volumes.pop(self, None) # pylint: disable=protected-access
self._cleanup()
self._prune_revisions(keep=0)
_remove_file(self._path_clean)
_remove_file(self._path_dirty)
_remove_empty_dir(os.path.dirname(self._path_dirty))
return self
def _cleanup(self):
for tmp in glob.iglob(glob.escape(self._path_vid) + '*.img*~*'):
_remove_file(tmp)
_remove_file(self._path_import)
def is_outdated(self):
if self.snap_on_start:
with suppress(FileNotFoundError):
@ -156,7 +190,9 @@ class ReflinkVolume(qubes.storage.Volume):
def is_dirty(self):
return self.save_on_stop and os.path.exists(self._path_dirty)
@_unblock
def start(self):
self._cleanup()
if self.is_dirty(): # implies self.save_on_stop
return self
if self.snap_on_start:
@ -168,24 +204,23 @@ class ReflinkVolume(qubes.storage.Volume):
_create_sparse_file(self._path_dirty, self.size)
return self
@_unblock
def stop(self):
if self.save_on_stop:
self._commit()
self._commit(self._path_dirty)
else:
_remove_file(self._path_dirty)
_remove_file(self._path_clean)
return self
def _commit(self):
def _commit(self, path_from):
self._add_revision()
self._prune_revisions()
_rename_file(self._path_dirty, self._path_clean)
_rename_file(path_from, self._path_clean)
def _add_revision(self):
if self.revisions_to_keep == 0:
return
if _get_file_disk_usage(self._path_clean) == 0:
return
ctime = os.path.getctime(self._path_clean)
timestamp = qubes.storage.isodate(int(ctime))
_copy_file(self._path_clean,
@ -198,7 +233,11 @@ class ReflinkVolume(qubes.storage.Volume):
for number, timestamp in list(self.revisions.items())[:-keep or None]:
_remove_file(self._path_revision(number, timestamp))
@_unblock
def revert(self, revision=None):
if self.is_dirty():
raise qubes.storage.StoragePoolException(
'Cannot revert: {} is not cleanly stopped'.format(self.vid))
if revision is None:
number, timestamp = list(self.revisions.items())[-1]
else:
@ -208,61 +247,58 @@ class ReflinkVolume(qubes.storage.Volume):
_rename_file(path_revision, self._path_clean)
return self
@_unblock
def resize(self, size):
''' Expand a read-write volume image; notify any corresponding
loop devices of the size change.
'''
if not self.rw:
raise qubes.storage.StoragePoolException(
'Cannot resize: {!s} is read-only'.format(self.vid))
'Cannot resize: {} is read-only'.format(self.vid))
if size < self.size:
raise qubes.storage.StoragePoolException(
'For your own safety, shrinking of {!s} is disabled'
' ({:d} < {:d}). If you really know what you are doing,'
'For your own safety, shrinking of {} is disabled'
' ({} < {}). If you really know what you are doing,'
' use "truncate" manually.'.format(self.vid, size, self.size))
try: # assume volume is not (cleanly) stopped ...
_resize_file(self._path_dirty, size)
self.size = size
except FileNotFoundError: # ... but it actually is.
_resize_file(self._path_clean, size)
self.size = size
return self
self.size = size
# resize any corresponding loop devices
out = _cmd('losetup', '--associated', self._path_dirty)
for match in re.finditer(br'^(/dev/loop[0-9]+): ', out, re.MULTILINE):
loop_dev = match.group(1).decode('ascii')
_cmd('losetup', '--set-capacity', loop_dev)
_update_loopdev_sizes(self._path_dirty)
return self
def _require_save_on_stop(self, method_name):
def export(self):
if not self.save_on_stop:
raise NotImplementedError(
'Cannot {!s}: {!s} is not save_on_stop'.format(
method_name, self.vid))
def export(self):
self._require_save_on_stop('export')
'Cannot export: {} is not save_on_stop'.format(self.vid))
return self._path_clean
def import_data(self):
self._require_save_on_stop('import_data')
_create_sparse_file(self._path_dirty, self.size)
return self._path_dirty
if not self.save_on_stop:
raise NotImplementedError(
'Cannot import_data: {} is not save_on_stop'.format(self.vid))
_create_sparse_file(self._path_import, self.size)
return self._path_import
def import_data_end(self, success):
if success:
self._commit()
self._commit(self._path_import)
else:
_remove_file(self._path_dirty)
_remove_file(self._path_import)
return self
@_unblock
def import_volume(self, src_volume):
self._require_save_on_stop('import_volume')
if not self.save_on_stop:
return self
try:
_copy_file(src_volume.export(), self._path_dirty)
_copy_file(src_volume.export(), self._path_import)
except:
self.import_data_end(False)
raise
@ -274,18 +310,6 @@ class ReflinkVolume(qubes.storage.Volume):
timestamp = self.revisions[number]
return self._path_clean + '.' + number + '@' + timestamp + 'Z'
@property
def _path_clean(self):
return os.path.join(self.pool.dir_path, self.vid + '.img')
@property
def _path_dirty(self):
return os.path.join(self.pool.dir_path, self.vid + '-dirty.img')
@property
def path(self):
return self._path_dirty
@property
def _next_revision_number(self):
numbers = self.revisions.keys()
@ -296,10 +320,10 @@ class ReflinkVolume(qubes.storage.Volume):
@property
def revisions(self):
prefix = self._path_clean + '.'
paths = glob.glob(glob.escape(prefix) + '*@*Z')
items = sorted((path[len(prefix):-1].split('@') for path in paths),
key=lambda item: int(item[0]))
return collections.OrderedDict(items)
paths = glob.iglob(glob.escape(prefix) + '*@*Z')
items = (path[len(prefix):-1].split('@') for path in paths)
return collections.OrderedDict(sorted(items,
key=lambda item: int(item[0])))
@property
def usage(self):
@ -391,39 +415,48 @@ def _create_sparse_file(path, size):
tmp.truncate(size)
LOGGER.info('Created sparse file: %s', tmp.name)
def _update_loopdev_sizes(img):
''' Resolve img; update the size of loop devices backed by it. '''
needle = os.fsencode(os.path.realpath(img)) + b'\n'
for sys_path in glob.iglob('/sys/block/loop[0-9]*/loop/backing_file'):
try:
with open(sys_path, 'rb') as sys_io:
if sys_io.read() != needle:
continue
except FileNotFoundError:
continue
with open('/dev/' + sys_path.split('/')[3]) as dev_io:
fcntl.ioctl(dev_io.fileno(), LOOP_SET_CAPACITY)
def _attempt_ficlone(src, dst):
try:
fcntl.ioctl(dst.fileno(), FICLONE, src.fileno())
return True
except OSError:
return False
def _copy_file(src, dst):
''' Copy src to dst as a reflink if possible, sparse if not. '''
if not os.path.exists(src):
raise FileNotFoundError(src)
with _replace_file(dst) as tmp:
LOGGER.info('Copying file: %s -> %s', src, tmp.name)
_cmd('cp', '--sparse=always', '--reflink=auto', src, tmp.name)
with _replace_file(dst) as tmp_io:
with open(src, 'rb') as src_io:
if _attempt_ficlone(src_io, tmp_io):
LOGGER.info('Reflinked file: %s -> %s', src, tmp_io.name)
return True
LOGGER.info('Copying file: %s -> %s', src, tmp_io.name)
cmd = 'cp', '--sparse=always', src, tmp_io.name
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if p.returncode != 0:
raise qubes.storage.StoragePoolException(str(p))
return False
def _cmd(*args):
''' Run command until finished; return stdout (as bytes) if it
exited 0. Otherwise, raise a detailed StoragePoolException.
'''
try:
return subprocess.run(args, check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE).stdout
except subprocess.CalledProcessError as ex:
msg = '{!s} err={!r} out={!r}'.format(ex, ex.stderr, ex.stdout)
raise qubes.storage.StoragePoolException(msg) from ex
def is_reflink_supported(dst_dir, src_dir=None):
def is_supported(dst_dir, src_dir=None):
''' Return whether destination directory supports reflink copies
from source directory. (A temporary file is created in each
directory, using O_TMPFILE if possible.)
'''
if src_dir is None:
src_dir = dst_dir
dst = tempfile.TemporaryFile(dir=dst_dir)
src = tempfile.TemporaryFile(dir=src_dir)
src.write(b'foo') # don't let any filesystem get clever with empty files
try:
fcntl.ioctl(dst.fileno(), FICLONE, src.fileno())
return True
except OSError:
return False
with tempfile.TemporaryFile(dir=src_dir) as src, \
tempfile.TemporaryFile(dir=dst_dir) as dst:
src.write(b'foo') # don't let any fs get clever with empty files
return _attempt_ficlone(src, dst)

View File

@ -379,8 +379,27 @@ class QubesTestCase(unittest.TestCase):
self.loop = asyncio.get_event_loop()
self.addCleanup(self.cleanup_loop)
self.addCleanup(self.cleanup_traceback)
self.addCleanup(qubes.ext.pci._cache_get.cache_clear)
def cleanup_traceback(self):
'''Remove local variables reference from tracebacks to allow garbage
collector to clean all Qubes*() objects, otherwise file descriptors
held by them will leak'''
exc_infos = [e for test_case, e in self._outcome.errors
if test_case is self]
if self._outcome.expectedFailure:
exc_infos.append(self._outcome.expectedFailure)
for exc_info in exc_infos:
if exc_info is None:
continue
ex = exc_info[1]
while ex is not None:
if isinstance(ex, qubes.exc.QubesVMError):
ex.vm = None
traceback.clear_frames(ex.__traceback__)
ex = ex.__context__
def cleanup_gc(self):
gc.collect()
leaked = [obj for obj in gc.get_objects() + gc.garbage
@ -397,6 +416,8 @@ class QubesTestCase(unittest.TestCase):
except ImportError:
pass
# do not keep leaked object references in locals()
leaked = bool(leaked)
assert not leaked
def cleanup_loop(self):
@ -421,6 +442,13 @@ class QubesTestCase(unittest.TestCase):
except asyncio.TimeoutError:
raise AssertionError('libvirt event impl drain timeout')
# this is stupid, but apparently it requires two passes
# to cleanup SIGCHLD handlers
self.loop.stop()
self.loop.run_forever()
self.loop.stop()
self.loop.run_forever()
# Check there are no Tasks left.
assert not self.loop._ready
assert not self.loop._scheduled
@ -763,20 +791,6 @@ class SystemTestCase(QubesTestCase):
vmname = vm.name
app = vm.app
# avoid race with DispVM.auto_cleanup=True
try:
self.loop.run_until_complete(
asyncio.wait_for(vm.startup_lock.acquire(), 10))
except asyncio.TimeoutError:
pass
try:
# XXX .is_running() may throw libvirtError if undefined
if vm.is_running():
self.loop.run_until_complete(vm.kill())
except: # pylint: disable=bare-except
pass
try:
self.loop.run_until_complete(vm.remove_from_disk())
except: # pylint: disable=bare-except
@ -842,7 +856,7 @@ class SystemTestCase(QubesTestCase):
'''
try:
volumes = subprocess.check_output(
['sudo', 'lvs', '--noheadings', '-o', 'vg_name,name',
['lvs', '--noheadings', '-o', 'vg_name,name',
'--separator', '/']).decode()
if ('/vm-' + prefix) not in volumes:
return
@ -857,18 +871,36 @@ class SystemTestCase(QubesTestCase):
vms = list(vms)
if not vms:
return
# first kill all the domains, to avoid side effects of changing netvm
for vm in vms:
try:
# XXX .is_running() may throw libvirtError if undefined
if vm.is_running():
self.loop.run_until_complete(vm.kill())
except: # pylint: disable=bare-except
pass
# break dependencies
for vm in vms:
vm.default_dispvm = None
# then remove in reverse topological order (wrt netvm), using naive
vm.netvm = None
# take app instance from any VM to be removed
app = vms[0].app
if app.default_dispvm in vms:
app.default_dispvm = None
if app.default_netvm in vms:
app.default_netvm = None
del app
# then remove in reverse topological order (wrt template), using naive
# algorithm
# this heavily depends on lack of netvm loops
# this heavily depends on lack of template loops, but those are
# impossible
while vms:
vm = vms.pop(0)
# make sure that all connected VMs are going to be removed,
# otherwise this will loop forever
assert all(x in vms for x in vm.connected_vms)
if list(vm.connected_vms):
child_vms = list(getattr(vm, 'appvms', []))
assert all(x in vms for x in child_vms)
if child_vms:
# if still something use this VM, put it at the end of queue
# and try next one
vms.append(vm)
@ -876,9 +908,16 @@ class SystemTestCase(QubesTestCase):
self._remove_vm_qubes(vm)
def remove_test_vms(self, xmlpath=XMLPATH, prefix=VMPREFIX):
'''Aggresively remove any domain that has name in testing namespace.
'''Aggressively remove any domain that has name in testing namespace.
:param prefix: name prefix of VMs to remove, can be a list of prefixes
'''
if isinstance(prefix, str):
prefixes = [prefix]
else:
prefixes = prefix
del prefix
# first, remove them Qubes-way
if os.path.exists(xmlpath):
try:
@ -891,7 +930,7 @@ class SystemTestCase(QubesTestCase):
except AttributeError:
host_app = qubes.Qubes()
self.remove_vms([vm for vm in app.domains
if vm.name.startswith(prefix) or
if any(vm.name.startswith(prefix) for prefix in prefixes) or
(isinstance(vm, qubes.vm.dispvm.DispVM) and vm.name
not in host_app.domains)])
if not hasattr(self, 'host_app'):
@ -907,7 +946,7 @@ class SystemTestCase(QubesTestCase):
# now remove what was only in libvirt
conn = libvirt.open(qubes.config.defaults['libvirt_uri'])
for dom in conn.listAllDomains():
if dom.name().startswith(prefix):
if any(dom.name().startswith(prefix) for prefix in prefixes):
self._remove_vm_libvirt(dom)
conn.close()
@ -922,11 +961,12 @@ class SystemTestCase(QubesTestCase):
if not os.path.exists(dirpath):
continue
for name in os.listdir(dirpath):
if name.startswith(prefix):
if any(name.startswith(prefix) for prefix in prefixes):
vmnames.add(name)
for vmname in vmnames:
self._remove_vm_disk(vmname)
self._remove_vm_disk_lvm(prefix)
for prefix in prefixes:
self._remove_vm_disk_lvm(prefix)
def qrexec_policy(self, service, source, destination, allow=True,
action=None):
@ -943,7 +983,25 @@ class SystemTestCase(QubesTestCase):
return _QrexecPolicyContext(service, source, destination,
allow=allow, action=action)
def wait_for_window(self, title, timeout=30, show=True):
@asyncio.coroutine
def wait_for_window_hide_coro(self, title, winid, timeout=30):
"""
Wait for window do disappear
:param winid: window id
:return:
"""
wait_count = 0
while subprocess.call(['xdotool', 'getwindowname', str(winid)],
stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) == 0:
wait_count += 1
if wait_count > timeout * 10:
self.fail("Timeout while waiting for {}({}) window to "
"disappear".format(title, winid))
yield from asyncio.sleep(0.1)
@asyncio.coroutine
def wait_for_window_coro(self, title, search_class=False, timeout=30,
show=True):
"""
Wait for a window with a given title. Depending on show parameter,
it will wait for either window to show or to disappear.
@ -952,19 +1010,59 @@ class SystemTestCase(QubesTestCase):
:param timeout: timeout of the operation, in seconds
:param show: if True - wait for the window to be visible,
otherwise - to not be visible
:return: None
:param search_class: search based on window class instead of title
:return: window id of found window, if show=True
"""
wait_count = 0
while subprocess.call(['xdotool', 'search', '--name', title],
stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) \
!= int(not show):
wait_count += 1
if wait_count > timeout*10:
self.fail("Timeout while waiting for {} window to {}".format(
title, "show" if show else "hide")
)
self.loop.run_until_complete(asyncio.sleep(0.1))
xdotool_search = ['xdotool', 'search', '--onlyvisible']
if search_class:
xdotool_search.append('--class')
else:
xdotool_search.append('--name')
if show:
xdotool_search.append('--sync')
if not show:
try:
winid = subprocess.check_output(xdotool_search + [title],
stderr=subprocess.DEVNULL).decode()
except subprocess.CalledProcessError:
# already gone
return
yield from self.wait_for_window_hide_coro(winid, title,
timeout=timeout)
return
winid = None
while not winid:
p = yield from asyncio.create_subprocess_exec(
*xdotool_search, title,
stderr=subprocess.DEVNULL, stdout=subprocess.PIPE)
try:
(winid, _) = yield from asyncio.wait_for(
p.communicate(), timeout)
# don't check exit code, getting winid on stdout is enough
# indicator of success; specifically ignore xdotool failing
# with BadWindow or such - when some window appears only for a
# moment by xdotool didn't manage to get its properties
except asyncio.TimeoutError:
self.fail(
"Timeout while waiting for {} window to show".format(title))
return winid.decode().strip()
def wait_for_window(self, *args, **kwargs):
"""
Wait for a window with a given title. Depending on show parameter,
it will wait for either window to show or to disappear.
:param title: title of the window to wait for
:param timeout: timeout of the operation, in seconds
:param show: if True - wait for the window to be visible,
otherwise - to not be visible
:param search_class: search based on window class instead of title
:return: window id of found window, if show=True
"""
return self.loop.run_until_complete(
self.wait_for_window_coro(*args, **kwargs))
def enter_keys_in_window(self, title, keys):
"""
@ -985,15 +1083,12 @@ class SystemTestCase(QubesTestCase):
subprocess.check_call(command)
def shutdown_and_wait(self, vm, timeout=60):
self.loop.run_until_complete(vm.shutdown())
while timeout > 0:
if not vm.is_running():
return
self.loop.run_until_complete(asyncio.sleep(1))
timeout -= 1
name = vm.name
del vm
self.fail("Timeout while waiting for VM {} shutdown".format(name))
try:
self.loop.run_until_complete(vm.shutdown(wait=True, timeout=timeout))
except qubes.exc.QubesException:
name = vm.name
del vm
self.fail("Timeout while waiting for VM {} shutdown".format(name))
def prepare_hvm_system_linux(self, vm, init_script, extra_files=None):
if not os.path.exists('/usr/lib/grub/i386-pc'):
@ -1109,21 +1204,30 @@ class SystemTestCase(QubesTestCase):
@asyncio.coroutine
def wait_for_session(self, vm):
timeout = 30
if getattr(vm, 'template', None) and 'whonix-ws' in vm.template.name:
# first boot of whonix-ws takes more time because of /home
# initialization, including Tor Browser copying
timeout = 120
yield from asyncio.wait_for(
vm.run_service_for_stdio(
'qubes.WaitForSession', input=vm.default_user.encode()),
timeout=30)
timeout=timeout)
_templates = None
def list_templates():
'''Returns tuple of template names available in the system.'''
global _templates
if _templates is None:
if 'QUBES_TEST_TEMPLATES' in os.environ:
_templates = os.environ['QUBES_TEST_TEMPLATES'].split()
if _templates is None:
try:
app = qubes.Qubes()
_templates = tuple(vm.name for vm in app.domains
if isinstance(vm, qubes.vm.templatevm.TemplateVM))
if isinstance(vm, qubes.vm.templatevm.TemplateVM) and
vm.features.get('os', None) != 'Windows')
app.close()
del app
except OSError:
@ -1165,12 +1269,29 @@ def create_testcases_for_templates(name, *bases, module, **kwds):
for template in list_templates():
clsname = name + '_' + template
if hasattr(module, clsname):
continue
cls = type(clsname, bases, {'template': template, **kwds})
cls.__module__ = module.__name__
# XXX I wonder what other __dunder__ attrs did I miss
setattr(module, clsname, cls)
yield '.'.join((module.__name__, clsname))
def maybe_create_testcases_on_import(create_testcases_gen):
'''If certain conditions are met, call *create_testcases_gen* to create
testcases for templates tests. The purpose is to use it on integration
tests module(s) import, so the test runner could discover tests without
using load tests protocol.
The conditions - any of:
- QUBES_TEST_TEMPLATES present in the environment (it's possible to
create test cases without opening qubes.xml)
- QUBES_TEST_LOAD_ALL present in the environment
'''
if 'QUBES_TEST_TEMPLATES' in os.environ or \
'QUBES_TEST_LOAD_ALL' in os.environ:
list(create_testcases_gen())
def extra_info(obj):
'''Return short info identifying object.
@ -1203,6 +1324,7 @@ def load_tests(loader, tests, pattern): # pylint: disable=unused-argument
'qubes.tests.vm.init',
'qubes.tests.storage',
'qubes.tests.storage_file',
'qubes.tests.storage_reflink',
'qubes.tests.storage_lvm',
'qubes.tests.storage_kernels',
'qubes.tests.ext',
@ -1240,11 +1362,13 @@ def load_tests(loader, tests, pattern): # pylint: disable=unused-argument
'qubes.tests.integ.basic',
'qubes.tests.integ.storage',
'qubes.tests.integ.pvgrub',
'qubes.tests.integ.devices_block',
'qubes.tests.integ.devices_pci',
'qubes.tests.integ.dom0_update',
'qubes.tests.integ.network',
'qubes.tests.integ.dispvm',
'qubes.tests.integ.vm_qrexec_gui',
'qubes.tests.integ.mime',
'qubes.tests.integ.salt',
'qubes.tests.integ.backup',
'qubes.tests.integ.backupcompatibility',

View File

@ -39,7 +39,7 @@ import qubes.storage
# properties defined in API
volume_properties = [
'pool', 'vid', 'size', 'usage', 'rw', 'source',
'pool', 'vid', 'size', 'usage', 'rw', 'source', 'path',
'save_on_stop', 'snap_on_start', 'revisions_to_keep']
@ -60,6 +60,7 @@ class AdminAPITestCase(qubes.tests.QubesTestCase):
app = qubes.Qubes('/tmp/qubes-test.xml', load=False)
app.vmm = unittest.mock.Mock(spec=qubes.app.VMMConnection)
app.load_initial_values()
app.setup_pools()
app.default_kernel = '1.0'
app.default_netvm = None
self.template = app.add_new_vm('TemplateVM', label='black',

View File

@ -131,11 +131,14 @@ class TC_00_API_Misc(qubes.tests.QubesTestCase):
self.assertEqual(self.src.mock_calls, [
mock.call.untrusted_qdb.read('/qubes-tools/qrexec'),
mock.call.untrusted_qdb.read('/qubes-tools/gui'),
mock.call.untrusted_qdb.read('/qubes-tools/gui-emulated'),
mock.call.untrusted_qdb.read('/qubes-tools/default-user'),
mock.call.untrusted_qdb.read('/qubes-tools/os'),
mock.call.fire_event_async('features-request', untrusted_features={
'gui': '1',
'default-user': 'user',
'qrexec': '1'}),
'qrexec': '1',
'os': 'Linux'}),
('fire_event_async().__iter__', (), {}),
])
self.assertEqual(self.app.mock_calls, [mock.call.save()])
@ -153,11 +156,14 @@ class TC_00_API_Misc(qubes.tests.QubesTestCase):
self.assertEqual(self.src.mock_calls, [
mock.call.untrusted_qdb.read('/qubes-tools/qrexec'),
mock.call.untrusted_qdb.read('/qubes-tools/gui'),
mock.call.untrusted_qdb.read('/qubes-tools/gui-emulated'),
mock.call.untrusted_qdb.read('/qubes-tools/default-user'),
mock.call.untrusted_qdb.read('/qubes-tools/os'),
mock.call.fire_event_async('features-request', untrusted_features={
'gui': '1',
'default-user': 'user',
'qrexec': '1'}),
'qrexec': '1',
'os': 'Linux'}),
('fire_event_async().__iter__', (), {}),
])
self.assertEqual(self.app.mock_calls, [mock.call.save()])

View File

@ -30,6 +30,7 @@ import qubes.events
import qubes.tests
import qubes.tests.init
import qubes.tests.storage_reflink
class TestApp(qubes.tests.TestEmitter):
pass
@ -264,6 +265,44 @@ class TC_30_VMCollection(qubes.tests.QubesTestCase):
# pass
class TC_80_QubesInitialPools(qubes.tests.QubesTestCase):
def setUp(self):
super().setUp()
self.app = qubes.Qubes('/tmp/qubestest.xml', load=False,
offline_mode=True)
self.test_dir = '/var/tmp/test-varlibqubes'
self.test_patch = mock.patch.dict(
qubes.config.defaults['pool_configs']['varlibqubes'],
{'dir_path': self.test_dir})
self.test_patch.start()
def tearDown(self):
self.test_patch.stop()
self.app.close()
del self.app
def get_driver(self, fs_type, accessible):
qubes.tests.storage_reflink.mkdir_fs(self.test_dir, fs_type,
accessible=accessible, cleanup_via=self.addCleanup)
self.app.load_initial_values()
varlibqubes = self.app.pools['varlibqubes']
self.assertEqual(varlibqubes.dir_path, self.test_dir)
return varlibqubes.driver
def test_100_varlibqubes_btrfs_accessible(self):
self.assertEqual(self.get_driver('btrfs', True), 'file-reflink')
def test_101_varlibqubes_btrfs_inaccessible(self):
self.assertEqual(self.get_driver('btrfs', False), 'file')
def test_102_varlibqubes_ext4_accessible(self):
self.assertEqual(self.get_driver('ext4', True), 'file')
def test_103_varlibqubes_ext4_inaccessible(self):
self.assertEqual(self.get_driver('ext4', False), 'file')
class TC_89_QubesEmpty(qubes.tests.QubesTestCase):
def tearDown(self):
try:

View File

@ -21,6 +21,8 @@
from unittest import mock
import qubes.ext.core_features
import qubes.ext.services
import qubes.ext.windows
import qubes.tests
@ -163,3 +165,143 @@ class TC_00_CoreFeatures(qubes.tests.QubesTestCase):
('features.__contains__', ('qrexec',), {}),
('features.__contains__', ('gui',), {}),
])
class TC_10_WindowsFeatures(qubes.tests.QubesTestCase):
def setUp(self):
super().setUp()
self.ext = qubes.ext.windows.WindowsFeatures()
self.vm = mock.MagicMock()
self.features = {}
self.vm.configure_mock(**{
'features.get.side_effect': self.features.get,
'features.__contains__.side_effect': self.features.__contains__,
'features.__setitem__.side_effect': self.features.__setitem__,
})
def test_000_notify_tools_full(self):
del self.vm.template
self.ext.qubes_features_request(self.vm, 'features-request',
untrusted_features={
'gui': '1',
'version': '1',
'default-user': 'user',
'qrexec': '1',
'os': 'Windows'})
self.assertEqual(self.vm.mock_calls, [
('features.__setitem__', ('os', 'Windows'), {}),
('features.__setitem__', ('rpc-clipboard', True), {}),
])
def test_001_notify_tools_no_qrexec(self):
del self.vm.template
self.ext.qubes_features_request(self.vm, 'features-request',
untrusted_features={
'gui': '1',
'version': '1',
'default-user': 'user',
'qrexec': '0',
'os': 'Windows'})
self.assertEqual(self.vm.mock_calls, [
('features.__setitem__', ('os', 'Windows'), {}),
])
def test_002_notify_tools_other_os(self):
del self.vm.template
self.ext.qubes_features_request(self.vm, 'features-request',
untrusted_features={
'gui': '1',
'version': '1',
'default-user': 'user',
'qrexec': '1',
'os': 'other'})
self.assertEqual(self.vm.mock_calls, [])
class TC_20_Services(qubes.tests.QubesTestCase):
def setUp(self):
super().setUp()
self.ext = qubes.ext.services.ServicesExtension()
self.vm = mock.MagicMock()
self.features = {}
self.vm.configure_mock(**{
'template': None,
'is_running.return_value': True,
'features.get.side_effect': self.features.get,
'features.items.side_effect': self.features.items,
'features.__iter__.side_effect': self.features.__iter__,
'features.__contains__.side_effect': self.features.__contains__,
'features.__setitem__.side_effect': self.features.__setitem__,
'features.__delitem__.side_effect': self.features.__delitem__,
})
def test_000_write_to_qdb(self):
self.features['service.test1'] = '1'
self.features['service.test2'] = ''
self.ext.on_domain_qdb_create(self.vm, 'domain-qdb-create')
self.assertEqual(sorted(self.vm.untrusted_qdb.mock_calls), [
('write', ('/qubes-service/test1', '1'), {}),
('write', ('/qubes-service/test2', '0'), {}),
])
def test_001_feature_set(self):
self.ext.on_domain_feature_set(self.vm,
'feature-set:service.test_no_oldvalue',
'service.test_no_oldvalue', '1')
self.ext.on_domain_feature_set(self.vm,
'feature-set:service.test_oldvalue',
'service.test_oldvalue', '1', '')
self.ext.on_domain_feature_set(self.vm,
'feature-set:service.test_disable',
'service.test_disable', '', '1')
self.ext.on_domain_feature_set(self.vm,
'feature-set:service.test_disable_no_oldvalue',
'service.test_disable_no_oldvalue', '')
self.assertEqual(sorted(self.vm.untrusted_qdb.mock_calls), sorted([
('write', ('/qubes-service/test_no_oldvalue', '1'), {}),
('write', ('/qubes-service/test_oldvalue', '1'), {}),
('write', ('/qubes-service/test_disable', '0'), {}),
('write', ('/qubes-service/test_disable_no_oldvalue', '0'), {}),
]))
def test_002_feature_delete(self):
self.ext.on_domain_feature_delete(self.vm,
'feature-delete:service.test3', 'service.test3')
self.assertEqual(sorted(self.vm.untrusted_qdb.mock_calls), [
('rm', ('/qubes-service/test3',), {}),
])
def test_010_supported_services(self):
self.ext.supported_services(self.vm, 'features-request',
untrusted_features={
'supported-service.test1': '1', # ok
'supported-service.test2': '0', # ignored
'supported-service.test3': 'some text', # ignored
'no-service': '1', # ignored
})
self.assertEqual(self.features, {
'supported-service.test1': True,
})
def test_011_supported_services_add(self):
self.features['supported-service.test1'] = '1'
self.ext.supported_services(self.vm, 'features-request',
untrusted_features={
'supported-service.test1': '1', # ok
'supported-service.test2': '1', # ok
})
# also check if existing one is untouched
self.assertEqual(self.features, {
'supported-service.test1': '1',
'supported-service.test2': True,
})
def test_012_supported_services_remove(self):
self.features['supported-service.test1'] = '1'
self.ext.supported_services(self.vm, 'features-request',
untrusted_features={
'supported-service.test2': '1', # ok
})
self.assertEqual(self.features, {
'supported-service.test2': True,
})

View File

@ -66,8 +66,9 @@ class VMWrapper(object):
def __hash__(self):
return hash(self._vm)
def start(self):
return self._loop.run_until_complete(self._vm.start())
def start(self, start_guid=True):
return self._loop.run_until_complete(
self._vm.start(start_guid=start_guid))
def shutdown(self):
return self._loop.run_until_complete(self._vm.shutdown())
@ -194,7 +195,8 @@ def load_tests(loader, tests, pattern):
for entry in pkg_resources.iter_entry_points('qubes.tests.extra'):
try:
for test_case in entry.load()():
tests.addTests(loader.loadTestsFromTestCase(test_case))
tests.addTests(loader.loadTestsFromNames([
'{}.{}'.format(test_case.__module__, test_case.__name__)]))
except Exception as err: # pylint: disable=broad-except
def runTest(self):
raise err
@ -207,10 +209,10 @@ def load_tests(loader, tests, pattern):
'qubes.tests.extra.for_template'):
try:
for test_case in entry.load()():
test.addTests(loader.loadTestsFromNames(
tests.addTests(loader.loadTestsFromNames(
qubes.tests.create_testcases_for_templates(
test_case.__name__, test_case,
globals=sys.modules[test_case.__module__].__dict__)))
module=sys.modules[test_case.__module__])))
except Exception as err: # pylint: disable=broad-except
def runTest(self):
raise err

View File

@ -480,16 +480,7 @@ class TC_00_Backup(BackupTestsMixin, qubes.tests.SystemTestCase):
os.mkdir(test_dir)
with open(os.path.join(test_dir, 'some-file.txt'), 'w') as f:
f.write('test file\n')
self.restore_backup(expect_errors=[
'Error restoring VM test-inst-test-net, skipping: Got empty '
'response from qubesd. See journalctl in dom0 for details.',
'Error setting test-inst-test1.netvm to test-inst-test-net: '
'\'"No such domain: \\\'test-inst-test-net\\\'"\'',
])
del vms_info['test-inst-test-net']
vms_info['test-inst-test1']['properties']['netvm'] = \
str(self.app.default_netvm)
vms_info['test-inst-test1']['default']['netvm'] = True
self.restore_backup()
self.assertCorrectlyRestored(vms_info, orig_hashes)
finally:
del vms
@ -650,9 +641,14 @@ class TC_10_BackupVMMixin(BackupTestsMixin):
del vms
def create_testcases_for_templates():
return qubes.tests.create_testcases_for_templates('TC_10_BackupVM',
TC_10_BackupVMMixin, qubes.tests.SystemTestCase,
module=sys.modules[__name__])
def load_tests(loader, tests, pattern):
tests.addTests(loader.loadTestsFromNames(
qubes.tests.create_testcases_for_templates('TC_10_BackupVM',
TC_10_BackupVMMixin, qubes.tests.SystemTestCase,
globals=globals())))
create_testcases_for_templates()))
return tests
qubes.tests.maybe_create_testcases_on_import(create_testcases_for_templates)

View File

@ -19,6 +19,7 @@
from multiprocessing import Queue
import asyncio
import os
import shutil
import subprocess
@ -122,7 +123,10 @@ class TC_00_BackupCompatibility(
qubes.tests.integ.backup.BackupTestsMixin, qubes.tests.SystemTestCase):
def tearDown(self):
self.remove_test_vms(prefix="test-")
prefixes = ["test-", "disp-test-"]
if 'disp-no-netvm' not in self.host_app.domains:
prefixes.append('disp-no-netvm')
self.remove_test_vms(prefix=prefixes)
super(TC_00_BackupCompatibility, self).tearDown()
def create_whitelisted_appmenus(self, filename):
@ -233,7 +237,7 @@ class TC_00_BackupCompatibility(
self.create_sparse(self.fullpath(
"vm-templates/test-template-clone/root.img"), 10*2**30)
self.fill_image(self.fullpath(
"vm-templates/test-template-clone/root.img"), 1*2**30, True)
"vm-templates/test-template-clone/root.img"), 100*2**20, True)
self.create_volatile_img(self.fullpath(
"vm-templates/test-template-clone/volatile.img"))
subprocess.check_call([
@ -382,7 +386,7 @@ class TC_00_BackupCompatibility(
def assertRestored(self, name, **kwargs):
with self.assertNotRaises((KeyError, qubes.exc.QubesException)):
vm = self.app.domains[name]
vm.storage.verify()
asyncio.get_event_loop().run_until_complete(vm.storage.verify())
for prop, value in kwargs.items():
if prop == 'klass':
self.assertIsInstance(vm, value)

View File

@ -35,6 +35,8 @@ import collections
import pkg_resources
import shutil
import sys
import qubes
import qubes.firewall
import qubes.tests
@ -106,10 +108,33 @@ class TC_00_Basic(qubes.tests.SystemTestCase):
self.assertTrue(self.vm.is_running())
# Type 'poweroff'
subprocess.check_call(['xdotool', 'search', '--name', self.vm.name,
'type', 'poweroff\r'])
self.loop.run_until_complete(asyncio.sleep(1))
'type', '--window', '%1', 'poweroff\r'])
for _ in range(10):
if not self.vm.is_running():
break
self.loop.run_until_complete(asyncio.sleep(1))
self.assertFalse(self.vm.is_running())
def test_130_autostart_disable_on_remove(self):
vm = self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=self.make_vm_name('vm'),
template=self.app.default_template,
label='red')
self.assertIsNotNone(vm)
self.loop.run_until_complete(vm.create_on_disk())
vm.autostart = True
self.assertTrue(os.path.exists(
'/etc/systemd/system/multi-user.target.wants/'
'qubes-vm@{}.service'.format(vm.name)),
"systemd service not enabled by autostart=True")
del self.app.domains[vm]
self.loop.run_until_complete(vm.remove_from_disk())
self.assertFalse(os.path.exists(
'/etc/systemd/system/multi-user.target.wants/'
'qubes-vm@{}.service'.format(vm.name)),
"systemd service not disabled on domain remove")
def _test_200_on_domain_start(self, vm, event, **_kwargs):
'''Simulate domain crash just after startup'''
vm.libvirt_domain.destroy()
@ -203,8 +228,10 @@ class TC_00_Basic(qubes.tests.SystemTestCase):
if self.test_failure_reason:
self.fail(self.test_failure_reason)
while self.vm.get_power_state() != 'Halted':
self.loop.run_until_complete(asyncio.sleep(1))
# and give a chance for both domain-shutdown handlers to execute
self.loop.run_until_complete(asyncio.sleep(1))
self.loop.run_until_complete(asyncio.sleep(3))
if self.test_failure_reason:
self.fail(self.test_failure_reason)
@ -227,6 +254,7 @@ class TC_00_Basic(qubes.tests.SystemTestCase):
try:
# first boot, mkfs private volume
self.loop.run_until_complete(vm.start())
self.loop.run_until_complete(self.wait_for_session(vm))
# get private volume UUID
private_uuid, _ = self.loop.run_until_complete(
vm.run_for_stdio('blkid -o value /dev/xvdb', user='root'))
@ -422,127 +450,6 @@ class TC_01_Properties(qubes.tests.SystemTestCase):
self.loop.run_until_complete(self.vm2.create_on_disk())
class TC_02_QvmPrefs(qubes.tests.SystemTestCase):
# pylint: disable=attribute-defined-outside-init
def setUp(self):
super(TC_02_QvmPrefs, self).setUp()
self.init_default_template()
self.sharedopts = ['--qubesxml', qubes.tests.XMLPATH]
def setup_appvm(self):
self.testvm = self.app.add_new_vm(
qubes.vm.appvm.AppVM,
name=self.make_vm_name("vm"),
label='red')
self.loop.run_until_complete(self.testvm.create_on_disk())
self.app.save()
def setup_hvm(self):
self.testvm = self.app.add_new_vm(
qubes.vm.appvm.AppVM,
name=self.make_vm_name("hvm"),
label='red')
self.testvm.virt_mode = 'hvm'
self.loop.run_until_complete(self.testvm.create_on_disk())
self.app.save()
def pref_set(self, name, value, valid=True):
self.loop.run_until_complete(self._pref_set(name, value, valid))
@asyncio.coroutine
def _pref_set(self, name, value, valid=True):
cmd = ['qvm-prefs']
if value != '-D':
cmd.append('--')
cmd.extend((self.testvm.name, name, value))
p = yield from asyncio.create_subprocess_exec(*cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(stdout, stderr) = yield from p.communicate()
if valid:
self.assertEqual(p.returncode, 0,
"qvm-prefs .. '{}' '{}' failed: {}{}".format(
name, value, stdout, stderr
))
else:
self.assertNotEquals(p.returncode, 0,
"qvm-prefs should reject value '{}' for "
"property '{}'".format(value, name))
def pref_get(self, name):
self.loop.run_until_complete(self._pref_get(name))
@asyncio.coroutine
def _pref_get(self, name):
p = yield from asyncio.create_subprocess_exec(
'qvm-prefs', *self.sharedopts, '--', self.testvm.name, name,
stdout=subprocess.PIPE)
(stdout, _) = yield from p.communicate()
self.assertEqual(p.returncode, 0)
return stdout.strip()
bool_test_values = [
('true', 'True', True),
('False', 'False', True),
('0', 'False', True),
('1', 'True', True),
('invalid', '', False)
]
def execute_tests(self, name, values):
"""
Helper function, which executes tests for given property.
:param values: list of tuples (value, expected, valid),
where 'value' is what should be set and 'expected' is what should
qvm-prefs returns as a property value and 'valid' marks valid and
invalid values - if it's False, qvm-prefs should reject the value
:return: None
"""
for (value, expected, valid) in values:
self.pref_set(name, value, valid)
if valid:
self.assertEqual(self.pref_get(name), expected)
@unittest.skip('test not converted to core3 API')
def test_006_template(self):
templates = [tpl for tpl in self.app.domains.values() if
isinstance(tpl, qubes.vm.templatevm.TemplateVM)]
if not templates:
self.skipTest("No templates installed")
some_template = templates[0].name
self.setup_appvm()
self.execute_tests('template', [
(some_template, some_template, True),
('invalid', '', False),
])
@unittest.skip('test not converted to core3 API')
def test_014_pcidevs(self):
self.setup_appvm()
self.execute_tests('pcidevs', [
('[]', '[]', True),
('[ "00:00.0" ]', "['00:00.0']", True),
('invalid', '', False),
('[invalid]', '', False),
# TODO:
# ('["12:12.0"]', '', False)
])
@unittest.skip('test not converted to core3 API')
def test_024_pv_reject_hvm_props(self):
self.setup_appvm()
self.execute_tests('guiagent_installed', [('False', '', False)])
self.execute_tests('qrexec_installed', [('False', '', False)])
self.execute_tests('drive', [('/tmp/drive.img', '', False)])
self.execute_tests('timezone', [('localtime', '', False)])
@unittest.skip('test not converted to core3 API')
def test_025_hvm_reject_pv_props(self):
self.setup_hvm()
self.execute_tests('kernel', [('default', '', False)])
self.execute_tests('kernelopts', [('default', '', False)])
class TC_03_QvmRevertTemplateChanges(qubes.tests.SystemTestCase):
# pylint: disable=attribute-defined-outside-init
@ -570,11 +477,13 @@ class TC_03_QvmRevertTemplateChanges(qubes.tests.SystemTestCase):
def get_rootimg_checksum(self):
return subprocess.check_output(
['sha1sum', self.test_template.volumes['root'].path])
['sha1sum', self.test_template.volumes['root'].export()]).\
decode().split(' ')[0]
def _do_test(self):
checksum_before = self.get_rootimg_checksum()
self.loop.run_until_complete(self.test_template.start())
self.loop.run_until_complete(self.wait_for_session(self.test_template))
self.shutdown_and_wait(self.test_template)
checksum_changed = self.get_rootimg_checksum()
if checksum_before == checksum_changed:
@ -787,21 +696,27 @@ class TC_06_AppVMMixin(object):
self.assertTrue(self.vm.is_running())
# Type 'poweroff'
subprocess.check_call(['xdotool', 'search', '--name', self.vm.name,
'type', 'poweroff\r'])
self.loop.run_until_complete(asyncio.sleep(1))
'type', '--window', '%1', 'poweroff\r'])
for _ in range(10):
if not self.vm.is_running():
break
self.loop.run_until_complete(asyncio.sleep(1))
self.assertFalse(self.vm.is_running())
def create_testcases_for_templates():
yield from qubes.tests.create_testcases_for_templates('TC_05_StandaloneVM',
TC_05_StandaloneVMMixin, qubes.tests.SystemTestCase,
module=sys.modules[__name__])
yield from qubes.tests.create_testcases_for_templates('TC_06_AppVM',
TC_06_AppVMMixin, qubes.tests.SystemTestCase,
module=sys.modules[__name__])
def load_tests(loader, tests, pattern):
tests.addTests(loader.loadTestsFromNames(
qubes.tests.create_testcases_for_templates('TC_05_StandaloneVM',
TC_05_StandaloneVMMixin, qubes.tests.SystemTestCase,
globals=globals())))
tests.addTests(loader.loadTestsFromNames(
qubes.tests.create_testcases_for_templates('TC_06_AppVM',
TC_06_AppVMMixin, qubes.tests.SystemTestCase,
globals=globals())))
create_testcases_for_templates()))
return tests
qubes.tests.maybe_create_testcases_on_import(create_testcases_for_templates)
# vim: ts=4 sw=4 et

View File

@ -2,7 +2,7 @@
#
# The Qubes OS Project, https://www.qubes-os.org/
#
# Copyright (C) 2016
# Copyright (C) 2018
# Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
#
# This library is free software; you can redistribute it and/or
@ -20,42 +20,44 @@
#
import os
import sys
import qubes
import qubes.tests
import qubes.qubesutils
import subprocess
# the same class for both dom0 and VMs
class TC_00_List(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
class TC_00_List(qubes.tests.SystemTestCase):
template = None
def setUp(self):
super(TC_00_List, self).setUp()
super().setUp()
self.img_path = '/tmp/test.img'
self.mount_point = '/tmp/test-dir'
if self.template is not None:
self.vm = self.qc.add_new_vm(
"QubesAppVm",
name=self.make_vm_name("vm"),
template=self.qc.get_vm_by_name(self.template))
self.vm.create_on_disk(verbose=False)
self.vm = self.app.add_new_vm(
"AppVM",
label='red',
name=self.make_vm_name("vm"))
self.loop.run_until_complete(
self.vm.create_on_disk())
self.app.save()
self.qc.unlock_db()
self.vm.start()
self.loop.run_until_complete(self.vm.start())
else:
self.qc.unlock_db()
self.vm = self.qc[0]
self.vm = self.app.domains[0]
def tearDown(self):
super(TC_00_List, self).tearDown()
super().tearDown()
if self.template is None:
if os.path.exists(self.mount_point):
subprocess.call(['sudo', 'umount', self.mount_point])
subprocess.call(['sudo', 'rmdir', self.mount_point])
subprocess.call(['sudo', 'dmsetup', 'remove', 'test-dm'])
if os.path.exists('/dev/mapper/test-dm'):
subprocess.call(['sudo', 'dmsetup', 'remove', 'test-dm'])
if os.path.exists(self.img_path):
loopdev = subprocess.check_output(['losetup', '-j',
self.img_path])
for dev in loopdev.splitlines():
for dev in loopdev.decode().splitlines():
subprocess.call(
['sudo', 'losetup', '-d', dev.split(':')[0]])
subprocess.call(['sudo', 'rm', '-f', self.img_path])
@ -67,9 +69,8 @@ class TC_00_List(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
elif user == "root":
subprocess.check_call(['sudo', 'sh', '-c', script])
else:
retcode = self.vm.run(script, user=user, wait=True)
if retcode != 0:
raise subprocess.CalledProcessError
self.loop.run_until_complete(
self.vm.run_for_stdio(script, user=user))
def test_000_list_loop(self):
if self.template is None:
@ -80,19 +81,18 @@ class TC_00_List(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
"losetup -f {path}; "
"udevadm settle".format(path=self.img_path), user="root")
dev_list = qubes.qubesutils.block_list_vm(self.vm)
dev_list = list(self.vm.devices['block'])
found = False
for dev in dev_list.keys():
if dev_list[dev]['desc'] == self.img_path:
self.assertTrue(dev.startswith(self.vm.name + ':loop'))
self.assertEquals(dev_list[dev]['mode'], 'w')
self.assertEquals(dev_list[dev]['size'], 1024 * 1024 * 128)
self.assertEquals(
dev_list[dev]['device'], '/dev/' + dev.split(':')[1])
for dev in dev_list:
if dev.description == self.img_path:
self.assertTrue(dev.ident.startswith('loop'))
self.assertEquals(dev.mode, 'w')
self.assertEquals(dev.size, 1024 * 1024 * 128)
found = True
if not found:
self.fail("Device {} not found in {!r}".format(self.img_path, dev_list))
self.fail("Device {} not found in {!r}".format(
self.img_path, dev_list))
def test_001_list_loop_mounted(self):
if self.template is None:
@ -108,9 +108,9 @@ class TC_00_List(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
mntdir=self.mount_point),
user="root")
dev_list = qubes.qubesutils.block_list_vm(self.vm)
for dev in dev_list.keys():
if dev_list[dev]['desc'] == self.img_path:
dev_list = list(self.vm.devices['block'])
for dev in dev_list:
if dev.description == self.img_path:
self.fail(
'Device {} ({}) should not be listed because is mounted'
.format(dev, self.img_path))
@ -125,19 +125,17 @@ class TC_00_List(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
"/sys/block/$(basename $loopdev)/dev) 0\";"
"udevadm settle".format(path=self.img_path), user="root")
dev_list = qubes.qubesutils.block_list_vm(self.vm)
dev_list = list(self.vm.devices['block'])
found = False
for dev in dev_list.keys():
if dev.startswith(self.vm.name + ':loop'):
self.assertNotEquals(dev_list[dev]['desc'], self.img_path,
for dev in dev_list:
if dev.ident.startswith('loop'):
self.assertNotEquals(dev.description, self.img_path,
"Device {} ({}) should not be listed as it is used in "
"device-mapper".format(dev, self.img_path)
)
elif dev_list[dev]['desc'] == 'test-dm':
self.assertEquals(dev_list[dev]['mode'], 'w')
self.assertEquals(dev_list[dev]['size'], 1024 * 1024 * 128)
self.assertEquals(
dev_list[dev]['device'], '/dev/' + dev.split(':')[1])
elif dev.description == 'test-dm':
self.assertEquals(dev.mode, 'w')
self.assertEquals(dev.size, 1024 * 1024 * 128)
found = True
if not found:
@ -159,15 +157,15 @@ class TC_00_List(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
mntdir=self.mount_point),
user="root")
dev_list = qubes.qubesutils.block_list_vm(self.vm)
for dev in dev_list.keys():
if dev.startswith(self.vm.name + ':loop'):
self.assertNotEquals(dev_list[dev]['desc'], self.img_path,
dev_list = list(self.vm.devices['block'])
for dev in dev_list:
if dev.ident.startswith('loop'):
self.assertNotEquals(dev.description, self.img_path,
"Device {} ({}) should not be listed as it is used in "
"device-mapper".format(dev, self.img_path)
)
else:
self.assertNotEquals(dev_list[dev]['desc'], 'test-dm',
self.assertNotEquals(dev.description, 'test-dm',
"Device {} ({}) should not be listed as it is "
"mounted".format(dev, 'test-dm')
)
@ -183,19 +181,17 @@ class TC_00_List(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
"/sys/block/$(basename $loopdev)/dev) 0\";"
"udevadm settle".format(path=self.img_path), user="root")
dev_list = qubes.qubesutils.block_list_vm(self.vm)
dev_list = list(self.vm.devices['block'])
found = False
for dev in dev_list.keys():
if dev.startswith(self.vm.name + ':loop'):
self.assertNotEquals(dev_list[dev]['desc'], self.img_path,
for dev in dev_list:
if dev.ident.startswith('loop'):
self.assertNotEquals(dev.description, self.img_path,
"Device {} ({}) should not be listed as it is used in "
"device-mapper".format(dev, self.img_path)
)
elif dev_list[dev]['desc'] == 'test-dm':
self.assertEquals(dev_list[dev]['mode'], 'w')
self.assertEquals(dev_list[dev]['size'], 1024 * 1024 * 128)
self.assertEquals(
dev_list[dev]['device'], '/dev/' + dev.split(':')[1])
elif dev.description == 'test-dm':
self.assertEquals(dev.mode, 'w')
self.assertEquals(dev.size, 1024 * 1024 * 128)
found = True
if not found:
@ -216,15 +212,13 @@ class TC_00_List(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
"dmsetup remove test-dm;"
"udevadm settle".format(path=self.img_path), user="root")
dev_list = qubes.qubesutils.block_list_vm(self.vm)
dev_list = list(self.vm.devices['block'])
found = False
for dev in dev_list.keys():
if dev_list[dev]['desc'] == self.img_path:
self.assertTrue(dev.startswith(self.vm.name + ':loop'))
self.assertEquals(dev_list[dev]['mode'], 'w')
self.assertEquals(dev_list[dev]['size'], 1024 * 1024 * 128)
self.assertEquals(
dev_list[dev]['device'], '/dev/' + dev.split(':')[1])
for dev in dev_list:
if dev.description == self.img_path:
self.assertTrue(dev.ident.startswith('loop'))
self.assertEquals(dev.mode, 'w')
self.assertEquals(dev.size, 1024 * 1024 * 128)
found = True
if not found:
@ -242,16 +236,14 @@ class TC_00_List(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
"blockdev --rereadpt $loopdev; "
"udevadm settle".format(path=self.img_path), user="root")
dev_list = qubes.qubesutils.block_list_vm(self.vm)
dev_list = list(self.vm.devices['block'])
found = False
for dev in dev_list.keys():
if dev_list[dev]['desc'] == self.img_path:
self.assertTrue(dev.startswith(self.vm.name + ':loop'))
self.assertEquals(dev_list[dev]['mode'], 'w')
self.assertEquals(dev_list[dev]['size'], 1024 * 1024 * 128)
self.assertEquals(
dev_list[dev]['device'], '/dev/' + dev.split(':')[1])
self.assertIn(dev + 'p1', dev_list)
for dev in dev_list:
if dev.description == self.img_path:
self.assertTrue(dev.ident.startswith('loop'))
self.assertEquals(dev.mode, 'w')
self.assertEquals(dev.size, 1024 * 1024 * 128)
self.assertIn(dev.ident + 'p1', [d.ident for d in dev_list])
found = True
if not found:
@ -274,14 +266,14 @@ class TC_00_List(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
path=self.img_path, mntdir=self.mount_point),
user="root")
dev_list = qubes.qubesutils.block_list_vm(self.vm)
for dev in dev_list.keys():
if dev_list[dev]['desc'] == self.img_path:
dev_list = list(self.vm.devices['block'])
for dev in dev_list:
if dev.description == self.img_path:
self.fail(
'Device {} ({}) should not be listed because its '
'partition is mounted'
.format(dev, self.img_path))
elif dev.startswith(self.vm.name + ':loop') and dev.endswith('p1'):
elif dev.ident.startswith('loop') and dev.ident.endswith('p1'):
# FIXME: risky assumption that only tests create partitioned
# loop devices
self.fail(
@ -289,21 +281,14 @@ class TC_00_List(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
.format(dev, self.img_path))
def load_tests(loader, tests, pattern):
try:
qc = qubes.qubes.QubesVmCollection()
qc.lock_db_for_reading()
qc.load()
qc.unlock_db()
templates = [vm.name for vm in qc.values() if
isinstance(vm, qubes.qubes.QubesTemplateVm)]
except OSError:
templates = []
for template in templates:
tests.addTests(loader.loadTestsFromTestCase(
type(
'TC_00_List_' + template,
(TC_00_List, qubes.tests.QubesTestCase),
{'template': template})))
def create_testcases_for_templates():
return qubes.tests.create_testcases_for_templates('TC_00_List',
TC_00_List,
module=sys.modules[__name__])
def load_tests(loader, tests, pattern):
tests.addTests(loader.loadTestsFromNames(
create_testcases_for_templates()))
return tests
qubes.tests.maybe_create_testcases_on_import(create_testcases_for_templates)

View File

@ -23,11 +23,14 @@ import subprocess
import tempfile
import time
import unittest
from contextlib import suppress
from distutils import spawn
import asyncio
import sys
import qubes.tests
class TC_04_DispVM(qubes.tests.SystemTestCase):
@ -67,7 +70,7 @@ class TC_04_DispVM(qubes.tests.SystemTestCase):
self.assertEqual(lines[0], "test")
dispvm_name = lines[1]
# wait for actual DispVM destruction
self.loop.run_until_complete(asyncio.sleep(1))
self.loop.run_until_complete(asyncio.sleep(5))
self.assertNotIn(dispvm_name, self.app.domains)
def test_003_cleanup_destroyed(self):
@ -86,7 +89,7 @@ class TC_04_DispVM(qubes.tests.SystemTestCase):
p.stdin.write(b"sudo poweroff\n")
# do not close p.stdin on purpose - wait to automatic disconnect when
# domain is destroyed
timeout = 30
timeout = 70
lines_task = asyncio.ensure_future(p.stdout.read())
self.loop.run_until_complete(asyncio.wait_for(p.wait(), timeout))
self.loop.run_until_complete(lines_task)
@ -160,8 +163,15 @@ class TC_20_DispVMMixin(object):
self.enter_keys_in_window(window_title, ['Return'])
# Wait for window to close
self.wait_for_window(window_title, show=False)
finally:
p.stdin.close()
self.loop.run_until_complete(
asyncio.wait_for(p.wait(), 30))
except:
with suppress(ProcessLookupError):
p.terminate()
self.loop.run_until_complete(p.wait())
raise
finally:
del p
finally:
self.loop.run_until_complete(dispvm.cleanup())
@ -169,7 +179,7 @@ class TC_20_DispVMMixin(object):
del dispvm
# give it a time for shutdown + cleanup
self.loop.run_until_complete(asyncio.sleep(2))
self.loop.run_until_complete(asyncio.sleep(5))
self.assertNotIn(dispvm_name, self.app.domains,
"DispVM not removed from qubes.xml")
@ -181,7 +191,7 @@ class TC_20_DispVMMixin(object):
window_title = window_title.decode().strip().\
replace('(', '\(').replace(')', '\)')
time.sleep(1)
if "gedit" in window_title:
if "gedit" in window_title or 'KWrite' in window_title:
subprocess.check_call(['xdotool', 'windowactivate', '--sync', winid,
'type', 'Test test 2'])
subprocess.check_call(['xdotool', 'key', '--window', winid,
@ -243,37 +253,44 @@ class TC_20_DispVMMixin(object):
self.testvm1.run_for_stdio("echo test1 > /home/user/test.txt"))
p = self.loop.run_until_complete(
self.testvm1.run("qvm-open-in-dvm /home/user/test.txt"))
self.testvm1.run("qvm-open-in-dvm /home/user/test.txt",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
wait_count = 0
# if first 5 windows isn't expected editor, there is no hope
winid = None
while True:
search = self.loop.run_until_complete(
asyncio.create_subprocess_exec(
'xdotool', 'search', '--onlyvisible', '--class', 'disp*',
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL))
stdout, _ = self.loop.run_until_complete(search.communicate())
if search.returncode == 0:
winid = stdout.strip()
# get window title
(window_title, _) = subprocess.Popen(
['xdotool', 'getwindowname', winid], stdout=subprocess.PIPE). \
communicate()
window_title = window_title.decode().strip()
# ignore LibreOffice splash screen and window with no title
# set yet
if window_title and not window_title.startswith("LibreOffice")\
and not window_title == 'VMapp command':
break
wait_count += 1
if wait_count > 100:
self.fail("Timeout while waiting for editor window")
self.loop.run_until_complete(asyncio.sleep(0.3))
for _ in range(5):
try:
winid = self.wait_for_window('disp[0-9]*', search_class=True)
except Exception as e:
try:
self.loop.run_until_complete(asyncio.wait_for(p.wait(), 1))
except asyncio.TimeoutError:
raise e
else:
stdout = self.loop.run_until_complete(p.stdout.read())
self.fail(
'qvm-open-in-dvm exited prematurely with {}: {}'.format(
p.returncode, stdout))
# get window title
(window_title, _) = subprocess.Popen(
['xdotool', 'getwindowname', winid], stdout=subprocess.PIPE). \
communicate()
window_title = window_title.decode().strip()
# ignore LibreOffice splash screen and window with no title
# set yet
if window_title and not window_title.startswith("LibreOffice")\
and not window_title == 'VMapp command' \
and 'whonixcheck' not in window_title \
and not window_title == 'NetworkManager Applet':
break
self.loop.run_until_complete(asyncio.sleep(1))
winid = None
if winid is None:
self.fail('Timeout waiting for editor window')
time.sleep(0.5)
self._handle_editor(winid)
self.loop.run_until_complete(p.wait())
self.loop.run_until_complete(p.communicate())
(test_txt_content, _) = self.loop.run_until_complete(
self.testvm1.run_for_stdio("cat /home/user/test.txt"))
# Drop BOM if added by editor
@ -281,9 +298,15 @@ class TC_20_DispVMMixin(object):
test_txt_content = test_txt_content[3:]
self.assertEqual(test_txt_content, b"Test test 2\ntest1\n")
def create_testcases_for_templates():
return qubes.tests.create_testcases_for_templates('TC_20_DispVM',
TC_20_DispVMMixin, qubes.tests.SystemTestCase,
module=sys.modules[__name__])
def load_tests(loader, tests, pattern):
tests.addTests(loader.loadTestsFromNames(
qubes.tests.create_testcases_for_templates('TC_20_DispVM',
TC_20_DispVMMixin, qubes.tests.SystemTestCase,
globals=globals())))
create_testcases_for_templates()))
return tests
qubes.tests.maybe_create_testcases_on_import(create_testcases_for_templates)

View File

@ -78,23 +78,18 @@ Expire-Date: 0
cls.keyid = cls.generate_key(cls.tmpdir)
p = subprocess.Popen(['sudo', 'dd',
'status=none', 'of=/etc/yum.repos.d/test.repo'],
stdin=subprocess.PIPE)
p.stdin.write(b'''
with open('/etc/yum.repos.d/test.repo', 'w') as repo_file:
repo_file.write('''
[test]
name = Test
baseurl = http://localhost:8080/
enabled = 1
''')
p.stdin.close()
p.wait()
@classmethod
def tearDownClass(cls):
subprocess.check_call(['sudo', 'rm', '-f',
'/etc/yum.repos.d/test.repo'])
os.unlink('/etc/yum.repos.d/test.repo')
shutil.rmtree(cls.tmpdir)
@ -113,9 +108,9 @@ enabled = 1
self.loop.run_until_complete(self.updatevm.create_on_disk())
self.app.updatevm = self.updatevm
self.app.save()
subprocess.call(['sudo', 'rpm', '-e', self.pkg_name],
subprocess.call(['rpm', '-e', self.pkg_name],
stderr=subprocess.DEVNULL)
subprocess.check_call(['sudo', 'rpm', '--import',
subprocess.check_call(['rpm', '--import',
os.path.join(self.tmpdir, 'pubkey.asc')])
self.loop.run_until_complete(self.updatevm.start())
self.repo_running = False
@ -126,11 +121,12 @@ enabled = 1
self.repo_proc.terminate()
self.loop.run_until_complete(self.repo_proc.wait())
del self.repo_proc
self.app.updatevm = None
super(TC_00_Dom0UpgradeMixin, self).tearDown()
subprocess.call(['sudo', 'rpm', '-e', self.pkg_name],
subprocess.call(['rpm', '-e', self.pkg_name],
stderr=subprocess.DEVNULL)
subprocess.call(['sudo', 'rpm', '-e', 'gpg-pubkey-{}'.format(
subprocess.call(['rpm', '-e', 'gpg-pubkey-{}'.format(
self.keyid)], stderr=subprocess.DEVNULL)
for pkg in os.listdir(self.tmpdir):
@ -165,7 +161,7 @@ Test package
spec_path])
pkg_path = os.path.join(dir, 'x86_64',
'{}-{}-1.x86_64.rpm'.format(name, version))
subprocess.check_call(['sudo', 'chmod', 'go-rw', '/dev/tty'])
subprocess.check_call(['chmod', 'go-rw', '/dev/tty'])
subprocess.check_call(
['rpm', '--quiet', '--define=_gpg_path {}'.format(dir),
'--define=_gpg_name {}'.format("Qubes test"),
@ -173,7 +169,7 @@ Test package
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT)
subprocess.check_call(['sudo', 'chmod', 'go+rw', '/dev/tty'])
subprocess.check_call(['chmod', 'go+rw', '/dev/tty'])
return pkg_path
def send_pkg(self, filename):
@ -212,7 +208,7 @@ Test package
- "updates pending" flag is cleared
"""
filename = self.create_pkg(self.tmpdir, self.pkg_name, '1.0')
subprocess.check_call(['sudo', 'rpm', '-i', filename])
subprocess.check_call(['rpm', '-i', filename])
filename = self.create_pkg(self.tmpdir, self.pkg_name, '2.0')
self.send_pkg(filename)
open(self.update_flag_path, 'a').close()
@ -331,7 +327,7 @@ Test package
self.pkg_name))
def test_020_install_wrong_sign(self):
subprocess.call(['sudo', 'rpm', '-e', 'gpg-pubkey-{}'.format(
subprocess.call(['rpm', '-e', 'gpg-pubkey-{}'.format(
self.keyid)])
filename = self.create_pkg(self.tmpdir, self.pkg_name, '1.0')
self.send_pkg(filename)
@ -385,9 +381,14 @@ Test package
'UNSIGNED package {}-1.0 installed'.format(self.pkg_name))
def create_testcases_for_templates():
return qubes.tests.create_testcases_for_templates('TC_00_Dom0Upgrade',
TC_00_Dom0UpgradeMixin, qubes.tests.SystemTestCase,
module=sys.modules[__name__])
def load_tests(loader, tests, pattern):
tests.addTests(loader.loadTestsFromNames(
qubes.tests.create_testcases_for_templates('TC_00_Dom0Upgrade',
TC_00_Dom0UpgradeMixin, qubes.tests.SystemTestCase,
module=sys.modules[__name__])))
create_testcases_for_templates()))
return tests
qubes.tests.maybe_create_testcases_on_import(create_testcases_for_templates)

View File

@ -20,113 +20,93 @@
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
#
#
from distutils import spawn
import os
from distutils import spawn
import re
import subprocess
import time
import unittest
import itertools
import asyncio
import sys
import qubes.tests
import qubes.qubes
from qubes.qubes import QubesVmCollection
import qubes
@unittest.skipUnless(
spawn.find_executable('xprop') and
spawn.find_executable('xdotool') and
spawn.find_executable('wmctrl'),
"xprop or xdotool or wmctrl not installed")
class TC_50_MimeHandlers(qubes.tests.SystemTestsMixin):
@classmethod
def setUpClass(cls):
if cls.template == 'whonix-gw' or 'minimal' in cls.template:
raise unittest.SkipTest(
'Template {} not supported by this test'.format(cls.template))
if cls.template == 'whonix-ws':
# TODO remove when Whonix-based DispVMs will work (Whonix 13?)
raise unittest.SkipTest(
'Template {} not supported by this test'.format(cls.template))
qc = QubesVmCollection()
cls._kill_test_vms(qc, prefix=qubes.tests.CLSVMPREFIX)
qc.lock_db_for_writing()
qc.load()
cls._remove_test_vms(qc, qubes.qubes.vmm.libvirt_conn,
prefix=qubes.tests.CLSVMPREFIX)
cls.source_vmname = cls.make_vm_name('source', True)
source_vm = qc.add_new_vm("QubesAppVm",
template=qc.get_vm_by_name(cls.template),
name=cls.source_vmname)
source_vm.create_on_disk(verbose=False)
cls.target_vmname = cls.make_vm_name('target', True)
target_vm = qc.add_new_vm("QubesAppVm",
template=qc.get_vm_by_name(cls.template),
name=cls.target_vmname)
target_vm.create_on_disk(verbose=False)
qc.save()
qc.unlock_db()
source_vm.start()
target_vm.start()
# make sure that DispVMs will be started of the same template
retcode = subprocess.call(['/usr/bin/qvm-create-default-dvm',
cls.template],
stderr=open(os.devnull, 'w'))
assert retcode == 0, "Error preparing DispVM"
class TC_50_MimeHandlers:
def setUp(self):
super(TC_50_MimeHandlers, self).setUp()
self.source_vm = self.qc.get_vm_by_name(self.source_vmname)
self.target_vm = self.qc.get_vm_by_name(self.target_vmname)
if self.template.startswith('whonix-gw') or 'minimal' in self.template:
raise unittest.SkipTest(
'Template {} not supported by this test'.format(self.template))
self.source_vmname = self.make_vm_name('source')
self.source_vm = self.app.add_new_vm("AppVM",
template=self.template,
name=self.source_vmname,
label='red')
self.loop.run_until_complete(self.source_vm.create_on_disk())
self.target_vmname = self.make_vm_name('target')
self.target_vm = self.app.add_new_vm("AppVM",
template=self.template,
name=self.target_vmname,
label='red')
self.loop.run_until_complete(self.target_vm.create_on_disk())
self.target_vm.template_for_dispvms = True
self.source_vm.default_dispvm = self.target_vm
done, not_done = self.loop.run_until_complete(asyncio.wait([
self.source_vm.start(),
self.target_vm.start()]))
for result in itertools.chain(done, not_done):
# catch any exceptions
result.result()
def get_window_class(self, winid, dispvm=False):
(vm_winid, _) = subprocess.Popen(
['xprop', '-id', winid, '_QUBES_VMWINDOWID'],
stdout=subprocess.PIPE
).communicate()
vm_winid = vm_winid.split("#")[1].strip('\n" ')
vm_winid = vm_winid.decode().split("#")[1].strip('\n" ')
if dispvm:
(vmname, _) = subprocess.Popen(
['xprop', '-id', winid, '_QUBES_VMNAME'],
stdout=subprocess.PIPE
).communicate()
vmname = vmname.split("=")[1].strip('\n" ')
window_class = None
while window_class is None:
# XXX to use self.qc.get_vm_by_name would require reloading
# qubes.xml, so use qvm-run instead
xprop = subprocess.Popen(
['qvm-run', '-p', vmname, 'xprop -id {} WM_CLASS'.format(
vm_winid)], stdout=subprocess.PIPE)
(window_class, _) = xprop.communicate()
if xprop.returncode != 0:
self.skipTest("xprop failed, not installed?")
if 'not found' in window_class:
# WM_CLASS not set yet, wait a little
time.sleep(0.1)
window_class = None
vmname = vmname.decode().split("=")[1].strip('\n" ')
vm = self.app.domains[vmname]
else:
window_class = None
while window_class is None:
xprop = self.target_vm.run(
'xprop -id {} WM_CLASS'.format(vm_winid),
passio_popen=True)
(window_class, _) = xprop.communicate()
if xprop.returncode != 0:
self.skipTest("xprop failed, not installed?")
if 'not found' in window_class:
# WM_CLASS not set yet, wait a little
time.sleep(0.1)
window_class = None
vm = self.target_vm
window_class = None
while window_class is None:
try:
window_class, _ = self.loop.run_until_complete(
vm.run_for_stdio('xprop -id {} WM_CLASS'.format(vm_winid)))
except subprocess.CalledProcessError as e:
if e.returncode == 127:
self.skipTest('xprop not installed')
self.fail(
"xprop -id {} WM_CLASS failed: {}".format(
vm_winid, e.stderr.decode()))
if b'not found' in window_class:
# WM_CLASS not set yet, wait a little
time.sleep(0.1)
window_class = None
# output: WM_CLASS(STRING) = "gnome-terminal-server", "Gnome-terminal"
try:
window_class = window_class.decode()
window_class = window_class.split("=")[1].split(",")[0].strip('\n" ')
except IndexError:
raise Exception(
@ -136,44 +116,45 @@ class TC_50_MimeHandlers(qubes.tests.SystemTestsMixin):
def open_file_and_check_viewer(self, filename, expected_app_titles,
expected_app_classes, dispvm=False):
self.qc.unlock_db()
if dispvm:
p = self.source_vm.run("qvm-open-in-dvm {}".format(filename),
passio_popen=True)
vmpattern = "disp*"
p = self.loop.run_until_complete(self.source_vm.run(
"qvm-open-in-dvm {}".format(filename), stdout=subprocess.PIPE))
vmpattern = "disp[0-9]*"
else:
self.qrexec_policy('qubes.OpenInVM', self.source_vm.name,
self.target_vmname)
self.qrexec_policy('qubes.OpenURL', self.source_vm.name,
self.target_vmname)
p = self.source_vm.run("qvm-open-in-vm {} {}".format(
self.target_vmname, filename), passio_popen=True)
p = self.loop.run_until_complete(self.source_vm.run(
"qvm-open-in-vm {} {}".format(self.target_vmname, filename),
stdout=subprocess.PIPE))
vmpattern = self.target_vmname
wait_count = 0
winid = None
window_title = None
while True:
search = subprocess.Popen(['xdotool', 'search',
'--onlyvisible', '--class', vmpattern],
stdout=subprocess.PIPE,
stderr=open(os.path.devnull, 'w'))
retcode = search.wait()
if retcode == 0:
winid = search.stdout.read().strip()
# get window title
(window_title, _) = subprocess.Popen(
['xdotool', 'getwindowname', winid], stdout=subprocess.PIPE). \
communicate()
window_title = window_title.strip()
# ignore LibreOffice splash screen and window with no title
# set yet
if window_title and not window_title.startswith("LibreOffice")\
and not window_title == 'VMapp command':
break
wait_count += 1
if wait_count > 100:
self.fail("Timeout while waiting for editor window")
time.sleep(0.3)
with self.qrexec_policy('qubes.OpenInVM', self.source_vm.name,
self.target_vmname):
with self.qrexec_policy('qubes.OpenURL', self.source_vm.name,
self.target_vmname):
while True:
search = subprocess.Popen(['xdotool', 'search',
'--onlyvisible', '--class', vmpattern],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL)
retcode = search.wait()
if retcode == 0:
winid = search.stdout.read().strip()
# get window title
(window_title, _) = subprocess.Popen(
['xdotool', 'getwindowname', winid], stdout=subprocess.PIPE). \
communicate()
window_title = window_title.decode('utf8').strip()
# ignore LibreOffice splash screen and window with no title
# set yet
if window_title and \
not window_title.startswith("LibreOffice") and\
not window_title.startswith("NetworkManager") and\
not window_title == 'VMapp command':
break
wait_count += 1
if wait_count > 100:
self.fail("Timeout while waiting for editor window")
self.loop.run_until_complete(asyncio.sleep(0.3))
# get window class
window_class = self.get_window_class(winid, dispvm)
@ -194,45 +175,66 @@ class TC_50_MimeHandlers(qubes.tests.SystemTestsMixin):
expected_app_titles, expected_app_classes))
def prepare_txt(self, filename):
p = self.source_vm.run("cat > {}".format(filename), passio_popen=True)
p.stdin.write("This is test\n")
p.stdin.close()
retcode = p.wait()
assert retcode == 0, "Failed to write {} file".format(filename)
self.loop.run_until_complete(
self.source_vm.run_for_stdio("cat > {}".format(filename),
input=b'This is test\n'))
def prepare_pdf(self, filename):
self.prepare_txt("/tmp/source.txt")
cmd = "convert /tmp/source.txt {}".format(filename)
retcode = self.source_vm.run(cmd, wait=True)
assert retcode == 0, "Failed to run '{}'".format(cmd)
cmd = "convert text:/tmp/source.txt {}".format(filename)
try:
self.loop.run_until_complete(
self.source_vm.run_for_stdio(cmd))
except subprocess.CalledProcessError as e:
self.fail('{} failed: {}'.format(cmd, e.stderr.decode()))
def prepare_doc(self, filename):
self.prepare_txt("/tmp/source.txt")
cmd = "unoconv -f doc -o {} /tmp/source.txt".format(filename)
retcode = self.source_vm.run(cmd, wait=True)
if retcode != 0:
self.skipTest("Failed to run '{}', not installed?".format(cmd))
try:
self.loop.run_until_complete(
self.source_vm.run_for_stdio(cmd))
except subprocess.CalledProcessError as e:
if e.returncode == 127:
self.skipTest("unoconv not installed".format(cmd))
self.skipTest("Failed to run '{}': {}".format(cmd,
e.stderr.decode()))
def prepare_pptx(self, filename):
self.prepare_txt("/tmp/source.txt")
cmd = "unoconv -f pptx -o {} /tmp/source.txt".format(filename)
retcode = self.source_vm.run(cmd, wait=True)
if retcode != 0:
self.skipTest("Failed to run '{}', not installed?".format(cmd))
try:
self.loop.run_until_complete(
self.source_vm.run_for_stdio(cmd))
except subprocess.CalledProcessError as e:
if e.returncode == 127:
self.skipTest("unoconv not installed".format(cmd))
self.skipTest("Failed to run '{}': {}".format(cmd,
e.stderr.decode()))
def prepare_png(self, filename):
self.prepare_txt("/tmp/source.txt")
cmd = "convert /tmp/source.txt {}".format(filename)
retcode = self.source_vm.run(cmd, wait=True)
if retcode != 0:
self.skipTest("Failed to run '{}', not installed?".format(cmd))
cmd = "convert text:/tmp/source.txt {}".format(filename)
try:
self.loop.run_until_complete(
self.source_vm.run_for_stdio(cmd))
except subprocess.CalledProcessError as e:
if e.returncode == 127:
self.skipTest("convert not installed".format(cmd))
self.skipTest("Failed to run '{}': {}".format(cmd,
e.stderr.decode()))
def prepare_jpg(self, filename):
self.prepare_txt("/tmp/source.txt")
cmd = "convert /tmp/source.txt {}".format(filename)
retcode = self.source_vm.run(cmd, wait=True)
if retcode != 0:
self.skipTest("Failed to run '{}', not installed?".format(cmd))
cmd = "convert text:/tmp/source.txt {}".format(filename)
try:
self.loop.run_until_complete(
self.source_vm.run_for_stdio(cmd))
except subprocess.CalledProcessError as e:
if e.returncode == 127:
self.skipTest("convert not installed".format(cmd))
self.skipTest("Failed to run '{}': {}".format(cmd,
e.stderr.decode()))
def test_000_txt(self):
filename = "/home/user/test_file.txt"
@ -334,20 +336,14 @@ class TC_50_MimeHandlers(qubes.tests.SystemTestsMixin):
["Firefox", "Iceweasel", "Navigator"],
dispvm=True)
def create_testcases_for_templates():
return qubes.tests.create_testcases_for_templates('TC_50_MimeHandlers',
TC_50_MimeHandlers, qubes.tests.SystemTestCase,
module=sys.modules[__name__])
def load_tests(loader, tests, pattern):
try:
qc = qubes.qubes.QubesVmCollection()
qc.lock_db_for_reading()
qc.load()
qc.unlock_db()
templates = [vm.name for vm in qc.values() if
isinstance(vm, qubes.qubes.QubesTemplateVm)]
except OSError:
templates = []
for template in templates:
tests.addTests(loader.loadTestsFromTestCase(
type(
'TC_50_MimeHandlers_' + template,
(TC_50_MimeHandlers, qubes.tests.QubesTestCase),
{'template': template})))
return tests
tests.addTests(loader.loadTestsFromNames(
create_testcases_for_templates()))
return tests
qubes.tests.maybe_create_testcases_on_import(create_testcases_for_templates)

View File

@ -22,8 +22,6 @@
from distutils import spawn
import asyncio
import multiprocessing
import os
import subprocess
import sys
import time
@ -31,13 +29,11 @@ import unittest
import qubes.tests
import qubes.firewall
import qubes.vm.qubesvm
import qubes.vm.appvm
class NcVersion:
Trad = 1
Nmap = 2
# noinspection PyAttributeOutsideInit
# noinspection PyAttributeOutsideInit,PyPep8Naming
class VmNetworkingMixin(object):
test_ip = '192.168.123.45'
test_name = 'test.example.com'
@ -50,21 +46,23 @@ class VmNetworkingMixin(object):
template = None
def run_cmd(self, vm, cmd, user="root"):
'''Run a command *cmd* in a *vm* as *user*. Return its exit code.
:type self: qubes.tests.SystemTestCase | VmNetworkingMixin
:param qubes.vm.qubesvm.QubesVM vm: VM object to run command in
:param str cmd: command to execute
:param std user: user to execute command as
:return int: command exit code
'''
try:
self.loop.run_until_complete(vm.run_for_stdio(cmd, user=user))
except subprocess.CalledProcessError as e:
return e.returncode
return 0
def check_nc_version(self, vm):
if self.run_cmd(vm, 'nc -h >/dev/null 2>&1') != 0:
self.skipTest('nc not installed')
if self.run_cmd(vm, 'nc -h 2>&1|grep -q nmap.org') == 0:
return NcVersion.Nmap
else:
return NcVersion.Trad
def setUp(self):
'''
:type self: qubes.tests.SystemTestCase | VMNetworkingMixin
'''
super(VmNetworkingMixin, self).setUp()
if self.template.startswith('whonix-'):
self.skipTest("Test not supported here - Whonix uses its own "
@ -75,6 +73,7 @@ class VmNetworkingMixin(object):
label='red')
self.loop.run_until_complete(self.testnetvm.create_on_disk())
self.testnetvm.provides_network = True
self.testnetvm.netvm = None
self.testvm1 = self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=self.make_vm_name('vm1'),
label='red')
@ -86,6 +85,9 @@ class VmNetworkingMixin(object):
def configure_netvm(self):
'''
:type self: qubes.tests.SystemTestCase | VMNetworkingMixin
'''
def run_netvm_cmd(cmd):
if self.run_cmd(self.testnetvm, cmd) != 0:
self.fail("Command '%s' failed" % cmd)
@ -102,7 +104,8 @@ class VmNetworkingMixin(object):
run_netvm_cmd("ip link add test0 type dummy")
run_netvm_cmd("ip link set test0 up")
run_netvm_cmd("ip addr add {}/24 dev test0".format(self.test_ip))
run_netvm_cmd("iptables -I INPUT -d {} -j ACCEPT".format(self.test_ip))
run_netvm_cmd("iptables -I INPUT -d {} -j ACCEPT --wait".format(
self.test_ip))
# ignore failure
self.run_cmd(self.testnetvm, "killall --wait dnsmasq")
run_netvm_cmd("dnsmasq -a {ip} -A /{name}/{ip} -i test0 -z".format(
@ -113,12 +116,18 @@ class VmNetworkingMixin(object):
def test_000_simple_networking(self):
'''
:type self: qubes.tests.SystemTestCase | VMNetworkingMixin
'''
self.loop.run_until_complete(self.testvm1.start())
self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0)
self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0)
def test_010_simple_proxyvm(self):
'''
:type self: qubes.tests.SystemTestCase | VMNetworkingMixin
'''
self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=self.make_vm_name('proxy'),
label='red')
@ -144,6 +153,9 @@ class VmNetworkingMixin(object):
@unittest.skipUnless(spawn.find_executable('xdotool'),
"xdotool not installed")
def test_020_simple_proxyvm_nm(self):
'''
:type self: qubes.tests.SystemTestCase | VMNetworkingMixin
'''
self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=self.make_vm_name('proxy'),
label='red')
@ -189,6 +201,9 @@ class VmNetworkingMixin(object):
def test_030_firewallvm_firewall(self):
'''
:type self: qubes.tests.SystemTestCase | VMNetworkingMixin
'''
self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=self.make_vm_name('proxy'),
label='red')
@ -198,8 +213,6 @@ class VmNetworkingMixin(object):
self.testvm1.netvm = self.proxy
self.app.save()
nc_version = self.check_nc_version(self.testnetvm)
# block all for first
self.testvm1.firewall.rules = [qubes.firewall.Rule(action='drop')]
@ -207,10 +220,8 @@ class VmNetworkingMixin(object):
self.loop.run_until_complete(self.testvm1.start())
self.assertTrue(self.proxy.is_running())
nc = self.loop.run_until_complete(self.testnetvm.run(
'nc -l --send-only -e /bin/hostname -k 1234'
if nc_version == NcVersion.Nmap
else 'while nc -l -e /bin/hostname -p 1234; do true; done'))
server = self.loop.run_until_complete(self.testnetvm.run(
'socat TCP-LISTEN:1234,fork EXEC:/bin/uname'))
try:
self.assertEqual(self.run_cmd(self.proxy, self.ping_ip), 0,
@ -220,11 +231,8 @@ class VmNetworkingMixin(object):
self.assertNotEqual(self.run_cmd(self.testvm1, self.ping_ip), 0,
"Ping by IP should be blocked")
if nc_version == NcVersion.Nmap:
nc_cmd = "nc -w 1 --recv-only {} 1234".format(self.test_ip)
else:
nc_cmd = "nc -w 1 {} 1234".format(self.test_ip)
self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
client_cmd = "socat TCP:{}:1234 -".format(self.test_ip)
self.assertNotEqual(self.run_cmd(self.testvm1, client_cmd), 0,
"TCP connection should be blocked")
# block all except ICMP
@ -253,7 +261,7 @@ class VmNetworkingMixin(object):
time.sleep(3)
self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0,
"Ping by name failed (should be allowed now)")
self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
self.assertNotEqual(self.run_cmd(self.testvm1, client_cmd), 0,
"TCP connection should be blocked")
# block all except target
@ -267,7 +275,7 @@ class VmNetworkingMixin(object):
# Ugly hack b/c there is no feedback when the rules are actually
# applied
time.sleep(3)
self.assertEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
self.assertEqual(self.run_cmd(self.testvm1, client_cmd), 0,
"TCP connection failed (should be allowed now)")
# allow all except target
@ -282,14 +290,17 @@ class VmNetworkingMixin(object):
# Ugly hack b/c there is no feedback when the rules are actually
# applied
time.sleep(3)
self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
self.assertNotEqual(self.run_cmd(self.testvm1, client_cmd), 0,
"TCP connection should be blocked")
finally:
nc.terminate()
self.loop.run_until_complete(nc.wait())
server.terminate()
self.loop.run_until_complete(server.wait())
def test_040_inter_vm(self):
'''
:type self: qubes.tests.SystemTestCase | VMNetworkingMixin
'''
self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=self.make_vm_name('proxy'),
label='red')
@ -327,7 +338,10 @@ class VmNetworkingMixin(object):
self.ping_cmd.format(target=self.testvm1.ip)), 0)
def test_050_spoof_ip(self):
"""Test if VM IP spoofing is blocked"""
'''Test if VM IP spoofing is blocked
:type self: qubes.tests.SystemTestCase | VMNetworkingMixin
'''
self.loop.run_until_complete(self.testvm1.start())
self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0)
@ -353,7 +367,10 @@ class VmNetworkingMixin(object):
self.assertEquals(packets, '0', 'Some packet hit the INPUT rule')
def test_100_late_xldevd_startup(self):
"""Regression test for #1990"""
'''Regression test for #1990
:type self: qubes.tests.SystemTestCase | VMNetworkingMixin
'''
# Simulater late xl devd startup
cmd = "systemctl stop xendriverdomain"
if self.run_cmd(self.testnetvm, cmd) != 0:
@ -367,7 +384,10 @@ class VmNetworkingMixin(object):
self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0)
def test_200_fake_ip_simple(self):
'''Test hiding VM real IP'''
'''Test hiding VM real IP
:type self: qubes.tests.SystemTestCase | VMNetworkingMixin
'''
self.testvm1.features['net.fake-ip'] = '192.168.1.128'
self.testvm1.features['net.fake-gateway'] = '192.168.1.1'
self.testvm1.features['net.fake-netmask'] = '255.255.255.0'
@ -398,7 +418,10 @@ class VmNetworkingMixin(object):
self.assertNotIn(str(self.testvm1.netvm.ip), output)
def test_201_fake_ip_without_gw(self):
'''Test hiding VM real IP'''
'''Test hiding VM real IP
:type self: qubes.tests.SystemTestCase | VMNetworkingMixin
'''
self.testvm1.features['net.fake-ip'] = '192.168.1.128'
self.app.save()
self.loop.run_until_complete(self.testvm1.start())
@ -417,7 +440,10 @@ class VmNetworkingMixin(object):
self.assertNotIn(str(self.testvm1.ip), output)
def test_202_fake_ip_firewall(self):
'''Test hiding VM real IP, firewall'''
'''Test hiding VM real IP, firewall
:type self: qubes.tests.SystemTestCase | VMNetworkingMixin
'''
self.testvm1.features['net.fake-ip'] = '192.168.1.128'
self.testvm1.features['net.fake-gateway'] = '192.168.1.1'
self.testvm1.features['net.fake-netmask'] = '255.255.255.0'
@ -431,8 +457,6 @@ class VmNetworkingMixin(object):
self.testvm1.netvm = self.proxy
self.app.save()
nc_version = self.check_nc_version(self.testnetvm)
# block all but ICMP and DNS
self.testvm1.firewall.rules = [
@ -443,10 +467,8 @@ class VmNetworkingMixin(object):
self.loop.run_until_complete(self.testvm1.start())
self.assertTrue(self.proxy.is_running())
nc = self.loop.run_until_complete(self.testnetvm.run(
'nc -l --send-only -e /bin/hostname -k 1234'
if nc_version == NcVersion.Nmap
else 'while nc -l -e /bin/hostname -p 1234; do true; done'))
server = self.loop.run_until_complete(self.testnetvm.run(
'socat TCP-LISTEN:1234,fork EXEC:/bin/uname'))
try:
self.assertEqual(self.run_cmd(self.proxy, self.ping_ip), 0,
@ -457,18 +479,18 @@ class VmNetworkingMixin(object):
"Ping by IP should be allowed")
self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0,
"Ping by name should be allowed")
if nc_version == NcVersion.Nmap:
nc_cmd = "nc -w 1 --recv-only {} 1234".format(self.test_ip)
else:
nc_cmd = "nc -w 1 {} 1234".format(self.test_ip)
self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
client_cmd = "socat TCP:{}:1234 -".format(self.test_ip)
self.assertNotEqual(self.run_cmd(self.testvm1, client_cmd), 0,
"TCP connection should be blocked")
finally:
nc.terminate()
self.loop.run_until_complete(nc.wait())
server.terminate()
self.loop.run_until_complete(server.wait())
def test_203_fake_ip_inter_vm_allow(self):
'''Access VM with "fake IP" from other VM (when firewall allows)'''
'''Access VM with "fake IP" from other VM (when firewall allows)
:type self: qubes.tests.SystemTestCase | VMNetworkingMixin
'''
self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=self.make_vm_name('proxy'),
label='red')
@ -490,9 +512,9 @@ class VmNetworkingMixin(object):
self.loop.run_until_complete(self.testvm1.start())
self.loop.run_until_complete(self.testvm2.start())
cmd = 'iptables -I FORWARD -s {} -d {} -j ACCEPT'.format(
self.testvm2.ip, self.testvm1.ip)
try:
cmd = 'iptables -I FORWARD -s {} -d {} -j ACCEPT'.format(
self.testvm2.ip, self.testvm1.ip)
self.loop.run_until_complete(self.proxy.run_for_stdio(
cmd, user='root'))
except subprocess.CalledProcessError as e:
@ -521,7 +543,10 @@ class VmNetworkingMixin(object):
'Packets didn\'t managed to the VM')
def test_204_fake_ip_proxy(self):
'''Test hiding VM real IP'''
'''Test hiding VM real IP
:type self: qubes.tests.SystemTestCase | VMNetworkingMixin
'''
self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=self.make_vm_name('proxy'),
label='red')
@ -545,7 +570,7 @@ class VmNetworkingMixin(object):
(output, _) = self.loop.run_until_complete(
self.proxy.run_for_stdio(
'ip addr show dev eth0', user='root'))
except subprocess.CalledProcessError as e:
except subprocess.CalledProcessError:
self.fail('ip addr show dev eth0 failed')
output = output.decode()
self.assertIn('192.168.1.128', output)
@ -555,7 +580,7 @@ class VmNetworkingMixin(object):
(output, _) = self.loop.run_until_complete(
self.proxy.run_for_stdio(
'ip route show', user='root'))
except subprocess.CalledProcessError as e:
except subprocess.CalledProcessError:
self.fail('ip route show failed')
output = output.decode()
self.assertIn('192.168.1.1', output)
@ -565,7 +590,7 @@ class VmNetworkingMixin(object):
(output, _) = self.loop.run_until_complete(
self.testvm1.run_for_stdio(
'ip addr show dev eth0', user='root'))
except subprocess.CalledProcessError as e:
except subprocess.CalledProcessError:
self.fail('ip addr show dev eth0 failed')
output = output.decode()
self.assertNotIn('192.168.1.128', output)
@ -575,14 +600,17 @@ class VmNetworkingMixin(object):
(output, _) = self.loop.run_until_complete(
self.testvm1.run_for_stdio(
'ip route show', user='root'))
except subprocess.CalledProcessError as e:
except subprocess.CalledProcessError:
self.fail('ip route show failed')
output = output.decode()
self.assertIn('192.168.1.128', output)
self.assertNotIn(str(self.proxy.ip), output)
def test_210_custom_ip_simple(self):
'''Custom AppVM IP'''
'''Custom AppVM IP
:type self: qubes.tests.SystemTestCase | VMNetworkingMixin
'''
self.testvm1.ip = '192.168.1.1'
self.app.save()
self.loop.run_until_complete(self.testvm1.start())
@ -590,7 +618,10 @@ class VmNetworkingMixin(object):
self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0)
def test_211_custom_ip_proxy(self):
'''Custom ProxyVM IP'''
'''Custom ProxyVM IP
:type self: qubes.tests.SystemTestCase | VMNetworkingMixin
'''
self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=self.make_vm_name('proxy'),
label='red')
@ -607,7 +638,10 @@ class VmNetworkingMixin(object):
self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0)
def test_212_custom_ip_firewall(self):
'''Custom VM IP and firewall'''
'''Custom VM IP and firewall
:type self: qubes.tests.SystemTestCase | VMNetworkingMixin
'''
self.testvm1.ip = '192.168.1.1'
self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
@ -619,8 +653,6 @@ class VmNetworkingMixin(object):
self.testvm1.netvm = self.proxy
self.app.save()
nc_version = self.check_nc_version(self.testnetvm)
# block all but ICMP and DNS
self.testvm1.firewall.rules = [
@ -631,10 +663,8 @@ class VmNetworkingMixin(object):
self.loop.run_until_complete(self.testvm1.start())
self.assertTrue(self.proxy.is_running())
nc = self.loop.run_until_complete(self.testnetvm.run(
'nc -l --send-only -e /bin/hostname -k 1234'
if nc_version == NcVersion.Nmap
else 'while nc -l -e /bin/hostname -p 1234; do true; done'))
server = self.loop.run_until_complete(self.testnetvm.run(
'socat TCP-LISTEN:1234,fork EXEC:/bin/uname'))
try:
self.assertEqual(self.run_cmd(self.proxy, self.ping_ip), 0,
@ -645,16 +675,14 @@ class VmNetworkingMixin(object):
"Ping by IP should be allowed")
self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0,
"Ping by name should be allowed")
if nc_version == NcVersion.Nmap:
nc_cmd = "nc -w 1 --recv-only {} 1234".format(self.test_ip)
else:
nc_cmd = "nc -w 1 {} 1234".format(self.test_ip)
self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
client_cmd = "socat TCP:{}:1234 -".format(self.test_ip)
self.assertNotEqual(self.run_cmd(self.testvm1, client_cmd), 0,
"TCP connection should be blocked")
finally:
nc.terminate()
self.loop.run_until_complete(nc.wait())
server.terminate()
self.loop.run_until_complete(server.wait())
# noinspection PyAttributeOutsideInit,PyPep8Naming
class VmIPv6NetworkingMixin(VmNetworkingMixin):
test_ip6 = '2000:abcd::1'
@ -666,6 +694,9 @@ class VmIPv6NetworkingMixin(VmNetworkingMixin):
self.ping6_name = self.ping6_cmd.format(target=self.test_name)
def configure_netvm(self):
'''
:type self: qubes.tests.SystemTestCase | VmIPv6NetworkingMixin
'''
self.testnetvm.features['ipv6'] = True
super(VmIPv6NetworkingMixin, self).configure_netvm()
@ -683,12 +714,18 @@ class VmIPv6NetworkingMixin(VmNetworkingMixin):
format(ip=self.test_ip, ip6=self.test_ip6, name=self.test_name))
def test_500_ipv6_simple_networking(self):
'''
:type self: qubes.tests.SystemTestCase | VmIPv6NetworkingMixin
'''
self.loop.run_until_complete(self.testvm1.start())
self.assertEqual(self.run_cmd(self.testvm1, self.ping6_ip), 0)
self.assertEqual(self.run_cmd(self.testvm1, self.ping6_name), 0)
def test_510_ipv6_simple_proxyvm(self):
'''
:type self: qubes.tests.SystemTestCase | VmIPv6NetworkingMixin
'''
self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=self.make_vm_name('proxy'),
label='red')
@ -714,6 +751,9 @@ class VmIPv6NetworkingMixin(VmNetworkingMixin):
@unittest.skipUnless(spawn.find_executable('xdotool'),
"xdotool not installed")
def test_520_ipv6_simple_proxyvm_nm(self):
'''
:type self: qubes.tests.SystemTestCase | VmIPv6NetworkingMixin
'''
self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=self.make_vm_name('proxy'),
label='red')
@ -764,6 +804,9 @@ class VmIPv6NetworkingMixin(VmNetworkingMixin):
def test_530_ipv6_firewallvm_firewall(self):
'''
:type self: qubes.tests.SystemTestCase | VmIPv6NetworkingMixin
'''
self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=self.make_vm_name('proxy'),
label='red')
@ -773,9 +816,6 @@ class VmIPv6NetworkingMixin(VmNetworkingMixin):
self.testvm1.netvm = self.proxy
self.app.save()
if self.run_cmd(self.testnetvm, 'ncat -h') != 0:
self.skipTest('nmap ncat not installed')
# block all for first
self.testvm1.firewall.rules = [qubes.firewall.Rule(action='drop')]
@ -783,8 +823,8 @@ class VmIPv6NetworkingMixin(VmNetworkingMixin):
self.loop.run_until_complete(self.testvm1.start())
self.assertTrue(self.proxy.is_running())
nc = self.loop.run_until_complete(self.testnetvm.run(
'ncat -l --send-only -e /bin/hostname -k 1234'))
server = self.loop.run_until_complete(self.testnetvm.run(
'socat TCP6-LISTEN:1234,fork EXEC:/bin/uname'))
try:
self.assertEqual(self.run_cmd(self.proxy, self.ping6_ip), 0,
@ -794,8 +834,9 @@ class VmIPv6NetworkingMixin(VmNetworkingMixin):
self.assertNotEqual(self.run_cmd(self.testvm1, self.ping6_ip), 0,
"Ping by IP should be blocked")
nc_cmd = "ncat -w 1 --recv-only {} 1234".format(self.test_ip6)
self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
client6_cmd = "socat TCP:[{}]:1234 -".format(self.test_ip6)
client4_cmd = "socat TCP:{}:1234 -".format(self.test_ip)
self.assertNotEqual(self.run_cmd(self.testvm1, client6_cmd), 0,
"TCP connection should be blocked")
# block all except ICMP
@ -825,13 +866,14 @@ class VmIPv6NetworkingMixin(VmNetworkingMixin):
time.sleep(3)
self.assertEqual(self.run_cmd(self.testvm1, self.ping6_name), 0,
"Ping by name failed (should be allowed now)")
self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
self.assertNotEqual(self.run_cmd(self.testvm1, client6_cmd), 0,
"TCP connection should be blocked")
# block all except target
self.testvm1.firewall.rules = [
qubes.firewall.Rule(None, action='accept', dsthost=self.test_ip6,
qubes.firewall.Rule(None, action='accept',
dsthost=self.test_ip6,
proto='tcp', dstports=1234),
]
self.testvm1.firewall.save()
@ -839,7 +881,7 @@ class VmIPv6NetworkingMixin(VmNetworkingMixin):
# Ugly hack b/c there is no feedback when the rules are actually
# applied
time.sleep(3)
self.assertEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
self.assertEqual(self.run_cmd(self.testvm1, client6_cmd), 0,
"TCP connection failed (should be allowed now)")
# block all except target - by name
@ -854,10 +896,9 @@ class VmIPv6NetworkingMixin(VmNetworkingMixin):
# Ugly hack b/c there is no feedback when the rules are actually
# applied
time.sleep(3)
self.assertEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
self.assertEqual(self.run_cmd(self.testvm1, client6_cmd), 0,
"TCP (IPv6) connection failed (should be allowed now)")
self.assertEqual(self.run_cmd(self.testvm1,
nc_cmd.replace(self.test_ip6, self.test_ip)),
self.assertEqual(self.run_cmd(self.testvm1, client4_cmd),
0,
"TCP (IPv4) connection failed (should be allowed now)")
@ -873,14 +914,17 @@ class VmIPv6NetworkingMixin(VmNetworkingMixin):
# Ugly hack b/c there is no feedback when the rules are actually
# applied
time.sleep(3)
self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
self.assertNotEqual(self.run_cmd(self.testvm1, client6_cmd), 0,
"TCP connection should be blocked")
finally:
nc.terminate()
self.loop.run_until_complete(nc.wait())
server.terminate()
self.loop.run_until_complete(server.wait())
def test_540_ipv6_inter_vm(self):
'''
:type self: qubes.tests.SystemTestCase | VmIPv6NetworkingMixin
'''
self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=self.make_vm_name('proxy'),
label='red')
@ -920,7 +964,10 @@ class VmIPv6NetworkingMixin(VmNetworkingMixin):
def test_550_ipv6_spoof_ip(self):
"""Test if VM IP spoofing is blocked"""
'''Test if VM IP spoofing is blocked
:type self: qubes.tests.SystemTestCase | VmIPv6NetworkingMixin
'''
self.loop.run_until_complete(self.testvm1.start())
self.assertEqual(self.run_cmd(self.testvm1, self.ping6_ip), 0)
@ -949,7 +996,10 @@ class VmIPv6NetworkingMixin(VmNetworkingMixin):
self.assertEquals(packets, '0', 'Some packet hit the INPUT rule')
def test_710_ipv6_custom_ip_simple(self):
'''Custom AppVM IP'''
'''Custom AppVM IP
:type self: qubes.tests.SystemTestCase | VmIPv6NetworkingMixin
'''
self.testvm1.ip6 = '2000:aaaa:bbbb::1'
self.app.save()
self.loop.run_until_complete(self.testvm1.start())
@ -957,7 +1007,10 @@ class VmIPv6NetworkingMixin(VmNetworkingMixin):
self.assertEqual(self.run_cmd(self.testvm1, self.ping6_name), 0)
def test_711_ipv6_custom_ip_proxy(self):
'''Custom ProxyVM IP'''
'''Custom ProxyVM IP
:type self: qubes.tests.SystemTestCase | VmIPv6NetworkingMixin
'''
self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=self.make_vm_name('proxy'),
label='red')
@ -974,7 +1027,10 @@ class VmIPv6NetworkingMixin(VmNetworkingMixin):
self.assertEqual(self.run_cmd(self.testvm1, self.ping6_name), 0)
def test_712_ipv6_custom_ip_firewall(self):
'''Custom VM IP and firewall'''
'''Custom VM IP and firewall
:type self: qubes.tests.SystemTestCase | VmIPv6NetworkingMixin
'''
self.testvm1.ip6 = '2000:aaaa:bbbb::1'
self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
@ -986,8 +1042,6 @@ class VmIPv6NetworkingMixin(VmNetworkingMixin):
self.testvm1.netvm = self.proxy
self.app.save()
nc_version = self.check_nc_version(self.testnetvm)
# block all but ICMP and DNS
self.testvm1.firewall.rules = [
@ -998,10 +1052,8 @@ class VmIPv6NetworkingMixin(VmNetworkingMixin):
self.loop.run_until_complete(self.testvm1.start())
self.assertTrue(self.proxy.is_running())
nc = self.loop.run_until_complete(self.testnetvm.run(
'nc -l --send-only -e /bin/hostname -k 1234'
if nc_version == NcVersion.Nmap
else 'while nc -l -e /bin/hostname -p 1234; do true; done'))
server = self.loop.run_until_complete(self.testnetvm.run(
'socat TCP6-LISTEN:1234,fork EXEC:/bin/uname'))
try:
self.assertEqual(self.run_cmd(self.proxy, self.ping6_ip), 0,
@ -1012,17 +1064,14 @@ class VmIPv6NetworkingMixin(VmNetworkingMixin):
"Ping by IP should be allowed")
self.assertEqual(self.run_cmd(self.testvm1, self.ping6_name), 0,
"Ping by name should be allowed")
if nc_version == NcVersion.Nmap:
nc_cmd = "nc -w 1 --recv-only {} 1234".format(self.test_ip6)
else:
nc_cmd = "nc -w 1 {} 1234".format(self.test_ip6)
self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
client_cmd = "socat TCP:[{}]:1234 -".format(self.test_ip6)
self.assertNotEqual(self.run_cmd(self.testvm1, client_cmd), 0,
"TCP connection should be blocked")
finally:
nc.terminate()
self.loop.run_until_complete(nc.wait())
server.terminate()
self.loop.run_until_complete(server.wait())
# noinspection PyAttributeOutsideInit
# noinspection PyAttributeOutsideInit,PyPep8Naming
class VmUpdatesMixin(object):
"""
Tests for VM updates
@ -1099,6 +1148,14 @@ class VmUpdatesMixin(object):
)
def run_cmd(self, vm, cmd, user="root"):
'''Run a command *cmd* in a *vm* as *user*. Return its exit code.
:type self: qubes.tests.SystemTestCase | VmUpdatesMixin
:param qubes.vm.qubesvm.QubesVM vm: VM object to run command in
:param str cmd: command to execute
:param std user: user to execute command as
:return int: command exit code
'''
try:
self.loop.run_until_complete(vm.run_for_stdio(cmd))
except subprocess.CalledProcessError as e:
@ -1106,6 +1163,9 @@ class VmUpdatesMixin(object):
return 0
def setUp(self):
'''
:type self: qubes.tests.SystemTestCase | VmUpdatesMixin
'''
if not self.template.count('debian') and \
not self.template.count('fedora'):
self.skipTest("Template {} not supported by this test".format(
@ -1142,6 +1202,9 @@ class VmUpdatesMixin(object):
self.loop.run_until_complete(self.testvm1.create_on_disk())
def test_000_simple_update(self):
'''
:type self: qubes.tests.SystemTestCase | VmUpdatesMixin
'''
self.app.save()
# reload the VM to have all the properties properly set (especially
# default netvm)
@ -1155,6 +1218,9 @@ class VmUpdatesMixin(object):
'{}: {}\n{}'.format(self.update_cmd, stdout, stderr))
def create_repo_apt(self):
'''
:type self: qubes.tests.SystemTestCase | VmUpdatesMixin
'''
pkg_file_name = "test-pkg_1.0-1_amd64.deb"
self.loop.run_until_complete(self.netvm_repo.run_for_stdio('''
mkdir /tmp/apt-repo \
@ -1209,6 +1275,9 @@ SHA256:
'''))
def create_repo_yum(self):
'''
:type self: qubes.tests.SystemTestCase | VmUpdatesMixin
'''
pkg_file_name = "test-pkg-1.0-1.fc21.x86_64.rpm"
self.loop.run_until_complete(self.netvm_repo.run_for_stdio('''
mkdir /tmp/yum-repo \
@ -1221,6 +1290,9 @@ SHA256:
'createrepo /tmp/yum-repo'))
def create_repo_and_serve(self):
'''
:type self: qubes.tests.SystemTestCase | VmUpdatesMixin
'''
if self.template.count("debian") or self.template.count("whonix"):
self.create_repo_apt()
self.loop.run_until_complete(self.netvm_repo.run(
@ -1242,6 +1314,8 @@ SHA256:
The critical part is to use "localhost" - this will work only when
accessed through update proxy and this is exactly what we want to
test here.
:type self: qubes.tests.SystemTestCase | VmUpdatesMixin
"""
if self.template.count("debian") or self.template.count("whonix"):
@ -1266,9 +1340,12 @@ SHA256:
self.template))
def test_010_update_via_proxy(self):
"""
Test both whether updates proxy works and whether is actually used by the VM
"""
'''
Test both whether updates proxy works and whether is actually used
by the VM
:type self: qubes.tests.SystemTestCase | VmUpdatesMixin
'''
if self.template.count("minimal"):
self.skipTest("Template {} not supported by this test".format(
self.template))
@ -1319,17 +1396,20 @@ SHA256:
self.assertIn(self.loop.run_until_complete(p.wait()), self.exit_code_ok,
'{}: {}\n{}'.format(self.update_cmd, stdout, stderr))
def create_testcases_for_templates():
yield from qubes.tests.create_testcases_for_templates('VmNetworking',
VmNetworkingMixin, qubes.tests.SystemTestCase,
module=sys.modules[__name__])
yield from qubes.tests.create_testcases_for_templates('VmIPv6Networking',
VmIPv6NetworkingMixin, qubes.tests.SystemTestCase,
module=sys.modules[__name__])
yield from qubes.tests.create_testcases_for_templates('VmUpdates',
VmUpdatesMixin, qubes.tests.SystemTestCase,
module=sys.modules[__name__])
def load_tests(loader, tests, pattern):
tests.addTests(loader.loadTestsFromNames(
qubes.tests.create_testcases_for_templates('VmNetworking',
VmNetworkingMixin, qubes.tests.SystemTestCase,
module=sys.modules[__name__])))
tests.addTests(loader.loadTestsFromNames(
qubes.tests.create_testcases_for_templates('VmIPv6Networking',
VmIPv6NetworkingMixin, qubes.tests.SystemTestCase,
module=sys.modules[__name__])))
tests.addTests(loader.loadTestsFromNames(
qubes.tests.create_testcases_for_templates('VmUpdates',
VmUpdates, qubes.tests.SystemTestCase,
module=sys.modules[__name__])))
create_testcases_for_templates()))
return tests
qubes.tests.maybe_create_testcases_on_import(create_testcases_for_templates)

View File

@ -70,7 +70,7 @@ class TC_40_PVGrub(object):
def get_kernel_version(self, vm):
if self.template.startswith('fedora-'):
cmd_get_kernel_version = 'rpm -q kernel-core|sort -n|tail -1|' \
cmd_get_kernel_version = 'rpm -q kernel-core|sort -V|tail -1|' \
'cut -d - -f 3-'
elif self.template.startswith('debian-'):
cmd_get_kernel_version = \
@ -137,10 +137,14 @@ class TC_40_PVGrub(object):
self.test_template.run_for_stdio('uname -r'))
self.assertEquals(actual_kver.strip(), kver)
def create_testcases_for_templates():
return qubes.tests.create_testcases_for_templates('TC_40_PVGrub',
TC_40_PVGrub, qubes.tests.SystemTestCase,
module=sys.modules[__name__])
def load_tests(loader, tests, pattern):
tests.addTests(loader.loadTestsFromNames(
qubes.tests.create_testcases_for_templates('TC_40_PVGrub',
TC_40_PVGrub, qubes.tests.SystemTestCase,
module=sys.modules[__name__])))
create_testcases_for_templates()))
return tests
qubes.tests.maybe_create_testcases_on_import(create_testcases_for_templates)

View File

@ -391,9 +391,15 @@ class SaltVMTestMixin(SaltTestMixin):
self.assertEqual(stderr, b'')
def create_testcases_for_templates():
return qubes.tests.create_testcases_for_templates('TC_10_VMSalt',
SaltVMTestMixin, qubes.tests.SystemTestCase,
module=sys.modules[__name__])
def load_tests(loader, tests, pattern):
tests.addTests(loader.loadTestsFromNames(
qubes.tests.create_testcases_for_templates('TC_10_VMSalt',
SaltVMTestMixin, qubes.tests.SystemTestCase,
module=sys.modules[__name__])))
create_testcases_for_templates()))
return tests
qubes.tests.maybe_create_testcases_on_import(create_testcases_for_templates)

View File

@ -26,6 +26,7 @@ import subprocess
import qubes.storage.lvm
import qubes.tests
import qubes.tests.storage_lvm
import qubes.tests.storage_reflink
import qubes.vm.appvm
@ -76,6 +77,7 @@ class StorageTestMixin(object):
del coro_maybe
self.app.save()
yield from (self.vm1.start())
yield from self.wait_for_session(self.vm1)
# volatile image not clean
yield from (self.vm1.run_for_stdio(
@ -111,6 +113,7 @@ class StorageTestMixin(object):
del coro_maybe
self.app.save()
yield from self.vm1.start()
yield from self.wait_for_session(self.vm1)
# non-volatile image not clean
yield from self.vm1.run_for_stdio(
'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.format(size),
@ -196,6 +199,9 @@ class StorageTestMixin(object):
self.app.save()
yield from self.vm1.start()
yield from self.vm2.start()
yield from asyncio.wait(
[self.wait_for_session(self.vm1), self.wait_for_session(self.vm2)])
try:
yield from self.vm1.run_for_stdio(
@ -284,6 +290,7 @@ class StorageTestMixin(object):
del coro_maybe
self.app.save()
yield from self.vm2.start()
yield from self.wait_for_session(self.vm2)
# snapshot image not clean
yield from self.vm2.run_for_stdio(
@ -318,6 +325,28 @@ class StorageFile(StorageTestMixin, qubes.tests.SystemTestCase):
super(StorageFile, self).tearDown()
class StorageReflinkMixin(StorageTestMixin):
def tearDown(self):
self.app.remove_pool(self.pool.name)
super().tearDown()
def init_pool(self, fs_type, **kwargs):
name = 'test-reflink-integration-on-' + fs_type
dir_path = os.path.join('/var/tmp', name)
qubes.tests.storage_reflink.mkdir_fs(dir_path, fs_type,
cleanup_via=self.addCleanup)
self.pool = self.app.add_pool(name=name, dir_path=dir_path,
driver='file-reflink', **kwargs)
class StorageReflinkOnBtrfs(StorageReflinkMixin, qubes.tests.SystemTestCase):
def init_pool(self):
super().init_pool('btrfs')
class StorageReflinkOnExt4(StorageReflinkMixin, qubes.tests.SystemTestCase):
def init_pool(self):
super().init_pool('ext4', setup_check='no')
@qubes.tests.storage_lvm.skipUnlessLvmPoolExists
class StorageLVM(StorageTestMixin, qubes.tests.SystemTestCase):
def init_pool(self):

View File

@ -24,11 +24,15 @@ import multiprocessing
import os
import subprocess
import sys
import tempfile
import unittest
from distutils import spawn
import grp
import qubes.config
import qubes.devices
import qubes.tests
import qubes.vm.appvm
import qubes.vm.templatevm
@ -60,6 +64,7 @@ class TC_00_AppVMMixin(object):
# TODO: wait_for, timeout
self.loop.run_until_complete(self.testvm1.start())
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.testvm1.shutdown(wait=True))
self.assertEqual(self.testvm1.get_power_state(), "Halted")
@ -72,32 +77,17 @@ class TC_00_AppVMMixin(object):
self.loop.run_until_complete(self.wait_for_session(self.testvm1))
p = self.loop.run_until_complete(self.testvm1.run('xterm'))
try:
wait_count = 0
title = 'user@{}'.format(self.testvm1.name)
if self.template.count("whonix"):
title = 'user@host'
while subprocess.call(
['xdotool', 'search', '--name', title],
stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) > 0:
wait_count += 1
if wait_count > 100:
self.fail("Timeout while waiting for xterm window")
self.loop.run_until_complete(asyncio.sleep(0.1))
self.wait_for_window(title)
self.loop.run_until_complete(asyncio.sleep(0.5))
subprocess.check_call(
['xdotool', 'search', '--name', title,
'windowactivate', 'type', 'exit\n'])
wait_count = 0
while subprocess.call(['xdotool', 'search', '--name', title],
stdout=open(os.path.devnull, 'w'),
stderr=subprocess.STDOUT) == 0:
wait_count += 1
if wait_count > 100:
self.fail("Timeout while waiting for xterm "
"termination")
self.loop.run_until_complete(asyncio.sleep(0.1))
self.wait_for_window(title, show=False)
finally:
try:
p.terminate()
@ -110,6 +100,8 @@ class TC_00_AppVMMixin(object):
def test_011_run_gnome_terminal(self):
if "minimal" in self.template:
self.skipTest("Minimal template doesn't have 'gnome-terminal'")
if 'whonix' in self.template:
self.skipTest("Whonix template doesn't have 'gnome-terminal'")
self.loop.run_until_complete(self.testvm1.start())
self.assertEqual(self.testvm1.get_power_state(), "Running")
self.loop.run_until_complete(self.wait_for_session(self.testvm1))
@ -118,15 +110,7 @@ class TC_00_AppVMMixin(object):
title = 'user@{}'.format(self.testvm1.name)
if self.template.count("whonix"):
title = 'user@host'
wait_count = 0
while subprocess.call(
['xdotool', 'search', '--name', title],
stdout=open(os.path.devnull, 'w'),
stderr=subprocess.STDOUT) > 0:
wait_count += 1
if wait_count > 100:
self.fail("Timeout while waiting for gnome-terminal window")
self.loop.run_until_complete(asyncio.sleep(0.1))
self.wait_for_window(title)
self.loop.run_until_complete(asyncio.sleep(0.5))
subprocess.check_call(
@ -172,30 +156,14 @@ class TC_00_AppVMMixin(object):
title = 'user@{}'.format(self.testvm1.name)
if self.template.count("whonix"):
title = 'user@host'
wait_count = 0
while subprocess.call(
['xdotool', 'search', '--name', title],
stdout=open(os.path.devnull, 'w'),
stderr=subprocess.STDOUT) > 0:
wait_count += 1
if wait_count > 100:
self.fail("Timeout while waiting for xterm window")
self.loop.run_until_complete(asyncio.sleep(0.1))
self.wait_for_window(title)
self.loop.run_until_complete(asyncio.sleep(0.5))
subprocess.check_call(
['xdotool', 'search', '--name', title,
'windowactivate', '--sync', 'type', 'exit\n'])
wait_count = 0
while subprocess.call(['xdotool', 'search', '--name', title],
stdout=open(os.path.devnull, 'w'),
stderr=subprocess.STDOUT) == 0:
wait_count += 1
if wait_count > 100:
self.fail("Timeout while waiting for xterm "
"termination")
self.loop.run_until_complete(asyncio.sleep(0.1))
self.wait_for_window(title, show=False)
def test_050_qrexec_simple_eof(self):
"""Test for data and EOF transmission dom0->VM"""
@ -217,7 +185,6 @@ class TC_00_AppVMMixin(object):
self.assertFalse(stderr,
'Some data was printed to stderr')
@unittest.skip('#2851, because there is no GUI in vm')
def test_051_qrexec_simple_eof_reverse(self):
"""Test for EOF transmission VM->dom0"""
@ -235,7 +202,7 @@ class TC_00_AppVMMixin(object):
p.stdin.write(TEST_DATA)
yield from p.stdin.drain()
p.stdin.close()
self.assertEqual(stdout.strip(), 'test',
self.assertEqual(stdout.strip(), b'test',
'Received data differs from what was expected')
# this may hang in some buggy cases
self.assertFalse((yield from p.stderr.read()),
@ -248,15 +215,18 @@ class TC_00_AppVMMixin(object):
"probably EOF wasn't transferred from the VM process")
self.loop.run_until_complete(self.testvm1.start())
self.loop.run_until_complete(self.wait_for_session(self.testvm1))
self.loop.run_until_complete(run(self))
@unittest.skip('#2851, because there is no GUI in vm')
def test_052_qrexec_vm_service_eof(self):
"""Test for EOF transmission VM(src)->VM(dst)"""
self.loop.run_until_complete(asyncio.wait([
self.testvm1.start(),
self.testvm2.start()]))
self.loop.run_until_complete(asyncio.wait([
self.wait_for_session(self.testvm1),
self.wait_for_session(self.testvm2)]))
self.loop.run_until_complete(self.testvm2.run_for_stdio(
'cat > /etc/qubes-rpc/test.EOF',
user='root',
@ -273,7 +243,7 @@ class TC_00_AppVMMixin(object):
except asyncio.TimeoutError:
self.fail("Timeout, probably EOF wasn't transferred")
self.assertEqual(stdout, b'test',
self.assertEqual(stdout, b'test\n',
'Received data differs from what was expected')
@unittest.expectedFailure
@ -394,23 +364,14 @@ class TC_00_AppVMMixin(object):
except asyncio.TimeoutError:
self.fail('Timeout, probably deadlock')
@unittest.skip('localcmd= argument went away')
def test_071_qrexec_dom0_simultaneous_write(self):
"""Test for simultaneous write in dom0(src)->VM(dst) connection
Similar to test_070_qrexec_vm_simultaneous_write, but with dom0
as a source.
"""
def run(self):
result.value = self.testvm2.run_service(
"test.write", localcmd="/bin/sh -c '"
# first write a lot of data to fill all the buffers
"dd if=/dev/zero bs=993 count=10000 iflag=fullblock & "
# then after some time start reading
"sleep 1; "
"dd of=/dev/null bs=993 count=10000 iflag=fullblock; "
"wait"
"'")
self.loop.run_until_complete(self.testvm2.start())
self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.write', '''\
# first write a lot of data
@ -418,58 +379,93 @@ class TC_00_AppVMMixin(object):
# and only then read something
dd of=/dev/null bs=993 count=10000 iflag=fullblock
''')
self.create_local_file('/etc/qubes-rpc/policy/test.write',
'{} {} allow'.format(self.testvm1.name, self.testvm2.name))
t = multiprocessing.Process(target=run, args=(self,))
t.start()
t.join(timeout=10)
if t.is_alive():
t.terminate()
# can't use subprocess.PIPE, because asyncio will claim those FDs
pipe1_r, pipe1_w = os.pipe()
pipe2_r, pipe2_w = os.pipe()
try:
local_proc = self.loop.run_until_complete(
asyncio.create_subprocess_shell(
# first write a lot of data to fill all the buffers
"dd if=/dev/zero bs=993 count=10000 iflag=fullblock & "
# then after some time start reading
"sleep 1; "
"dd of=/dev/null bs=993 count=10000 iflag=fullblock; "
"wait", stdin=pipe1_r, stdout=pipe2_w))
service_proc = self.loop.run_until_complete(self.testvm2.run_service(
"test.write", stdin=pipe2_r, stdout=pipe1_w))
finally:
os.close(pipe1_r)
os.close(pipe1_w)
os.close(pipe2_r)
os.close(pipe2_w)
try:
self.loop.run_until_complete(
asyncio.wait_for(service_proc.wait(), timeout=10))
except asyncio.TimeoutError:
self.fail("Timeout, probably deadlock")
self.assertEqual(result.value, 0, "Service call failed")
else:
self.assertEqual(service_proc.returncode, 0,
"Service call failed")
finally:
try:
service_proc.terminate()
except ProcessLookupError:
pass
@unittest.skip('localcmd= argument went away')
def test_072_qrexec_to_dom0_simultaneous_write(self):
"""Test for simultaneous write in dom0(src)<-VM(dst) connection
Similar to test_071_qrexec_dom0_simultaneous_write, but with dom0
as a "hanging" side.
"""
result = multiprocessing.Value('i', -1)
def run(self):
result.value = self.testvm2.run_service(
"test.write", localcmd="/bin/sh -c '"
# first write a lot of data to fill all the buffers
"dd if=/dev/zero bs=993 count=10000 iflag=fullblock "
# then, only when all written, read something
"dd of=/dev/null bs=993 count=10000 iflag=fullblock; "
"'")
self.loop.run_until_complete(self.testvm2.start())
p = self.testvm2.run("cat > /etc/qubes-rpc/test.write", user="root",
passio_popen=True)
# first write a lot of data
p.stdin.write(b"dd if=/dev/zero bs=993 count=10000 iflag=fullblock &\n")
# and only then read something
p.stdin.write(b"dd of=/dev/null bs=993 count=10000 iflag=fullblock\n")
p.stdin.write(b"sleep 1; \n")
p.stdin.write(b"wait\n")
p.stdin.close()
p.wait()
policy = open("/etc/qubes-rpc/policy/test.write", "w")
policy.write("%s %s allow" % (self.testvm1.name, self.testvm2.name))
policy.close()
self.addCleanup(os.unlink, "/etc/qubes-rpc/policy/test.write")
t = multiprocessing.Process(target=run, args=(self,))
t.start()
t.join(timeout=10)
if t.is_alive():
t.terminate()
self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.write', '''\
# first write a lot of data
dd if=/dev/zero bs=993 count=10000 iflag=fullblock &
# and only then read something
dd of=/dev/null bs=993 count=10000 iflag=fullblock
sleep 1;
wait
''')
# can't use subprocess.PIPE, because asyncio will claim those FDs
pipe1_r, pipe1_w = os.pipe()
pipe2_r, pipe2_w = os.pipe()
try:
local_proc = self.loop.run_until_complete(
asyncio.create_subprocess_shell(
# first write a lot of data to fill all the buffers
"dd if=/dev/zero bs=993 count=10000 iflag=fullblock & "
# then, only when all written, read something
"dd of=/dev/null bs=993 count=10000 iflag=fullblock; ",
stdin=pipe1_r, stdout=pipe2_w))
service_proc = self.loop.run_until_complete(self.testvm2.run_service(
"test.write", stdin=pipe2_r, stdout=pipe1_w))
finally:
os.close(pipe1_r)
os.close(pipe1_w)
os.close(pipe2_r)
os.close(pipe2_w)
try:
self.loop.run_until_complete(
asyncio.wait_for(service_proc.wait(), timeout=10))
except asyncio.TimeoutError:
self.fail("Timeout, probably deadlock")
self.assertEqual(result.value, 0, "Service call failed")
else:
self.assertEqual(service_proc.returncode, 0,
"Service call failed")
finally:
try:
service_proc.terminate()
except ProcessLookupError:
pass
def test_080_qrexec_service_argument_allow_default(self):
"""Qrexec service call with argument"""
@ -742,7 +738,8 @@ class TC_00_AppVMMixin(object):
if self.template.startswith('whonix-'):
self.skipTest('qvm-sync-clock disabled for Whonix VMs')
self.loop.run_until_complete(asyncio.wait([
self.testvm1.start()]))
self.testvm1.start(),
self.testvm2.start(),]))
start_time = subprocess.check_output(['date', '-u', '+%s'])
try:
@ -752,11 +749,11 @@ class TC_00_AppVMMixin(object):
subprocess.check_call(['sudo', 'date', '-s', '2001-01-01T12:34:56'],
stdout=subprocess.DEVNULL)
self.loop.run_until_complete(
self.testvm1.run_for_stdio('date -s 2001-01-01T12:34:56',
self.testvm2.run_for_stdio('date -s 2001-01-01T12:34:56',
user='root'))
self.loop.run_until_complete(
self.testvm1.run_for_stdio('qvm-sync-clock',
self.testvm2.run_for_stdio('qvm-sync-clock',
user='root'))
p = self.loop.run_until_complete(
@ -765,7 +762,7 @@ class TC_00_AppVMMixin(object):
self.loop.run_until_complete(p.wait())
self.assertEqual(p.returncode, 0)
vm_time, _ = self.loop.run_until_complete(
self.testvm1.run_for_stdio('date -u +%s'))
self.testvm2.run_for_stdio('date -u +%s'))
self.assertAlmostEquals(int(vm_time), int(start_time), delta=30)
dom0_time = subprocess.check_output(['date', '-u', '+%s'])
@ -779,6 +776,154 @@ class TC_00_AppVMMixin(object):
finally:
self.app.clockvm = None
@unittest.skipUnless(spawn.find_executable('parecord'),
"pulseaudio-utils not installed in dom0")
def test_220_audio_playback(self):
if 'whonix-gw' in self.template:
self.skipTest('whonix-gw have no audio')
self.loop.run_until_complete(self.testvm1.start())
try:
self.loop.run_until_complete(
self.testvm1.run_for_stdio('which parecord'))
except subprocess.CalledProcessError:
self.skipTest('pulseaudio-utils not installed in VM')
self.loop.run_until_complete(
self.wait_for_session(self.testvm1))
# and some more...
self.loop.run_until_complete(asyncio.sleep(1))
# generate some "audio" data
audio_in = b'\x20' * 44100
self.loop.run_until_complete(
self.testvm1.run_for_stdio('cat > audio_in.raw', input=audio_in))
local_user = grp.getgrnam('qubes').gr_mem[0]
with tempfile.NamedTemporaryFile() as recorded_audio:
os.chmod(recorded_audio.name, 0o666)
# FIXME: -d 0 assumes only one audio device
p = subprocess.Popen(['sudo', '-E', '-u', local_user,
'parecord', '-d', '0', '--raw', recorded_audio.name],
stdout=subprocess.PIPE)
self.loop.run_until_complete(
self.testvm1.run_for_stdio('paplay --raw audio_in.raw'))
# wait for possible parecord buffering
self.loop.run_until_complete(asyncio.sleep(1))
p.terminate()
# for some reason sudo do not relay SIGTERM sent above
subprocess.check_call(['pkill', 'parecord'])
p.wait()
# allow few bytes missing, don't use assertIn, to avoid printing
# the whole data in error message
if audio_in[:-8] not in recorded_audio.file.read():
self.fail('played sound not found in dom0')
def _configure_audio_recording(self, vm):
'''Connect VM's output-source to sink monitor instead of mic'''
local_user = grp.getgrnam('qubes').gr_mem[0]
sudo = ['sudo', '-E', '-u', local_user]
source_outputs = subprocess.check_output(
sudo + ['pacmd', 'list-source-outputs']).decode()
last_index = None
found = False
for line in source_outputs.splitlines():
if line.startswith(' index: '):
last_index = line.split(':')[1].strip()
elif line.startswith('\t\tapplication.name = '):
app_name = line.split('=')[1].strip('" ')
if vm.name == app_name:
found = True
break
if not found:
self.fail('source-output for VM {} not found'.format(vm.name))
subprocess.check_call(sudo +
['pacmd', 'move-source-output', last_index, '0'])
@unittest.skipUnless(spawn.find_executable('parecord'),
"pulseaudio-utils not installed in dom0")
def test_221_audio_record_muted(self):
if 'whonix-gw' in self.template:
self.skipTest('whonix-gw have no audio')
self.loop.run_until_complete(self.testvm1.start())
try:
self.loop.run_until_complete(
self.testvm1.run_for_stdio('which parecord'))
except subprocess.CalledProcessError:
self.skipTest('pulseaudio-utils not installed in VM')
self.loop.run_until_complete(
self.wait_for_session(self.testvm1))
# and some more...
self.loop.run_until_complete(asyncio.sleep(1))
# connect VM's recording source output monitor (instead of mic)
self._configure_audio_recording(self.testvm1)
# generate some "audio" data
audio_in = b'\x20' * 44100
local_user = grp.getgrnam('qubes').gr_mem[0]
record = self.loop.run_until_complete(
self.testvm1.run('parecord --raw audio_rec.raw'))
# give it time to start recording
self.loop.run_until_complete(asyncio.sleep(0.5))
p = subprocess.Popen(['sudo', '-E', '-u', local_user,
'paplay', '--raw'],
stdin=subprocess.PIPE)
p.communicate(audio_in)
# wait for possible parecord buffering
self.loop.run_until_complete(asyncio.sleep(1))
self.loop.run_until_complete(
self.testvm1.run_for_stdio('pkill parecord'))
record.wait()
recorded_audio, _ = self.loop.run_until_complete(
self.testvm1.run_for_stdio('cat audio_rec.raw'))
# should be empty or silence, so check just a little fragment
if audio_in[:32] in recorded_audio:
self.fail('VM recorded something, even though mic disabled')
@unittest.skipUnless(spawn.find_executable('parecord'),
"pulseaudio-utils not installed in dom0")
def test_222_audio_record_unmuted(self):
if 'whonix-gw' in self.template:
self.skipTest('whonix-gw have no audio')
self.loop.run_until_complete(self.testvm1.start())
try:
self.loop.run_until_complete(
self.testvm1.run_for_stdio('which parecord'))
except subprocess.CalledProcessError:
self.skipTest('pulseaudio-utils not installed in VM')
self.loop.run_until_complete(
self.wait_for_session(self.testvm1))
# and some more...
self.loop.run_until_complete(asyncio.sleep(1))
da = qubes.devices.DeviceAssignment(self.app.domains[0], 'mic')
self.loop.run_until_complete(
self.testvm1.devices['mic'].attach(da))
# connect VM's recording source output monitor (instead of mic)
self._configure_audio_recording(self.testvm1)
# generate some "audio" data
audio_in = b'\x20' * 44100
local_user = grp.getgrnam('qubes').gr_mem[0]
record = self.loop.run_until_complete(
self.testvm1.run('parecord --raw audio_rec.raw'))
# give it time to start recording
self.loop.run_until_complete(asyncio.sleep(0.5))
p = subprocess.Popen(['sudo', '-E', '-u', local_user,
'paplay', '--raw'],
stdin=subprocess.PIPE)
p.communicate(audio_in)
# wait for possible parecord buffering
self.loop.run_until_complete(asyncio.sleep(1))
self.loop.run_until_complete(
self.testvm1.run_for_stdio('pkill parecord'))
record.wait()
recorded_audio, _ = self.loop.run_until_complete(
self.testvm1.run_for_stdio('cat audio_rec.raw'))
# allow few bytes to be missing
if audio_in[:-8] not in recorded_audio:
self.fail('VM not recorded expected data')
def test_250_resize_private_img(self):
"""
Test private.img resize, both offline and online
@ -882,10 +1027,10 @@ int main(int argc, char **argv) {
input=allocator_c.encode())
try:
stdout, stderr = yield from self.testvm1.run_for_stdio(
yield from self.testvm1.run_for_stdio(
'gcc allocator.c -o allocator')
except subprocess.CalledProcessError:
self.skipTest('allocator compile failed: {}'.format(stderr))
except subprocess.CalledProcessError as e:
self.skipTest('allocator compile failed: {}'.format(e.stderr))
# drop caches to have even more memory pressure
yield from self.testvm1.run_for_stdio(
@ -928,15 +1073,12 @@ int main(int argc, char **argv) {
proc = yield from self.testvm1.run(
'xterm -maximized -e top')
# help xdotool a little...
yield from asyncio.sleep(2)
if proc.returncode is not None:
self.fail('xterm failed to start')
# get window ID
winid = (yield from asyncio.get_event_loop().run_in_executor(None,
subprocess.check_output,
['xdotool', 'search', '--sync', '--onlyvisible', '--class',
self.testvm1.name + ':xterm'])).decode()
winid = yield from self.wait_for_window_coro(
self.testvm1.name + ':xterm',
search_class=True)
xprop = yield from asyncio.get_event_loop().run_in_executor(None,
subprocess.check_output,
['xprop', '-notype', '-id', winid, '_QUBES_VMWINDOWID'])
@ -959,6 +1101,9 @@ int main(int argc, char **argv) {
# wait for damage notify - top updates every 3 sec by default
yield from asyncio.sleep(6)
# stop changing the window content
subprocess.check_call(['xdotool', 'key', '--window', winid, 'd'])
# now take screenshot of the window, from dom0 and VM
# choose pnm format, as it doesn't have any useless metadata - easy
# to compare
@ -1011,10 +1156,14 @@ class TC_10_Generic(qubes.tests.SystemTestCase):
'Flag file created (service was run) even though should be denied,'
' qrexec-client-vm output: {} {}'.format(stdout, stderr))
def create_testcases_for_templates():
return qubes.tests.create_testcases_for_templates('TC_00_AppVM',
TC_00_AppVMMixin, qubes.tests.SystemTestCase,
module=sys.modules[__name__])
def load_tests(loader, tests, pattern):
tests.addTests(loader.loadTestsFromNames(
qubes.tests.create_testcases_for_templates('TC_00_AppVM',
TC_00_AppVMMixin, qubes.tests.SystemTestCase,
module=sys.modules[__name__])))
create_testcases_for_templates()))
return tests
qubes.tests.maybe_create_testcases_on_import(create_testcases_for_templates)

View File

@ -22,6 +22,7 @@ import qubes.storage
from qubes.exc import QubesException
from qubes.storage import pool_drivers
from qubes.storage.file import FilePool
from qubes.storage.reflink import ReflinkPool
from qubes.tests import SystemTestCase
# :pylint: disable=invalid-name
@ -107,10 +108,11 @@ class TC_00_Pool(SystemTestCase):
pool_drivers())
def test_002_get_pool_klass(self):
""" Expect the default pool to be `FilePool` """
""" Expect the default pool to be `FilePool` or `ReflinkPool` """
# :pylint: disable=protected-access
result = self.app.get_pool('varlibqubes')
self.assertIsInstance(result, FilePool)
self.assertTrue(isinstance(result, FilePool)
or isinstance(result, ReflinkPool))
def test_003_pool_exists_default(self):
""" Expect the default pool to exists """

View File

@ -24,15 +24,15 @@
'volume_group/thin_pool' combination. Pool variables without a prefix
represent a :py:class:`qubes.storage.lvm.ThinPool`.
'''
import os
import subprocess
import tempfile
import unittest
import unittest.mock
import qubes.tests
import qubes.storage
from qubes.storage.lvm import ThinPool, ThinVolume
from qubes.storage.lvm import ThinPool, ThinVolume, qubes_lvm
if 'DEFAULT_LVM_POOL' in os.environ.keys():
DEFAULT_LVM_POOL = os.environ['DEFAULT_LVM_POOL']
@ -84,7 +84,7 @@ class ThinPoolBase(qubes.tests.QubesTestCase):
''' Returns the pool matching the specified ``volume_group`` &
``thin_pool``, or None.
'''
pools = [p for p in self.app.pools
pools = [p for p in self.app.pools.values()
if issubclass(p.__class__, ThinPool)]
for pool in pools:
if pool.volume_group == volume_group \
@ -136,10 +136,10 @@ class TC_00_ThinPool(ThinPoolBase):
self.assertEqual(volume.name, 'root')
self.assertEqual(volume.pool, self.pool.name)
self.assertEqual(volume.size, qubes.config.defaults['root_img_size'])
volume.create()
self.loop.run_until_complete(volume.create())
path = "/dev/%s" % volume.vid
self.assertTrue(os.path.exists(path))
volume.remove()
self.assertTrue(os.path.exists(path), path)
self.loop.run_until_complete(volume.remove())
def test_003_read_write_volume(self):
''' Test read-write volume creation '''
@ -156,10 +156,10 @@ class TC_00_ThinPool(ThinPoolBase):
self.assertEqual(volume.name, 'root')
self.assertEqual(volume.pool, self.pool.name)
self.assertEqual(volume.size, qubes.config.defaults['root_img_size'])
volume.create()
self.loop.run_until_complete(volume.create())
path = "/dev/%s" % volume.vid
self.assertTrue(os.path.exists(path))
volume.remove()
self.assertTrue(os.path.exists(path), path)
self.loop.run_until_complete(volume.remove())
def test_004_size(self):
with self.assertNotRaises(NotImplementedError):
@ -207,11 +207,11 @@ class TC_00_ThinPool(ThinPoolBase):
}
vm = qubes.tests.storage.TestVM(self)
volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
volume.create()
self.addCleanup(volume.remove)
self.loop.run_until_complete(volume.create())
self.addCleanup(self.loop.run_until_complete, volume.remove())
path = "/dev/%s" % volume.vid
new_size = 64 * 1024 ** 2
volume.resize(new_size)
self.loop.run_until_complete(volume.resize(new_size))
self.assertEqual(self._get_size(path), new_size)
self.assertEqual(volume.size, new_size)
@ -226,20 +226,711 @@ class TC_00_ThinPool(ThinPoolBase):
}
vm = qubes.tests.storage.TestVM(self)
volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
volume.create()
self.addCleanup(volume.remove)
volume.start()
self.loop.run_until_complete(volume.create())
self.addCleanup(self.loop.run_until_complete, volume.remove())
self.loop.run_until_complete(volume.start())
path = "/dev/%s" % volume.vid
path2 = "/dev/%s" % volume._vid_snap
new_size = 64 * 1024 ** 2
volume.resize(new_size)
self.loop.run_until_complete(volume.resize(new_size))
self.assertEqual(self._get_size(path), old_size)
self.assertEqual(self._get_size(path2), new_size)
self.assertEqual(volume.size, new_size)
volume.stop()
self.loop.run_until_complete(volume.stop())
self.assertEqual(self._get_size(path), new_size)
self.assertEqual(volume.size, new_size)
def _get_lv_uuid(self, lv):
sudo = [] if os.getuid() == 0 else ['sudo']
lvs_output = subprocess.check_output(
sudo + ['lvs', '--noheadings', '-o', 'lv_uuid', lv])
return lvs_output.strip()
def _get_lv_origin_uuid(self, lv):
sudo = [] if os.getuid() == 0 else ['sudo']
if qubes.storage.lvm.lvm_is_very_old:
# no support for origin_uuid directly
lvs_output = subprocess.check_output(
sudo + ['lvs', '--noheadings', '-o', 'origin', lv])
lvs_output = subprocess.check_output(
sudo + ['lvs', '--noheadings', '-o', 'lv_uuid',
lv.rsplit('/', 1)[0] + '/' + lvs_output.strip().decode()])
else:
lvs_output = subprocess.check_output(
sudo + ['lvs', '--noheadings', '-o', 'origin_uuid', lv])
return lvs_output.strip()
def test_008_commit(self):
''' Test volume changes commit'''
config = {
'name': 'root',
'pool': self.pool.name,
'save_on_stop': True,
'rw': True,
'size': qubes.config.defaults['root_img_size'],
}
vm = qubes.tests.storage.TestVM(self)
volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
self.loop.run_until_complete(volume.create())
path_snap = '/dev/' + volume._vid_snap
self.assertFalse(os.path.exists(path_snap), path_snap)
origin_uuid = self._get_lv_uuid(volume.path)
self.loop.run_until_complete(volume.start())
snap_uuid = self._get_lv_uuid(path_snap)
self.assertNotEqual(origin_uuid, snap_uuid)
path = volume.path
self.assertTrue(path.startswith('/dev/' + volume.vid),
'{} does not start with /dev/{}'.format(path, volume.vid))
self.assertTrue(os.path.exists(path), path)
self.loop.run_until_complete(volume.remove())
def test_009_interrupted_commit(self):
''' Test volume changes commit'''
config = {
'name': 'root',
'pool': self.pool.name,
'save_on_stop': True,
'rw': True,
'revisions_to_keep': 2,
'size': qubes.config.defaults['root_img_size'],
}
vm = qubes.tests.storage.TestVM(self)
volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
# mock logging, to not interfere with time.time() mock
volume.log = unittest.mock.Mock()
# do not call volume.create(), do it manually to simulate
# interrupted commit
revisions = ['-1521065904-back', '-1521065905-back', '-snap']
orig_uuids = {}
for rev in revisions:
cmd = ['create', self.pool._pool_id,
volume.vid.split('/')[1] + rev, str(config['size'])]
qubes_lvm(cmd)
orig_uuids[rev] = self._get_lv_uuid(volume.vid + rev)
qubes.storage.lvm.reset_cache()
path_snap = '/dev/' + volume._vid_snap
self.assertTrue(volume.is_dirty())
self.assertEqual(volume.path,
'/dev/' + volume.vid + revisions[1])
expected_revisions = {
revisions[0].lstrip('-'): '2018-03-14T22:18:24',
revisions[1].lstrip('-'): '2018-03-14T22:18:25',
}
self.assertEqual(volume.revisions, expected_revisions)
self.loop.run_until_complete(volume.start())
self.assertEqual(volume.revisions, expected_revisions)
snap_uuid = self._get_lv_uuid(path_snap)
self.assertEqual(orig_uuids['-snap'], snap_uuid)
self.assertTrue(volume.is_dirty())
self.assertEqual(volume.path,
'/dev/' + volume.vid + revisions[1])
with unittest.mock.patch('time.time') as mock_time:
mock_time.side_effect = [521065906]
self.loop.run_until_complete(volume.stop())
expected_revisions = {
revisions[0].lstrip('-'): '2018-03-14T22:18:24',
revisions[1].lstrip('-'): '2018-03-14T22:18:25',
}
self.assertFalse(volume.is_dirty())
self.assertEqual(volume.revisions, expected_revisions)
self.assertEqual(volume.path, '/dev/' + volume.vid)
self.assertEqual(snap_uuid, self._get_lv_uuid(volume.path))
self.assertFalse(os.path.exists(path_snap), path_snap)
self.loop.run_until_complete(volume.remove())
def test_010_migration1(self):
'''Start with old revisions, then start interacting using new code'''
config = {
'name': 'root',
'pool': self.pool.name,
'save_on_stop': True,
'rw': True,
'revisions_to_keep': 2,
'size': qubes.config.defaults['root_img_size'],
}
vm = qubes.tests.storage.TestVM(self)
volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
# mock logging, to not interfere with time.time() mock
volume.log = unittest.mock.Mock()
# do not call volume.create(), do it manually to have old LV naming
revisions = ['', '-1521065904-back', '-1521065905-back']
orig_uuids = {}
for rev in revisions:
cmd = ['create', self.pool._pool_id,
volume.vid.split('/')[1] + rev, str(config['size'])]
qubes_lvm(cmd)
orig_uuids[rev] = self._get_lv_uuid(volume.vid + rev)
qubes.storage.lvm.reset_cache()
path_snap = '/dev/' + volume._vid_snap
self.assertFalse(os.path.exists(path_snap), path_snap)
expected_revisions = {
revisions[1].lstrip('-'): '2018-03-14T22:18:24',
revisions[2].lstrip('-'): '2018-03-14T22:18:25',
}
self.assertEqual(volume.revisions, expected_revisions)
self.assertEqual(volume.path, '/dev/' + volume.vid)
self.loop.run_until_complete(volume.start())
snap_uuid = self._get_lv_uuid(path_snap)
self.assertNotEqual(orig_uuids[''], snap_uuid)
snap_origin_uuid = self._get_lv_origin_uuid(path_snap)
self.assertEqual(orig_uuids[''], snap_origin_uuid)
path = volume.path
self.assertEqual(path, '/dev/' + volume.vid)
self.assertTrue(os.path.exists(path), path)
with unittest.mock.patch('time.time') as mock_time:
mock_time.side_effect = ('1521065906', '1521065907')
self.loop.run_until_complete(volume.stop())
revisions.extend(['-1521065906-back'])
expected_revisions = {
revisions[2].lstrip('-'): '2018-03-14T22:18:25',
revisions[3].lstrip('-'): '2018-03-14T22:18:26',
}
self.assertEqual(volume.revisions, expected_revisions)
self.assertEqual(volume.path, '/dev/' + volume.vid)
path_snap = '/dev/' + volume._vid_snap
self.assertFalse(os.path.exists(path_snap), path_snap)
self.assertTrue(os.path.exists('/dev/' + volume.vid))
self.assertEqual(self._get_lv_uuid(volume.path), snap_uuid)
prev_path = '/dev/' + volume.vid + revisions[3]
self.assertEqual(self._get_lv_uuid(prev_path), orig_uuids[''])
self.loop.run_until_complete(volume.remove())
for rev in revisions:
path = '/dev/' + volume.vid + rev
self.assertFalse(os.path.exists(path), path)
def test_011_migration2(self):
'''VM started with old code, stopped with new'''
config = {
'name': 'root',
'pool': self.pool.name,
'save_on_stop': True,
'rw': True,
'revisions_to_keep': 1,
'size': qubes.config.defaults['root_img_size'],
}
vm = qubes.tests.storage.TestVM(self)
volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
# mock logging, to not interfere with time.time() mock
volume.log = unittest.mock.Mock()
# do not call volume.create(), do it manually to have old LV naming
revisions = ['', '-snap']
orig_uuids = {}
for rev in revisions:
cmd = ['create', self.pool._pool_id,
volume.vid.split('/')[1] + rev, str(config['size'])]
qubes_lvm(cmd)
orig_uuids[rev] = self._get_lv_uuid(volume.vid + rev)
qubes.storage.lvm.reset_cache()
path_snap = '/dev/' + volume._vid_snap
self.assertTrue(os.path.exists(path_snap), path_snap)
expected_revisions = {}
self.assertEqual(volume.revisions, expected_revisions)
self.assertEqual(volume.path, '/dev/' + volume.vid)
self.assertTrue(volume.is_dirty())
path = volume.path
self.assertEqual(path, '/dev/' + volume.vid)
self.assertTrue(os.path.exists(path), path)
with unittest.mock.patch('time.time') as mock_time:
mock_time.side_effect = ('1521065906', '1521065907')
self.loop.run_until_complete(volume.stop())
revisions.extend(['-1521065906-back'])
expected_revisions = {
revisions[2].lstrip('-'): '2018-03-14T22:18:26',
}
self.assertEqual(volume.revisions, expected_revisions)
self.assertEqual(volume.path, '/dev/' + volume.vid)
path_snap = '/dev/' + volume._vid_snap
self.assertFalse(os.path.exists(path_snap), path_snap)
self.assertTrue(os.path.exists('/dev/' + volume.vid))
self.assertEqual(self._get_lv_uuid(volume.path), orig_uuids['-snap'])
prev_path = '/dev/' + volume.vid + revisions[2]
self.assertEqual(self._get_lv_uuid(prev_path), orig_uuids[''])
self.loop.run_until_complete(volume.remove())
for rev in revisions:
path = '/dev/' + volume.vid + rev
self.assertFalse(os.path.exists(path), path)
def test_012_migration3(self):
'''VM started with old code, started again with new, stopped with new'''
config = {
'name': 'root',
'pool': self.pool.name,
'save_on_stop': True,
'rw': True,
'revisions_to_keep': 1,
'size': qubes.config.defaults['root_img_size'],
}
vm = qubes.tests.storage.TestVM(self)
volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
# mock logging, to not interfere with time.time() mock
volume.log = unittest.mock.Mock()
# do not call volume.create(), do it manually to have old LV naming
revisions = ['', '-snap']
orig_uuids = {}
for rev in revisions:
cmd = ['create', self.pool._pool_id,
volume.vid.split('/')[1] + rev, str(config['size'])]
qubes_lvm(cmd)
orig_uuids[rev] = self._get_lv_uuid(volume.vid + rev)
qubes.storage.lvm.reset_cache()
path_snap = '/dev/' + volume._vid_snap
self.assertTrue(os.path.exists(path_snap), path_snap)
expected_revisions = {}
self.assertEqual(volume.revisions, expected_revisions)
self.assertTrue(volume.path, '/dev/' + volume.vid)
self.assertTrue(volume.is_dirty())
self.loop.run_until_complete(volume.start())
self.assertEqual(volume.revisions, expected_revisions)
self.assertEqual(volume.path, '/dev/' + volume.vid)
# -snap LV should be unchanged
self.assertEqual(self._get_lv_uuid(volume._vid_snap),
orig_uuids['-snap'])
self.loop.run_until_complete(volume.remove())
for rev in revisions:
path = '/dev/' + volume.vid + rev
self.assertFalse(os.path.exists(path), path)
def test_013_migration4(self):
'''revisions_to_keep=0, VM started with old code, stopped with new'''
config = {
'name': 'root',
'pool': self.pool.name,
'save_on_stop': True,
'rw': True,
'revisions_to_keep': 0,
'size': qubes.config.defaults['root_img_size'],
}
vm = qubes.tests.storage.TestVM(self)
volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
# mock logging, to not interfere with time.time() mock
volume.log = unittest.mock.Mock()
# do not call volume.create(), do it manually to have old LV naming
revisions = ['', '-snap']
orig_uuids = {}
for rev in revisions:
cmd = ['create', self.pool._pool_id,
volume.vid.split('/')[1] + rev, str(config['size'])]
qubes_lvm(cmd)
orig_uuids[rev] = self._get_lv_uuid(volume.vid + rev)
qubes.storage.lvm.reset_cache()
path_snap = '/dev/' + volume._vid_snap
self.assertTrue(os.path.exists(path_snap), path_snap)
expected_revisions = {}
self.assertEqual(volume.revisions, expected_revisions)
self.assertEqual(volume.path, '/dev/' + volume.vid)
self.assertTrue(volume.is_dirty())
with unittest.mock.patch('time.time') as mock_time:
mock_time.side_effect = ('1521065906', '1521065907')
self.loop.run_until_complete(volume.stop())
expected_revisions = {}
self.assertEqual(volume.revisions, expected_revisions)
self.assertEqual(volume.path, '/dev/' + volume.vid)
self.loop.run_until_complete(volume.remove())
for rev in revisions:
path = '/dev/' + volume.vid + rev
self.assertFalse(os.path.exists(path), path)
def test_014_commit_keep_0(self):
''' Test volume changes commit, with revisions_to_keep=0'''
config = {
'name': 'root',
'pool': self.pool.name,
'save_on_stop': True,
'rw': True,
'revisions_to_keep': 0,
'size': qubes.config.defaults['root_img_size'],
}
vm = qubes.tests.storage.TestVM(self)
volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
# mock logging, to not interfere with time.time() mock
volume.log = unittest.mock.Mock()
self.loop.run_until_complete(volume.create())
self.assertFalse(volume.is_dirty())
path = volume.path
expected_revisions = {}
self.assertEqual(volume.revisions, expected_revisions)
self.loop.run_until_complete(volume.start())
self.assertEqual(volume.revisions, expected_revisions)
path_snap = '/dev/' + volume._vid_snap
snap_uuid = self._get_lv_uuid(path_snap)
self.assertTrue(volume.is_dirty())
self.assertEqual(volume.path, path)
with unittest.mock.patch('time.time') as mock_time:
mock_time.side_effect = [521065906]
self.loop.run_until_complete(volume.stop())
self.assertFalse(volume.is_dirty())
self.assertEqual(volume.revisions, {})
self.assertEqual(volume.path, '/dev/' + volume.vid)
self.assertEqual(snap_uuid, self._get_lv_uuid(volume.path))
self.assertFalse(os.path.exists(path_snap), path_snap)
self.loop.run_until_complete(volume.remove())
def test_020_revert_last(self):
''' Test volume revert'''
config = {
'name': 'root',
'pool': self.pool.name,
'save_on_stop': True,
'rw': True,
'revisions_to_keep': 2,
'size': qubes.config.defaults['root_img_size'],
}
vm = qubes.tests.storage.TestVM(self)
volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
self.loop.run_until_complete(volume.create())
self.loop.run_until_complete(volume.start())
self.loop.run_until_complete(volume.stop())
self.loop.run_until_complete(volume.start())
self.loop.run_until_complete(volume.stop())
self.assertEqual(len(volume.revisions), 2)
revisions = volume.revisions
revision_id = max(revisions.keys())
current_path = volume.path
current_uuid = self._get_lv_uuid(volume.path)
rev_uuid = self._get_lv_uuid(volume.vid + '-' + revision_id)
self.assertFalse(volume.is_dirty())
self.assertNotEqual(current_uuid, rev_uuid)
self.loop.run_until_complete(volume.revert())
path_snap = '/dev/' + volume._vid_snap
self.assertFalse(os.path.exists(path_snap), path_snap)
self.assertEqual(current_path, volume.path)
new_uuid = self._get_lv_origin_uuid(volume.path)
self.assertEqual(new_uuid, rev_uuid)
self.assertEqual(volume.revisions, revisions)
self.loop.run_until_complete(volume.remove())
def test_021_revert_earlier(self):
''' Test volume revert'''
config = {
'name': 'root',
'pool': self.pool.name,
'save_on_stop': True,
'rw': True,
'revisions_to_keep': 2,
'size': qubes.config.defaults['root_img_size'],
}
vm = qubes.tests.storage.TestVM(self)
volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
self.loop.run_until_complete(volume.create())
self.loop.run_until_complete(volume.start())
self.loop.run_until_complete(volume.stop())
self.loop.run_until_complete(volume.start())
self.loop.run_until_complete(volume.stop())
self.assertEqual(len(volume.revisions), 2)
revisions = volume.revisions
revision_id = min(revisions.keys())
current_path = volume.path
current_uuid = self._get_lv_uuid(volume.path)
rev_uuid = self._get_lv_uuid(volume.vid + '-' + revision_id)
self.assertFalse(volume.is_dirty())
self.assertNotEqual(current_uuid, rev_uuid)
self.loop.run_until_complete(volume.revert(revision_id))
path_snap = '/dev/' + volume._vid_snap
self.assertFalse(os.path.exists(path_snap), path_snap)
self.assertEqual(current_path, volume.path)
new_uuid = self._get_lv_origin_uuid(volume.path)
self.assertEqual(new_uuid, rev_uuid)
self.assertEqual(volume.revisions, revisions)
self.loop.run_until_complete(volume.remove())
def test_030_import_data(self):
''' Test volume import'''
config = {
'name': 'root',
'pool': self.pool.name,
'save_on_stop': True,
'rw': True,
'revisions_to_keep': 2,
'size': qubes.config.defaults['root_img_size'],
}
vm = qubes.tests.storage.TestVM(self)
volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
self.loop.run_until_complete(volume.create())
current_uuid = self._get_lv_uuid(volume.path)
self.assertFalse(volume.is_dirty())
import_path = self.loop.run_until_complete(volume.import_data())
import_uuid = self._get_lv_uuid(import_path)
self.assertNotEqual(current_uuid, import_uuid)
# success - commit data
self.loop.run_until_complete(volume.import_data_end(True))
new_current_uuid = self._get_lv_uuid(volume.path)
self.assertEqual(new_current_uuid, import_uuid)
revisions = volume.revisions
self.assertEqual(len(revisions), 1)
revision = revisions.popitem()[0]
self.assertEqual(current_uuid,
self._get_lv_uuid(volume.vid + '-' + revision))
self.assertFalse(os.path.exists(import_path), import_path)
self.loop.run_until_complete(volume.remove())
def test_031_import_data_fail(self):
''' Test volume import'''
config = {
'name': 'root',
'pool': self.pool.name,
'save_on_stop': True,
'rw': True,
'revisions_to_keep': 2,
'size': qubes.config.defaults['root_img_size'],
}
vm = qubes.tests.storage.TestVM(self)
volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
self.loop.run_until_complete(volume.create())
current_uuid = self._get_lv_uuid(volume.path)
self.assertFalse(volume.is_dirty())
import_path = self.loop.run_until_complete(volume.import_data())
import_uuid = self._get_lv_uuid(import_path)
self.assertNotEqual(current_uuid, import_uuid)
# fail - discard data
self.loop.run_until_complete(volume.import_data_end(False))
new_current_uuid = self._get_lv_uuid(volume.path)
self.assertEqual(new_current_uuid, current_uuid)
revisions = volume.revisions
self.assertEqual(len(revisions), 0)
self.assertFalse(os.path.exists(import_path), import_path)
self.loop.run_until_complete(volume.remove())
def test_032_import_volume_same_pool(self):
'''Import volume from the same pool'''
# source volume
config = {
'name': 'root',
'pool': self.pool.name,
'save_on_stop': True,
'rw': True,
'revisions_to_keep': 2,
'size': qubes.config.defaults['root_img_size'],
}
vm = qubes.tests.storage.TestVM(self)
source_volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
self.loop.run_until_complete(source_volume.create())
source_uuid = self._get_lv_uuid(source_volume.path)
# destination volume
config = {
'name': 'root2',
'pool': self.pool.name,
'save_on_stop': True,
'rw': True,
'revisions_to_keep': 2,
'size': qubes.config.defaults['root_img_size'],
}
volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
volume.log = unittest.mock.Mock()
with unittest.mock.patch('time.time') as mock_time:
mock_time.side_effect = [1521065905]
self.loop.run_until_complete(volume.create())
self.assertEqual(volume.revisions, {})
uuid_before = self._get_lv_uuid(volume.path)
with unittest.mock.patch('time.time') as mock_time:
mock_time.side_effect = [1521065906]
self.loop.run_until_complete(
volume.import_volume(source_volume))
uuid_after = self._get_lv_uuid(volume.path)
self.assertNotEqual(uuid_after, uuid_before)
# also should be different than source volume (clone, not the same LV)
self.assertNotEqual(uuid_after, source_uuid)
self.assertEqual(self._get_lv_origin_uuid(volume.path), source_uuid)
expected_revisions = {
'1521065906-back': '2018-03-14T22:18:26',
}
self.assertEqual(volume.revisions, expected_revisions)
self.loop.run_until_complete(volume.remove())
self.loop.run_until_complete(source_volume.remove())
def test_033_import_volume_different_pool(self):
'''Import volume from a different pool'''
source_volume = unittest.mock.Mock()
# destination volume
config = {
'name': 'root2',
'pool': self.pool.name,
'save_on_stop': True,
'rw': True,
'revisions_to_keep': 2,
'size': qubes.config.defaults['root_img_size'],
}
vm = qubes.tests.storage.TestVM(self)
volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
volume.log = unittest.mock.Mock()
with unittest.mock.patch('time.time') as mock_time:
mock_time.side_effect = [1521065905]
self.loop.run_until_complete(volume.create())
self.assertEqual(volume.revisions, {})
uuid_before = self._get_lv_uuid(volume.path)
with tempfile.NamedTemporaryFile() as source_volume_file:
source_volume_file.write(b'test-content')
source_volume_file.flush()
source_volume.size = 16 * 1024 * 1024 # 16MiB
source_volume.export.return_value = source_volume_file.name
with unittest.mock.patch('time.time') as mock_time:
mock_time.side_effect = [1521065906]
self.loop.run_until_complete(
volume.import_volume(source_volume))
uuid_after = self._get_lv_uuid(volume.path)
self.assertNotEqual(uuid_after, uuid_before)
self.assertEqual(volume.size, 16 * 1024 * 1024)
volume_content = subprocess.check_output(['sudo', 'cat', volume.path])
self.assertEqual(volume_content.rstrip(b'\0'), b'test-content')
expected_revisions = {
'1521065906-back': '2018-03-14T22:18:26',
}
self.assertEqual(volume.revisions, expected_revisions)
self.loop.run_until_complete(volume.remove())
def test_040_volatile(self):
'''Volatile volume test'''
config = {
'name': 'volatile',
'pool': self.pool.name,
'rw': True,
'size': qubes.config.defaults['root_img_size'],
}
vm = qubes.tests.storage.TestVM(self)
volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
# volatile volume don't need any file, verify should succeed
self.assertTrue(volume.verify())
self.loop.run_until_complete(volume.create())
self.assertTrue(volume.verify())
self.assertFalse(volume.save_on_stop)
self.assertFalse(volume.snap_on_start)
path = volume.path
self.assertEqual(path, '/dev/' + volume.vid)
self.assertFalse(os.path.exists(path))
self.loop.run_until_complete(volume.start())
self.assertTrue(os.path.exists(path))
vol_uuid = self._get_lv_uuid(path)
self.loop.run_until_complete(volume.start())
self.assertTrue(os.path.exists(path))
vol_uuid2 = self._get_lv_uuid(path)
self.assertNotEqual(vol_uuid, vol_uuid2)
self.loop.run_until_complete(volume.stop())
self.assertFalse(os.path.exists(path))
def test_050_snapshot_volume(self):
''' Test snapshot volume creation '''
config_origin = {
'name': 'root',
'pool': self.pool.name,
'save_on_stop': True,
'rw': True,
'size': qubes.config.defaults['root_img_size'],
}
vm = qubes.tests.storage.TestVM(self)
volume_origin = self.app.get_pool(self.pool.name).init_volume(
vm, config_origin)
self.loop.run_until_complete(volume_origin.create())
config_snapshot = {
'name': 'root2',
'pool': self.pool.name,
'snap_on_start': True,
'source': volume_origin,
'rw': True,
'size': qubes.config.defaults['root_img_size'],
}
volume = self.app.get_pool(self.pool.name).init_volume(
vm, config_snapshot)
self.assertIsInstance(volume, ThinVolume)
self.assertEqual(volume.name, 'root2')
self.assertEqual(volume.pool, self.pool.name)
self.assertEqual(volume.size, qubes.config.defaults['root_img_size'])
# only origin volume really needs to exist, verify should succeed
# even before create
self.assertTrue(volume.verify())
self.loop.run_until_complete(volume.create())
path = volume.path
self.assertEqual(path, '/dev/' + volume.vid)
self.assertFalse(os.path.exists(path), path)
self.loop.run_until_complete(volume.start())
# snapshot volume isn't considered dirty at any time
self.assertFalse(volume.is_dirty())
# not outdated yet
self.assertFalse(volume.is_outdated())
origin_uuid = self._get_lv_uuid(volume_origin.path)
snap_origin_uuid = self._get_lv_origin_uuid(volume._vid_snap)
self.assertEqual(origin_uuid, snap_origin_uuid)
# now make it outdated
self.loop.run_until_complete(volume_origin.start())
self.loop.run_until_complete(volume_origin.stop())
self.assertTrue(volume.is_outdated())
origin_uuid = self._get_lv_uuid(volume_origin.path)
self.assertNotEqual(origin_uuid, snap_origin_uuid)
self.loop.run_until_complete(volume.stop())
# stopped volume is never outdated
self.assertFalse(volume.is_outdated())
path = volume.path
self.assertFalse(os.path.exists(path), path)
path = '/dev/' + volume._vid_snap
self.assertFalse(os.path.exists(path), path)
self.loop.run_until_complete(volume.remove())
self.loop.run_until_complete(volume_origin.remove())
def test_100_pool_list_volumes(self):
config = {
'name': 'root',
'pool': self.pool.name,
'save_on_stop': True,
'rw': True,
'revisions_to_keep': 2,
'size': qubes.config.defaults['root_img_size'],
}
config2 = config.copy()
vm = qubes.tests.storage.TestVM(self)
volume1 = self.app.get_pool(self.pool.name).init_volume(vm, config)
self.loop.run_until_complete(volume1.create())
config2['name'] = 'private'
volume2 = self.app.get_pool(self.pool.name).init_volume(vm, config2)
self.loop.run_until_complete(volume2.create())
# create some revisions
self.loop.run_until_complete(volume1.start())
self.loop.run_until_complete(volume1.stop())
# and have one in dirty state
self.loop.run_until_complete(volume2.start())
self.assertIn(volume1, list(self.pool.volumes))
self.assertIn(volume2, list(self.pool.volumes))
self.loop.run_until_complete(volume1.remove())
self.assertNotIn(volume1, list(self.pool.volumes))
self.assertIn(volume2, list(self.pool.volumes))
self.loop.run_until_complete(volume2.remove())
self.assertNotIn(volume1, list(self.pool.volumes))
self.assertNotIn(volume1, list(self.pool.volumes))
@skipUnlessLvmPoolExists
class TC_01_ThinPool(ThinPoolBase, qubes.tests.SystemTestCase):
@ -255,26 +946,27 @@ class TC_01_ThinPool(ThinPoolBase, qubes.tests.SystemTestCase):
vm = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM, name=name,
label='red')
vm.clone_properties(template_vm)
vm.clone_disk_files(template_vm, pool='test-lvm')
self.loop.run_until_complete(
vm.clone_disk_files(template_vm, pool=self.pool.name))
for v_name, volume in vm.volumes.items():
if volume.save_on_stop:
expected = "/dev/{!s}/vm-{!s}-{!s}".format(
DEFAULT_LVM_POOL.split('/')[0], vm.name, v_name)
self.assertEqual(volume.path, expected)
with self.assertNotRaises(qubes.exc.QubesException):
vm.start()
self.loop.run_until_complete(vm.start())
def test_005_create_appvm(self):
vm = self.app.add_new_vm(cls=qubes.vm.appvm.AppVM,
name=self.make_vm_name('appvm'), label='red')
vm.create_on_disk(pool='test-lvm')
self.loop.run_until_complete(vm.create_on_disk(pool=self.pool.name))
for v_name, volume in vm.volumes.items():
if volume.save_on_stop:
expected = "/dev/{!s}/vm-{!s}-{!s}".format(
DEFAULT_LVM_POOL.split('/')[0], vm.name, v_name)
self.assertEqual(volume.path, expected)
with self.assertNotRaises(qubes.exc.QubesException):
vm.start()
self.loop.run_until_complete(vm.start())
@skipUnlessLvmPoolExists
class TC_02_StorageHelpers(ThinPoolBase):

View File

@ -0,0 +1,154 @@
#
# The Qubes OS Project, https://www.qubes-os.org
#
# Copyright (C) 2018 Rusty Bird <rustybird@net-c.com>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
#
''' Tests for the file-reflink storage driver '''
# pylint: disable=protected-access
# pylint: disable=invalid-name
import os
import shutil
import subprocess
import sys
import qubes.tests
from qubes.storage import reflink
class ReflinkMixin:
def setUp(self, fs_type='btrfs'): # pylint: disable=arguments-differ
super().setUp()
self.test_dir = '/var/tmp/test-reflink-units-on-' + fs_type
mkdir_fs(self.test_dir, fs_type, cleanup_via=self.addCleanup)
def test_000_copy_file(self):
source = os.path.join(self.test_dir, 'source-file')
dest = os.path.join(self.test_dir, 'new-directory', 'dest-file')
content = os.urandom(1024**2)
with open(source, 'wb') as source_io:
source_io.write(content)
ficlone_succeeded = reflink._copy_file(source, dest)
self.assertEqual(ficlone_succeeded, self.ficlone_supported)
self.assertNotEqual(os.stat(source).st_ino, os.stat(dest).st_ino)
with open(source, 'rb') as source_io:
self.assertEqual(source_io.read(), content)
with open(dest, 'rb') as dest_io:
self.assertEqual(dest_io.read(), content)
def test_001_create_and_resize_files_and_update_loopdevs(self):
img_real = os.path.join(self.test_dir, 'img-real')
img_sym = os.path.join(self.test_dir, 'img-sym')
size_initial = 111 * 1024**2
size_resized = 222 * 1024**2
os.symlink(img_real, img_sym)
reflink._create_sparse_file(img_real, size_initial)
self.assertEqual(reflink._get_file_disk_usage(img_real), 0)
self.assertEqual(os.stat(img_real).st_size, size_initial)
dev_from_real = setup_loopdev(img_real, cleanup_via=self.addCleanup)
dev_from_sym = setup_loopdev(img_sym, cleanup_via=self.addCleanup)
reflink._resize_file(img_real, size_resized)
self.assertEqual(reflink._get_file_disk_usage(img_real), 0)
self.assertEqual(os.stat(img_real).st_size, size_resized)
reflink_update_loopdev_sizes(os.path.join(self.test_dir, 'unrelated'))
for dev in (dev_from_real, dev_from_sym):
self.assertEqual(get_blockdev_size(dev), size_initial)
reflink_update_loopdev_sizes(img_sym)
for dev in (dev_from_real, dev_from_sym):
self.assertEqual(get_blockdev_size(dev), size_resized)
class TC_00_ReflinkOnBtrfs(ReflinkMixin, qubes.tests.QubesTestCase):
def setUp(self): # pylint: disable=arguments-differ
super().setUp('btrfs')
self.ficlone_supported = True
class TC_01_ReflinkOnExt4(ReflinkMixin, qubes.tests.QubesTestCase):
def setUp(self): # pylint: disable=arguments-differ
super().setUp('ext4')
self.ficlone_supported = False
def setup_loopdev(img, cleanup_via=None):
dev = str.strip(cmd('sudo', 'losetup', '-f', '--show', img).decode())
if cleanup_via is not None:
cleanup_via(detach_loopdev, dev)
return dev
def detach_loopdev(dev):
cmd('sudo', 'losetup', '-d', dev)
def get_fs_type(directory):
# 'stat -f -c %T' would identify ext4 as 'ext2/ext3'
return cmd('df', '--output=fstype', directory).decode().splitlines()[1]
def mkdir_fs(directory, fs_type,
accessible=True, max_size=100*1024**3, cleanup_via=None):
os.mkdir(directory)
if get_fs_type(directory) != fs_type:
img = os.path.join(directory, 'img')
with open(img, 'xb') as img_io:
img_io.truncate(max_size)
cmd('mkfs.' + fs_type, img)
dev = setup_loopdev(img)
os.remove(img)
cmd('sudo', 'mount', dev, directory)
detach_loopdev(dev)
if accessible:
cmd('sudo', 'chmod', '777', directory)
else:
cmd('sudo', 'chmod', '000', directory)
cmd('sudo', 'chattr', '+i', directory) # cause EPERM on write as root
if cleanup_via is not None:
cleanup_via(rmtree_fs, directory)
def rmtree_fs(directory):
if os.path.ismount(directory):
cmd('sudo', 'umount', '-l', directory)
# loop device and backing file are garbage collected automatically
cmd('sudo', 'chattr', '-i', directory)
cmd('sudo', 'chmod', '777', directory)
shutil.rmtree(directory)
def get_blockdev_size(dev):
return int(cmd('sudo', 'blockdev', '--getsize64', dev))
def reflink_update_loopdev_sizes(img):
env = [k + '=' + v for k, v in os.environ.items() # 'sudo -E' alone would
if k.startswith('PYTHON')] # drop some of these
code = ('from qubes.storage import reflink\n'
'reflink._update_loopdev_sizes(%r)' % img)
cmd('sudo', '-E', 'env', *env, sys.executable, '-c', code)
def cmd(*argv):
p = subprocess.run(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if p.returncode != 0:
raise Exception(str(p)) # this will show stdout and stderr
return p.stdout

View File

@ -88,6 +88,7 @@ class TestApp(qubes.tests.TestEmitter):
self.default_pool_root = 'default'
self.default_pool_private = 'default'
self.default_pool_kernel = 'linux-kernel'
self.default_qrexec_timeout = 60
self.default_netvm = None
self.domains = TestVMsCollection()
#: jinja2 environment for libvirt XML templates

View File

@ -141,6 +141,50 @@ class TC_00_setters(qubes.tests.QubesTestCase):
with self.assertRaises(ValueError):
qubes.vm.qubesvm._setter_virt_mode(self.vm, self.prop, 'True')
class TC_10_default(qubes.tests.QubesTestCase):
def setUp(self):
super().setUp()
self.app = TestApp()
self.vm = TestVM(app=self.app)
self.prop = TestProp()
def test_000_default_with_template_simple(self):
default_getter = qubes.vm.qubesvm._default_with_template('kernel',
'dfl-kernel')
self.assertEqual(default_getter(self.vm), 'dfl-kernel')
self.vm.template = None
self.assertEqual(default_getter(self.vm), 'dfl-kernel')
self.vm.template = unittest.mock.Mock()
self.vm.template.kernel = 'template-kernel'
self.assertEqual(default_getter(self.vm), 'template-kernel')
def test_001_default_with_template_callable(self):
default_getter = qubes.vm.qubesvm._default_with_template('kernel',
lambda x: x.app.default_kernel)
self.app.default_kernel = 'global-dfl-kernel'
self.assertEqual(default_getter(self.vm), 'global-dfl-kernel')
self.vm.template = None
self.assertEqual(default_getter(self.vm), 'global-dfl-kernel')
self.vm.template = unittest.mock.Mock()
self.vm.template.kernel = 'template-kernel'
self.assertEqual(default_getter(self.vm), 'template-kernel')
def test_010_default_virt_mode(self):
default_getter = qubes.vm.qubesvm._default_with_template('kernel',
lambda x: x.app.default_kernel)
self.assertEqual(qubes.vm.qubesvm._default_virt_mode(self.vm),
'pvh')
self.vm.template = unittest.mock.Mock()
self.vm.template.virt_mode = 'hvm'
self.assertEqual(qubes.vm.qubesvm._default_virt_mode(self.vm),
'hvm')
self.vm.template = None
self.assertEqual(qubes.vm.qubesvm._default_virt_mode(self.vm),
'pvh')
self.vm.devices['pci'].persistent().append('some-dev')
self.assertEqual(qubes.vm.qubesvm._default_virt_mode(self.vm),
'hvm')
class QubesVMTestsMixin(object):
property_no_default = object()
@ -415,6 +459,15 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase):
self.assertPropertyInvalidValue(vm, 'qrexec_timeout', '-2')
self.assertPropertyInvalidValue(vm, 'qrexec_timeout', '')
def test_272_qrexec_timeout_global_changed(self):
self.app.default_qrexec_timeout = 123
vm = self.get_vm()
self.assertPropertyDefaultValue(vm, 'qrexec_timeout', 123)
self.assertPropertyValue(vm, 'qrexec_timeout', 3, 3, '3')
del vm.qrexec_timeout
self.assertPropertyDefaultValue(vm, 'qrexec_timeout', 123)
self.assertPropertyValue(vm, 'qrexec_timeout', '3', 3, '3')
def test_280_autostart(self):
vm = self.get_vm()
# FIXME any better idea to not involve systemctl call at this stage?

View File

@ -18,7 +18,7 @@
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
#
'''qvm-create - Create new Qubes OS store'''
'''qubes-create - Create new Qubes OS store'''
import sys
import qubes
@ -38,7 +38,7 @@ def main(args=None):
args = parser.parse_args(args)
qubes.Qubes.create_empty_store(args.app,
offline_mode=args.offline_mode)
offline_mode=args.offline_mode).setup_pools()
return 0

View File

@ -41,7 +41,7 @@ def get_timezone():
if os.path.islink('/etc/localtime'):
return '/'.join(os.readlink('/etc/localtime').split('/')[-2:])
# <=fc17
elif os.path.exists('/etc/sysconfig/clock'):
if os.path.exists('/etc/sysconfig/clock'):
clock_config = open('/etc/sysconfig/clock', "r")
clock_config_lines = clock_config.readlines()
clock_config.close()
@ -50,18 +50,17 @@ def get_timezone():
line_match = zone_re.match(line)
if line_match:
return line_match.group(1)
else:
# last resort way, some applications makes /etc/localtime
# hardlink instead of symlink...
tz_info = os.stat('/etc/localtime')
if not tz_info:
return None
if tz_info.st_nlink > 1:
p = subprocess.Popen(['find', '/usr/share/zoneinfo',
'-inum', str(tz_info.st_ino), '-print', '-quit'],
stdout=subprocess.PIPE)
tz_path = p.communicate()[0].strip()
return tz_path.replace('/usr/share/zoneinfo/', '')
# last resort way, some applications makes /etc/localtime
# hardlink instead of symlink...
tz_info = os.stat('/etc/localtime')
if not tz_info:
return None
if tz_info.st_nlink > 1:
p = subprocess.Popen(['find', '/usr/share/zoneinfo',
'-inum', str(tz_info.st_ino), '-print', '-quit'],
stdout=subprocess.PIPE)
tz_path = p.communicate()[0].strip()
return tz_path.replace(b'/usr/share/zoneinfo/', b'')
return None
@ -132,9 +131,9 @@ def size_to_human(size):
"""Humane readable size, with 1/10 precision"""
if size < 1024:
return str(size)
elif size < 1024 * 1024:
if size < 1024 * 1024:
return str(round(size / 1024.0, 1)) + ' KiB'
elif size < 1024 * 1024 * 1024:
if size < 1024 * 1024 * 1024:
return str(round(size / (1024.0 * 1024), 1)) + ' MiB'
return str(round(size / (1024.0 * 1024 * 1024), 1)) + ' GiB'
@ -181,6 +180,6 @@ def match_vm_name_with_special(vm, name):
or @type:...'''
if name.startswith('@tag:'):
return name[len('@tag:'):] in vm.tags
elif name.startswith('@type:'):
if name.startswith('@type:'):
return name[len('@type:'):] == vm.__class__.__name__
return name == vm.name

View File

@ -569,10 +569,9 @@ class VMProperty(qubes.property):
if self.allow_none:
super(VMProperty, self).__set__(instance, value)
return
else:
raise ValueError(
'Property {!r} does not allow setting to {!r}'.format(
self.__name__, value))
raise ValueError(
'Property {!r} does not allow setting to {!r}'.format(
self.__name__, value))
app = instance if isinstance(instance, qubes.Qubes) else instance.app

View File

@ -147,12 +147,11 @@ class AdminVM(qubes.vm.BaseVM):
if self.app.vmm.offline_mode:
# default value passed on xen cmdline
return 4096
else:
try:
return self.app.vmm.libvirt_conn.getInfo()[1]
except libvirt.libvirtError as e:
self.log.warning('Failed to get memory limit for dom0: %s', e)
return 4096
try:
return self.app.vmm.libvirt_conn.getInfo()[1]
except libvirt.libvirtError as e:
self.log.warning('Failed to get memory limit for dom0: %s', e)
return 4096
def verify_files(self):
'''Always :py:obj:`True`
@ -181,7 +180,7 @@ class AdminVM(qubes.vm.BaseVM):
@property
def icon_path(self):
return None
pass
@property
def untrusted_qdb(self):

View File

@ -142,8 +142,8 @@ class DispVM(qubes.vm.qubesvm.QubesVM):
def _auto_cleanup(self):
'''Do auto cleanup if enabled'''
if self.auto_cleanup and self in self.app.domains:
yield from self.remove_from_disk()
del self.app.domains[self]
yield from self.remove_from_disk()
self.app.save()
@classmethod
@ -193,8 +193,8 @@ class DispVM(qubes.vm.qubesvm.QubesVM):
pass
# if auto_cleanup is set, this will be done automatically
if not self.auto_cleanup:
yield from self.remove_from_disk()
del self.app.domains[self]
yield from self.remove_from_disk()
self.app.save()
@asyncio.coroutine

View File

@ -24,7 +24,6 @@ from __future__ import absolute_import
import asyncio
import base64
import errno
import grp
import os
import os.path
@ -102,7 +101,25 @@ def _setter_virt_mode(self, prop, value):
def _default_virt_mode(self):
if self.devices['pci'].persistent():
return 'hvm'
return 'pvh'
try:
return self.template.virt_mode
except AttributeError:
return 'pvh'
def _default_with_template(prop, default):
'''Return a callable for 'default' argument of a property. Use a value
from a template (if any), otherwise *default*
'''
def _func(self):
try:
return getattr(self.template, prop)
except AttributeError:
if callable(default):
return default(self)
return default
return _func
class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
@ -158,6 +175,32 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
*other arguments are as in :py:meth:`start`*
.. event:: domain-start-failed (subject, event, reason)
Fired when :py:meth:`start` method fails.
*reason* argument is a textual error message.
Handler for this event can be asynchronous (a coroutine).
:param subject: Event emitter (the qube object)
:param event: Event name (``'domain-start'``)
*other arguments are as in :py:meth:`start`*
.. event:: domain-paused (subject, event)
Fired when the domain has been paused.
:param subject: Event emitter (the qube object)
:param event: Event name (``'domain-paused'``)
.. event:: domain-unpaused (subject, event)
Fired when the domain has been unpaused.
:param subject: Event emitter (the qube object)
:param event: Event name (``'domain-unpaused'``)
.. event:: domain-stopped (subject, event)
Fired when domain has been stopped.
@ -387,7 +430,8 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
type=str, setter=_setter_virt_mode,
default=_default_virt_mode,
doc='''Virtualisation mode: full virtualisation ("HVM"),
or paravirtualisation ("PV"), or hybrid ("PVH")''')
or paravirtualisation ("PV"), or hybrid ("PVH"). TemplateBasedVMs use its '
'template\'s value by default.''')
installed_by_rpm = qubes.property('installed_by_rpm',
type=bool, setter=qubes.property.bool,
@ -397,17 +441,19 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
memory = qubes.property('memory', type=int,
setter=_setter_positive_int,
default=(lambda self:
default=_default_with_template('memory', lambda self:
qubes.config.defaults[
'hvm_memory' if self.virt_mode == 'hvm' else 'memory']),
doc='Memory currently available for this VM.')
doc='Memory currently available for this VM. TemplateBasedVMs use its '
'template\'s value by default.')
maxmem = qubes.property('maxmem', type=int,
setter=_setter_positive_int,
default=(lambda self:
int(min(self.app.host.memory_total / 1024 / 2, 4000))),
default=_default_with_template('maxmem', (lambda self:
int(min(self.app.host.memory_total / 1024 / 2, 4000)))),
doc='''Maximum amount of memory available for this VM (for the purpose
of the memory balancer).''')
of the memory balancer). TemplateBasedVMs use its '
'template\'s value by default.''')
stubdom_mem = qubes.property('stubdom_mem', type=int,
setter=_setter_positive_int,
@ -417,14 +463,17 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
vcpus = qubes.property('vcpus',
type=int,
setter=_setter_positive_int,
default=2,
doc='Number of virtual CPUs for a qube')
default=_default_with_template('vcpus', 2),
doc='Number of virtual CPUs for a qube. TemplateBasedVMs use its '
'template\'s value by default.')
# CORE2: swallowed uses_default_kernel
kernel = qubes.property('kernel', type=str,
setter=_setter_kernel,
default=(lambda self: self.app.default_kernel),
doc='Kernel used by this domain.')
default=_default_with_template('kernel',
lambda self: self.app.default_kernel),
doc='Kernel used by this domain. TemplateBasedVMs use its '
'template\'s value by default.')
# CORE2: swallowed uses_default_kernelopts
# pylint: disable=no-member
@ -434,7 +483,8 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
if list(self.devices['pci'].persistent())
else self.template.kernelopts if hasattr(self, 'template')
else qubes.config.defaults['kernelopts']),
doc='Kernel command line passed to domain.')
doc='Kernel command line passed to domain. TemplateBasedVMs use its '
'template\'s value by default.')
debug = qubes.property('debug', type=bool, default=False,
setter=qubes.property.bool,
@ -445,10 +495,10 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
# only plain property?
default_user = qubes.property('default_user', type=str,
# pylint: disable=no-member
default=(lambda self: self.template.default_user
if hasattr(self, 'template') else 'user'),
default=_default_with_template('default_user', 'user'),
setter=_setter_default_user,
doc='FIXME')
doc='Default user to start applications as. TemplateBasedVMs use its '
'template\'s value by default.')
# pylint: enable=no-member
@ -459,12 +509,22 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
# else:
# return self._default_user
qrexec_timeout = qubes.property('qrexec_timeout', type=int, default=60,
qrexec_timeout = qubes.property('qrexec_timeout', type=int,
default=_default_with_template('qrexec_timeout',
lambda self: self.app.default_qrexec_timeout),
setter=_setter_positive_int,
doc='''Time in seconds after which qrexec connection attempt is deemed
failed. Operating system inside VM should be able to boot in this
time.''')
shutdown_timeout = qubes.property('shutdown_timeout', type=int,
default=_default_with_template('shutdown_timeout',
lambda self: self.app.default_shutdown_timeout),
setter=_setter_positive_int,
doc='''Time in seconds for shutdown of the VM, after which VM may be
forcefully powered off. Operating system inside VM should be
able to fully shutdown in this time.''')
autostart = qubes.property('autostart', default=False,
type=bool, setter=qubes.property.bool,
doc='''Setting this to `True` means that VM should be autostarted on
@ -518,10 +578,9 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
except libvirt.libvirtError as e:
if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
return -1
else:
self.log.exception('libvirt error code: {!r}'.format(
e.get_error_code()))
raise
self.log.exception('libvirt error code: {!r}'.format(
e.get_error_code()))
raise
@qubes.stateless_property
def stubdom_xid(self):
@ -815,10 +874,56 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
raise qubes.exc.QubesException(
'Failed to reset autostart for VM in systemd')
@qubes.events.handler('domain-remove-from-disk')
def on_remove_from_disk(self, event, **kwargs):
# pylint: disable=unused-argument
if self.autostart:
subprocess.call(
['sudo', 'systemctl', 'disable',
'qubes-vm@{}.service'.format(self.name)])
@qubes.events.handler('domain-create-on-disk')
def on_create_on_disk(self, event, **kwargs):
# pylint: disable=unused-argument
if self.autostart:
subprocess.call(
['sudo', 'systemctl', 'enable',
'qubes-vm@{}.service'.format(self.name)])
#
# methods for changing domain state
#
@asyncio.coroutine
def _ensure_shutdown_handled(self):
'''Make sure previous shutdown is fully handled.
MUST NOT be called when domain is running.
'''
with (yield from self._domain_stopped_lock):
# Don't accept any new stopped event's till a new VM has been
# created. If we didn't received any stopped event or it wasn't
# handled yet we will handle this in the next lines.
self._domain_stopped_event_received = True
if self._domain_stopped_future is not None:
# Libvirt stopped event was already received, so cancel the
# future. If it didn't generate the Qubes events yet we
# will do it below.
self._domain_stopped_future.cancel()
self._domain_stopped_future = None
if not self._domain_stopped_event_handled:
# No Qubes domain-stopped events have been generated yet.
# So do this now.
# Set this immediately such that we don't generate events
# twice if an exception gets thrown.
self._domain_stopped_event_handled = True
yield from self.fire_event_async('domain-stopped')
yield from self.fire_event_async('domain-shutdown')
@asyncio.coroutine
def start(self, start_guid=True, notify_function=None,
mem_required=None):
@ -835,39 +940,28 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
if self.get_power_state() != 'Halted':
return self
with (yield from self._domain_stopped_lock):
# Don't accept any new stopped event's till a new VM has been
# created. If we didn't received any stopped event or it wasn't
# handled yet we will handle this in the next lines.
self._domain_stopped_event_received = True
if self._domain_stopped_future is not None:
# Libvirt stopped event was already received, so cancel the
# future. If it didn't generate the Qubes events yet we
# will do it below.
self._domain_stopped_future.cancel()
self._domain_stopped_future = None
if not self._domain_stopped_event_handled:
# No Qubes domain-stopped events have been generated yet.
# So do this now.
# Set this immediately such that we don't generate events
# twice if an exception gets thrown.
self._domain_stopped_event_handled = True
yield from self.fire_event_async('domain-stopped')
yield from self.fire_event_async('domain-shutdown')
yield from self._ensure_shutdown_handled()
self.log.info('Starting {}'.format(self.name))
yield from self.fire_event_async('domain-pre-start',
pre_event=True,
start_guid=start_guid, mem_required=mem_required)
try:
yield from self.fire_event_async('domain-pre-start',
pre_event=True,
start_guid=start_guid, mem_required=mem_required)
except Exception as exc:
yield from self.fire_event_async('domain-start-failed',
reason=str(exc))
raise
for devclass in self.devices:
for dev in self.devices[devclass].persistent():
if isinstance(dev, qubes.devices.UnknownDevice):
raise qubes.exc.QubesException(
'{} device {} not available'.format(devclass, dev))
qmemman_client = None
try:
if self.virt_mode == 'pvh' and self.kernel is None:
if self.virt_mode == 'pvh' and not self.kernel:
raise qubes.exc.QubesException(
'virt_mode PVH require kernel to be set')
yield from self.storage.verify()
@ -960,8 +1054,8 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
return
if self._domain_stopped_event_received:
self.log.warning('Duplicated stopped event from libvirt received!')
# ignore this unexpected event
# ignore this event - already triggered by shutdown(), kill(),
# or subsequent start()
return
self._domain_stopped_event_received = True
@ -993,9 +1087,13 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
self.name)
@asyncio.coroutine
def shutdown(self, force=False, wait=False):
def shutdown(self, force=False, wait=False, timeout=None):
'''Shutdown domain.
:param force: ignored
:param wait: wait for shutdown to complete
:param timeout: shutdown wait timeout (for *wait*=True), defaults to
:py:attr:`shutdown_timeout`
:raises qubes.exc.QubesVMNotStartedError: \
when domain is already shut down.
'''
@ -1008,8 +1106,18 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
self.libvirt_domain.shutdown()
while wait and not self.is_halted():
yield from asyncio.sleep(0.25)
if wait:
if timeout is None:
timeout = self.shutdown_timeout
while timeout > 0 and not self.is_halted():
yield from asyncio.sleep(0.25)
timeout -= 0.25
with (yield from self.startup_lock):
if self.is_halted():
# make sure all shutdown tasks are completed
yield from self._ensure_shutdown_handled()
else:
raise qubes.exc.QubesVMShutdownTimeoutError(self)
return self
@ -1024,13 +1132,17 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
if not self.is_running() and not self.is_paused():
raise qubes.exc.QubesVMNotStartedError(self)
try:
self.libvirt_domain.destroy()
except libvirt.libvirtError as e:
if e.get_error_code() == libvirt.VIR_ERR_OPERATION_INVALID:
raise qubes.exc.QubesVMNotStartedError(self)
else:
raise
with (yield from self.startup_lock):
try:
self.libvirt_domain.destroy()
except libvirt.libvirtError as e:
if e.get_error_code() == libvirt.VIR_ERR_OPERATION_INVALID:
raise qubes.exc.QubesVMNotStartedError(self)
else:
raise
# make sure all shutdown tasks are completed
yield from self._ensure_shutdown_handled()
return self
@ -1377,6 +1489,8 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
'creation'.format(self.dir_path))
raise
if os.path.exists(self.icon_path):
os.unlink(self.icon_path)
self.log.info('Creating icon symlink: {} -> {}'.format(
self.icon_path, self.label.icon_path))
if hasattr(os, "symlink"):
@ -1395,15 +1509,18 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
"Can't remove VM {!s}, beacuse it's in state {!r}.".format(
self, self.get_power_state()))
# make sure shutdown is handled before removing anything, but only if
# handling is pending; if not, we may be called from within
# domain-shutdown event (DispVM._auto_cleanup), which would deadlock
if not self._domain_stopped_event_handled:
yield from self._ensure_shutdown_handled()
yield from self.fire_event_async('domain-remove-from-disk')
try:
# TODO: make it async?
shutil.rmtree(self.dir_path)
except OSError as e:
if e.errno == errno.ENOENT:
pass
else:
raise
except FileNotFoundError:
pass
yield from self.storage.remove()
@asyncio.coroutine
@ -1575,8 +1692,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
except libvirt.libvirtError as e:
if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
return 'Halted'
else:
raise
raise
libvirt_domain = self.libvirt_domain
if libvirt_domain is None:
@ -1587,19 +1703,17 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
# pylint: disable=line-too-long
if libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_PAUSED:
return "Paused"
elif libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_CRASHED:
if libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_CRASHED:
return "Crashed"
elif libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_SHUTDOWN:
if libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_SHUTDOWN:
return "Halting"
elif libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_SHUTOFF:
if libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_SHUTOFF:
return "Dying"
elif libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_PMSUSPENDED: # nopep8
if libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_PMSUSPENDED: # nopep8
return "Suspended"
else:
if not self.is_fully_usable():
return "Transient"
return "Running"
if not self.is_fully_usable():
return "Transient"
return "Running"
return 'Halted'
except libvirt.libvirtError as e:
@ -1639,8 +1753,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
except libvirt.libvirtError as e:
if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
return False
else:
raise
raise
return bool(self.libvirt_domain.isActive())
@ -1662,7 +1775,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
:py:obj:`False` otherwise.
:rtype: bool
'''
if self.xid < 0:
if self.xid < 0: # pylint: disable=comparison-with-callable
return False
return os.path.exists('/var/run/qubes/qrexec.%s' % self.name)
@ -1706,10 +1819,9 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
libvirt.VIR_ERR_INTERNAL_ERROR):
return 0
else:
self.log.exception(
'libvirt error code: {!r}'.format(e.get_error_code()))
raise
self.log.exception(
'libvirt error code: {!r}'.format(e.get_error_code()))
raise
def get_mem_static_max(self):
'''Get maximum memory available to VM.
@ -1733,10 +1845,9 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
libvirt.VIR_ERR_INTERNAL_ERROR):
return 0
else:
self.log.exception(
'libvirt error code: {!r}'.format(e.get_error_code()))
raise
self.log.exception(
'libvirt error code: {!r}'.format(e.get_error_code()))
raise
def get_cputime(self):
'''Get total CPU time burned by this domain since start.
@ -1772,10 +1883,9 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
libvirt.VIR_ERR_INTERNAL_ERROR):
return 0
else:
self.log.exception(
'libvirt error code: {!r}'.format(e.get_error_code()))
raise
self.log.exception(
'libvirt error code: {!r}'.format(e.get_error_code()))
raise
# miscellanous
@ -1936,7 +2046,9 @@ def _patch_pool_config(config, pool=None, pools=None):
if not is_snapshot:
config['pool'] = str(pools[name])
else:
msg = "Can't clone a snapshot volume {!s} to pool {!s} " \
msg = "Snapshot volume {0!s} must be in the same pool as its " \
"origin ({0!s} volume of template)," \
"cannot move to pool {1!s} " \
.format(name, pools[name])
raise qubes.exc.QubesException(msg)
return config

View File

@ -18,6 +18,8 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
# pylint: disable=no-else-return,useless-object-inheritance,try-except-raise
''' Qrexec policy parser and evaluator '''
import enum
import itertools

View File

@ -33,7 +33,7 @@ import qubespolicy.rpcconfirmation
import qubespolicy.policycreateconfirmation
# pylint: enable=wrong-import-position
class PolicyAgent(object):
class PolicyAgent:
# pylint: disable=too-few-public-methods
dbus = """
<node>

View File

@ -72,12 +72,12 @@ def main(args=None):
if not log.handlers:
handler = logging.handlers.SysLogHandler(address='/dev/log')
log.addHandler(handler)
log_prefix = 'qrexec: {}: {} -> {}: '.format(
log_prefix = 'qrexec: {}: {} -> {}:'.format(
args.service_name, args.domain, args.target)
try:
system_info = qubespolicy.get_system_info()
except qubespolicy.QubesMgmtException as e:
log.error(log_prefix + 'error getting system info: ' + str(e))
log.error('%s error getting system info: %s', log_prefix, str(e))
return 1
try:
try:
@ -130,13 +130,13 @@ def main(args=None):
action.handle_user_response(True, response)
else:
action.handle_user_response(False)
log.info(log_prefix + 'allowed to {}'.format(action.target))
log.info('%s allowed to %s', log_prefix, str(action.target))
action.execute(caller_ident)
except qubespolicy.PolicySyntaxError as e:
log.error(log_prefix + 'error loading policy: ' + str(e))
log.error('%s error loading policy: %s', log_prefix, str(e))
return 1
except qubespolicy.AccessDenied as e:
log.info(log_prefix + 'denied: ' + str(e))
log.info('%s denied: %s', log_prefix, str(e))
return 1
return 0

View File

@ -28,7 +28,7 @@ gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# pylint: enable=import-error
class PolicyCreateConfirmationWindow(object):
class PolicyCreateConfirmationWindow:
# pylint: disable=too-few-public-methods
_source_file = pkg_resources.resource_filename('qubespolicy',
os.path.join('glade', "PolicyCreateConfirmationWindow.glade"))

View File

@ -32,6 +32,11 @@ def main():
app = Qubes()
clockvm = app.clockvm
if not clockvm.is_running():
sys.stderr.write('ClockVM {} is not running, aborting.\n'.format(
clockvm.name))
sys.exit(0)
p = clockvm.run_service('qubes.GetDate')
untrusted_date_out = p.stdout.read(25).decode('ascii', errors='strict')
untrusted_date_out = untrusted_date_out.strip()
@ -42,7 +47,7 @@ def main():
date_out = untrusted_date_out
subprocess.check_call(['date', '-u', '-Iseconds', '-s', date_out],
stdout=subprocess.DEVNULL)
subprocess.check_call(['hwclock', '--systohc'],
subprocess.check_call(['/sbin/hwclock', '--systohc'],
stdout=subprocess.DEVNULL)
if __name__ == '__main__':

View File

@ -122,6 +122,7 @@ make -C doc PYTHON=%{__python3} SPHINXBUILD=sphinx-build-%{python3_version} man
make install \
DESTDIR=$RPM_BUILD_ROOT \
BACKEND_VMM=%{backend_vmm} \
UNITDIR=%{_unitdir} \
PYTHON_SITEPATH=%{python3_sitelib} \
SYSCONFDIR=%{_sysconfdir}
@ -284,6 +285,7 @@ fi
%{python3_sitelib}/qubes/ext/qubesmanager.py
%{python3_sitelib}/qubes/ext/r3compatibility.py
%{python3_sitelib}/qubes/ext/services.py
%{python3_sitelib}/qubes/ext/windows.py
%dir %{python3_sitelib}/qubes/tests
%dir %{python3_sitelib}/qubes/tests/__pycache__
@ -304,6 +306,7 @@ fi
%{python3_sitelib}/qubes/tests/init.py
%{python3_sitelib}/qubes/tests/storage.py
%{python3_sitelib}/qubes/tests/storage_file.py
%{python3_sitelib}/qubes/tests/storage_reflink.py
%{python3_sitelib}/qubes/tests/storage_kernels.py
%{python3_sitelib}/qubes/tests/storage_lvm.py
%{python3_sitelib}/qubes/tests/tarwriter.py
@ -336,9 +339,11 @@ fi
%{python3_sitelib}/qubes/tests/integ/backup.py
%{python3_sitelib}/qubes/tests/integ/backupcompatibility.py
%{python3_sitelib}/qubes/tests/integ/basic.py
%{python3_sitelib}/qubes/tests/integ/devices_block.py
%{python3_sitelib}/qubes/tests/integ/devices_pci.py
%{python3_sitelib}/qubes/tests/integ/dispvm.py
%{python3_sitelib}/qubes/tests/integ/dom0_update.py
%{python3_sitelib}/qubes/tests/integ/mime.py
%{python3_sitelib}/qubes/tests/integ/network.py
%{python3_sitelib}/qubes/tests/integ/pvgrub.py
%{python3_sitelib}/qubes/tests/integ/salt.py

View File

@ -74,6 +74,7 @@ if __name__ == '__main__':
'qubes.ext.pci = qubes.ext.pci:PCIDeviceExtension',
'qubes.ext.block = qubes.ext.block:BlockDeviceExtension',
'qubes.ext.services = qubes.ext.services:ServicesExtension',
'qubes.ext.windows = qubes.ext.windows:WindowsFeatures',
],
'qubes.devices': [
'pci = qubes.ext.pci:PCIDevice',

View File

@ -1,49 +0,0 @@
PYTHON_TESTSPATH = $(PYTHON_SITEPATH)/qubes/tests
all:
python -m compileall .
python -O -m compileall .
install:
ifndef PYTHON_SITEPATH
$(error PYTHON_SITEPATH not defined)
endif
mkdir -p $(DESTDIR)$(PYTHON_TESTSPATH)
cp __init__.py $(DESTDIR)$(PYTHON_TESTSPATH)
cp __init__.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
cp backup.py $(DESTDIR)$(PYTHON_TESTSPATH)
cp backup.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
cp backupcompatibility.py $(DESTDIR)$(PYTHON_TESTSPATH)
cp backupcompatibility.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
cp basic.py $(DESTDIR)$(PYTHON_TESTSPATH)
cp basic.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
cp block.py $(DESTDIR)$(PYTHON_TESTSPATH)
cp block.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
cp dispvm.py $(DESTDIR)$(PYTHON_TESTSPATH)
cp dispvm.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
cp dom0_update.py $(DESTDIR)$(PYTHON_TESTSPATH)
cp dom0_update.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
cp extra.py $(DESTDIR)$(PYTHON_TESTSPATH)
cp extra.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
cp hardware.py $(DESTDIR)$(PYTHON_TESTSPATH)
cp hardware.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
cp hvm.py $(DESTDIR)$(PYTHON_TESTSPATH)
cp hvm.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
cp mime.py $(DESTDIR)$(PYTHON_TESTSPATH)
cp mime.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
cp network.py $(DESTDIR)$(PYTHON_TESTSPATH)
cp network.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
cp pvgrub.py $(DESTDIR)$(PYTHON_TESTSPATH)
cp pvgrub.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
cp regressions.py $(DESTDIR)$(PYTHON_TESTSPATH)
cp regressions.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
cp run.py $(DESTDIR)$(PYTHON_TESTSPATH)
cp run.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
cp storage.py $(DESTDIR)$(PYTHON_TESTSPATH)
cp storage.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
cp storage_file.py $(DESTDIR)$(PYTHON_TESTSPATH)
cp storage_file.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
cp storage_xen.py $(DESTDIR)$(PYTHON_TESTSPATH)
cp storage_xen.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
cp vm_qrexec_gui.py $(DESTDIR)$(PYTHON_TESTSPATH)
cp vm_qrexec_gui.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)

View File

@ -1,74 +0,0 @@
#!/usr/bin/python2
# -*- coding: utf-8 -*-
#
# The Qubes OS Project, http://www.qubes-os.org
#
# Copyright (C) 2016 Marek Marczykowski-Górecki
# <marmarek@invisiblethingslab.com>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
#
#
import os
import qubes.tests
import time
import subprocess
from unittest import expectedFailure
class TC_00_HVM(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
def setUp(self):
super(TC_00_HVM, self).setUp()
self.vm = self.qc.add_new_vm("QubesHVm",
name=self.make_vm_name('vm1'))
self.vm.create_on_disk(verbose=False)
@expectedFailure
def test_000_pci_passthrough_presence(self):
pcidev = os.environ.get('QUBES_TEST_PCIDEV', None)
if pcidev is None:
self.skipTest('Specify PCI device with QUBES_TEST_PCIDEV '
'environment variable')
self.vm.pcidevs = [pcidev]
self.vm.pci_strictreset = False
self.qc.save()
self.qc.unlock_db()
init_script = (
"#!/bin/sh\n"
"set -e\n"
"lspci -n > /dev/xvdb\n"
"poweroff\n"
)
self.prepare_hvm_system_linux(self.vm, init_script,
['/usr/sbin/lspci'])
self.vm.start()
timeout = 60
while timeout > 0:
if not self.vm.is_running():
break
time.sleep(1)
timeout -= 1
if self.vm.is_running():
self.fail("Timeout while waiting for VM shutdown")
with open(self.vm.storage.private_img, 'r') as f:
lspci_vm = f.read(512).strip('\0')
p = subprocess.Popen(['lspci', '-ns', pcidev], stdout=subprocess.PIPE)
(lspci_host, _) = p.communicate()
# strip BDF, as it is different in VM
pcidev_desc = ' '.join(lspci_host.strip().split(' ')[1:])
self.assertIn(pcidev_desc, lspci_vm)

View File

@ -1,124 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# The Qubes OS Project, http://www.qubes-os.org
#
# Copyright (C) 2016 Marek Marczykowski-Górecki
# <marmarek@invisiblethingslab.com>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
#
#
import qubes.tests
from qubes.qubes import QubesException
class TC_10_HVM(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
# TODO: test with some OS inside
# TODO: windows tools tests
def test_000_create_start(self):
testvm1 = self.qc.add_new_vm("QubesHVm",
name=self.make_vm_name('vm1'))
testvm1.create_on_disk(verbose=False)
self.qc.save()
self.qc.unlock_db()
testvm1.start()
self.assertEquals(testvm1.get_power_state(), "Running")
def test_010_create_start_template(self):
templatevm = self.qc.add_new_vm("QubesTemplateHVm",
name=self.make_vm_name('template'))
templatevm.create_on_disk(verbose=False)
self.qc.save()
self.qc.unlock_db()
templatevm.start()
self.assertEquals(templatevm.get_power_state(), "Running")
def test_020_create_start_template_vm(self):
templatevm = self.qc.add_new_vm("QubesTemplateHVm",
name=self.make_vm_name('template'))
templatevm.create_on_disk(verbose=False)
testvm2 = self.qc.add_new_vm("QubesHVm",
name=self.make_vm_name('vm2'),
template=templatevm)
testvm2.create_on_disk(verbose=False)
self.qc.save()
self.qc.unlock_db()
testvm2.start()
self.assertEquals(testvm2.get_power_state(), "Running")
def test_030_prevent_simultaneus_start(self):
templatevm = self.qc.add_new_vm("QubesTemplateHVm",
name=self.make_vm_name('template'))
templatevm.create_on_disk(verbose=False)
testvm2 = self.qc.add_new_vm("QubesHVm",
name=self.make_vm_name('vm2'),
template=templatevm)
testvm2.create_on_disk(verbose=False)
self.qc.save()
self.qc.unlock_db()
templatevm.start()
self.assertEquals(templatevm.get_power_state(), "Running")
self.assertRaises(QubesException, testvm2.start)
templatevm.force_shutdown()
testvm2.start()
self.assertEquals(testvm2.get_power_state(), "Running")
self.assertRaises(QubesException, templatevm.start)
def test_100_resize_root_img(self):
testvm1 = self.qc.add_new_vm("QubesHVm",
name=self.make_vm_name('vm1'))
testvm1.create_on_disk(verbose=False)
self.qc.save()
self.qc.unlock_db()
testvm1.resize_root_img(30*1024**3)
self.assertEquals(testvm1.get_root_img_sz(), 30*1024**3)
testvm1.start()
self.assertEquals(testvm1.get_power_state(), "Running")
# TODO: launch some OS there and check the size
def test_200_start_invalid_drive(self):
"""Regression test for #1619"""
testvm1 = self.qc.add_new_vm("QubesHVm",
name=self.make_vm_name('vm1'))
testvm1.create_on_disk(verbose=False)
testvm1.drive = 'hd:dom0:/invalid'
self.qc.save()
self.qc.unlock_db()
try:
testvm1.start()
except Exception as e:
self.assertIsInstance(e, QubesException)
else:
self.fail('No exception raised')
def test_201_start_invalid_drive_cdrom(self):
"""Regression test for #1619"""
testvm1 = self.qc.add_new_vm("QubesHVm",
name=self.make_vm_name('vm1'))
testvm1.create_on_disk(verbose=False)
testvm1.drive = 'cdrom:dom0:/invalid'
self.qc.save()
self.qc.unlock_db()
try:
testvm1.start()
except Exception as e:
self.assertIsInstance(e, QubesException)
else:
self.fail('No exception raised')

View File

@ -1 +1 @@
4.0.27
4.0.33