diff --git a/Makefile b/Makefile
index 3bea491f0..7a410ade1 100644
--- a/Makefile
+++ b/Makefile
@@ -60,26 +60,38 @@ ADMIN_API_METHODS_SIMPLE = \
admin.vm.Shutdown \
admin.vm.Start \
admin.vm.Unpause \
+ admin.vm.device.pci.Assign \
+ admin.vm.device.pci.Assigned \
admin.vm.device.pci.Attach \
+ admin.vm.device.pci.Attached \
admin.vm.device.pci.Available \
admin.vm.device.pci.Detach \
- admin.vm.device.pci.List \
- admin.vm.device.pci.Set.persistent \
+ admin.vm.device.pci.Set.required \
+ admin.vm.device.pci.Unassign \
+ admin.vm.device.block.Assign \
+ admin.vm.device.block.Assigned \
admin.vm.device.block.Attach \
+ admin.vm.device.block.Attached \
admin.vm.device.block.Available \
admin.vm.device.block.Detach \
- admin.vm.device.block.List \
- admin.vm.device.block.Set.persistent \
+ admin.vm.device.block.Set.required \
+ admin.vm.device.block.Unassign \
+ admin.vm.device.usb.Assign \
+ admin.vm.device.usb.Assigned \
admin.vm.device.usb.Attach \
+ admin.vm.device.usb.Attached \
admin.vm.device.usb.Available \
admin.vm.device.usb.Detach \
- admin.vm.device.usb.List \
- admin.vm.device.usb.Set.persistent \
+ admin.vm.device.usb.Set.required \
+ admin.vm.device.usb.Unassign \
+ admin.vm.device.mic.Assign \
+ admin.vm.device.mic.Assigned \
admin.vm.device.mic.Attach \
+ admin.vm.device.mic.Attached \
admin.vm.device.mic.Available \
admin.vm.device.mic.Detach \
- admin.vm.device.mic.List \
- admin.vm.device.mic.Set.persistent \
+ admin.vm.device.mic.Set.required \
+ admin.vm.device.mic.Unassign \
admin.vm.feature.CheckWithNetvm \
admin.vm.feature.CheckWithTemplate \
admin.vm.feature.CheckWithAdminVM \
@@ -211,8 +223,11 @@ endif
admin.vm.CreateInPool.AdminVM \
admin.vm.device.testclass.Attach \
admin.vm.device.testclass.Detach \
- admin.vm.device.testclass.List \
- admin.vm.device.testclass.Set.persistent \
+ admin.vm.device.testclass.Assign \
+ admin.vm.device.testclass.Unassign \
+ admin.vm.device.testclass.Attached \
+ admin.vm.device.testclass.Assigned \
+ admin.vm.device.testclass.Set.required \
admin.vm.device.testclass.Available
install -d $(DESTDIR)/etc/qubes/policy.d/include
install -m 0644 qubes-rpc-policy/admin-local-ro \
diff --git a/doc/qubes-devices.rst b/doc/qubes-devices.rst
index 61404c08c..e64811a3e 100644
--- a/doc/qubes-devices.rst
+++ b/doc/qubes-devices.rst
@@ -1,6 +1,131 @@
:py:mod:`qubes.devices` -- Devices
===================================
+Main concept is that some domain (backend) may expose (potentially multiple)
+devices, which can be attached to other domains (frontend). Devices can be of
+different buses (like 'pci', 'usb', etc.). Each device bus is implemented by
+an extension (see :py:mod:`qubes.ext`).
+
+Devices are identified by pair of (backend domain, `ident`), where `ident` is
+:py:class:`str` and can contain only characters from `[a-zA-Z0-9._-]` set.
+
+
+Device Assignment vs Attachment
+-------------------------------
+
+:py:class:`qubes.device_protocol.DeviceAssignment` describes the assignment of a device
+to a frontend VM. For clarity let's us introduce two types of assignments:
+*potential* and *real* (attachment). Attachment indicates that the device
+has been attached by the Qubes backend to its frontend VM and is visible
+from its perspective. Potential assignment, on the other hand,
+has two additional options: `automatically_attach` and `required`.
+For detailed descriptions, refer to the `DeviceAssignment` documentation.
+In general we refer to potential assignment as assignment
+and real assignment as attachment. To check whether the device is currently
+attached, we check :py:meth:`qubes.device_protocol.DeviceAssignment.attached`,
+while to check whether an (potential) assignment exists,
+we check :py:meth:`qubes.device_protocol.DeviceAssignment.attach_automatically`.
+Potential and real connections may coexist at the same time,
+in which case both values will be true.
+
+
+Actions
+-------
+
+The `assign` action signifies that a device will be assigned to the frontend VM
+in a potential form (this does not change the current system state).
+This will result in an attempt to automatically attach the device
+upon the next VM startup. If `required=True`, and the device cannot be attached,
+the VM startup will fail. Additionally, upon device detection (`device-added`),
+an attempt will be made to attach the device. However, at any time
+(unless `required=True`), the user can manually modify this state by performing
+`attach` or `detach` on the device, changing the current system state.
+This will not alter the assignment, and automatic attachment attempts
+will still be made in the future. To remove the assignment the user
+need to perform `unassign` (see next section).
+
+Assignment Management
+---------------------
+
+Assignments can be edited at any time: regardless of whether the VM is running
+or the device is currently attached. An exception is `required=True`,
+in which case the VM must be shut down. Removing the assignment does not change the real system state, so if the device is currently attached
+and the user remove the assignment, it will not be detached,
+but it will not be automatically attached in the future.
+Similarly, it works the other way around with `assign`.
+
+Proper Assignment States
+------------------------
+
+In short, we can think of device assignment in terms of three flags:
+#. `attached` - indicating whether the device is currently assigned,
+#. `attach_automatically` - indicating whether the device will be
+automatically attached by the system daemon,
+#. `required` - determining whether the failure of automatic attachment should
+result in the domain startup being interrupted.
+
+Then the proper states of assignment
+(`attached`, `automatically_attached`, `required`) are as follow:
+#. `(True, False, False)` -> domain is running, device is manually attached
+and could be manually detach any time.
+#. `(True, True, False)` -> domain is running, device is attached
+and could be manually detach any time (see 4.),
+but in the future will be auto-attached again.
+#. `(True, True, True)` -> domain is running, device is attached
+and couldn't be detached.
+#. `(False, True, False)` -> device is assigned to domain, but not attached
+because either (i) domain is halted, device (ii) manually detached or
+(iii) attach to different domain.
+#. `(False, True, True)` -> domain is halted, device assigned to domain
+and required to start domain.
+
+
+PCI Devices
+-----------
+
+PCI devices cannot be manually attached to a VM at any time.
+We must first create an assignment (`assign`) as required
+(in client we can use `--required` flag) while the VM is turned off.
+Then, it will be automatically attached upon each VM startup.
+However, if a PCI device is currently in use by another VM,
+the startup of the second VM will fail.
+PCI devices can only be assigned with the `required=True`, which does not
+allow for manual modification of the state during VM operation (attach/detach).
+
+Microphone
+----------
+
+The microphone cannot be assigned (potentially) to any VM (attempting to attach the microphone during VM startup fails).
+
+Understanding Device Self Identity
+----------------------------------
+
+It is important to understand that :py:class:`qubes.device_protocol.Device` does not
+correspond to the device itself, but rather to the *port* to which the device
+is connected. Therefore, when assigning a device to a VM, such as
+`sys-usb:1-1.1`, the port `1-1.1` is actually assigned, and thus
+*every* devices connected to it will be automatically attached.
+Similarly, when assigning `vm:sda`, every block device with the name `sda`
+will be automatically attached. We can limit this using :py:meth:`qubes.device_protocol.DeviceInfo.self_identity`, which returns a string containing information
+presented by the device, such as, `vendor_id`, `product_id`, `serial_number`,
+and encoded interfaces. In the case of block devices, `self_identity`
+consists of the parent port to which the device is connected (if any),
+the parent's `self_identity`, and the interface/partition number.
+In practice, this means that, a partition on a USB drive will only be
+automatically attached to a frontend domain if the parent presents
+the correct serial number etc., and is connected to a specific port.
+
+Port Assignment
+---------------
+
+It is possible to not assign a specific device but rather a port,
+(e.g., we can use the `--port` flag in the client). In this case,
+the value `any` will appear in the `identity` field of the `qubes.xml` file.
+This indicates that the identity presented by the devices will be ignored,
+and all connected devices will be automatically attached. Note that to create
+an assignment, *any* device must currently be connected to the port.
+
+
.. automodule:: qubes.devices
:members:
:show-inheritance:
diff --git a/qubes-rpc-policy/90-admin-default.policy.header b/qubes-rpc-policy/90-admin-default.policy.header
index fefdb207a..864872ba4 100644
--- a/qubes-rpc-policy/90-admin-default.policy.header
+++ b/qubes-rpc-policy/90-admin-default.policy.header
@@ -20,14 +20,20 @@
!include-service admin.vm.volume.Import * include/admin-local-rwx
!include-service admin.vm.volume.ImportWithSize * include/admin-local-rwx
+!include-service admin.vm.device.mic.Assign * include/admin-local-rwx
+!include-service admin.vm.device.mic.Assigned * include/admin-local-ro
!include-service admin.vm.device.mic.Attach * include/admin-local-rwx
+!include-service admin.vm.device.mic.Attached * include/admin-local-ro
!include-service admin.vm.device.mic.Available * include/admin-local-ro
!include-service admin.vm.device.mic.Detach * include/admin-local-rwx
-!include-service admin.vm.device.mic.List * include/admin-local-ro
-!include-service admin.vm.device.mic.Set.persistent * include/admin-local-rwx
+!include-service admin.vm.device.mic.Set.required * include/admin-local-rwx
+!include-service admin.vm.device.mic.Unassign * include/admin-local-rwx
+!include-service admin.vm.device.usb.Assign * include/admin-local-rwx
+!include-service admin.vm.device.usb.Assigned * include/admin-local-ro
!include-service admin.vm.device.usb.Attach * include/admin-local-rwx
+!include-service admin.vm.device.usb.Attached * include/admin-local-ro
!include-service admin.vm.device.usb.Available * include/admin-local-ro
!include-service admin.vm.device.usb.Detach * include/admin-local-rwx
-!include-service admin.vm.device.usb.List * include/admin-local-ro
-!include-service admin.vm.device.usb.Set.persistent * include/admin-local-rwx
+!include-service admin.vm.device.usb.Set.required * include/admin-local-rwx
+!include-service admin.vm.device.usb.Unassign * include/admin-local-rwx
diff --git a/qubes/api/__init__.py b/qubes/api/__init__.py
index 700eca39b..28b0d99d4 100644
--- a/qubes/api/__init__.py
+++ b/qubes/api/__init__.py
@@ -30,13 +30,7 @@
import traceback
import qubes.exc
-
-class ProtocolError(AssertionError):
- '''Raised when something is wrong with data received'''
-
-
-class PermissionDenied(Exception):
- '''Raised deliberately by handlers when we decide not to cooperate'''
+from qubes.exc import ProtocolError, PermissionDenied
def method(name, *, no_payload=False, endpoints=None, **classifiers):
@@ -127,8 +121,8 @@ def __init__(self, app, src, method_name, dest, arg, send_event=None):
#: destination qube
self.dest = self.app.domains[vm]
except KeyError:
- # normally this should filtered out by qrexec policy, but there are
- # two cases it might not be:
+ # normally this should be filtered out by qrexec policy, but there
+ # are two cases it might not be:
# 1. The call comes from dom0, which bypasses qrexec policy
# 2. Domain was removed between checking the policy and here
# we inform the client accordingly
@@ -213,11 +207,11 @@ def enforce(predicate):
def validate_size(self, untrusted_size: bytes) -> int:
self.enforce(isinstance(untrusted_size, bytes))
if not untrusted_size.isdigit():
- raise qubes.api.ProtocolError('Size must be ASCII digits (only)')
+ raise qubes.exc.ProtocolError('Size must be ASCII digits (only)')
if len(untrusted_size) >= 20:
- raise qubes.api.ProtocolError('Sizes limited to 19 decimal digits')
+ raise qubes.exc.ProtocolError('Sizes limited to 19 decimal digits')
if untrusted_size[0] == 48 and untrusted_size != b'0':
- raise qubes.api.ProtocolError('Spurious leading zeros not allowed')
+ raise qubes.exc.ProtocolError('Spurious leading zeros not allowed')
return int(untrusted_size)
class QubesDaemonProtocol(asyncio.Protocol):
diff --git a/qubes/api/admin.py b/qubes/api/admin.py
index cfcc113a6..39b9311e4 100644
--- a/qubes/api/admin.py
+++ b/qubes/api/admin.py
@@ -23,7 +23,6 @@
import asyncio
import functools
-import itertools
import os
import string
import subprocess
@@ -38,12 +37,15 @@
import qubes.backup
import qubes.config
import qubes.devices
+import qubes.exc
+import qubes.ext
import qubes.firewall
import qubes.storage
import qubes.utils
import qubes.vm
import qubes.vm.adminvm
import qubes.vm.qubesvm
+from qubes.device_protocol import Device
class QubesMgmtEventsDispatcher:
@@ -501,7 +503,7 @@ async def vm_volume_set_rw(self, untrusted_payload):
newvalue = qubes.property.bool(None, None,
untrusted_payload.decode('ascii'))
except (UnicodeDecodeError, ValueError):
- raise qubes.api.ProtocolError('Invalid value')
+ raise qubes.exc.ProtocolError('Invalid value')
del untrusted_payload
self.fire_event_for_permission(newvalue=newvalue)
@@ -520,7 +522,7 @@ async def vm_volume_set_ephemeral(self, untrusted_payload):
newvalue = qubes.property.bool(None, None,
untrusted_payload.decode('ascii'))
except (UnicodeDecodeError, ValueError):
- raise qubes.api.ProtocolError('Invalid value')
+ raise qubes.exc.ProtocolError('Invalid value')
del untrusted_payload
self.fire_event_for_permission(newvalue=newvalue)
@@ -751,7 +753,7 @@ async def pool_set_ephemeral(self, untrusted_payload):
newvalue = qubes.property.bool(None, None,
untrusted_payload.decode('ascii', 'strict'))
except (UnicodeDecodeError, ValueError):
- raise qubes.api.ProtocolError('Invalid value')
+ raise qubes.exc.ProtocolError('Invalid value')
del untrusted_payload
self.fire_event_for_permission(newvalue=newvalue)
@@ -1076,7 +1078,7 @@ async def _vm_create(self, vm_type, allow_pool=False,
errors='strict').split(' '):
untrusted_key, untrusted_value = untrusted_param.split('=', 1)
if untrusted_key in kwargs:
- raise qubes.api.ProtocolError('duplicated parameters')
+ raise qubes.exc.ProtocolError('duplicated parameters')
if untrusted_key == 'name':
qubes.vm.validate_name(None, None, untrusted_value)
@@ -1094,7 +1096,7 @@ async def _vm_create(self, vm_type, allow_pool=False,
elif untrusted_key == 'pool' and allow_pool:
if pool is not None:
- raise qubes.api.ProtocolError('duplicated pool parameter')
+ raise qubes.exc.ProtocolError('duplicated pool parameter')
pool = self.app.get_pool(untrusted_value)
elif untrusted_key.startswith('pool:') and allow_pool:
untrusted_volume = untrusted_key.split(':', 1)[1]
@@ -1104,19 +1106,19 @@ async def _vm_create(self, vm_type, allow_pool=False,
'root', 'private', 'volatile', 'kernel'])
volume = untrusted_volume
if volume in pools:
- raise qubes.api.ProtocolError(
+ raise qubes.exc.ProtocolError(
'duplicated pool:{} parameter'.format(volume))
pools[volume] = self.app.get_pool(untrusted_value)
else:
- raise qubes.api.ProtocolError('Invalid param name')
+ raise qubes.exc.ProtocolError('Invalid param name')
del untrusted_payload
if 'name' not in kwargs or 'label' not in kwargs:
- raise qubes.api.ProtocolError('Missing name or label')
+ raise qubes.exc.ProtocolError('Missing name or label')
if pool and pools:
- raise qubes.api.ProtocolError(
+ raise qubes.exc.ProtocolError(
'Only one of \'pool=\' and \'pool:volume=\' can be used')
if kwargs['name'] in self.app.domains:
@@ -1155,8 +1157,8 @@ async def create_disposable(self):
return dispvm.name
- @qubes.api.method('admin.vm.Remove', no_payload=True,
- scope='global', write=True)
+ @qubes.api.method(
+ 'admin.vm.Remove', no_payload=True, scope='global', write=True)
async def vm_remove(self):
self.enforce(not self.arg)
@@ -1197,118 +1199,161 @@ async def deviceclass_list(self):
scope='local', read=True)
async def vm_device_available(self, endpoint):
devclass = endpoint
- devices = self.dest.devices[devclass].available()
+ devices = self.dest.devices[devclass].get_exposed_devices()
if self.arg:
devices = [dev for dev in devices if dev.ident == self.arg]
- # no duplicated devices, but device may not exists, in which case
+ # no duplicated devices, but device may not exist, in which case
# the list is empty
self.enforce(len(devices) <= 1)
devices = self.fire_event_for_filter(devices, devclass=devclass)
-
- dev_info = {}
- for dev in devices:
- non_default_attrs = set(attr for attr in dir(dev) if
- not attr.startswith('_')).difference((
- 'backend_domain', 'ident', 'frontend_domain',
- 'description', 'options', 'regex'))
- properties_txt = ' '.join(
- '{}={!s}'.format(prop, value) for prop, value
- in itertools.chain(
- ((key, getattr(dev, key)) for key in non_default_attrs),
- # keep description as the last one, according to API
- # specification
- (('description', dev.description),)
- ))
- self.enforce('\n' not in properties_txt)
- dev_info[dev.ident] = properties_txt
-
+ dev_info = {dev.ident: dev.serialize().decode() for dev in devices}
return ''.join('{} {}\n'.format(ident, dev_info[ident])
- for ident in sorted(dev_info))
+ for ident in sorted(dev_info))
- @qubes.api.method('admin.vm.device.{endpoint}.List', endpoints=(ep.name
+ @qubes.api.method('admin.vm.device.{endpoint}.Assigned', endpoints=(ep.name
for ep in importlib.metadata.entry_points(group='qubes.devices')),
no_payload=True,
scope='local', read=True)
async def vm_device_list(self, endpoint):
devclass = endpoint
- device_assignments = self.dest.devices[devclass].assignments()
+ device_assignments = list(
+ self.dest.devices[devclass].get_assigned_devices())
if self.arg:
select_backend, select_ident = self.arg.split('+', 1)
device_assignments = [dev for dev in device_assignments
if (str(dev.backend_domain), dev.ident)
== (select_backend, select_ident)]
- # no duplicated devices, but device may not exists, in which case
+ # no duplicated devices, but device may not exist, in which case
# the list is empty
self.enforce(len(device_assignments) <= 1)
- device_assignments = self.fire_event_for_filter(device_assignments,
- devclass=devclass)
-
- dev_info = {}
- for dev in device_assignments:
- properties_txt = ' '.join(
- '{}={!s}'.format(opt, value) for opt, value
- in itertools.chain(
- dev.options.items(),
- (('persistent', 'yes' if dev.persistent else 'no'),)
- ))
- self.enforce('\n' not in properties_txt)
- ident = '{!s}+{!s}'.format(dev.backend_domain, dev.ident)
- dev_info[ident] = properties_txt
+ device_assignments = self.fire_event_for_filter(
+ device_assignments, devclass=devclass)
+
+ dev_info = {
+ f'{assignment.backend_domain}+{assignment.ident}':
+ assignment.serialize().decode('ascii', errors="ignore")
+ for assignment in device_assignments}
return ''.join('{} {}\n'.format(ident, dev_info[ident])
for ident in sorted(dev_info))
- # Attach/Detach action can both modify persistent state (with
- # persistent=True) and volatile state of running VM (with persistent=False).
- # For this reason, write=True + execute=True
- @qubes.api.method('admin.vm.device.{endpoint}.Attach', endpoints=(ep.name
+ @qubes.api.method(
+ 'admin.vm.device.{endpoint}.Attached',
+ endpoints=(
+ ep.name
+ for ep in importlib.metadata.entry_points(group='qubes.devices')),
+ no_payload=True, scope='local', read=True)
+ async def vm_device_attached(self, endpoint):
+ devclass = endpoint
+ device_assignments = self.dest.devices[devclass].get_attached_devices()
+ if self.arg:
+ select_backend, select_ident = self.arg.split('+', 1)
+ device_assignments = [dev for dev in device_assignments
+ if (str(dev.backend_domain), dev.ident)
+ == (select_backend, select_ident)]
+ # no duplicated devices, but device may not exist, in which case
+ # the list is empty
+ self.enforce(len(device_assignments) <= 1)
+ device_assignments = self.fire_event_for_filter(device_assignments,
+ devclass=devclass)
+
+ dev_info = {
+ f'{assignment.backend_domain}+{assignment.ident}':
+ assignment.serialize().decode('ascii', errors="ignore")
+ for assignment in device_assignments}
+
+ return ''.join('{} {}\n'.format(ident, dev_info[ident])
+ for ident in sorted(dev_info))
+
+ # Assign/Unassign action can modify only persistent state of running VM.
+ # For this reason, write=True
+ @qubes.api.method('admin.vm.device.{endpoint}.Assign', endpoints=(ep.name
+ for ep in importlib.metadata.entry_points(group='qubes.devices')),
+ scope='local', write=True)
+ async def vm_device_assign(self, endpoint, untrusted_payload):
+ devclass = endpoint
+
+ # qrexec already verified that no strange characters are in self.arg
+ backend_domain, ident = self.arg.split('+', 1)
+ # may raise KeyError, either on domain or ident
+ dev = self.app.domains[backend_domain].devices[devclass][ident]
+
+ assignment = qubes.device_protocol.DeviceAssignment.deserialize(
+ untrusted_payload, expected_device=dev
+ )
+
+ self.fire_event_for_permission(
+ device=dev, devclass=devclass,
+ required=assignment.required,
+ attach_automatically=assignment.attach_automatically,
+ options=assignment.options
+ )
+
+ await self.dest.devices[devclass].assign(assignment)
+ self.app.save()
+
+ # Assign/Unassign action can modify only persistent state of running VM.
+ # For this reason, write=True
+ @qubes.api.method(
+ 'admin.vm.device.{endpoint}.Unassign',
+ endpoints=(
+ ep.name
for ep in importlib.metadata.entry_points(group='qubes.devices')),
- scope='local', write=True, execute=True)
+ no_payload=True, scope='local', write=True)
+ async def vm_device_unassign(self, endpoint):
+ devclass = endpoint
+
+ # qrexec already verified that no strange characters are in self.arg
+ backend_domain, ident = self.arg.split('+', 1)
+ # may raise KeyError; if a device isn't found, it will be UnknownDevice
+ # instance - but allow it, otherwise it will be impossible to unassign
+ # an already removed device
+ dev = self.app.domains[backend_domain].devices[devclass][ident]
+
+ self.fire_event_for_permission(device=dev, devclass=devclass)
+
+ assignment = qubes.device_protocol.DeviceAssignment(
+ dev.backend_domain, dev.ident, devclass)
+ await self.dest.devices[devclass].unassign(assignment)
+ self.app.save()
+
+ # Attach/Detach action can modify only volatile state of running VM.
+ # For this reason, execute=True
+ @qubes.api.method(
+ 'admin.vm.device.{endpoint}.Attach',
+ endpoints=(
+ ep.name
+ for ep in importlib.metadata.entry_points(group='qubes.devices')),
+ scope='local', execute=True)
async def vm_device_attach(self, endpoint, untrusted_payload):
devclass = endpoint
- options = {}
- persistent = False
- for untrusted_option in untrusted_payload.decode('ascii',
- 'strict').split():
- try:
- untrusted_key, untrusted_value = untrusted_option.split('=', 1)
- except ValueError:
- raise qubes.api.ProtocolError('Invalid options format')
- if untrusted_key == 'persistent':
- persistent = qubes.property.bool(None, None, untrusted_value)
- else:
- allowed_chars_key = string.digits + string.ascii_letters + '-_.'
- allowed_chars_value = allowed_chars_key + ',+:'
- if any(x not in allowed_chars_key for x in untrusted_key):
- raise qubes.api.ProtocolError(
- 'Invalid chars in option name')
- if any(x not in allowed_chars_value for x in untrusted_value):
- raise qubes.api.ProtocolError(
- 'Invalid chars in option value')
- options[untrusted_key] = untrusted_value
# qrexec already verified that no strange characters are in self.arg
backend_domain, ident = self.arg.split('+', 1)
# may raise KeyError, either on domain or ident
dev = self.app.domains[backend_domain].devices[devclass][ident]
- self.fire_event_for_permission(device=dev,
- devclass=devclass, persistent=persistent,
- options=options)
+ assignment = qubes.device_protocol.DeviceAssignment.deserialize(
+ untrusted_payload, expected_device=dev
+ )
+
+ self.fire_event_for_permission(
+ device=dev, devclass=devclass,
+ required=assignment.required,
+ attach_automatically=assignment.attach_automatically,
+ options=assignment.options
+ )
- assignment = qubes.devices.DeviceAssignment(
- dev.backend_domain, dev.ident,
- options=options, persistent=persistent)
await self.dest.devices[devclass].attach(assignment)
- self.app.save()
- # Attach/Detach action can both modify persistent state (with
- # persistent=True) and volatile state of running VM (with persistent=False).
- # For this reason, write=True + execute=True
- @qubes.api.method('admin.vm.device.{endpoint}.Detach', endpoints=(ep.name
+ # Attach/Detach action can modify only a volatile state of running VM.
+ # For this reason, execute=True
+ @qubes.api.method(
+ 'admin.vm.device.{endpoint}.Detach',
+ endpoints=(
+ ep.name
for ep in importlib.metadata.entry_points(group='qubes.devices')),
- no_payload=True,
- scope='local', write=True, execute=True)
+ no_payload=True, scope='local', execute=True)
async def vm_device_detach(self, endpoint):
devclass = endpoint
@@ -1319,41 +1364,42 @@ async def vm_device_detach(self, endpoint):
# already removed device
dev = self.app.domains[backend_domain].devices[devclass][ident]
- self.fire_event_for_permission(device=dev,
- devclass=devclass)
+ self.fire_event_for_permission(device=dev, devclass=devclass)
- assignment = qubes.devices.DeviceAssignment(
- dev.backend_domain, dev.ident)
+ assignment = qubes.device_protocol.DeviceAssignment(
+ dev.backend_domain, dev.ident, devclass)
await self.dest.devices[devclass].detach(assignment)
- self.app.save()
- # Attach/Detach action can both modify persistent state (with
- # persistent=True) and volatile state of running VM (with persistent=False).
- # For this reason, write=True + execute=True
- @qubes.api.method('admin.vm.device.{endpoint}.Set.persistent',
+ # Assign/Unassign action can modify only a persistent state of running VM.
+ # For this reason, write=True
+ @qubes.api.method('admin.vm.device.{endpoint}.Set.required',
endpoints=(ep.name
for ep in importlib.metadata.entry_points(group='qubes.devices')),
- scope='local', write=True, execute=True)
- async def vm_device_set_persistent(self, endpoint, untrusted_payload):
+ scope='local', write=True)
+ async def vm_device_set_required(self, endpoint, untrusted_payload):
+ """
+ Update `required` flag of an already assigned device.
+
+ Payload:
+ `False` -> device will be auto-attached to qube
+ `True` -> device is required to start qube
+ """
devclass = endpoint
self.enforce(untrusted_payload in (b'True', b'False'))
- persistent = untrusted_payload == b'True'
+ # now is safe to eval, since the value of untrusted_payload is trusted
+ # pylint: disable=eval-used
+ assignment = eval(untrusted_payload)
del untrusted_payload
# qrexec already verified that no strange characters are in self.arg
- backend_domain, ident = self.arg.split('+', 1)
- # device must be already attached
- matching_devices = [dev for dev
- in self.dest.devices[devclass].attached()
- if dev.backend_domain.name == backend_domain and dev.ident == ident]
- self.enforce(len(matching_devices) == 1)
- dev = matching_devices[0]
+ backend_domain_name, ident = self.arg.split('+', 1)
+ backend_domain = self.app.domains[backend_domain_name]
+ dev = Device(backend_domain, ident, devclass)
- self.fire_event_for_permission(device=dev,
- persistent=persistent)
+ self.fire_event_for_permission(device=dev, assignment=assignment)
- self.dest.devices[devclass].update_persistent(dev, persistent)
+ await self.dest.devices[devclass].update_required(dev, assignment)
self.app.save()
@qubes.api.method('admin.vm.firewall.Get', no_payload=True,
@@ -1497,7 +1543,7 @@ async def backup_execute(self):
profile_path = os.path.join(qubes.config.backup_profile_dir,
self.arg + '.conf')
if not os.path.exists(profile_path):
- raise qubes.api.PermissionDenied(
+ raise qubes.exc.PermissionDenied(
'Backup profile {} does not exist'.format(self.arg))
if not hasattr(self.app, 'api_admin_running_backups'):
@@ -1549,7 +1595,7 @@ async def backup_info(self):
profile_path = os.path.join(qubes.config.backup_profile_dir,
self.arg + '.conf')
if not os.path.exists(profile_path):
- raise qubes.api.PermissionDenied(
+ raise qubes.exc.PermissionDenied(
'Backup profile {} does not exist'.format(self.arg))
backup = await self._load_backup_profile(self.arg,
diff --git a/qubes/api/misc.py b/qubes/api/misc.py
index 85591796c..ebc2ff955 100644
--- a/qubes/api/misc.py
+++ b/qubes/api/misc.py
@@ -18,8 +18,8 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, see .
-''' Interface for methods not being part of Admin API, but still handled by
-qubesd. '''
+""" Interface for methods not being part of Admin API, but still handled by
+qubesd. """
import string
from datetime import datetime
@@ -34,7 +34,7 @@ class QubesMiscAPI(qubes.api.AbstractQubesAPI):
@qubes.api.method('qubes.FeaturesRequest', no_payload=True)
async def qubes_features_request(self):
- ''' qubes.FeaturesRequest handler
+ """ qubes.FeaturesRequest handler
VM (mostly templates) can request some features from dom0 for itself.
Then dom0 (qubesd extension) may respect this request or ignore it.
@@ -44,7 +44,7 @@ async def qubes_features_request(self):
dispatch 'features-request' event, which may be handled by
appropriate extensions. Requests not explicitly handled by some
extension are ignored.
- '''
+ """
self.enforce(self.dest.name == 'dom0')
self.enforce(not self.arg)
@@ -66,9 +66,9 @@ async def qubes_features_request(self):
@qubes.api.method('qubes.NotifyTools', no_payload=True)
async def qubes_notify_tools(self):
- '''
+ """
Legacy version of qubes.FeaturesRequest, used by Qubes Windows Tools
- '''
+ """
self.enforce(self.dest.name == 'dom0')
self.enforce(not self.arg)
@@ -92,12 +92,12 @@ async def qubes_notify_tools(self):
@qubes.api.method('qubes.NotifyUpdates')
async def qubes_notify_updates(self, untrusted_payload):
- '''
+ """
Receive VM notification about updates availability
Payload contains a single integer - either 0 (no updates) or some
positive value (some updates).
- '''
+ """
untrusted_update_count = untrusted_payload.strip()
self.enforce(untrusted_update_count.isdigit())
diff --git a/qubes/app.py b/qubes/app.py
index f85b4848e..df0b98071 100644
--- a/qubes/app.py
+++ b/qubes/app.py
@@ -1052,7 +1052,7 @@ def load(self, lock=False):
vm.fire_event('domain-load')
# get a file timestamp (before closing it - still holding the lock!),
- # to detect whether anyone else have modified it in the meantime
+ # to detect whether anyone else has modified it in the meantime
self.__load_timestamp = os.path.getmtime(self._store)
if not lock:
@@ -1509,8 +1509,7 @@ def on_domain_pre_deleted(self, event, vm):
assignment.ident for assignment in assignments)
raise qubes.exc.QubesVMInUseError(
vm,
- 'VM has devices attached persistently to other VMs: ' +
- desc)
+ 'VM has devices assigned to other VMs: ' + desc)
@qubes.events.handler('domain-delete')
def on_domain_deleted(self, event, vm):
@@ -1564,7 +1563,7 @@ def on_property_set_default_netvm(self, event, name, newvalue,
if hasattr(vm, 'provides_network') and not vm.provides_network and \
hasattr(vm, 'netvm') and vm.property_is_default('netvm'):
# fire property-reset:netvm as it is responsible for resetting
- # netvm to it's default value
+ # netvm to its default value
vm.fire_event('property-reset:netvm',
name='netvm', oldvalue=oldvalue)
@@ -1576,6 +1575,6 @@ def on_property_set_default_dispvm(self, event, name, newvalue,
if hasattr(vm, 'default_dispvm') and \
vm.property_is_default('default_dispvm'):
# fire property-reset:default_dispvm as it is responsible for
- # resetting dispvm to it's default value
+ # resetting dispvm to its default value
vm.fire_event('property-reset:default_dispvm',
name='default_dispvm', oldvalue=oldvalue)
diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py
new file mode 100644
index 000000000..3d8b9aaa6
--- /dev/null
+++ b/qubes/device_protocol.py
@@ -0,0 +1,952 @@
+# -*- encoding: utf8 -*-
+#
+# The Qubes OS Project, http://www.qubes-os.org
+#
+# Copyright (C) 2010-2016 Joanna Rutkowska
+# Copyright (C) 2015-2016 Wojtek Porczyk
+# Copyright (C) 2016 Bahtiar `kalkin-` Gadimov
+# Copyright (C) 2017 Marek Marczykowski-Górecki
+#
+# Copyright (C) 2024 Piotr Bartman-Szwarc
+#
+#
+# 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 .
+
+"""
+Common part of device API.
+
+The same in `qubes-core-admin` and `qubes-core-admin-client`,
+should be moved to one place.
+"""
+import string
+import sys
+from enum import Enum
+from typing import Optional, Dict, Any, List, Union, Tuple
+
+import qubes.utils
+
+from qubes.exc import ProtocolError
+
+QubesVM = 'qubes.vm.BaseVM'
+
+
+class UnexpectedDeviceProperty(qubes.exc.QubesException, ValueError):
+ """
+ Device has unexpected property such as backend_domain, devclass etc.
+ """
+
+
+def qbool(value):
+ return qubes.property.bool(None, None, value)
+
+
+class Device:
+ """
+ Basic class of a *bus* device with *ident* exposed by a *backend domain*.
+
+ Attributes:
+ backend_domain (QubesVM): The domain which exposes devices,
+ e.g.`sys-usb`.
+ ident (str): A unique identifier for the device within
+ the backend domain.
+ devclass (str, optional): The class of the device (e.g., 'usb', 'pci').
+ """
+ ALLOWED_CHARS_KEY = set(
+ string.digits + string.ascii_letters
+ + r"!#$%&()*+,-./:;<>?@[\]^_{|}~")
+ ALLOWED_CHARS_PARAM = ALLOWED_CHARS_KEY.union(set(string.punctuation + ' '))
+
+ def __init__(self, backend_domain, ident, devclass=None):
+ self.__backend_domain = backend_domain
+ self.__ident = ident
+ self.__bus = devclass
+
+ def __hash__(self):
+ return hash((str(self.backend_domain), self.ident))
+
+ def __eq__(self, other):
+ if isinstance(other, Device):
+ return (
+ self.backend_domain == other.backend_domain and
+ self.ident == other.ident
+ )
+ raise TypeError(f"Comparing instances of 'Device' and '{type(other)}' "
+ "is not supported")
+
+ def __lt__(self, other):
+ if isinstance(other, Device):
+ return (self.backend_domain.name, self.ident) < \
+ (other.backend_domain.name, other.ident)
+ raise TypeError(f"Comparing instances of 'Device' and '{type(other)}' "
+ "is not supported")
+
+ def __repr__(self):
+ return "[%s]:%s" % (self.backend_domain, self.ident)
+
+ def __str__(self):
+ return '{!s}:{!s}'.format(self.backend_domain, self.ident)
+
+ @property
+ def ident(self) -> str:
+ """
+ Immutable device identifier.
+
+ Unique for given domain and device type.
+ """
+ return self.__ident
+
+ @property
+ def backend_domain(self) -> QubesVM:
+ """ Which domain provides this device. (immutable)"""
+ return self.__backend_domain
+
+ @property
+ def devclass(self) -> str:
+ """ Immutable* Device class such like: 'usb', 'pci' etc.
+
+ For unknown devices "peripheral" is returned.
+
+ *see `@devclass.setter`
+ """
+ if self.__bus:
+ return self.__bus
+ return "peripheral"
+
+ @property
+ def devclass_is_set(self) -> bool:
+ """
+ Returns true if devclass is already initialised.
+ """
+ return bool(self.__bus)
+
+ @devclass.setter
+ def devclass(self, devclass: str):
+ """ Once a value is set, it should not be overridden.
+
+ However, if it has not been set, i.e., the value is `None`,
+ we can override it."""
+ if self.__bus is not None:
+ raise TypeError("Attribute devclass is immutable")
+ self.__bus = devclass
+
+ @classmethod
+ def unpack_properties(
+ cls, untrusted_serialization: bytes
+ ) -> Tuple[Dict, Dict]:
+ """
+ Unpacks basic device properties from a serialized encoded string.
+
+ Returns:
+ tuple: A tuple containing two dictionaries, properties and options,
+ extracted from the serialization.
+
+ Raises:
+ ValueError: If unexpected characters are found in property
+ names or values.
+ """
+ ut_decoded = untrusted_serialization.decode(
+ 'ascii', errors='strict').strip()
+
+ properties = {}
+ options = {}
+
+ if not ut_decoded:
+ return properties, options
+
+ keys = []
+ values = []
+ ut_key, _, ut_rest = ut_decoded.partition("='")
+
+ key = sanitize_str(
+ ut_key, cls.ALLOWED_CHARS_KEY,
+ error_message='Invalid chars in property name: ')
+ keys.append(key)
+ while "='" in ut_rest:
+ ut_value_key, _, ut_rest = ut_rest.partition("='")
+ ut_value, _, ut_key = ut_value_key.rpartition("' ")
+ value = sanitize_str(
+ deserialize_str(ut_value), cls.ALLOWED_CHARS_PARAM,
+ error_message='Invalid chars in property value: ')
+ values.append(value)
+ key = sanitize_str(
+ ut_key, cls.ALLOWED_CHARS_KEY,
+ error_message='Invalid chars in property name: ')
+ keys.append(key)
+ ut_value = ut_rest[:-1] # ending '
+ value = sanitize_str(
+ deserialize_str(ut_value), cls.ALLOWED_CHARS_PARAM,
+ error_message='Invalid chars in property value: ')
+ values.append(value)
+
+ for key, value in zip(keys, values):
+ if key.startswith("_"):
+ # it's handled in cls.__init__
+ options[key[1:]] = value
+ else:
+ properties[key] = value
+
+ return properties, options
+
+ @classmethod
+ def pack_property(cls, key: str, value: str):
+ """
+ Add property `key=value` to serialization.
+ """
+ key = sanitize_str(
+ key, cls.ALLOWED_CHARS_KEY,
+ error_message='Invalid chars in property name: ')
+ value = sanitize_str(
+ serialize_str(value), cls.ALLOWED_CHARS_PARAM,
+ error_message='Invalid chars in property value: ')
+ return key.encode('ascii') + b'=' + value.encode('ascii')
+
+ @staticmethod
+ def check_device_properties(
+ expected_device: 'Device', properties: Dict[str, Any]):
+ """
+ Validates properties against an expected device configuration.
+
+ Modifies `properties`.
+
+ Raises:
+ UnexpectedDeviceProperty: If any property does not match
+ the expected values.
+ """
+ expected = expected_device
+ exp_vm_name = expected.backend_domain.name
+ if properties.get('backend_domain', exp_vm_name) != exp_vm_name:
+ raise UnexpectedDeviceProperty(
+ f"Got device exposed by {properties['backend_domain']}"
+ f"when expected devices from {exp_vm_name}.")
+ properties['backend_domain'] = expected.backend_domain
+
+ if properties.get('ident', expected.ident) != expected.ident:
+ raise UnexpectedDeviceProperty(
+ f"Got device with id: {properties['ident']} "
+ f"when expected id: {expected.ident}.")
+ properties['ident'] = expected.ident
+
+ if expected.devclass_is_set:
+ if (properties.get('devclass', expected.devclass)
+ != expected.devclass):
+ raise UnexpectedDeviceProperty(
+ f"Got {properties['devclass']} device "
+ f"when expected {expected.devclass}.")
+ properties['devclass'] = expected.devclass
+
+
+class DeviceCategory(Enum):
+ """
+ Category of a peripheral device.
+
+ Arbitrarily selected interfaces that are important to users,
+ thus deserving special recognition such as a custom icon, etc.
+ """
+ # pylint: disable=invalid-name
+ Other = "*******"
+
+ Communication = ("u02****", "p07****") # eg. modems
+ Input = ("u03****", "p09****") # HID etc.
+ Keyboard = ("u03**01", "p0900**")
+ Mouse = ("u03**02", "p0902**")
+ Printer = ("u07****",)
+ Scanner = ("p0903**",)
+ # Multimedia = Audio, Video, Displays etc.
+ Microphone = ("m******",)
+ Multimedia = ("u01****", "u0e****", "u06****", "u10****", "p03****",
+ "p04****")
+ Wireless = ("ue0****", "p0d****")
+ Bluetooth = ("ue00101", "p0d11**")
+ Mass_Data = ("b******", "u08****", "p01****")
+ Network = ("p02****",)
+ Memory = ("p05****",)
+ PCI_Bridge = ("p06****",)
+ Docking_Station = ("p0a****",)
+ Processor = ("p0b****", "p40****")
+ PCI_Serial_Bus = ("p0c****",)
+ PCI_USB = ("p0c03**",)
+
+ @staticmethod
+ def from_str(interface_encoding: str) -> 'DeviceCategory':
+ """
+ Returns `DeviceCategory` from data encoded in string.
+ """
+ result = DeviceCategory.Other
+ if len(interface_encoding) != len(DeviceCategory.Other.value):
+ return result
+ best_score = 0
+
+ for interface in DeviceCategory:
+ for pattern in interface.value:
+ score = 0
+ for itf, pat in zip(interface_encoding, pattern):
+ if itf == pat:
+ score += 1
+ elif pat != "*":
+ score = -1 # inconsistent with a pattern
+ break
+
+ if score > best_score:
+ best_score = score
+ result = interface
+
+ return result
+
+
+class DeviceInterface:
+ """
+ Peripheral device interface wrapper.
+ """
+
+ def __init__(self, interface_encoding: str, devclass: Optional[str] = None):
+ ifc_padded = interface_encoding.ljust(6, '*')
+ if devclass:
+ if len(ifc_padded) > 6:
+ print(
+ f"{interface_encoding=} is too long "
+ f"(is {len(interface_encoding)}, expected max. 6) "
+ f"for given {devclass=}",
+ file=sys.stderr
+ )
+ ifc_full = devclass[0] + ifc_padded
+ else:
+ known_devclasses = {
+ 'p': 'pci', 'u': 'usb', 'b': 'block', 'm': 'mic'}
+ devclass = known_devclasses.get(interface_encoding[0], None)
+ if len(ifc_padded) > 7:
+ print(
+ f"{interface_encoding=} is too long "
+ f"(is {len(interface_encoding)}, expected max. 7)",
+ file=sys.stderr
+ )
+ ifc_full = ifc_padded
+ elif len(ifc_padded) == 6:
+ ifc_full = '?' + ifc_padded
+ else:
+ ifc_full = ifc_padded
+
+ self._devclass = devclass
+ self._interface_encoding = ifc_full
+ self._category = DeviceCategory.from_str(self._interface_encoding)
+
+ @property
+ def devclass(self) -> Optional[str]:
+ """ Immutable Device class such like: 'usb', 'pci' etc. """
+ return self._devclass
+
+ @property
+ def category(self) -> DeviceCategory:
+ """ Immutable Device category such like: 'Mouse', 'Mass_Data' etc. """
+ return self._category
+
+ @classmethod
+ def unknown(cls) -> 'DeviceInterface':
+ """ Value for unknown device interface. """
+ return cls("?******")
+
+ def __repr__(self):
+ return self._interface_encoding
+
+ def __hash__(self):
+ return hash(repr(self))
+
+ def __eq__(self, other):
+ if not isinstance(other, DeviceInterface):
+ return False
+ return repr(self) == repr(other)
+
+ def __str__(self):
+ if self.devclass == "block":
+ return "Block device"
+ if self.devclass in ("usb", "pci"):
+ # try subclass first as in `lspci`
+ result = self._load_classes(self.devclass).get(
+ self._interface_encoding[1:-2] + '**', None)
+ if (result is None or result.lower()
+ in ('none', 'no subclass', 'unused', 'undefined')):
+ # if not, try interface
+ result = self._load_classes(self.devclass).get(
+ self._interface_encoding[1:], None)
+ if (result is None or result.lower()
+ in ('none', 'no subclass', 'unused', 'undefined')):
+ # if not, try class
+ result = self._load_classes(self.devclass).get(
+ self._interface_encoding[1:-4] + '****', None)
+ if result is None:
+ result = f"Unclassified {self.devclass} device"
+ return result
+ if self.devclass == 'mic':
+ return "Microphone"
+ return repr(self)
+
+ @staticmethod
+ def _load_classes(bus: str):
+ """
+ List of known device classes, subclasses and programming interfaces.
+ """
+ # Syntax:
+ # C class class_name
+ # subclass subclass_name <-- single tab
+ # prog-if prog-if_name <-- two tabs
+ result = {}
+ with open(f'/usr/share/hwdata/{bus}.ids',
+ encoding='utf-8', errors='ignore') as pciids:
+ # for `class_name` and `subclass_name`
+ # pylint: disable=used-before-assignment
+ class_id = None
+ subclass_id = None
+ for line in pciids.readlines():
+ line = line.rstrip()
+ if line.startswith('\t\t') \
+ and class_id is not None and subclass_id is not None:
+ (progif_id, _, progif_name) = line[2:].split(' ', 2)
+ result[class_id + subclass_id + progif_id] = \
+ progif_name
+ elif line.startswith('\t') and class_id:
+ (subclass_id, _, subclass_name) = line[1:].split(' ', 2)
+ # store both prog-if specific entry and generic one
+ result[class_id + subclass_id + '**'] = \
+ subclass_name
+ elif line.startswith('C '):
+ (_, class_id, _, class_name) = line.split(' ', 3)
+ result[class_id + '****'] = class_name
+ subclass_id = None
+
+ return result
+
+
+class DeviceInfo(Device):
+ """ Holds all information about a device """
+
+ def __init__(
+ self,
+ backend_domain: QubesVM,
+ ident: str,
+ devclass: Optional[str] = None,
+ vendor: Optional[str] = None,
+ product: Optional[str] = None,
+ manufacturer: Optional[str] = None,
+ name: Optional[str] = None,
+ serial: Optional[str] = None,
+ interfaces: Optional[List[DeviceInterface]] = None,
+ parent: Optional[Device] = None,
+ attachment: Optional[QubesVM] = None,
+ self_identity: Optional[str] = None,
+ **kwargs
+ ):
+ super().__init__(backend_domain, ident, devclass)
+
+ self._vendor = vendor
+ self._product = product
+ self._manufacturer = manufacturer
+ self._name = name
+ self._serial = serial
+ self._interfaces = interfaces
+ self._parent = parent
+ self._attachment = attachment
+ self._self_identity = self_identity
+
+ self.data = kwargs
+
+ @property
+ def vendor(self) -> str:
+ """
+ Device vendor name from local database.
+
+ Could be empty string or "unknown".
+
+ Override this method to return proper name from `/usr/share/hwdata/*`.
+ """
+ if not self._vendor:
+ return "unknown"
+ return self._vendor
+
+ @property
+ def product(self) -> str:
+ """
+ Device name from local database.
+
+ Could be empty string or "unknown".
+
+ Override this method to return proper name from `/usr/share/hwdata/*`.
+ """
+ if not self._product:
+ return "unknown"
+ return self._product
+
+ @property
+ def manufacturer(self) -> str:
+ """
+ The name of the manufacturer of the device introduced by device itself.
+
+ Could be empty string or "unknown".
+
+ Override this method to return proper name directly from device itself.
+ """
+ if not self._manufacturer:
+ return "unknown"
+ return self._manufacturer
+
+ @property
+ def name(self) -> str:
+ """
+ The name of the device it introduced itself with.
+
+ Could be empty string or "unknown".
+
+ Override this method to return proper name directly from device itself.
+ """
+ if not self._name:
+ return "unknown"
+ return self._name
+
+ @property
+ def serial(self) -> str:
+ """
+ The serial number of the device it introduced itself with.
+
+ Could be empty string or "unknown".
+
+ Override this method to return proper name directly from device itself.
+ """
+ if not self._serial:
+ return "unknown"
+ return self._serial
+
+ @property
+ def description(self) -> str:
+ """
+ Short human-readable description.
+
+ For unknown device returns `unknown device (unknown vendor)`.
+ For unknown USB device returns `unknown usb device (unknown vendor)`.
+ For unknown USB device with known serial number returns
+ ` (unknown vendor)`.
+ """
+ if self.product and self.product != "unknown":
+ prod = self.product
+ elif self.name and self.name != "unknown":
+ prod = self.name
+ elif self.serial and self.serial != "unknown":
+ prod = self.serial
+ elif self.parent_device is not None:
+ return f"sub-device of {self.parent_device}"
+ else:
+ prod = f"unknown {self.devclass if self.devclass else ''} device"
+
+ if self.vendor and self.vendor != "unknown":
+ vendor = self.vendor
+ elif self.manufacturer and self.manufacturer != "unknown":
+ vendor = self.manufacturer
+ else:
+ vendor = "unknown vendor"
+
+ main_interface = str(self.interfaces[0])
+ return f"{main_interface}: {vendor} {prod}"
+
+ @property
+ def interfaces(self) -> List[DeviceInterface]:
+ """
+ Non-empty list of device interfaces.
+
+ Every device should have at least one interface.
+ """
+ if not self._interfaces:
+ return [DeviceInterface.unknown()]
+ return self._interfaces
+
+ @property
+ def parent_device(self) -> Optional[Device]:
+ """
+ The parent device, if any.
+
+ If the device is part of another device (e.g., it's a single
+ partition of a USB stick), the parent device id should be here.
+ """
+ return self._parent
+
+ @property
+ def subdevices(self) -> List['DeviceInfo']:
+ """
+ The list of children devices if any.
+
+ If the device has subdevices (e.g., partitions of a USB stick),
+ the subdevices id should be here.
+ """
+ return [dev for dev in self.backend_domain.devices[self.devclass]
+ if dev.parent_device.ident == self.ident]
+
+ @property
+ def attachment(self) -> Optional[QubesVM]:
+ """
+ VM to which device is attached (frontend domain).
+ """
+ return self._attachment
+
+ def serialize(self) -> bytes:
+ """
+ Serialize an object to be transmitted via Qubes API.
+ """
+ # 'backend_domain', 'attachment', 'interfaces', 'data', 'parent_device'
+ # are not string, so they need special treatment
+ default_attrs = {
+ 'ident', 'devclass', 'vendor', 'product', 'manufacturer', 'name',
+ 'serial', 'self_identity'}
+ properties = b' '.join(
+ self.pack_property(key, value)
+ for key, value in ((key, getattr(self, key))
+ for key in default_attrs))
+
+ properties += b' ' + self.pack_property(
+ 'backend_domain', self.backend_domain.name)
+
+ if self.attachment:
+ properties = self.pack_property(
+ 'attachment', self.attachment.name)
+
+ properties += b' ' + self.pack_property(
+ 'interfaces',
+ ''.join(repr(ifc) for ifc in self.interfaces))
+
+ if self.parent_device is not None:
+ properties += b' ' + self.pack_property(
+ 'parent_ident', self.parent_device.ident)
+ properties += b' ' + self.pack_property(
+ 'parent_devclass', self.parent_device.devclass)
+
+ for key, value in self.data.items():
+ properties += b' ' + self.pack_property("_" + key, value)
+
+ return properties
+
+ @classmethod
+ def deserialize(
+ cls,
+ serialization: bytes,
+ expected_backend_domain: QubesVM,
+ expected_devclass: Optional[str] = None,
+ ) -> 'DeviceInfo':
+ """
+ Recovers a serialized object, see: :py:meth:`serialize`.
+ """
+ ident, _, rest = serialization.partition(b' ')
+ ident = ident.decode('ascii', errors='ignore')
+ device = UnknownDevice(
+ backend_domain=expected_backend_domain,
+ ident=ident,
+ devclass=expected_devclass,
+ )
+
+ try:
+ device = cls._deserialize(rest, device)
+ # pylint: disable=broad-exception-caught
+ except Exception as exc:
+ print(exc, file=sys.stderr)
+
+ return device
+
+ @classmethod
+ def _deserialize(
+ cls,
+ untrusted_serialization: bytes,
+ expected_device: Device
+ ) -> 'DeviceInfo':
+ """
+ Actually deserializes the object.
+ """
+ properties, options = cls.unpack_properties(untrusted_serialization)
+ properties.update(options)
+
+ cls.check_device_properties(expected_device, properties)
+
+ if 'attachment' not in properties or not properties['attachment']:
+ properties['attachment'] = None
+ else:
+ app = expected_device.backend_domain.app
+ properties['attachment'] = app.domains.get_blind(
+ properties['attachment'])
+
+ if (expected_device.devclass_is_set
+ and properties['devclass'] != expected_device.devclass):
+ raise UnexpectedDeviceProperty(
+ f"Got {properties['devclass']} device "
+ f"when expected {expected_device.devclass}.")
+
+ if 'interfaces' in properties:
+ interfaces = properties['interfaces']
+ interfaces = [
+ DeviceInterface(interfaces[i:i + 7])
+ for i in range(0, len(interfaces), 7)]
+ properties['interfaces'] = interfaces
+
+ if 'parent_ident' in properties:
+ properties['parent'] = Device(
+ backend_domain=expected_device.backend_domain,
+ ident=properties['parent_ident'],
+ devclass=properties['parent_devclass'],
+ )
+ del properties['parent_ident']
+ del properties['parent_devclass']
+
+ return cls(**properties)
+
+ @property
+ def self_identity(self) -> str:
+ """
+ Get additional identification of device presented by device itself.
+
+ For pci/usb we expect:
+ :::
+ For block devices:
+ :
+
+ In addition to the description returns presented interfaces.
+ It is used to auto-attach usb devices, so an attacking device needs to
+ mimic not only a name, but also interfaces of trusted device (and have
+ to be plugged to the same port). For a common user it is all the data
+ she uses to recognize the device.
+ """
+ if not self._self_identity:
+ return "0000:0000::?******"
+ return self._self_identity
+
+
+def serialize_str(value: str):
+ """
+ Serialize python string to ensure consistency.
+ """
+ return "'" + str(value).replace("'", r"\'") + "'"
+
+
+def deserialize_str(value: str):
+ """
+ Deserialize python string to ensure consistency.
+ """
+ return value.replace(r"\'", "'")
+
+
+def sanitize_str(
+ untrusted_value: str,
+ allowed_chars: set,
+ replace_char: str = None,
+ error_message: str = ""
+) -> str:
+ """
+ Sanitize given untrusted string.
+
+ If `replace_char` is not None, ignore `error_message` and replace invalid
+ characters with the string.
+ """
+ if replace_char is None:
+ not_allowed_chars = set(untrusted_value) - allowed_chars
+ if not_allowed_chars:
+ raise ProtocolError(error_message + repr(not_allowed_chars))
+ return untrusted_value
+ result = ""
+ for char in untrusted_value:
+ if char in allowed_chars:
+ result += char
+ else:
+ result += replace_char
+ return result
+
+
+class UnknownDevice(DeviceInfo):
+ # pylint: disable=too-few-public-methods
+ """Unknown device - for example, exposed by domain not running currently"""
+
+ def __init__(self, backend_domain, ident, *, devclass, **kwargs):
+ super().__init__(backend_domain, ident, devclass=devclass, **kwargs)
+
+
+class DeviceAssignment(Device):
+ """ Maps a device to a frontend_domain.
+
+ There are 3 flags `attached`, `automatically_attached` and `required`.
+ The meaning of valid combinations is as follows:
+ 1. (True, False, False) -> domain is running, device is manually attached
+ and could be manually detach any time.
+ 2. (True, True, False) -> domain is running, device is attached
+ and could be manually detach any time (see 4.),
+ but in the future will be auto-attached again.
+ 3. (True, True, True) -> domain is running, device is attached
+ and couldn't be detached.
+ 4. (False, Ture, False) -> device is assigned to domain, but not attached
+ because either (i) domain is halted,
+ device (ii) manually detached or
+ (iii) attach to different domain.
+ 5. (False, True, True) -> domain is halted, device assigned to domain
+ and required to start domain.
+ """
+
+ def __init__(self, backend_domain, ident, options=None,
+ frontend_domain=None, devclass=None,
+ required=False, attach_automatically=False):
+ super().__init__(backend_domain, ident, devclass)
+ self.__options = options or {}
+ if required:
+ assert attach_automatically
+ self.__required = required
+ self.__attach_automatically = attach_automatically
+ self.frontend_domain = frontend_domain
+
+ def clone(self, **kwargs):
+ """
+ Clone object and substitute attributes with explicitly given.
+ """
+ attr = {
+ "backend_domain": self.backend_domain,
+ "ident": self.ident,
+ "options": self.options,
+ "required": self.required,
+ "attach_automatically": self.attach_automatically,
+ "frontend_domain": self.frontend_domain,
+ "devclass": self.devclass,
+ }
+ attr.update(kwargs)
+ return self.__class__(**attr)
+
+ @classmethod
+ def from_device(cls, device: Device, **kwargs) -> 'DeviceAssignment':
+ """
+ Get assignment of the device.
+ """
+ return cls(
+ backend_domain=device.backend_domain,
+ ident=device.ident,
+ devclass=device.devclass,
+ **kwargs
+ )
+
+ @property
+ def device(self) -> DeviceInfo:
+ """Get DeviceInfo object corresponding to this DeviceAssignment"""
+ return self.backend_domain.devices[self.devclass][self.ident]
+
+ @property
+ def frontend_domain(self) -> Optional[QubesVM]:
+ """ Which domain the device is attached/assigned to. """
+ return self.__frontend_domain
+
+ @frontend_domain.setter
+ def frontend_domain(
+ self, frontend_domain: Optional[Union[str, QubesVM]]
+ ):
+ """ Which domain the device is attached/assigned to. """
+ if isinstance(frontend_domain, str):
+ frontend_domain = self.backend_domain.app.domains[frontend_domain]
+ self.__frontend_domain = frontend_domain
+
+ @property
+ def attached(self) -> bool:
+ """
+ Is the device attached to the fronted domain?
+
+ Returns False if device is attached to different domain
+ """
+ return self.device.attachment == self.frontend_domain
+
+ @property
+ def required(self) -> bool:
+ """
+ Is the presence of this device required for the domain to start? If yes,
+ it will be attached automatically.
+ """
+ return self.__required
+
+ @required.setter
+ def required(self, required: bool):
+ self.__required = required
+
+ @property
+ def attach_automatically(self) -> bool:
+ """
+ Should this device automatically connect to the frontend domain when
+ available and not connected to other qubes?
+ """
+ return self.__attach_automatically
+
+ @attach_automatically.setter
+ def attach_automatically(self, attach_automatically: bool):
+ self.__attach_automatically = attach_automatically
+
+ @property
+ def options(self) -> Dict[str, Any]:
+ """ Device options (same as in the legacy API). """
+ return self.__options
+
+ @options.setter
+ def options(self, options: Optional[Dict[str, Any]]):
+ """ Device options (same as in the legacy API). """
+ self.__options = options or {}
+
+ def serialize(self) -> bytes:
+ """
+ Serialize an object to be transmitted via Qubes API.
+ """
+ properties = b' '.join(
+ self.pack_property(key, value)
+ for key, value in (
+ ('required', 'yes' if self.required else 'no'),
+ ('attach_automatically',
+ 'yes' if self.attach_automatically else 'no'),
+ ('ident', self.ident),
+ ('devclass', self.devclass)))
+
+ properties += b' ' + self.pack_property(
+ 'backend_domain', self.backend_domain.name)
+
+ if self.frontend_domain is not None:
+ properties += b' ' + self.pack_property(
+ 'frontend_domain', self.frontend_domain.name)
+
+ for key, value in self.options.items():
+ properties += b' ' + self.pack_property("_" + key, value)
+
+ return properties
+
+ @classmethod
+ def deserialize(
+ cls,
+ serialization: bytes,
+ expected_device: Device,
+ ) -> 'DeviceAssignment':
+ """
+ Recovers a serialized object, see: :py:meth:`serialize`.
+ """
+ try:
+ result = cls._deserialize(serialization, expected_device)
+ except Exception as exc:
+ raise ProtocolError() from exc
+ return result
+
+ @classmethod
+ def _deserialize(
+ cls,
+ untrusted_serialization: bytes,
+ expected_device: Device,
+ ) -> 'DeviceAssignment':
+ """
+ Actually deserializes the object.
+ """
+ properties, options = cls.unpack_properties(untrusted_serialization)
+ properties['options'] = options
+
+ cls.check_device_properties(expected_device, properties)
+
+ properties['attach_automatically'] = qbool(
+ properties.get('attach_automatically', 'no'))
+ properties['required'] = qbool(properties.get('required', 'no'))
+
+ return cls(**properties)
diff --git a/qubes/devices.py b/qubes/devices.py
index a277aeeb2..c073ef759 100644
--- a/qubes/devices.py
+++ b/qubes/devices.py
@@ -4,6 +4,8 @@
# Copyright (C) 2010-2016 Joanna Rutkowska
# Copyright (C) 2015-2016 Wojtek Porczyk
# Copyright (C) 2016 Bahtiar `kalkin-` Gadimov
+# Copyright (C) 2024 Piotr Bartman-Szwarc
+#
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -19,11 +21,11 @@
# License along with this library; if not, see .
#
-'''API for various types of devices.
+"""API for various types of devices.
Main concept is that some domain may
expose (potentially multiple) devices, which can be attached to other domains.
-Devices can be of different buses (like 'pci', 'usb', etc). Each device
+Devices can be of different buses (like 'pci', 'usb', etc.). Each device
bus is implemented by an extension.
Devices are identified by pair of (backend domain, `ident`), where `ident` is
@@ -31,8 +33,8 @@
Such extension should:
- provide `qubes.devices` endpoint - a class descendant from
- :py:class:`qubes.devices.DeviceInfo`, designed to hold device description
- (including bus-specific properties)
+ :py:class:`qubes.device_protocol.DeviceInfo`, designed to hold device
+ description (including bus-specific properties)
- handle `device-attach:bus` and `device-detach:bus` events for
performing the attach/detach action; events are fired even when domain isn't
running and extension should be prepared for this; handlers for those events
@@ -41,9 +43,10 @@
domain; it should return a list of appropriate DeviceInfo objects
- handle `device-get:bus` event - get one device object exposed by this
domain of given identifier
- - handle `device-list-attached:class` event - list devices currently attached
+ - handle `device-list-attached:bus` event - list devices currently attached
to this domain
- - fire `device-list-change:class` event when a device list change is detected
+ - fire `device-list-change:bus` and following `device-added:bus` or
+ `device-removed:bus` events when a device list change is detected
(new/removed device)
Note that device-listing event handlers cannot be asynchronous. This for
@@ -54,110 +57,42 @@
Extension may use QubesDB watch API (QubesVM.watch_qdb_path(path), then handle
`domain-qdb-change:path`) to detect changes and fire
`device-list-change:class` event.
-'''
-from typing import Optional
+"""
+import itertools
+from typing import Iterable
+import qubes.exc
import qubes.utils
+from qubes.device_protocol import (Device, DeviceInfo, UnknownDevice,
+ DeviceAssignment)
+
+
+class DeviceNotAssigned(qubes.exc.QubesException, KeyError):
+ """
+ Trying to unassign not assigned device.
+ """
-class DeviceNotAttached(qubes.exc.QubesException, KeyError):
- '''Trying to detach not attached device'''
class DeviceAlreadyAttached(qubes.exc.QubesException, KeyError):
- '''Trying to attach already attached device'''
-
-class DeviceInfo:
- ''' Holds all information about a device '''
- # pylint: disable=too-few-public-methods
- def __init__(self, backend_domain, ident, description=None,
- frontend_domain=None):
- #: domain providing this device
- self.backend_domain = backend_domain
- #: device identifier (unique for given domain and device type)
- self.ident = ident
- # allow redefining those as dynamic properties in subclasses
- try:
- #: human readable description/name of the device
- self.description = description
- except AttributeError:
- pass
- try:
- #: (running) domain to which device is currently attached
- self.frontend_domain = frontend_domain
- except AttributeError:
- pass
-
- if hasattr(self, 'regex'):
- # pylint: disable=no-member
- dev_match = self.regex.match(ident)
- if not dev_match:
- raise ValueError('Invalid device identifier: {!r}'.format(
- ident))
-
- for group in self.regex.groupindex:
- setattr(self, group, dev_match.group(group))
-
- def __hash__(self):
- return hash((self.backend_domain, self.ident))
-
- def __eq__(self, other):
- return (
- self.backend_domain == other.backend_domain and
- self.ident == other.ident
- )
-
- def __lt__(self, other):
- if isinstance(other, DeviceInfo):
- return (self.backend_domain, self.ident) < \
- (other.backend_domain, other.ident)
- return NotImplemented
-
- def __str__(self):
- return '{!s}:{!s}'.format(self.backend_domain, self.ident)
-
-
-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,
- bus=None):
- self.backend_domain = backend_domain
- self.ident = ident
- self.options = options or {}
- self.persistent = persistent
- self.bus = bus
-
- def __repr__(self):
- return "[%s]:%s" % (self.backend_domain, self.ident)
-
- def __hash__(self):
- # it's important to use the same hash as DeviceInfo
- return hash((self.backend_domain, self.ident))
-
- def __eq__(self, other):
- if not isinstance(self, other.__class__):
- return NotImplemented
-
- return self.backend_domain == other.backend_domain \
- and self.ident == other.ident
-
- def clone(self):
- '''Clone object instance'''
- return self.__class__(
- self.backend_domain,
- self.ident,
- self.options,
- self.persistent,
- self.bus,
- )
-
- @property
- def device(self):
- '''Get DeviceInfo object corresponding to this DeviceAssignment'''
- return self.backend_domain.devices[self.bus][self.ident]
+ """
+ Trying to attach already attached device.
+ """
+
+
+class DeviceAlreadyAssigned(qubes.exc.QubesException, KeyError):
+ """
+ Trying to assign already assigned device.
+ """
+
+
+class UnrecognizedDevice(qubes.exc.QubesException, ValueError):
+ """
+ Device identity is not as expected.
+ """
class DeviceCollection:
- '''Bag for devices.
+ """Bag for devices.
Used as default value for :py:meth:`DeviceManager.__missing__` factory.
@@ -166,6 +101,14 @@ class DeviceCollection:
This class emits following events on VM object:
+ .. event:: device-added: (device)
+
+ Fired when new device is discovered.
+
+ .. event:: device-removed: (device)
+
+ Fired when device is no longer exposed by a backend VM.
+
.. event:: device-attach: (device, options)
Fired when device is attached to a VM.
@@ -199,6 +142,23 @@ class DeviceCollection:
:param device: :py:class:`DeviceInfo` object to be attached
+ .. event:: device-assign: (device, options)
+
+ Fired when device is assigned to a VM.
+
+ Handler for this event may be asynchronous.
+
+ :param device: :py:class:`DeviceInfo` object to be assigned
+ :param options: :py:class:`dict` of assignment options
+
+ .. event:: device-unassign: (device)
+
+ Fired when device is unassigned from a VM.
+
+ Handler for this event can be asynchronous (a coroutine).
+
+ :param device: :py:class:`DeviceInfo` object to be unassigned
+
.. event:: device-list:
Fired to get list of devices exposed by a VM. Handlers of this
@@ -212,169 +172,214 @@ class DeviceCollection:
:py:class:`DeviceInfo`, or :py:obj:`None`. Especially should not
raise :py:class:`exceptions.KeyError`.
- .. event:: device-list-attached: (persistent)
+ .. event:: device-list-attached:
Fired to get list of currently attached devices to a VM. Handlers
of this event should return list of devices actually attached to
a domain, regardless of its settings.
- '''
+ """
def __init__(self, vm, bus):
self._vm = vm
self._bus = bus
- self._set = PersistentCollection()
+ self._set = AssignedCollection()
self.devclass = qubes.utils.get_entry_point_one(
'qubes.devices', self._bus)
- async def attach(self, device_assignment: DeviceAssignment):
- '''Attach (add) device to domain.
-
- :param DeviceInfo device: device object
- '''
+ async def attach(self, assignment: DeviceAssignment):
+ """
+ Attach device to domain.
+ """
- if device_assignment.bus is None:
- device_assignment.bus = self._bus
- elif device_assignment.bus != self._bus:
+ if not assignment.devclass_is_set:
+ assignment.devclass = self._bus
+ elif assignment.devclass != self._bus:
raise ValueError(
- 'Trying to attach DeviceAssignment of a different device class')
+ f'Trying to attach {assignment.devclass} device '
+ f'when {self._bus} device expected.')
- if not device_assignment.persistent and self._vm.is_halted():
- raise qubes.exc.QubesVMNotRunningError(self._vm,
- "VM not running, can only attach device with persistent flag")
- device = device_assignment.device
- if device in self.assignments():
+ if self._vm.is_halted():
+ raise qubes.exc.QubesVMNotRunningError(
+ self._vm,"VM not running, cannot attach device,"
+ " do you mean `assign`?")
+
+ device = assignment.device
+ if device in self.get_attached_devices():
raise DeviceAlreadyAttached(
'device {!s} of class {} already attached to {!s}'.format(
device, self._bus, self._vm))
- await self._vm.fire_event_async('device-pre-attach:' + self._bus,
- pre_event=True,
- device=device, options=device_assignment.options)
- if device_assignment.persistent:
- self._set.add(device_assignment)
- await self._vm.fire_event_async('device-attach:' + self._bus,
- device=device, options=device_assignment.options)
- def load_persistent(self, device_assignment: DeviceAssignment):
- '''Load DeviceAssignment retrieved from qubes.xml
+ await self._vm.fire_event_async(
+ 'device-pre-attach:' + self._bus,
+ pre_event=True, device=device, options=assignment.options)
+
+ await self._vm.fire_event_async(
+ 'device-attach:' + self._bus,
+ device=device, options=assignment.options)
+
+ async def assign(self, assignment: DeviceAssignment):
+ """
+ Assign device to domain.
+ """
+ if not assignment.devclass_is_set:
+ assignment.devclass = self._bus
+ elif assignment.devclass != self._bus:
+ raise ValueError(
+ f'Trying to attach {assignment.devclass} device '
+ f'when {self._bus} device expected.')
+
+ device = assignment.device
+ if device in self.get_assigned_devices():
+ raise DeviceAlreadyAssigned(
+ 'device {!s} of class {} already assigned to {!s}'.format(
+ device, self._bus, self._vm))
+
+ self._set.add(assignment)
+
+ await self._vm.fire_event_async(
+ 'device-assign:' + self._bus,
+ device=device, options=assignment.options)
+
+ def load_assignment(self, device_assignment: DeviceAssignment):
+ """Load DeviceAssignment retrieved from qubes.xml
This can be used only for loading qubes.xml, when VM events are not
enabled yet.
- '''
+ """
assert not self._vm.events_enabled
- assert device_assignment.persistent
- device_assignment.bus = self._bus
+ assert device_assignment.attach_automatically
+ device_assignment.devclass = self._bus
self._set.add(device_assignment)
- def update_persistent(self, device: DeviceInfo, persistent: bool):
- '''Update `persistent` flag of already attached device.
- '''
+ async def update_required(self, device: Device, required: bool):
+ """
+ Update `required` flag of an already attached device.
+ :param Device device: device for which change required flag
+ :param bool required: new assignment:
+ `False` -> device will be auto-attached to qube
+ `True` -> device is required to start qube
+ """
if self._vm.is_halted():
- raise qubes.exc.QubesVMNotStartedError(self._vm,
- 'VM must be running to modify device persistence flag')
- assignments = [a for a in self.assignments() if a.device == device]
+ raise qubes.exc.QubesVMNotStartedError(
+ self._vm,
+ 'VM must be running to modify device assignment'
+ )
+ assignments = [a for a in self.get_assigned_devices()
+ if a == device]
if not assignments:
- raise qubes.exc.QubesValueError('Device not assigned')
+ raise qubes.exc.QubesValueError(
+ f'Device {device} not assigned to {self._vm.name}')
assert len(assignments) == 1
assignment = assignments[0]
# be careful to use already present assignment, not the provided one
# - to not change options as a side effect
- if persistent and device not in self._set:
- assignment.persistent = True
- self._set.add(assignment)
- elif not persistent and device in self._set:
- self._set.discard(assignment)
-
- async def detach(self, device_assignment: DeviceAssignment):
- '''Detach (remove) device from domain.
-
- :param DeviceInfo device: device object
- '''
-
- if device_assignment.bus is None:
- device_assignment.bus = self._bus
+ if assignment.required == required:
+ return
+
+ assignment.required = required
+ await self._vm.fire_event_async(
+ 'device-assignment-changed:' + self._bus, device=device)
+
+ async def detach(self, device: Device):
+ """
+ Detach device from domain.
+ """
+ for assign in self.get_attached_devices():
+ if device == assign:
+ # load all options
+ assignment = assign
+ break
+ else:
+ raise DeviceNotAssigned(
+ f'device {device.ident!s} of class {self._bus} not '
+ f'attached to {self._vm!s}')
+
+ if assignment.required and not self._vm.is_halted():
+ raise qubes.exc.QubesVMNotHaltedError(
+ self._vm,
+ "Can not detach a required device from a non halted qube. "
+ "You need to unassign device first.")
+
+ # use the local object
+ device = assignment.device
+ await self._vm.fire_event_async(
+ 'device-pre-detach:' + self._bus, pre_event=True, device=device)
+
+ await self._vm.fire_event_async(
+ 'device-detach:' + self._bus, device=device)
+
+ async def unassign(self, device_assignment: DeviceAssignment):
+ """
+ Unassign device from domain.
+ """
+ for assignment in self.get_assigned_devices():
+ if device_assignment == assignment:
+ # load all options
+ device_assignment = assignment
+ break
else:
- assert device_assignment.bus == self._bus, \
- "Trying to attach DeviceAssignment of a different device class"
+ raise DeviceNotAssigned(
+ f'device {device_assignment.ident!s} of class {self._bus} not '
+ f'assigned to {self._vm!s}')
- if device_assignment in self._set and not self._vm.is_halted():
- raise qubes.exc.QubesVMNotHaltedError(self._vm,
- "Can not remove a persistent attachment from a non halted vm")
- if device_assignment not in self.assignments():
- raise DeviceNotAttached(
- 'device {!s} of class {} not attached to {!s}'.format(
- device_assignment.ident, self._bus, self._vm))
+ self._set.discard(assignment)
device = device_assignment.device
- await self._vm.fire_event_async('device-pre-detach:' + self._bus,
- pre_event=True, device=device)
- if device in self._set:
- device_assignment.persistent = True
- self._set.discard(device_assignment)
-
- await self._vm.fire_event_async('device-detach:' + self._bus,
- device=device)
-
- def attached(self):
- '''List devices which are (or may be) attached to this vm '''
- attached = self._vm.fire_event('device-list-attached:' + self._bus,
- persistent=None)
- if attached:
- return [dev for dev, _ in attached]
-
- return []
-
- def persistent(self):
- ''' Devices persistently attached and safe to access before libvirt
- bootstrap.
- '''
- return [a.device for a in self._set]
-
- def assignments(self, persistent: Optional[bool]=None):
- '''List assignments for devices which are (or may be) attached to the
- vm.
-
- Devices may be attached persistently (so they are included in
- :file:`qubes.xml`) or not. Device can also be in :file:`qubes.xml`,
- but be temporarily detached.
-
- :param Optional[bool] persistent: only include devices which are or are
- not attached persistently.
- '''
-
- try:
- devices = self._vm.fire_event('device-list-attached:' + self._bus,
- persistent=persistent)
- except Exception: # pylint: disable=broad-except
- self._vm.log.exception('Failed to list {} devices'.format(
- self._bus))
- if persistent is True:
- # don't break app.save()
- return list(self._set)
- raise
- result = []
- if persistent is not False: # None or True
- result.extend(self._set)
- if not persistent: # None or False
- for dev, options in devices:
- if dev not in self._set:
- result.append(
- DeviceAssignment(
- backend_domain=dev.backend_domain,
- ident=dev.ident, options=options,
- bus=self._bus))
- return result
-
- def available(self):
- '''List devices exposed by this vm'''
- devices = self._vm.fire_event('device-list:' + self._bus)
- return devices
-
- def __iter__(self):
- return iter(self.available())
+ await self._vm.fire_event_async(
+ 'device-unassign:' + self._bus, device=device)
+
+ def get_dedicated_devices(self) -> Iterable[DeviceAssignment]:
+ """
+ List devices which are attached or assigned to this vm.
+ """
+ yield from itertools.chain(
+ self.get_attached_devices(), self.get_assigned_devices())
+
+ def get_attached_devices(self) -> Iterable[DeviceAssignment]:
+ """
+ List devices which are attached to this vm.
+ """
+ attached = self._vm.fire_event('device-list-attached:' + self._bus)
+ for dev, options in attached:
+ for assignment in self._set:
+ if dev == assignment:
+ yield assignment
+ break
+ else:
+ yield DeviceAssignment(
+ backend_domain=dev.backend_domain,
+ ident=dev.ident,
+ options=options,
+ frontend_domain=self._vm,
+ devclass=dev.devclass,
+ attach_automatically=False,
+ required=False,
+ )
+
+ def get_assigned_devices(
+ self, required_only: bool = False
+ ) -> Iterable[DeviceAssignment]:
+ """
+ Devices assigned to this vm (included in :file:`qubes.xml`).
+
+ Safe to access before libvirt bootstrap.
+ """
+ for dev in self._set:
+ if required_only and not dev.required:
+ continue
+ yield dev
+
+ def get_exposed_devices(self) -> Iterable[DeviceInfo]:
+ """
+ List devices exposed by this vm.
+ """
+ yield from self._vm.fire_event('device-list:' + self._bus)
+
+ __iter__ = get_exposed_devices
def __getitem__(self, ident):
'''Get device object with given ident.
@@ -394,14 +399,15 @@ def __getitem__(self, ident):
assert len(dev) == 1
return dev[0]
- return UnknownDevice(self._vm, ident)
+ return UnknownDevice(self._vm, ident, devclass=self._bus)
class DeviceManager(dict):
- '''Device manager that hold all devices by their classes.
+ """
+ Device manager that hold all devices by their classes.
:param vm: VM for which we manage devices
- '''
+ """
def __init__(self, vm):
super().__init__()
@@ -412,29 +418,17 @@ def __missing__(self, key):
return self[key]
-class UnknownDevice(DeviceInfo):
- # pylint: disable=too-few-public-methods
- '''Unknown device - for example exposed by domain not running currently'''
-
- def __init__(self, backend_domain, ident, description=None,
- frontend_domain=None):
- if description is None:
- description = "Unknown device"
- super().__init__(backend_domain, ident, description,
- frontend_domain)
-
-
-class PersistentCollection:
-
- ''' Helper object managing persistent `DeviceAssignment`s.
- '''
+class AssignedCollection:
+ """
+ Helper object managing assigned devices.
+ """
def __init__(self):
self._dict = {}
def add(self, assignment: DeviceAssignment):
- ''' Add assignment to collection '''
- assert assignment.persistent
+ """ Add assignment to collection """
+ assert assignment.attach_automatically
vm = assignment.backend_domain
ident = assignment.ident
key = (vm, ident)
@@ -442,9 +436,11 @@ def add(self, assignment: DeviceAssignment):
self._dict[key] = assignment
- def discard(self, assignment):
- ''' Discard assignment from collection '''
- assert assignment.persistent
+ def discard(self, assignment: DeviceAssignment):
+ """
+ Discard assignment from a collection.
+ """
+ assert assignment.attach_automatically
vm = assignment.backend_domain
ident = assignment.ident
key = (vm, ident)
@@ -456,8 +452,9 @@ def __contains__(self, device) -> bool:
return (device.backend_domain, device.ident) in self._dict
def get(self, device: DeviceInfo) -> DeviceAssignment:
- ''' Returns the corresponding `qubes.devices.DeviceAssignment` for the
- device. '''
+ """
+ Returns the corresponding `DeviceAssignment` for the device.
+ """
return self._dict[(device.backend_domain, device.ident)]
def __iter__(self):
diff --git a/qubes/exc.py b/qubes/exc.py
index 5139a5724..45a67fab0 100644
--- a/qubes/exc.py
+++ b/qubes/exc.py
@@ -219,3 +219,11 @@ def __init__(self, label):
def __str__(self):
# KeyError overrides __str__ method
return QubesException.__str__(self)
+
+
+class ProtocolError(AssertionError):
+ '''Raised when something is wrong with data received'''
+
+
+class PermissionDenied(Exception):
+ '''Raised deliberately by handlers when we decide not to cooperate'''
diff --git a/qubes/ext/__init__.py b/qubes/ext/__init__.py
index 0530a3442..4a1949815 100644
--- a/qubes/ext/__init__.py
+++ b/qubes/ext/__init__.py
@@ -30,8 +30,8 @@
class Extension:
- '''Base class for all extensions
- ''' # pylint: disable=too-few-public-methods
+ '''Base class for all extensions'''
+ # pylint: disable=too-few-public-methods
def __new__(cls):
if '_instance' not in cls.__dict__:
diff --git a/qubes/ext/admin.py b/qubes/ext/admin.py
index 39a6e34a6..4abc04418 100644
--- a/qubes/ext/admin.py
+++ b/qubes/ext/admin.py
@@ -19,6 +19,7 @@
import qubes.api
import qubes.api.internal
+import qubes.exc
import qubes.ext
import qubes.vm.adminvm
from qrexec.policy import utils, parser
@@ -52,7 +53,7 @@ def on_tag_set_or_remove(self, vm, event, arg, **kwargs):
# pylint: disable=unused-argument
if arg.startswith('created-by-') and \
not isinstance(vm, qubes.vm.adminvm.AdminVM):
- raise qubes.api.PermissionDenied(
+ raise qubes.exc.PermissionDenied(
'changing this tag is prohibited by {}.{}'.format(
__name__, type(self).__name__))
diff --git a/qubes/ext/block.py b/qubes/ext/block.py
index 4d53c2c13..3a3eb1e58 100644
--- a/qubes/ext/block.py
+++ b/qubes/ext/block.py
@@ -4,6 +4,7 @@
#
# Copyright (C) 2017 Marek Marczykowski-Górecki
#
+# Copyright (C) 2024 Piotr Bartman-Szwarc
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -18,13 +19,20 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, see .
-''' Qubes block devices extensions '''
+""" Qubes block devices extensions """
+import asyncio
+import collections
import re
import string
+import sys
+from typing import Optional, List
+
import lxml.etree
+import qubes.device_protocol
import qubes.devices
import qubes.ext
+from qubes.ext.utils import device_list_change
name_re = re.compile(r"\A[a-z0-9-]{1,12}\Z")
device_re = re.compile(r"\A[a-z0-9/-]{1,64}\Z")
@@ -42,34 +50,73 @@
SYSTEM_DISKS_DOM0_KERNEL = SYSTEM_DISKS + ('xvdd',)
-class BlockDevice(qubes.devices.DeviceInfo):
+class BlockDevice(qubes.device_protocol.DeviceInfo):
def __init__(self, backend_domain, ident):
- super().__init__(backend_domain=backend_domain,
- ident=ident)
- self._description = None
+ super().__init__(
+ backend_domain=backend_domain, ident=ident, devclass="block")
+
+ # lazy loading
self._mode = None
self._size = None
+ self._interface_num = None
@property
- def description(self):
- '''Human readable device description'''
- if self._description is None:
- if not self.backend_domain.is_running():
- return self.ident
- safe_set = {ord(c) for c in
- string.ascii_letters + string.digits + '()+,-.:=_/ '}
- untrusted_desc = self.backend_domain.untrusted_qdb.read(
- '/qubes-block-devices/{}/desc'.format(self.ident))
- if not untrusted_desc:
- return ''
- desc = ''.join((chr(c) if c in safe_set else '_')
- for c in untrusted_desc)
- self._description = desc
- return self._description
+ def name(self):
+ """
+ The name of the device it introduced itself with.
+
+ Could be empty string or "unknown".
+ """
+ if self._name is None:
+ name, _ = self._load_lazily_name_and_serial()
+ return name
+ return self._name
+
+ @property
+ def serial(self) -> str:
+ """
+ The serial number of the device it introduced itself with.
+
+ Could be empty string or "unknown".
+
+ Override this method to return proper name directly from device itself.
+ """
+ if self._serial is None:
+ _, serial = self._load_lazily_name_and_serial()
+ return serial
+ return self._serial
+
+ def _load_lazily_name_and_serial(self):
+ if not self.backend_domain.is_running():
+ return "unknown", "unknown"
+ untrusted_desc = self.backend_domain.untrusted_qdb.read(
+ f'/qubes-block-devices/{self.ident}/desc')
+ if not untrusted_desc:
+ return "unknown", "unknown"
+ desc = BlockDevice._sanitize(
+ untrusted_desc,
+ string.ascii_letters + string.digits + '()+,-.:=_/ ')
+ model, _, label = desc.partition(' ')
+ if model:
+ serial = self._serial = model.replace('_', ' ').strip()
+ else:
+ serial = "unknown"
+ # label: '(EXAMPLE)' or '()'
+ if label[1:-1]:
+ name = self._name = label.replace('_', ' ')[1:-1].strip()
+ else:
+ name = "unknown"
+ return name, serial
+
+ @property
+ def manufacturer(self) -> str:
+ if self.parent_device:
+ return f"sub-device of {self.parent_device}"
+ return f"hosted by {self.backend_domain!s}"
@property
def mode(self):
- '''Device mode, either 'w' for read-write, or 'r' for read-only'''
+ """Device mode, either 'w' for read-write, or 'r' for read-only"""
if self._mode is None:
if not self.backend_domain.is_running():
return 'w'
@@ -87,7 +134,7 @@ def mode(self):
@property
def size(self):
- '''Device size in bytes'''
+ """Device size in bytes"""
if self._size is None:
if not self.backend_domain.is_running():
return None
@@ -105,29 +152,212 @@ def size(self):
@property
def device_node(self):
- '''Device node in backend domain'''
+ """Device node in backend domain"""
return '/dev/' + self.ident.replace('_', '/')
+ @property
+ def interfaces(self) -> List[qubes.device_protocol.DeviceInterface]:
+ """
+ List of device interfaces.
+
+ Every device should have at least one interface.
+ """
+ return [qubes.device_protocol.DeviceInterface("******", "block")]
+
+ @property
+ def parent_device(self) -> Optional[qubes.device_protocol.Device]:
+ """
+ The parent device, if any.
+
+ If the device is part of another device (e.g., it's a single
+ partition of an usb stick), the parent device id should be here.
+ """
+ if self._parent is None:
+ if not self.backend_domain.is_running():
+ return None
+ untrusted_parent_info = self.backend_domain.untrusted_qdb.read(
+ f'/qubes-block-devices/{self.ident}/parent')
+ if untrusted_parent_info is None:
+ return None
+ # '4-4.1:1.0' -> parent_ident='4-4.1', interface_num='1.0'
+ # 'sda' -> parent_ident='sda', interface_num=''
+ parent_ident, sep, interface_num = self._sanitize(
+ untrusted_parent_info).partition(":")
+ devclass = 'usb' if sep == ':' else 'block'
+ if not parent_ident:
+ return None
+ try:
+ self._parent = (
+ self.backend_domain.devices)[devclass][parent_ident]
+ except KeyError:
+ self._parent = qubes.device_protocol.UnknownDevice(
+ self.backend_domain, parent_ident, devclass=devclass)
+ self._interface_num = interface_num
+ return self._parent
+
+ @property
+ def attachment(self) -> Optional['qubes.vm.BaseVM']:
+ """
+ Warning: this property is time-consuming, do not run in loop!
+ """
+ if not self.backend_domain.is_running():
+ return None
+ for vm in self.backend_domain.app.domains:
+ if not vm.is_running():
+ continue
+ if self._is_attached_to(vm):
+ return vm
+
+ return None
+
+ def _is_attached_to(self, vm):
+ xml_desc = lxml.etree.fromstring(vm.libvirt_domain.XMLDesc())
+
+ for disk in xml_desc.findall('devices/disk'):
+ info = _try_get_block_device_info(vm.app, disk)
+ if not info:
+ continue
+ backend_domain, ident = info
+
+ if backend_domain.name != self.backend_domain.name:
+ continue
+
+ if self.ident == ident:
+ return True
+ return False
+
+ @property
+ def self_identity(self) -> str:
+ """
+ Get identification of a device not related to port.
+ """
+ parent_identity = ''
+ p = self.parent_device
+ if p is not None:
+ p_info = p.backend_domain.devices[p.devclass][p.ident]
+ parent_identity = p_info.self_identity
+ if p.devclass == 'usb':
+ parent_identity = f'{p.ident}:{parent_identity}'
+ if self._interface_num:
+ # device interface number (not partition)
+ self_id = self._interface_num
+ else:
+ self_id = self._get_possible_partition_number()
+ return f'{parent_identity}:{self_id}'
+
+ def _get_possible_partition_number(self) -> Optional[int]:
+ """
+ If the device is partition return partition number.
+
+ The behavior is undefined for the rest block devices.
+ """
+ # partition number: 'xxxxx12' -> '12' (partition)
+ numbers = re.findall(r'\d+$', self.ident)
+ return int(numbers[-1]) if numbers else None
+
+ @staticmethod
+ def _sanitize(
+ untrusted_parent: bytes,
+ safe_chars: str =
+ string.ascii_letters + string.digits + string.punctuation
+ ) -> str:
+ untrusted_device_desc = untrusted_parent.decode(
+ 'ascii', errors='ignore')
+ return ''.join(
+ c if c in set(safe_chars) else '_' for c in untrusted_device_desc
+ )
+
+
+def _try_get_block_device_info(app, disk):
+ if disk.get('type') != 'block':
+ return None
+ dev_path_node = disk.find('source')
+ if dev_path_node is None:
+ return None
+
+ backend_domain_node = disk.find('backenddomain')
+ if backend_domain_node is not None:
+ backend_domain = app.domains[
+ backend_domain_node.get('name')]
+ else:
+ backend_domain = app.domains[0]
+
+ dev_path = dev_path_node.get('dev')
+
+ if dev_path.startswith('/dev/'):
+ ident = dev_path[len('/dev/'):]
+ else:
+ ident = dev_path
+
+ ident = ident.replace('/', '_')
+
+ return backend_domain, ident
+
class BlockDeviceExtension(qubes.ext.Extension):
+ def __init__(self):
+ super().__init__()
+ self.devices_cache = collections.defaultdict(dict)
+
@qubes.ext.handler('domain-init', 'domain-load')
def on_domain_init_load(self, vm, event):
- '''Initialize watching for changes'''
+ """Initialize watching for changes"""
# pylint: disable=unused-argument
vm.watch_qdb_path('/qubes-block-devices')
+ if vm.app.vmm.offline_mode:
+ self.devices_cache[vm.name] = {}
+ return
+ if event == 'domain-load':
+ # avoid building a cache on domain-init, as it isn't fully set yet,
+ # and definitely isn't running yet
+ device_attachments = self.get_device_attachments(vm)
+ current_devices = dict(
+ (dev.ident, device_attachments.get(dev.ident, None))
+ for dev in self.on_device_list_block(vm, None))
+ self.devices_cache[vm.name] = current_devices
+ else:
+ self.devices_cache[vm.name] = {}
@qubes.ext.handler('domain-qdb-change:/qubes-block-devices')
def on_qdb_change(self, vm, event, path):
- '''A change in QubesDB means a change in device list'''
+ """A change in QubesDB means a change in a device list."""
# pylint: disable=unused-argument
- vm.fire_event('device-list-change:block')
+ device_attachments = self.get_device_attachments(vm)
+ current_devices = dict(
+ (dev.ident, device_attachments.get(dev.ident, None))
+ for dev in self.on_device_list_block(vm, None))
+ device_list_change(self, current_devices, vm, path, BlockDevice)
+
+ @staticmethod
+ def get_device_attachments(vm_):
+ result = {}
+ for vm in vm_.app.domains:
+ if not vm.is_running():
+ continue
+
+ if vm.app.vmm.offline_mode:
+ return result
+
+ xml_desc = lxml.etree.fromstring(vm.libvirt_domain.XMLDesc())
- def device_get(self, vm, ident):
- '''Read information about device from QubesDB
+ for disk in xml_desc.findall('devices/disk'):
+ info = _try_get_block_device_info(vm.app, disk)
+ if not info:
+ continue
+ _backend_domain, ident = info
+
+ result[ident] = vm
+ return result
+
+ @staticmethod
+ def device_get(vm, ident):
+ """
+ Read information about a device from QubesDB
:param vm: backend VM object
:param ident: device identifier
- :returns BlockDevice'''
+ :returns BlockDevice
+ """
untrusted_qubes_device_attrs = vm.untrusted_qdb.list(
'/qubes-block-devices/{}/'.format(ident))
@@ -221,9 +451,11 @@ def on_device_list_attached(self, vm, event, **kwargs):
yield (BlockDevice(backend_domain, ident), options)
- def find_unused_frontend(self, vm, devtype='disk'):
- '''Find unused block frontend device node for
- parameter'''
+ @staticmethod
+ def find_unused_frontend(vm, devtype='disk'):
+ """
+ Find unused block frontend device node for parameter
+ """
assert vm.is_running()
xml = vm.libvirt_domain.XMLDesc()
@@ -242,6 +474,18 @@ def find_unused_frontend(self, vm, devtype='disk'):
@qubes.ext.handler('device-pre-attach:block')
def on_device_pre_attached_block(self, vm, event, device, options):
# pylint: disable=unused-argument
+ self.pre_attachment_internal(vm, device, options)
+
+ vm.libvirt_domain.attachDevice(
+ vm.app.env.get_template('libvirt/devices/block.xml').render(
+ device=device, vm=vm, options=options))
+
+ def pre_attachment_internal(
+ self, vm, device, options, expected_attachment=None):
+ if isinstance(device, qubes.device_protocol.UnknownDevice):
+ print(f'{device.devclass.capitalize()} device {device} '
+ 'not available, skipping.', file=sys.stderr)
+ raise qubes.devices.UnrecognizedDevice()
# validate options
for option, value in options.items():
@@ -257,6 +501,15 @@ def on_device_pre_attached_block(self, vm, event, device, options):
raise qubes.exc.QubesValueError(
'devtype option can only have '
'\'disk\' or \'cdrom\' value')
+ elif option == 'identity':
+ identity = value
+ if identity not in ('any', device.self_identity):
+ print("Unrecognized identity, skipping attachment of"
+ f" {device}", file=sys.stderr)
+ raise qubes.devices.UnrecognizedDevice(
+ f"Device presented identity {device.self_identity} "
+ f"does not match expected {identity}"
+ )
else:
raise qubes.exc.QubesValueError(
'Unsupported option {}'.format(option))
@@ -268,20 +521,89 @@ def on_device_pre_attached_block(self, vm, event, device, options):
'This device can be attached only read-only')
if not vm.is_running():
+ print(f"Can not attach device, qube {vm.name} is not running."
+ , file=sys.stderr)
return
+ if not isinstance(device, BlockDevice):
+ print("The device is not recognized as block device, "
+ f"skipping attachment of {device}",
+ file=sys.stderr)
+ return
+
+ if device.attachment and device.attachment != expected_attachment:
+ raise qubes.devices.DeviceAlreadyAttached(
+ 'Device {!s} already attached to {!s}'.format(
+ device, device.attachment)
+ )
+
if not device.backend_domain.is_running():
- raise qubes.exc.QubesVMNotRunningError(device.backend_domain,
- 'Domain {} needs to be running to attach device from '
- 'it'.format(device.backend_domain.name))
+ raise qubes.exc.QubesVMNotRunningError(
+ device.backend_domain,
+ f'Domain {device.backend_domain.name} needs to be running '
+ f'to attach device from it')
+
+ self.devices_cache[device.backend_domain.name][device.ident] = vm
if 'frontend-dev' not in options:
options['frontend-dev'] = self.find_unused_frontend(
vm, options.get('devtype', 'disk'))
- vm.libvirt_domain.attachDevice(
- vm.app.env.get_template('libvirt/devices/block.xml').render(
- device=device, vm=vm, options=options))
+ @qubes.ext.handler('domain-start')
+ async def on_domain_start(self, vm, _event, **_kwargs):
+ # pylint: disable=unused-argument
+ for assignment in vm.devices['block'].get_assigned_devices():
+ self.notify_auto_attached(
+ vm, assignment.device, assignment.options)
+
+ def notify_auto_attached(self, vm, device, options):
+ self.pre_attachment_internal(
+ vm, device, options, expected_attachment=vm)
+ asyncio.ensure_future(vm.fire_event_async(
+ 'device-attach:block', device=device, options=options))
+
+ async def attach_and_notify(self, vm, device, options):
+ # bypass DeviceCollection logic preventing double attach
+ # we expected that these devices are already attached to this vm
+ self.notify_auto_attached(vm, device, options)
+
+ @qubes.ext.handler('domain-shutdown')
+ async def on_domain_shutdown(self, vm, event, **_kwargs):
+ """
+ Remove from cache devices attached to or exposed by the vm.
+ """
+ # pylint: disable=unused-argument
+
+ new_cache = {}
+ for domain in vm.app.domains.values():
+ new_cache[domain.name] = {}
+ if domain == vm:
+ for dev_id, front_vm in self.devices_cache[domain.name].items():
+ if front_vm is None:
+ continue
+ dev = BlockDevice(vm, dev_id)
+ await self._detach_and_notify(vm, dev, options=None)
+ continue
+ for dev_id, front_vm in self.devices_cache[domain.name].items():
+ if front_vm == vm:
+ dev = BlockDevice(vm, dev_id)
+ asyncio.ensure_future(front_vm.fire_event_async(
+ 'device-detach:block', device=dev))
+ else:
+ new_cache[domain.name][dev_id] = front_vm
+ self.devices_cache = new_cache.copy()
+
+ async def _detach_and_notify(self, vm, device, options):
+ # bypass DeviceCollection logic preventing double attach
+ self.on_device_pre_detached_block(
+ vm, 'device-pre-detach:block', device)
+ await vm.fire_event_async(
+ 'device-detach:block', device=device, options=options)
+
+ @qubes.ext.handler('qubes-close', system=True)
+ def on_qubes_close(self, app, event):
+ # pylint: disable=unused-argument
+ self.devices_cache.clear()
@qubes.ext.handler('device-pre-detach:block')
def on_device_pre_detached_block(self, vm, event, device):
@@ -289,10 +611,12 @@ def on_device_pre_detached_block(self, vm, event, device):
if not vm.is_running():
return
- # need to enumerate attached device to find frontend_dev option (at
+ # need to enumerate attached devices to find frontend_dev option (at
# least)
for attached_device, options in self.on_device_list_attached(vm, event):
if attached_device == device:
+ self.devices_cache[device.backend_domain.name][
+ device.ident] = None
vm.libvirt_domain.detachDevice(
vm.app.env.get_template('libvirt/devices/block.xml').render(
device=device, vm=vm, options=options))
diff --git a/qubes/ext/pci.py b/qubes/ext/pci.py
index 4b1cf32db..1535a7f9d 100644
--- a/qubes/ext/pci.py
+++ b/qubes/ext/pci.py
@@ -18,16 +18,20 @@
# License along with this library; if not, see .
#
-''' Qubes PCI Extensions '''
+""" Qubes PCI Extensions """
import functools
import os
import re
+import string
import subprocess
+from typing import Optional, List, Dict
+
import libvirt
import lxml
import lxml.etree
+import qubes.device_protocol
import qubes.devices
import qubes.ext
@@ -35,7 +39,7 @@
pci_classes = None
-#: emit warning on unspported device only once
+#: emit warning on unsupported device only once
unsupported_devices_warned = set()
@@ -44,14 +48,14 @@ class UnsupportedDevice(Exception):
def load_pci_classes():
- ''' List of known device classes, subclasses and programming interfaces. '''
+ """ List of known device classes, subclasses and programming interfaces. """
# Syntax:
# C class class_name
# subclass subclass_name <-- single tab
# prog-if prog-if_name <-- two tabs
result = {}
with open('/usr/share/hwdata/pci.ids',
- encoding='utf-8', errors='ignore') as pciids:
+ encoding='utf-8', errors='ignore') as pciids:
class_id = None
subclass_id = None
for line in pciids.readlines():
@@ -83,7 +87,7 @@ def pcidev_class(dev_xmldesc):
with open(sysfs_path + '/class', encoding='ascii') as f_class:
class_id = f_class.read().strip()
except OSError:
- return "Unknown"
+ return "unknown"
if not qubes.ext.pci.pci_classes:
qubes.ext.pci.pci_classes = load_pci_classes()
@@ -93,7 +97,21 @@ def pcidev_class(dev_xmldesc):
# ignore prog-if
return qubes.ext.pci.pci_classes[class_id[0:4]]
except KeyError:
- return "Unknown"
+ return "unknown"
+
+
+def pcidev_interface(dev_xmldesc):
+ sysfs_path = dev_xmldesc.findtext('path')
+ assert sysfs_path
+ try:
+ with open(sysfs_path + '/class', encoding='ascii') as f_class:
+ class_id = f_class.read().strip()
+ except OSError:
+ return "000000"
+
+ if class_id.startswith('0x'):
+ class_id = class_id[2:]
+ return class_id
def attached_devices(app):
@@ -134,7 +152,7 @@ def _device_desc(hostdev_xml):
)
-class PCIDevice(qubes.devices.DeviceInfo):
+class PCIDevice(qubes.device_protocol.DeviceInfo):
# pylint: disable=too-few-public-methods
regex = re.compile(
r'\A(?P[0-9a-f]+)_(?P[0-9a-f]+)\.'
@@ -150,16 +168,84 @@ def __init__(self, backend_domain, ident, libvirt_name=None):
raise UnsupportedDevice(libvirt_name)
ident = '{bus}_{device}.{function}'.format(**dev_match.groupdict())
- super().__init__(backend_domain, ident, None)
+ super().__init__(
+ backend_domain=backend_domain, ident=ident, devclass="pci")
+
+ dev_match = self.regex.match(ident)
+ if not dev_match:
+ raise ValueError('Invalid device identifier: {!r}'.format(
+ ident))
+
+ for group in self.regex.groupindex:
+ setattr(self, group, dev_match.group(group))
# lazy loading
self._description = None
+ self._vendor_id = None
+ self._product_id = None
+
+ @property
+ def vendor(self) -> str:
+ """
+ Device vendor from local database `/usr/share/hwdata/pci.ids`
+
+ Could be empty string or "unknown".
+
+ Lazy loaded.
+ """
+ if self._vendor is None:
+ result = self._load_desc()["vendor"]
+ else:
+ result = self._vendor
+ return result
+
+ @property
+ def product(self) -> str:
+ """
+ Device name from local database `/usr/share/hwdata/usb.ids`
+
+ Could be empty string or "unknown".
+
+ Lazy loaded.
+ """
+ if self._product is None:
+ result = self._load_desc()["product"]
+ else:
+ result = self._product
+ return result
+
+ @property
+ def interfaces(self) -> List[qubes.device_protocol.DeviceInterface]:
+ """
+ List of device interfaces.
+
+ Every device should have at least one interface.
+ """
+ if self._interfaces is None:
+ hostdev_details = \
+ self.backend_domain.app.vmm.libvirt_conn.nodeDeviceLookupByName(
+ self.libvirt_name
+ )
+ interface_encoding = pcidev_interface(lxml.etree.fromstring(
+ hostdev_details.XMLDesc()))
+ self._interfaces = [qubes.device_protocol.DeviceInterface(
+ interface_encoding, devclass='pci')]
+ return self._interfaces
+
+ @property
+ def parent_device(self) -> Optional[qubes.device_protocol.DeviceInfo]:
+ """
+ The parent device, if any.
+
+ PCI device has no parents.
+ """
+ return None
@property
def libvirt_name(self):
# pylint: disable=no-member
# noinspection PyUnresolvedReferences
- return 'pci_0000_{}_{}_{}'.format(self.bus, self.device, self.function)
+ return f'pci_0000_{self.bus}_{self.device}_{self.function}'
@property
def description(self):
@@ -172,6 +258,54 @@ def description(self):
hostdev_details.XMLDesc()))
return self._description
+ @property
+ def self_identity(self) -> str:
+ """
+ Get identification of the device not related to port.
+ """
+ allowed_chars = string.digits + string.ascii_letters + '-_.'
+ if self._vendor_id is None:
+ vendor_id = self._load_desc()["vendor ID"]
+ self._vendor_id = ''.join(
+ c if c in set(allowed_chars) else '_' for c in vendor_id)
+ if self._product_id is None:
+ product_id = self._load_desc()["product ID"]
+ self._product_id = ''.join(
+ c if c in set(allowed_chars) else '_' for c in product_id)
+ interfaces = ''.join(repr(ifc) for ifc in self.interfaces)
+ serial = self._serial if self._serial else ""
+ return \
+ f'{self._vendor_id}:{self._product_id}:{serial}:{interfaces}'
+
+ def _load_desc(self) -> Dict[str, str]:
+ unknown = "unknown"
+ result = {"vendor": unknown,
+ "vendor ID": "0000",
+ "product": unknown,
+ "product ID": "0000",
+ "manufacturer": unknown,
+ "name": unknown,
+ "serial": unknown}
+ if not self.backend_domain.is_running():
+ # don't cache these values
+ return result
+ hostdev_details = \
+ self.backend_domain.app.vmm.libvirt_conn.nodeDeviceLookupByName(
+ self.libvirt_name
+ )
+
+ # Data successfully loaded, cache these values
+ hostdev_xml = lxml.etree.fromstring(hostdev_details.XMLDesc())
+ self._vendor = result["vendor"] = hostdev_xml.findtext(
+ 'capability/vendor')
+ self._vendor_id = result["vendor ID"] = hostdev_xml.xpath(
+ "//vendor/@id")[0]
+ self._product = result["product"] = hostdev_xml.findtext(
+ 'capability/product')
+ self._product_id = result["product ID"] = hostdev_xml.xpath(
+ "//product/@id")[0]
+ return result
+
@property
def frontend_domain(self):
# TODO: cache this
@@ -179,7 +313,6 @@ def frontend_domain(self):
return all_attached.get(self.ident, None)
-
class PCIDeviceExtension(qubes.ext.Extension):
def __init__(self):
super().__init__()
@@ -304,7 +437,7 @@ def on_device_pre_detached_pci(self, vm, event, device):
@qubes.ext.handler('domain-pre-start')
def on_domain_pre_start(self, vm, _event, **_kwargs):
# Bind pci devices to pciback driver
- for assignment in vm.devices['pci'].persistent():
+ for assignment in vm.devices['pci'].get_assigned_devices():
device = _cache_get(assignment.backend_domain, assignment.ident)
self.bind_pci_to_pciback(vm.app, device)
diff --git a/qubes/ext/utils.py b/qubes/ext/utils.py
new file mode 100644
index 000000000..1c7831922
--- /dev/null
+++ b/qubes/ext/utils.py
@@ -0,0 +1,102 @@
+# coding=utf-8
+#
+# The Qubes OS Project, https://www.qubes-os.org
+#
+# Copyright (C) 2023 Piotr Bartman-Szwarc
+#
+# 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, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+import asyncio
+
+import qubes
+
+
+def device_list_change(
+ ext: qubes.ext.Extension, current_devices,
+ vm, path, device_class: qubes.device_protocol.DeviceInfo
+):
+ devclass = device_class.__name__[:-len('Device')].lower()
+
+ if path is not None:
+ vm.fire_event(f'device-list-change:{devclass}')
+
+ added, attached, detached, removed = (
+ compare_device_cache(vm, ext.devices_cache, current_devices))
+
+ # send events about devices detached/attached outside by themselves
+ for dev_id, front_vm in detached.items():
+ dev = device_class(vm, dev_id)
+ asyncio.ensure_future(front_vm.fire_event_async(
+ f'device-detach:{devclass}', device=dev))
+ for dev_id in removed:
+ device = device_class(vm, dev_id)
+ vm.fire_event(f'device-removed:{devclass}', device=device)
+ for dev_id in added:
+ device = device_class(vm, dev_id)
+ vm.fire_event(f'device-added:{devclass}', device=device)
+ for dev_ident, front_vm in attached.items():
+ dev = device_class(vm, dev_ident)
+ # options are unknown, device already attached
+ asyncio.ensure_future(front_vm.fire_event_async(
+ f'device-attach:{devclass}', device=dev, options={}))
+
+ ext.devices_cache[vm.name] = current_devices
+
+ for front_vm in vm.app.domains:
+ if not front_vm.is_running():
+ continue
+ for assignment in front_vm.devices[devclass].get_assigned_devices():
+ if (assignment.backend_domain == vm
+ and assignment.ident in added
+ and assignment.ident not in attached
+ ):
+ asyncio.ensure_future(ext.attach_and_notify(
+ front_vm, assignment.device, assignment.options))
+
+
+def compare_device_cache(vm, devices_cache, current_devices):
+ # compare cached devices and current devices, collect:
+ # - newly appeared devices (ident)
+ # - devices attached from a vm to frontend vm (ident: frontend_vm)
+ # - devices detached from frontend vm (ident: frontend_vm)
+ # - disappeared devices, e.g., plugged out (ident)
+ added = set()
+ attached = {}
+ detached = {}
+ removed = set()
+ cache = devices_cache[vm.name]
+ for dev_id, front_vm in current_devices.items():
+ if dev_id not in cache:
+ added.add(dev_id)
+ if front_vm is not None:
+ attached[dev_id] = front_vm
+ elif cache[dev_id] != front_vm:
+ cached_front = cache[dev_id]
+ if front_vm is None:
+ detached[dev_id] = cached_front
+ elif cached_front is None:
+ attached[dev_id] = front_vm
+ else:
+ # a front changed from one to another, so we signal it as:
+ # detach from the first one and attach to the second one.
+ detached[dev_id] = cached_front
+ attached[dev_id] = front_vm
+
+ for dev_id, cached_front in cache.items():
+ if dev_id not in current_devices:
+ removed.add(dev_id)
+ if cached_front is not None:
+ detached[dev_id] = cached_front
+ return added, attached, detached, removed
diff --git a/qubes/tests/api.py b/qubes/tests/api.py
index 54ca186d9..c900307c1 100644
--- a/qubes/tests/api.py
+++ b/qubes/tests/api.py
@@ -23,6 +23,7 @@
import unittest.mock
import qubes.api
+import qubes.exc
import qubes.tests
@@ -43,7 +44,7 @@ def __init__(self, app, src, method, dest, arg, send_event=None):
'mgmt.event': self.event,
}[self.method.decode()]
except KeyError:
- raise qubes.api.ProtocolError('Invalid method')
+ raise qubes.exc.ProtocolError('Invalid method')
def execute(self, untrusted_payload):
self.task = asyncio.Task(self.function(
diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py
index c24d29f12..6b752b392 100644
--- a/qubes/tests/api_admin.py
+++ b/qubes/tests/api_admin.py
@@ -34,6 +34,7 @@
import qubes
import qubes.devices
+import qubes.exc
import qubes.firewall
import qubes.api.admin
import qubes.api.internal
@@ -124,6 +125,7 @@ def call_internal_mgmt_func(self, method, dest, arg=b'', payload=b''):
return response
+# noinspection PyUnresolvedReferences
class TC_00_VMs(AdminAPITestCase):
def test_000_vm_list(self):
value = self.call_mgmt_func(b'admin.vm.List', b'dom0')
@@ -483,7 +485,7 @@ def test_080_vm_volume_info_invalid_volume(self):
'keys.return_value': ['root', 'private', 'volatile', 'kernel']
}
self.vm.volumes.configure_mock(**volumes_conf)
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.vm.volume.Info', b'test-vm1',
b'no-such-volume')
self.assertEqual(self.vm.volumes.mock_calls,
@@ -511,7 +513,7 @@ def test_090_vm_volume_listsnapshots_invalid_volume(self):
'keys.return_value': ['root', 'private', 'volatile', 'kernel']
}
self.vm.volumes.configure_mock(**volumes_conf)
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.vm.volume.ListSnapshots', b'test-vm1',
b'no-such-volume')
self.assertEqual(self.vm.volumes.mock_calls,
@@ -530,7 +532,7 @@ def test_100_vm_volume_snapshot_invalid_volume(self):
{'rev2': '2018-02-22T22:22:22', 'rev1': '2018-01-11T11:11:11'},
}
self.vm.volumes.configure_mock(**volumes_conf)
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.vm.volume.Snapshots',
b'test-vm1', b'no-such-volume')
self.assertEqual(self.vm.volumes.mock_calls,
@@ -543,7 +545,7 @@ def test_100_vm_volume_snapshot_invalid_revision(self):
'keys.return_value': ['root', 'private', 'volatile', 'kernel']
}
self.vm.volumes.configure_mock(**volumes_conf)
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.vm.volume.Snapshots',
b'test-vm1', b'private', b'no-such-rev')
self.assertEqual(self.vm.volumes.mock_calls,
@@ -582,7 +584,7 @@ def test_110_vm_volume_revert_invalid_rev(self):
}
self.vm.volumes.configure_mock(**volumes_conf)
self.vm.storage = unittest.mock.Mock()
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.vm.volume.Revert',
b'test-vm1', b'private', b'no-such-rev')
self.assertEqual(self.vm.volumes.mock_calls,
@@ -630,7 +632,7 @@ def test_120_vm_volume_resize_invalid_size1(self):
self.vm.volumes.configure_mock(**volumes_conf)
self.vm.storage = unittest.mock.Mock()
self.vm.storage.resize.side_effect = self.dummy_coro
- with self.assertRaises(qubes.api.ProtocolError):
+ with self.assertRaises(qubes.exc.ProtocolError):
self.call_mgmt_func(b'admin.vm.volume.Resize',
b'test-vm1', b'private', b'no-int-size')
self.assertEqual(self.vm.volumes.mock_calls,
@@ -645,7 +647,7 @@ def test_120_vm_volume_resize_invalid_size2(self):
self.vm.volumes.configure_mock(**volumes_conf)
self.vm.storage = unittest.mock.Mock()
self.vm.storage.resize.side_effect = self.dummy_coro
- with self.assertRaises(qubes.api.ProtocolError):
+ with self.assertRaises(qubes.exc.ProtocolError):
self.call_mgmt_func(b'admin.vm.volume.Resize',
b'test-vm1', b'private', b'-1')
self.assertEqual(self.vm.volumes.mock_calls,
@@ -660,7 +662,7 @@ def test_120_vm_volume_resize_invalid_size3(self):
self.vm.volumes.configure_mock(**volumes_conf)
self.vm.storage = unittest.mock.Mock()
self.vm.storage.resize.side_effect = self.dummy_coro
- with self.assertRaises(qubes.api.ProtocolError):
+ with self.assertRaises(qubes.exc.ProtocolError):
self.call_mgmt_func(b'admin.vm.volume.Resize',
b'test-vm1', b'private', b'10000000000000000000')
self.assertEqual(self.vm.volumes.mock_calls,
@@ -675,7 +677,7 @@ def test_120_vm_volume_resize_invalid_size4(self):
self.vm.volumes.configure_mock(**volumes_conf)
self.vm.storage = unittest.mock.Mock()
self.vm.storage.resize.side_effect = self.dummy_coro
- with self.assertRaises(qubes.api.ProtocolError):
+ with self.assertRaises(qubes.exc.ProtocolError):
self.call_mgmt_func(b'admin.vm.volume.Resize',
b'test-vm1', b'private', b'01')
self.assertEqual(self.vm.volumes.mock_calls,
@@ -867,7 +869,7 @@ def test_160_pool_add_missing_name(self, mock_parameters, mock_drivers):
add_pool_mock, self.app.add_pool = self.coroutine_mock()
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.pool.Add', b'dom0',
b'driver1', b'param1=value\nparam2=some-value\n')
self.assertEqual(mock_drivers.mock_calls, [unittest.mock.call()])
@@ -892,7 +894,7 @@ def test_160_pool_add_existing_pool(self, mock_parameters, mock_drivers):
add_pool_mock, self.app.add_pool = self.coroutine_mock()
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.pool.Add', b'dom0',
b'driver1', b'name=file\nparam1=value\nparam2=some-value\n')
self.assertEqual(mock_drivers.mock_calls, [unittest.mock.call()])
@@ -918,7 +920,7 @@ def test_160_pool_add_invalid_config_format(self, mock_parameters,
add_pool_mock, self.app.add_pool = self.coroutine_mock()
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.pool.Add', b'dom0',
b'driver1', b'name=test-pool\nparam 1=value\n_param2\n')
self.assertEqual(mock_drivers.mock_calls, [unittest.mock.call()])
@@ -946,7 +948,7 @@ def test_170_pool_remove_invalid_pool(self):
'test-pool': unittest.mock.Mock(),
}
remove_pool_mock, self.app.remove_pool = self.coroutine_mock()
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.pool.Remove', b'dom0',
b'no-such-pool')
self.assertEqual(remove_pool_mock.mock_calls, [])
@@ -1003,7 +1005,7 @@ def test_200_label_create_invalid_color(self):
'keys.return_value': range(1, 9),
}
self.app.labels.configure_mock(**labels_config)
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.label.Create', b'dom0', b'cyan',
b'abcd')
self.assertEqual(self.app.get_label.mock_calls,
@@ -1019,13 +1021,13 @@ def test_200_label_create_invalid_name(self):
'keys.return_value': range(1, 9),
}
self.app.labels.configure_mock(**labels_config)
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.label.Create', b'dom0', b'01',
b'0xff0000')
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.label.Create', b'dom0', b'../xxx',
b'0xff0000')
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.label.Create', b'dom0',
b'strange-name!@#$',
b'0xff0000')
@@ -1067,7 +1069,7 @@ def test_210_label_remove_default_label(self):
self.app.labels = unittest.mock.MagicMock(wraps=self.app.labels)
self.app.get_label = unittest.mock.Mock(
**{'return_value.index': 6})
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.label.Remove', b'dom0',
b'blue')
self.assertEqual(self.app.labels.mock_calls, [])
@@ -1077,7 +1079,7 @@ def test_210_label_remove_in_use(self):
self.app.labels = unittest.mock.MagicMock(wraps=self.app.labels)
self.app.get_label = unittest.mock.Mock(
**{'return_value.index': 1})
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.label.Remove', b'dom0',
b'red')
self.assertEqual(self.app.labels.mock_calls, [])
@@ -1149,7 +1151,7 @@ def test_234_shutdown_force_wait_invalid(self):
async def coroutine_mock(*args, **kwargs):
return func_mock(*args, **kwargs)
self.vm.shutdown = coroutine_mock
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.vm.Shutdown', b'test-vm1', b'forcewait')
func_mock.assert_not_called()
@@ -1267,7 +1269,7 @@ async def fire_event():
mgmt_obj.execute(untrusted_payload=b''))
event_task = asyncio.ensure_future(fire_event())
loop.run_until_complete(execute_task)
- vm2 = event_task.result()
+ event_task.result()
self.assertIsNone(execute_task.result())
self.assertEqual(send_event.mock_calls,
[
@@ -1496,7 +1498,7 @@ def test_334_vm_create_invalid_name(self, storage_mock):
@unittest.mock.patch('qubes.storage.Storage.create')
def test_335_vm_create_missing_name(self, storage_mock):
storage_mock.side_effect = self.dummy_coro
- with self.assertRaises(qubes.api.ProtocolError):
+ with self.assertRaises(qubes.exc.ProtocolError):
self.call_mgmt_func(b'admin.vm.Create.AppVM',
b'dom0', b'test-template', b'label=red')
@@ -1505,7 +1507,7 @@ def test_335_vm_create_missing_name(self, storage_mock):
@unittest.mock.patch('qubes.storage.Storage.create')
def test_336_vm_create_spurious_pool(self, storage_mock):
storage_mock.side_effect = self.dummy_coro
- with self.assertRaises(qubes.api.ProtocolError):
+ with self.assertRaises(qubes.exc.ProtocolError):
self.call_mgmt_func(b'admin.vm.Create.AppVM',
b'dom0', b'test-template',
b'name=test-vm2 label=red pool=default')
@@ -1526,7 +1528,7 @@ def test_337_vm_create_duplicate_name(self, storage_mock):
@unittest.mock.patch('qubes.storage.Storage.create')
def test_338_vm_create_name_twice(self, storage_mock):
storage_mock.side_effect = self.dummy_coro
- with self.assertRaises(qubes.api.ProtocolError):
+ with self.assertRaises(qubes.exc.ProtocolError):
self.call_mgmt_func(b'admin.vm.Create.AppVM',
b'dom0', b'test-template',
b'name=test-vm2 name=test-vm3 label=red')
@@ -1608,7 +1610,7 @@ def test_343_vm_create_in_pool_invalid_pool2(self, storage_mock):
@unittest.mock.patch('qubes.storage.Storage.create')
def test_344_vm_create_in_pool_invalid_volume(self, storage_mock):
storage_mock.side_effect = self.dummy_coro
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.vm.CreateInPool.AppVM',
b'dom0', b'test-template', b'name=test-vm2 label=red '
b'pool:invalid=test')
@@ -1634,7 +1636,7 @@ def test_346_vm_create_in_pool_duplicate_pool(self, storage_mock):
# setting custom pool for 'root' volume of AppVM should not be
# allowed - this volume belongs to the template
storage_mock.side_effect = self.dummy_coro
- with self.assertRaises(qubes.api.ProtocolError):
+ with self.assertRaises(qubes.exc.ProtocolError):
self.call_mgmt_func(b'admin.vm.CreateInPool.AppVM',
b'dom0', b'test-template', b'name=test-vm2 label=red '
b'pool=test pool:root=test')
@@ -1687,30 +1689,52 @@ def test_450_property_reset(self):
def device_list_testclass(self, vm, event):
if vm is not self.vm:
return
- dev = qubes.devices.DeviceInfo(self.vm, '1234')
- dev.description = 'Some device'
+ dev = qubes.device_protocol.DeviceInfo(
+ self.vm, '1234', product='Some device')
dev.extra_prop = 'xx'
yield dev
- dev = qubes.devices.DeviceInfo(self.vm, '4321')
- dev.description = 'Some other device'
+ dev = qubes.device_protocol.DeviceInfo(
+ self.vm, '4321', product='Some other device')
yield dev
+ def assertSerializedEqual(self, actual, expected):
+ """
+ Works only for props without spaces!
+ """
+ actual = sorted(actual.split(' '))
+ expected = sorted(expected.split(' '))
+ self.assertEqual(actual, expected)
+
def test_460_vm_device_available(self):
self.vm.add_handler('device-list:testclass', self.device_list_testclass)
value = self.call_mgmt_func(b'admin.vm.device.testclass.Available',
b'test-vm1')
- self.assertEqual(value,
- '1234 extra_prop=xx description=Some '
- 'device\n'
- '4321 description=Some other device\n')
+ # the only props with sapces
+ value = value.replace("'Some device'", "'Some_device'")
+ value = value.replace("'Some other device'", "'Some_other_device'")
+ self.assertSerializedEqual(value,
+ "1234 serial='unknown' manufacturer='unknown' "
+ "self_identity='0000:0000::?******' vendor='unknown' "
+ "devclass='peripheral' product='Some_device' ident='1234' "
+ "name='unknown' backend_domain='test-vm1' interfaces='?******'\n"
+ "4321 serial='unknown' manufacturer='unknown' "
+ "self_identity='0000:0000::?******' vendor='unknown' "
+ "devclass='peripheral' product='Some_other_device' "
+ "ident='4321' name='unknown' backend_domain='test-vm1' "
+ "interfaces='?******'\n")
self.assertFalse(self.app.save.called)
def test_461_vm_device_available_specific(self):
self.vm.add_handler('device-list:testclass', self.device_list_testclass)
value = self.call_mgmt_func(b'admin.vm.device.testclass.Available',
b'test-vm1', b'4321')
- self.assertEqual(value,
- '4321 description=Some other device\n')
+ value = value.replace("'Some other device'", "'Some_other_device'")
+ self.assertSerializedEqual(value,
+ "4321 serial='unknown' manufacturer='unknown' "
+ "self_identity='0000:0000::?******' vendor='unknown' "
+ "devclass='peripheral' product='Some_other_device' "
+ "ident='4321' name='unknown' backend_domain='test-vm1' "
+ "interfaces='?******'\n")
self.assertFalse(self.app.save.called)
def test_462_vm_device_available_invalid(self):
@@ -1720,156 +1744,205 @@ def test_462_vm_device_available_invalid(self):
self.assertEqual(value, '')
self.assertFalse(self.app.save.called)
- def test_470_vm_device_list_persistent(self):
- assignment = qubes.devices.DeviceAssignment(self.vm, '1234',
- persistent=True)
+ def test_470_vm_device_list_assigned(self):
+ assignment = qubes.device_protocol.DeviceAssignment(self.vm, '1234',
+ attach_automatically=True, required=True)
self.loop.run_until_complete(
- self.vm.devices['testclass'].attach(assignment))
- value = self.call_mgmt_func(b'admin.vm.device.testclass.List',
+ self.vm.devices['testclass'].assign(assignment))
+ value = self.call_mgmt_func(b'admin.vm.device.testclass.Assigned',
b'test-vm1')
self.assertEqual(value,
- 'test-vm1+1234 persistent=yes\n')
+ "test-vm1+1234 required='yes' attach_automatically='yes' "
+ "ident='1234' devclass='testclass' backend_domain='test-vm1'\n")
self.assertFalse(self.app.save.called)
- def test_471_vm_device_list_persistent_options(self):
- assignment = qubes.devices.DeviceAssignment(self.vm, '1234',
- persistent=True, options={'opt1': 'value'})
+ def test_471_vm_device_list_assigned_options(self):
+ assignment = qubes.device_protocol.DeviceAssignment(self.vm, '1234',
+ attach_automatically=True, required=True, options={'opt1': 'value'})
self.loop.run_until_complete(
- self.vm.devices['testclass'].attach(assignment))
- assignment = qubes.devices.DeviceAssignment(self.vm, '4321',
- persistent=True)
+ self.vm.devices['testclass'].assign(assignment))
+ assignment = qubes.device_protocol.DeviceAssignment(self.vm, '4321',
+ attach_automatically=True, required=True)
self.loop.run_until_complete(
- self.vm.devices['testclass'].attach(assignment))
- value = self.call_mgmt_func(b'admin.vm.device.testclass.List',
+ self.vm.devices['testclass'].assign(assignment))
+ value = self.call_mgmt_func(b'admin.vm.device.testclass.Assigned',
b'test-vm1')
self.assertEqual(value,
- 'test-vm1+1234 opt1=value persistent=yes\n'
- 'test-vm1+4321 persistent=yes\n')
+ "test-vm1+1234 required='yes' attach_automatically='yes' "
+ "ident='1234' devclass='testclass' backend_domain='test-vm1' "
+ "_opt1='value'\n"
+ "test-vm1+4321 required='yes' attach_automatically='yes' "
+ "ident='4321' devclass='testclass' backend_domain='test-vm1'\n")
self.assertFalse(self.app.save.called)
- def device_list_attached_testclass(self, vm, event, **kwargs):
+ def device_list_single_attached_testclass(self, vm, event, **kwargs):
if vm is not self.vm:
return
- dev = qubes.devices.DeviceInfo(self.vm, '1234')
+ dev = qubes.device_protocol.DeviceInfo(self.vm, '1234', 'testclass')
yield (dev, {'attach_opt': 'value'})
- def test_472_vm_device_list_temporary(self):
+ def test_472_vm_device_list_attached(self):
self.vm.add_handler('device-list-attached:testclass',
- self.device_list_attached_testclass)
- value = self.call_mgmt_func(b'admin.vm.device.testclass.List',
+ self.device_list_single_attached_testclass)
+ value = self.call_mgmt_func(b'admin.vm.device.testclass.Attached',
b'test-vm1')
self.assertEqual(value,
- 'test-vm1+1234 attach_opt=value persistent=no\n')
+ "test-vm1+1234 required='no' attach_automatically='no' "
+ "ident='1234' devclass='testclass' backend_domain='test-vm1' "
+ "frontend_domain='test-vm1' _attach_opt='value'\n")
self.assertFalse(self.app.save.called)
- def test_473_vm_device_list_mixed(self):
- self.vm.add_handler('device-list-attached:testclass',
- self.device_list_attached_testclass)
- assignment = qubes.devices.DeviceAssignment(self.vm, '4321',
- persistent=True)
+ def test_473_vm_device_list_assigned_specific(self):
+ assignment = qubes.device_protocol.DeviceAssignment(
+ self.vm, '1234', attach_automatically=True, required=True)
self.loop.run_until_complete(
- self.vm.devices['testclass'].attach(assignment))
- value = self.call_mgmt_func(b'admin.vm.device.testclass.List',
- b'test-vm1')
+ self.vm.devices['testclass'].assign(assignment))
+ assignment = qubes.device_protocol.DeviceAssignment(
+ self.vm, '4321', attach_automatically=True, required=True)
+ self.loop.run_until_complete(
+ self.vm.devices['testclass'].assign(assignment))
+ value = self.call_mgmt_func(b'admin.vm.device.testclass.Assigned',
+ b'test-vm1', b'test-vm1+1234')
self.assertEqual(value,
- 'test-vm1+1234 attach_opt=value persistent=no\n'
- 'test-vm1+4321 persistent=yes\n')
+ "test-vm1+1234 required='yes' attach_automatically='yes' "
+ "ident='1234' devclass='testclass' backend_domain='test-vm1'\n")
self.assertFalse(self.app.save.called)
- def test_474_vm_device_list_specific(self):
+ def device_list_multiple_attached_testclass(self, vm, event, **kwargs):
+ if vm is not self.vm:
+ return
+ dev = qubes.device_protocol.DeviceInfo(self.vm, '1234', 'testclass')
+ yield (dev, {'attach_opt': 'value'})
+ dev = qubes.device_protocol.DeviceInfo(self.vm, '4321', 'testclass')
+ yield (dev, {'attach_opt': 'value'})
+
+ def test_474_vm_device_list_attached_specific(self):
self.vm.add_handler('device-list-attached:testclass',
- self.device_list_attached_testclass)
- assignment = qubes.devices.DeviceAssignment(self.vm, '4321',
- persistent=True)
- self.loop.run_until_complete(
- self.vm.devices['testclass'].attach(assignment))
- value = self.call_mgmt_func(b'admin.vm.device.testclass.List',
+ self.device_list_multiple_attached_testclass)
+ value = self.call_mgmt_func(b'admin.vm.device.testclass.Attached',
b'test-vm1', b'test-vm1+1234')
self.assertEqual(value,
- 'test-vm1+1234 attach_opt=value persistent=no\n')
+ "test-vm1+1234 required='no' attach_automatically='no' "
+ "ident='1234' devclass='testclass' backend_domain='test-vm1' "
+ "frontend_domain='test-vm1' _attach_opt='value'\n")
self.assertFalse(self.app.save.called)
def test_480_vm_device_attach(self):
self.vm.add_handler('device-list:testclass', self.device_list_testclass)
- mock_attach = unittest.mock.Mock()
- mock_attach.return_value = None
- del mock_attach._is_coroutine
- self.vm.add_handler('device-attach:testclass', mock_attach)
+ mock_action = unittest.mock.Mock()
+ mock_action.return_value = None
+ del mock_action._is_coroutine
+ self.vm.add_handler(f'device-attach:testclass', mock_action)
with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
'is_halted', lambda _: False):
value = self.call_mgmt_func(b'admin.vm.device.testclass.Attach',
- b'test-vm1', b'test-vm1+1234')
+ b'test-vm1', b'test-vm1+1234')
self.assertIsNone(value)
- mock_attach.assert_called_once_with(self.vm, 'device-attach:testclass',
+ mock_action.assert_called_once_with(
+ self.vm, f'device-attach:testclass',
device=self.vm.devices['testclass']['1234'],
options={})
- self.assertEqual(len(self.vm.devices['testclass'].persistent()), 0)
+ self.assertEqual(
+ len(list(self.vm.devices['testclass'].get_assigned_devices())),
+ 0)
+ self.assertFalse(self.app.save.called)
+
+ def test_481_vm_device_assign(self):
+ self.vm.add_handler('device-list:testclass', self.device_list_testclass)
+ mock_action = unittest.mock.Mock()
+ mock_action.return_value = None
+ del mock_action._is_coroutine
+ self.vm.add_handler(f'device-assign:testclass', mock_action)
+ with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
+ 'is_halted', lambda _: False):
+ value = self.call_mgmt_func(
+ b'admin.vm.device.testclass.Assign',
+ b'test-vm1', b'test-vm1+1234',
+ b"attach_automatically='yes'")
+ self.assertIsNone(value)
+ mock_action.assert_called_once_with(
+ self.vm, f'device-assign:testclass',
+ device=self.vm.devices['testclass']['1234'],
+ options={})
+ self.assertEqual(
+ len(list(self.vm.devices['testclass'].get_assigned_devices())),
+ 1)
self.app.save.assert_called_once_with()
- def test_481_vm_device_attach(self):
+ def test_483_vm_device_assign_required(self):
self.vm.add_handler('device-list:testclass', self.device_list_testclass)
- mock_attach = unittest.mock.Mock()
- mock_attach.return_value = None
- del mock_attach._is_coroutine
- self.vm.add_handler('device-attach:testclass', mock_attach)
+ mock_action = unittest.mock.Mock()
+ mock_action.return_value = None
+ del mock_action._is_coroutine
+ self.vm.add_handler(f'device-assign:testclass', mock_action)
with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
'is_halted', lambda _: False):
- value = self.call_mgmt_func(b'admin.vm.device.testclass.Attach',
- b'test-vm1', b'test-vm1+1234', b'persistent=no')
+ value = self.call_mgmt_func(
+ b'admin.vm.device.testclass.Assign',
+ b'test-vm1', b'test-vm1+1234',
+ b"attach_automatically='yes' required='yes'")
self.assertIsNone(value)
- mock_attach.assert_called_once_with(self.vm, 'device-attach:testclass',
+ mock_action.assert_called_once_with(
+ self.vm, f'device-assign:testclass',
device=self.vm.devices['testclass']['1234'],
options={})
- self.assertEqual(len(self.vm.devices['testclass'].persistent()), 0)
+ self.assertEqual(
+ len(list(self.vm.devices['testclass'].get_assigned_devices())),
+ 1)
self.app.save.assert_called_once_with()
- def test_482_vm_device_attach_not_running(self):
+ def test_484_vm_device_attach_not_running(self):
self.vm.add_handler('device-list:testclass', self.device_list_testclass)
- mock_attach = unittest.mock.Mock()
- del mock_attach._is_coroutine
- self.vm.add_handler('device-attach:testclass', mock_attach)
+ mock_action = unittest.mock.Mock()
+ del mock_action._is_coroutine
+ self.vm.add_handler('device-attach:testclass', mock_action)
with self.assertRaises(qubes.exc.QubesVMNotRunningError):
self.call_mgmt_func(b'admin.vm.device.testclass.Attach',
b'test-vm1', b'test-vm1+1234')
- self.assertFalse(mock_attach.called)
- self.assertEqual(len(self.vm.devices['testclass'].persistent()), 0)
+ self.assertFalse(mock_action.called)
+ self.assertEqual(
+ len(list(self.vm.devices['testclass'].get_assigned_devices())), 0)
self.assertFalse(self.app.save.called)
- def test_483_vm_device_attach_persistent(self):
+ def test_485_vm_device_assign_not_running(self):
self.vm.add_handler('device-list:testclass', self.device_list_testclass)
- mock_attach = unittest.mock.Mock()
- mock_attach.return_value = None
- del mock_attach._is_coroutine
- self.vm.add_handler('device-attach:testclass', mock_attach)
+ mock_action = unittest.mock.Mock()
+ mock_action.return_value = None
+ del mock_action._is_coroutine
+ self.vm.add_handler('device-assign:testclass', mock_action)
with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
- 'is_halted', lambda _: False):
- value = self.call_mgmt_func(b'admin.vm.device.testclass.Attach',
- b'test-vm1', b'test-vm1+1234', b'persistent=yes')
- self.assertIsNone(value)
- dev = self.vm.devices['testclass']['1234']
- mock_attach.assert_called_once_with(self.vm, 'device-attach:testclass',
- device=dev,
+ 'is_halted', lambda _: False):
+ self.call_mgmt_func(b'admin.vm.device.testclass.Assign',
+ b'test-vm1', b'test-vm1+1234',
+ b"attach_automatically='yes'")
+ mock_action.assert_called_once_with(
+ self.vm, f'device-assign:testclass',
+ device=self.vm.devices['testclass']['1234'],
options={})
- self.assertIn(dev, self.vm.devices['testclass'].persistent())
+ self.assertEqual(
+ len(list(self.vm.devices['testclass'].get_assigned_devices())), 1)
self.app.save.assert_called_once_with()
- def test_484_vm_device_attach_persistent_not_running(self):
+ def test_486_vm_device_assign_required_not_running(self):
self.vm.add_handler('device-list:testclass', self.device_list_testclass)
- mock_attach = unittest.mock.Mock()
- mock_attach.return_value = None
- del mock_attach._is_coroutine
- self.vm.add_handler('device-attach:testclass', mock_attach)
- value = self.call_mgmt_func(b'admin.vm.device.testclass.Attach',
- b'test-vm1', b'test-vm1+1234', b'persistent=yes')
- self.assertIsNone(value)
- dev = self.vm.devices['testclass']['1234']
- mock_attach.assert_called_once_with(self.vm, 'device-attach:testclass',
- device=dev,
+ mock_action = unittest.mock.Mock()
+ mock_action.return_value = None
+ del mock_action._is_coroutine
+ self.vm.add_handler('device-assign:testclass', mock_action)
+ with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
+ 'is_halted', lambda _: False):
+ self.call_mgmt_func(b'admin.vm.device.testclass.Assign',
+ b'test-vm1', b'test-vm1+1234',
+ b"attach_automatically='yes' required='yes'")
+ mock_action.assert_called_once_with(
+ self.vm, f'device-assign:testclass',
+ device=self.vm.devices['testclass']['1234'],
options={})
- self.assertIn(dev, self.vm.devices['testclass'].persistent())
+ self.assertEqual(
+ len(list(self.vm.devices['testclass'].get_assigned_devices())), 1)
self.app.save.assert_called_once_with()
- def test_485_vm_device_attach_options(self):
+ def test_487_vm_device_attach_options(self):
self.vm.add_handler('device-list:testclass', self.device_list_testclass)
mock_attach = unittest.mock.Mock()
mock_attach.return_value = None
@@ -1878,18 +1951,149 @@ def test_485_vm_device_attach_options(self):
with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
'is_halted', lambda _: False):
value = self.call_mgmt_func(b'admin.vm.device.testclass.Attach',
- b'test-vm1', b'test-vm1+1234', b'option1=value2')
+ b'test-vm1', b'test-vm1+1234', b"_option1='value2'")
self.assertIsNone(value)
dev = self.vm.devices['testclass']['1234']
- mock_attach.assert_called_once_with(self.vm, 'device-attach:testclass',
- device=dev,
+ mock_attach.assert_called_once_with(
+ self.vm, 'device-attach:testclass', device=dev,
options={'option1': 'value2'})
+ self.assertFalse(self.app.save.called)
+
+ def test_488_vm_device_assign_options(self):
+ self.vm.add_handler('device-list:testclass',
+ self.device_list_testclass)
+ mock_attach = unittest.mock.Mock()
+ mock_attach.return_value = None
+ del mock_attach._is_coroutine
+ self.vm.add_handler('device-assign:testclass', mock_attach)
+ with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
+ 'is_halted', lambda _: False):
+ value = self.call_mgmt_func(
+ b'admin.vm.device.testclass.Assign',
+ b'test-vm1', b'test-vm1+1234',
+ b"attach_automatically='yes' _option1='value2'")
+ self.assertIsNone(value)
+ dev = self.vm.devices['testclass']['1234']
+ mock_attach.assert_called_once_with(
+ self.vm, 'device-assign:testclass', device=dev,
+ options={'option1': 'value2'})
+ self.app.save.assert_called_once_with()
+
+ def test_490_vm_device_unassign_from_running(self):
+ assignment = qubes.device_protocol.DeviceAssignment(
+ self.vm, '1234', attach_automatically=True, required=False,
+ options={'opt1': 'value'})
+ self.loop.run_until_complete(
+ self.vm.devices['testclass'].assign(assignment))
+ mock_action = unittest.mock.Mock()
+ mock_action.return_value = None
+ del mock_action._is_coroutine
+ self.vm.add_handler('device-unassign:testclass', mock_action)
+ with unittest.mock.patch.object(
+ qubes.vm.qubesvm.QubesVM, 'is_halted', lambda _: False):
+ value = self.call_mgmt_func(b'admin.vm.device.testclass.Unassign',
+ b'test-vm1', b'test-vm1+1234')
+ self.assertIsNone(value)
+ mock_action.assert_called_once_with(
+ self.vm, 'device-unassign:testclass',
+ device=self.vm.devices['testclass']['1234'])
+ self.app.save.assert_called_once_with()
+
+ def test_491_vm_device_unassign_required_from_running(self):
+ assignment = qubes.device_protocol.DeviceAssignment(
+ self.vm, '1234', attach_automatically=True, required=True,
+ options={'opt1': 'value'})
+ self.loop.run_until_complete(
+ self.vm.devices['testclass'].assign(assignment))
+ mock_action = unittest.mock.Mock()
+ mock_action.return_value = None
+ del mock_action._is_coroutine
+ self.vm.add_handler('device-unassign:testclass', mock_action)
+ with unittest.mock.patch.object(
+ qubes.vm.qubesvm.QubesVM, 'is_halted', lambda _: False):
+ value = self.call_mgmt_func(
+ b'admin.vm.device.testclass.Unassign',
+ b'test-vm1', b'test-vm1+1234')
+ self.assertIsNone(value)
+ mock_action.assert_called_once_with(
+ self.vm, 'device-unassign:testclass',
+ device=self.vm.devices['testclass']['1234'])
+ self.app.save.assert_called_once_with()
+
+ def test_492_vm_device_unassign_from_halted(self):
+ assignment = qubes.device_protocol.DeviceAssignment(
+ self.vm, '1234', attach_automatically=True, required=False,
+ options={'opt1': 'value'})
+ self.loop.run_until_complete(
+ self.vm.devices['testclass'].assign(assignment))
+ mock_action = unittest.mock.Mock()
+ mock_action.return_value = None
+ del mock_action._is_coroutine
+ self.vm.add_handler('device-unassign:testclass', mock_action)
+ self.call_mgmt_func(
+ b'admin.vm.device.testclass.Unassign',
+ b'test-vm1', b'test-vm1+1234')
+ mock_action.assert_called_once_with(
+ self.vm, 'device-unassign:testclass',
+ device=self.vm.devices['testclass']['1234'])
self.app.save.assert_called_once_with()
- def test_490_vm_device_detach(self):
+ def test_493_vm_device_unassign_required_from_halted(self):
+ assignment = qubes.device_protocol.DeviceAssignment(
+ self.vm, '1234', attach_automatically=True, required=True,
+ options={'opt1': 'value'})
+ self.loop.run_until_complete(
+ self.vm.devices['testclass'].assign(assignment))
+ mock_action = unittest.mock.Mock()
+ mock_action.return_value = None
+ del mock_action._is_coroutine
+ self.vm.add_handler('device-unassign:testclass', mock_action)
+ self.call_mgmt_func(
+ b'admin.vm.device.testclass.Unassign',
+ b'test-vm1', b'test-vm1+1234')
+ mock_action.assert_called_once_with(
+ self.vm, 'device-unassign:testclass',
+ device=self.vm.devices['testclass']['1234'])
+ self.app.save.assert_called_once_with()
+
+ def test_494_vm_device_unassign_attached(self):
self.vm.add_handler('device-list:testclass', self.device_list_testclass)
self.vm.add_handler('device-list-attached:testclass',
- self.device_list_attached_testclass)
+ self.device_list_single_attached_testclass)
+ assignment = qubes.device_protocol.DeviceAssignment(
+ self.vm, '1234', attach_automatically=True, required=False,
+ options={'opt1': 'value'})
+ self.loop.run_until_complete(
+ self.vm.devices['testclass'].assign(assignment))
+ mock_action = unittest.mock.Mock()
+ mock_action.return_value = None
+ del mock_action._is_coroutine
+ self.vm.add_handler('device-unassign:testclass', mock_action)
+ self.call_mgmt_func(
+ b'admin.vm.device.testclass.Unassign',
+ b'test-vm1', b'test-vm1+1234')
+ mock_action.assert_called_once_with(
+ self.vm, 'device-unassign:testclass',
+ device=self.vm.devices['testclass']['1234'])
+ self.app.save.assert_called_once_with()
+
+ def test_495_vm_device_unassign_not_assigned(self):
+ mock_detach = unittest.mock.Mock()
+ mock_detach.return_value = None
+ del mock_detach._is_coroutine
+ self.vm.add_handler('device-detach:testclass', mock_detach)
+ with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
+ 'is_halted', lambda _: False):
+ with self.assertRaises(qubes.devices.DeviceNotAssigned):
+ self.call_mgmt_func(b'admin.vm.device.testclass.Detach',
+ b'test-vm1', b'test-vm1+1234')
+ self.assertFalse(mock_detach.called)
+ self.assertFalse(self.app.save.called)
+
+ def test_496_vm_device_detach(self):
+ self.vm.add_handler('device-list:testclass', self.device_list_testclass)
+ self.vm.add_handler('device-list-attached:testclass',
+ self.device_list_single_attached_testclass)
mock_detach = unittest.mock.Mock()
mock_detach.return_value = None
del mock_detach._is_coroutine
@@ -1901,16 +2105,16 @@ def test_490_vm_device_detach(self):
self.assertIsNone(value)
mock_detach.assert_called_once_with(self.vm, 'device-detach:testclass',
device=self.vm.devices['testclass']['1234'])
- self.app.save.assert_called_once_with()
+ self.assertFalse(self.app.save.called)
- def test_491_vm_device_detach_not_attached(self):
+ def test_497_vm_device_detach_not_attached(self):
mock_detach = unittest.mock.Mock()
mock_detach.return_value = None
del mock_detach._is_coroutine
self.vm.add_handler('device-detach:testclass', mock_detach)
with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
'is_halted', lambda _: False):
- with self.assertRaises(qubes.devices.DeviceNotAttached):
+ with self.assertRaises(qubes.devices.DeviceNotAssigned):
self.call_mgmt_func(b'admin.vm.device.testclass.Detach',
b'test-vm1', b'test-vm1+1234')
self.assertFalse(mock_detach.called)
@@ -1943,10 +2147,10 @@ def test_501_vm_remove_running(self, mock_rmtree, mock_remove):
@unittest.mock.patch('shutil.rmtree')
def test_502_vm_remove_attached(self, mock_rmtree, mock_remove):
self.setup_for_clone()
- assignment = qubes.devices.DeviceAssignment(
- self.vm, '1234', persistent=True)
+ assignment = qubes.device_protocol.DeviceAssignment(
+ self.vm, '1234', attach_automatically=True, required=True)
self.loop.run_until_complete(
- self.vm2.devices['testclass'].attach(assignment))
+ self.vm2.devices['testclass'].assign(assignment))
mock_remove.side_effect = self.dummy_coro
with self.assertRaises(qubes.exc.QubesVMInUseError):
@@ -2075,7 +2279,7 @@ def test_520_vm_volume_clone(self):
def test_521_vm_volume_clone_invalid_volume(self):
self.setup_for_clone()
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.vm.volume.CloneFrom',
b'test-vm1', b'private123', b'')
self.assertNotIn('init_volume().import_volume',
@@ -2087,7 +2291,7 @@ def test_522_vm_volume_clone_invalid_volume2(self):
token = self.call_mgmt_func(b'admin.vm.volume.CloneFrom',
b'test-vm1', b'private', b'')
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.vm.volume.CloneTo',
b'test-vm1', b'private123', token.encode())
self.assertNotIn('init_volume().import_volume',
@@ -2105,7 +2309,7 @@ def get_volume(vid):
else:
return unittest.mock.DEFAULT
self.pool.get_volume.side_effect = get_volume
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.vm.volume.CloneTo',
b'test-vm1', b'private', token.encode())
self.assertNotIn('init_volume().import_volume',
@@ -2115,7 +2319,7 @@ def get_volume(vid):
def test_524_vm_volume_clone_invlid_token(self):
self.setup_for_clone()
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'admin.vm.volume.CloneTo',
b'test-vm1', b'private', b'no-such-token')
self.assertNotIn('init_volume().import_volume',
@@ -2631,88 +2835,114 @@ def test_642_vm_create_disposable_not_allowed(self, storage_mock):
b'test-vm1')
self.assertFalse(self.app.save.called)
- def test_650_vm_device_set_persistent_true(self):
- self.vm.add_handler('device-list:testclass',
- self.device_list_testclass)
- self.vm.add_handler('device-list-attached:testclass',
- self.device_list_attached_testclass)
+ def test_650_vm_device_set_required_true(self):
+ assignment = qubes.device_protocol.DeviceAssignment(
+ self.vm, '1234', attach_automatically=True, required=False,
+ options={'opt1': 'value'})
+ self.loop.run_until_complete(
+ self.vm.devices['testclass'].assign(assignment))
+ mock_action = unittest.mock.Mock()
+ mock_action.return_value = None
+ del mock_action._is_coroutine
+ self.vm.add_handler(f'device-assignment-changed:testclass', mock_action)
+
with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
'is_halted', lambda _: False):
value = self.call_mgmt_func(
- b'admin.vm.device.testclass.Set.persistent',
+ b'admin.vm.device.testclass.Set.required',
b'test-vm1', b'test-vm1+1234', b'True')
+
self.assertIsNone(value)
- dev = qubes.devices.DeviceInfo(self.vm, '1234')
- self.assertIn(dev, self.vm.devices['testclass'].persistent())
+ dev = qubes.device_protocol.DeviceInfo(self.vm, '1234')
+ required = self.vm.devices['testclass'].get_assigned_devices(
+ required_only=True)
+ self.assertIn(dev, required)
+ self.assertEventFired(
+ self.emitter,
+ 'admin-permission:admin.vm.device.testclass.Set.required')
+ mock_action.assert_called_once_with(
+ self.vm, f'device-assignment-changed:testclass',
+ device=self.vm.devices['testclass']['1234'])
self.app.save.assert_called_once_with()
- def test_651_vm_device_set_persistent_false_unchanged(self):
- self.vm.add_handler('device-list:testclass',
- self.device_list_testclass)
- self.vm.add_handler('device-list-attached:testclass',
- self.device_list_attached_testclass)
+ def test_651_vm_device_set_required_false(self):
+ assignment = qubes.device_protocol.DeviceAssignment(
+ self.vm, '1234', attach_automatically=True, required=True,
+ options={'opt1': 'value'})
+ self.loop.run_until_complete(
+ self.vm.devices['testclass'].assign(assignment))
+ mock_action = unittest.mock.Mock()
+ mock_action.return_value = None
+ del mock_action._is_coroutine
+ self.vm.add_handler(f'device-assignment-changed:testclass', mock_action)
+
with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
'is_halted', lambda _: False):
value = self.call_mgmt_func(
- b'admin.vm.device.testclass.Set.persistent',
+ b'admin.vm.device.testclass.Set.required',
b'test-vm1', b'test-vm1+1234', b'False')
+
self.assertIsNone(value)
- dev = qubes.devices.DeviceInfo(self.vm, '1234')
- self.assertNotIn(dev, self.vm.devices['testclass'].persistent())
+ dev = qubes.device_protocol.DeviceInfo(self.vm, '1234')
+ required = self.vm.devices['testclass'].get_assigned_devices(
+ required_only=True)
+ self.assertNotIn(dev, required)
+ self.assertEventFired(
+ self.emitter,
+ 'admin-permission:admin.vm.device.testclass.Set.required')
+ mock_action.assert_called_once_with(
+ self.vm, f'device-assignment-changed:testclass',
+ device=self.vm.devices['testclass']['1234'])
self.app.save.assert_called_once_with()
- def test_652_vm_device_set_persistent_false(self):
- self.vm.add_handler('device-list:testclass',
- self.device_list_testclass)
- assignment = qubes.devices.DeviceAssignment(self.vm, '1234', {},
- True)
+ def test_652_vm_device_set_required_true_unchanged(self):
+ assignment = qubes.device_protocol.DeviceAssignment(
+ self.vm, '1234', attach_automatically=True, required=True,
+ options={'opt1': 'value'})
self.loop.run_until_complete(
- self.vm.devices['testclass'].attach(assignment))
- self.vm.add_handler('device-list-attached:testclass',
- self.device_list_attached_testclass)
- dev = qubes.devices.DeviceInfo(self.vm, '1234')
- self.assertIn(dev, self.vm.devices['testclass'].persistent())
+ self.vm.devices['testclass'].assign(assignment))
with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
'is_halted', lambda _: False):
value = self.call_mgmt_func(
- b'admin.vm.device.testclass.Set.persistent',
- b'test-vm1', b'test-vm1+1234', b'False')
+ b'admin.vm.device.testclass.Set.required',
+ b'test-vm1', b'test-vm1+1234', b'True')
self.assertIsNone(value)
- self.assertNotIn(dev, self.vm.devices['testclass'].persistent())
- self.assertIn(dev, self.vm.devices['testclass'].attached())
+ dev = qubes.device_protocol.DeviceInfo(self.vm, '1234')
+ required = self.vm.devices['testclass'].get_assigned_devices(
+ required_only=True)
+ self.assertIn(dev, required)
self.app.save.assert_called_once_with()
- def test_653_vm_device_set_persistent_true_unchanged(self):
- self.vm.add_handler('device-list:testclass',
- self.device_list_testclass)
- assignment = qubes.devices.DeviceAssignment(self.vm, '1234', {},
- True)
+ def test_653_vm_device_set_required_false_unchanged(self):
+ assignment = qubes.device_protocol.DeviceAssignment(
+ self.vm, '1234', attach_automatically=True, required=False,
+ options={'opt1': 'value'})
self.loop.run_until_complete(
- self.vm.devices['testclass'].attach(assignment))
- self.vm.add_handler('device-list-attached:testclass',
- self.device_list_attached_testclass)
+ self.vm.devices['testclass'].assign(assignment))
with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
'is_halted', lambda _: False):
value = self.call_mgmt_func(
- b'admin.vm.device.testclass.Set.persistent',
- b'test-vm1', b'test-vm1+1234', b'True')
+ b'admin.vm.device.testclass.Set.required',
+ b'test-vm1', b'test-vm1+1234', b'False')
self.assertIsNone(value)
- dev = qubes.devices.DeviceInfo(self.vm, '1234')
- self.assertIn(dev, self.vm.devices['testclass'].persistent())
- self.assertIn(dev, self.vm.devices['testclass'].attached())
+ dev = qubes.device_protocol.DeviceInfo(self.vm, '1234')
+ required = self.vm.devices['testclass'].get_assigned_devices(
+ required_only=True)
+ self.assertNotIn(dev, required)
self.app.save.assert_called_once_with()
- def test_654_vm_device_set_persistent_not_attached(self):
+ def test_654_vm_device_set_persistent_not_assigned(self):
self.vm.add_handler('device-list:testclass',
self.device_list_testclass)
with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
'is_halted', lambda _: False):
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.QubesValueError):
self.call_mgmt_func(
- b'admin.vm.device.testclass.Set.persistent',
+ b'admin.vm.device.testclass.Set.required',
b'test-vm1', b'test-vm1+1234', b'True')
- dev = qubes.devices.DeviceInfo(self.vm, '1234')
- self.assertNotIn(dev, self.vm.devices['testclass'].persistent())
+ dev = qubes.device_protocol.DeviceInfo(self.vm, '1234')
+ self.assertNotIn(
+ dev, self.vm.devices['testclass'].get_assigned_devices())
self.assertFalse(self.app.save.called)
def test_655_vm_device_set_persistent_invalid_value(self):
@@ -2720,12 +2950,12 @@ def test_655_vm_device_set_persistent_invalid_value(self):
self.device_list_testclass)
with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
'is_halted', lambda _: False):
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(
- b'admin.vm.device.testclass.Set.persistent',
+ b'admin.vm.device.testclass.Set.required',
b'test-vm1', b'test-vm1+1234', b'maybe')
- dev = qubes.devices.DeviceInfo(self.vm, '1234')
- self.assertNotIn(dev, self.vm.devices['testclass'].persistent())
+ dev = qubes.device_protocol.DeviceInfo(self.vm, '1234')
+ self.assertNotIn(dev, self.vm.devices['testclass'].get_assigned_devices())
self.assertFalse(self.app.save.called)
def test_660_pool_set_revisions_to_keep(self):
@@ -2739,7 +2969,7 @@ def test_660_pool_set_revisions_to_keep(self):
def test_661_pool_set_revisions_to_keep_negative(self):
self.app.pools['test-pool'] = unittest.mock.Mock()
- with self.assertRaises(qubes.api.ProtocolError):
+ with self.assertRaises(qubes.exc.ProtocolError):
self.call_mgmt_func(b'admin.pool.Set.revisions_to_keep',
b'dom0', b'test-pool', b'-2')
self.assertEqual(self.app.pools['test-pool'].mock_calls, [])
@@ -2747,7 +2977,7 @@ def test_661_pool_set_revisions_to_keep_negative(self):
def test_662_pool_set_revisions_to_keep_not_a_number(self):
self.app.pools['test-pool'] = unittest.mock.Mock()
- with self.assertRaises(qubes.api.ProtocolError):
+ with self.assertRaises(qubes.exc.ProtocolError):
self.call_mgmt_func(b'admin.pool.Set.revisions_to_keep',
b'dom0', b'test-pool', b'abc')
self.assertEqual(self.app.pools['test-pool'].mock_calls, [])
@@ -2764,7 +2994,7 @@ def test_663_pool_set_ephemeral(self):
def test_664_pool_set_ephemeral_not_a_boolean(self):
self.app.pools['test-pool'] = unittest.mock.Mock()
- with self.assertRaises(qubes.api.ProtocolError):
+ with self.assertRaises(qubes.exc.ProtocolError):
self.call_mgmt_func(b'admin.pool.Set.ephemeral_volatile',
b'dom0', b'test-pool', b'abc')
self.assertEqual(self.app.pools['test-pool'].mock_calls, [])
@@ -2793,7 +3023,7 @@ def test_671_vm_volume_set_revisions_to_keep_negative(self):
}
self.vm.volumes.configure_mock(**volumes_conf)
self.vm.storage = unittest.mock.Mock()
- with self.assertRaises(qubes.api.ProtocolError):
+ with self.assertRaises(qubes.exc.ProtocolError):
self.call_mgmt_func(b'admin.vm.volume.Set.revisions_to_keep',
b'test-vm1', b'private', b'-2')
@@ -2804,7 +3034,7 @@ def test_672_vm_volume_set_revisions_to_keep_not_a_number(self):
}
self.vm.volumes.configure_mock(**volumes_conf)
self.vm.storage = unittest.mock.Mock()
- with self.assertRaises(qubes.api.ProtocolError):
+ with self.assertRaises(qubes.exc.ProtocolError):
self.call_mgmt_func(b'admin.vm.volume.Set.revisions_to_keep',
b'test-vm1', b'private', b'abc')
@@ -2831,7 +3061,7 @@ def test_681_vm_volume_set_rw_invalid(self):
}
self.vm.volumes.configure_mock(**volumes_conf)
self.vm.storage = unittest.mock.Mock()
- with self.assertRaises(qubes.api.ProtocolError):
+ with self.assertRaises(qubes.exc.ProtocolError):
self.call_mgmt_func(b'admin.vm.volume.Set.rw',
b'test-vm1', b'private', b'abc')
self.assertFalse(self.app.save.called)
@@ -2859,7 +3089,7 @@ def test_686_vm_volume_set_ephemeral_invalid(self):
}
self.vm.volumes.configure_mock(**volumes_conf)
self.vm.storage = unittest.mock.Mock()
- with self.assertRaises(qubes.api.ProtocolError):
+ with self.assertRaises(qubes.exc.ProtocolError):
self.call_mgmt_func(b'admin.vm.volume.Set.ephemeral',
b'test-vm1', b'volatile', b'abc')
self.assertFalse(self.app.save.called)
@@ -3014,14 +3244,14 @@ def test_990_vm_unexpected_payload(self):
for method in methods_with_no_payload:
# should reject payload regardless of having argument or not
with self.subTest(method.decode('ascii')):
- with self.assertRaises(qubes.api.ProtocolError):
+ with self.assertRaises(qubes.exc.ProtocolError):
self.call_mgmt_func(method, b'test-vm1', b'',
b'unexpected-payload')
self.assertFalse(vm_mock.called)
self.assertFalse(self.app.save.called)
with self.subTest(method.decode('ascii') + '+arg'):
- with self.assertRaises(qubes.api.ProtocolError):
+ with self.assertRaises(qubes.exc.ProtocolError):
self.call_mgmt_func(method, b'test-vm1', b'some-arg',
b'unexpected-payload')
self.assertFalse(vm_mock.called)
@@ -3052,11 +3282,11 @@ def test_991_vm_unexpected_argument(self):
vm_mock.qid = self.vm.qid
vm_mock.__lt__ = (lambda x, y: x.qid < y.qid)
self.app.domains._dict[self.vm.qid] = vm_mock
- exceptions = (qubes.api.PermissionDenied, qubes.api.ProtocolError)
+ exceptions = (qubes.exc.PermissionDenied, qubes.exc.ProtocolError)
for method in methods_with_no_argument:
# should reject argument regardless of having payload or not
with self.subTest(method.decode('ascii')):
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(method, b'test-vm1', b'some-arg',
b'')
self.assertFalse(vm_mock.called)
@@ -3099,14 +3329,14 @@ def test_992_dom0_unexpected_payload(self):
for method in methods_with_no_payload:
# should reject payload regardless of having argument or not
with self.subTest(method.decode('ascii')):
- with self.assertRaises(qubes.api.ProtocolError):
+ with self.assertRaises(qubes.exc.ProtocolError):
self.call_mgmt_func(method, b'dom0', b'',
b'unexpected-payload')
self.assertFalse(vm_mock.called)
self.assertFalse(self.app.save.called)
with self.subTest(method.decode('ascii') + '+arg'):
- with self.assertRaises(qubes.api.ProtocolError):
+ with self.assertRaises(qubes.exc.ProtocolError):
self.call_mgmt_func(method, b'dom0', b'some-arg',
b'unexpected-payload')
self.assertFalse(vm_mock.called)
@@ -3129,11 +3359,11 @@ def test_993_dom0_unexpected_argument(self):
vm_mock.qid = self.vm.qid
vm_mock.__lt__ = (lambda x, y: x.qid < y.qid)
self.app.domains._dict[self.vm.qid] = vm_mock
- exceptions = (qubes.api.PermissionDenied, qubes.api.ProtocolError)
+ exceptions = (qubes.exc.PermissionDenied, qubes.exc.ProtocolError)
for method in methods_with_no_argument:
# should reject argument regardless of having payload or not
with self.subTest(method.decode('ascii')):
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(method, b'dom0', b'some-arg',
b'')
self.assertFalse(vm_mock.called)
@@ -3185,7 +3415,7 @@ def test_994_dom0_only_calls(self):
vm_mock.qid = self.vm.qid
vm_mock.__lt__ = (lambda x, y: x.qid < y.qid)
self.app.domains._dict[self.vm.qid] = vm_mock
- exceptions = (qubes.api.PermissionDenied, qubes.api.ProtocolError)
+ exceptions = (qubes.exc.PermissionDenied, qubes.exc.ProtocolError)
for method in methods_for_dom0_only:
# should reject call regardless of having payload or not
with self.subTest(method.decode('ascii')):
@@ -3272,7 +3502,7 @@ def test_995_vm_only_calls(self):
vm_mock.qid = self.vm.qid
vm_mock.__lt__ = (lambda x, y: x.qid < y.qid)
self.app.domains._dict[self.vm.qid] = vm_mock
- exceptions = (qubes.api.PermissionDenied, qubes.api.ProtocolError)
+ exceptions = (qubes.exc.PermissionDenied, qubes.exc.ProtocolError)
for method in methods_for_vm_only:
# should reject payload regardless of having argument or not
# should reject call regardless of having payload or not
diff --git a/qubes/tests/api_misc.py b/qubes/tests/api_misc.py
index 7c8776dae..3345cea8c 100644
--- a/qubes/tests/api_misc.py
+++ b/qubes/tests/api_misc.py
@@ -20,6 +20,8 @@
import asyncio
from datetime import datetime
from unittest import mock
+
+import qubes.exc
import qubes.tests
import qubes.api.misc
@@ -98,7 +100,7 @@ def test_002_features_request_invalid1(self):
'/features-request/feature1': b'test spaces',
}
self.configure_qdb(qdb_entries)
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'qubes.FeaturesRequest')
self.assertEqual(self.app.mock_calls, [])
self.assertEqual(self.src.mock_calls, [
@@ -180,7 +182,7 @@ def test_015_notify_tools_invalid_value_qrexec(self):
'/qubes-tools/default-user': b'user',
}
self.configure_qdb(qdb_entries)
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'qubes.NotifyTools')
self.assertEqual(self.app.mock_calls, [])
self.assertEqual(self.src.mock_calls, [
@@ -196,7 +198,7 @@ def test_016_notify_tools_invalid_value_gui(self):
'/qubes-tools/default-user': b'user',
}
self.configure_qdb(qdb_entries)
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'qubes.NotifyTools')
self.assertEqual(self.app.mock_calls, [])
self.assertEqual(self.src.mock_calls, [
@@ -242,7 +244,7 @@ def test_021_notify_updates_standalone2(self):
def test_022_notify_updates_invalid(self):
del self.src.template
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'qubes.NotifyUpdates', payload=b'')
self.assertEqual(self.src.mock_calls, [])
self.assertEqual(self.tpl.mock_calls, [])
@@ -250,7 +252,7 @@ def test_022_notify_updates_invalid(self):
def test_023_notify_updates_invalid2(self):
del self.src.template
- with self.assertRaises(qubes.api.PermissionDenied):
+ with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(b'qubes.NotifyUpdates', payload=b'no updates')
self.assertEqual(self.src.mock_calls, [])
self.assertEqual(self.tpl.mock_calls, [])
diff --git a/qubes/tests/devices.py b/qubes/tests/devices.py
index 50bb6f131..22db41c35 100644
--- a/qubes/tests/devices.py
+++ b/qubes/tests/devices.py
@@ -21,10 +21,13 @@
#
import qubes.devices
+from qubes.device_protocol import (Device, DeviceInfo, DeviceAssignment,
+ DeviceInterface, UnknownDevice)
import qubes.tests
-class TestDevice(qubes.devices.DeviceInfo):
+
+class TestDevice(DeviceInfo):
# pylint: disable=too-few-public-methods
pass
@@ -45,7 +48,7 @@ def __init__(self, app, name, *args, **kwargs):
super(TestVM, self).__init__(*args, **kwargs)
self.app = app
self.name = name
- self.device = TestDevice(self, 'testdev', 'Description')
+ self.device = TestDevice(self, 'testdev', 'testclass')
self.events_enabled = True
self.devices = {
'testclass': qubes.devices.DeviceCollection(self, 'testclass')
@@ -58,9 +61,9 @@ def __str__(self):
return self.name
@qubes.events.handler('device-list-attached:testclass')
- def dev_testclass_list_attached(self, event, persistent = False):
+ def dev_testclass_list_attached(self, event, persistent=False):
for vm in self.app.domains:
- if vm.device.frontend_domain == self:
+ if vm.device.data.get('test_frontend_domain', None) == self:
yield (vm.device, {})
@qubes.events.handler('device-list:testclass')
@@ -73,6 +76,10 @@ def is_halted(self):
def is_running(self):
return self.running
+ class log:
+ @staticmethod
+ def exception(message):
+ pass
class TC_00_DeviceCollection(qubes.tests.QubesTestCase):
@@ -83,121 +90,242 @@ def setUp(self):
self.app.domains['vm'] = self.emitter
self.device = self.emitter.device
self.collection = self.emitter.devices['testclass']
- self.assignment = qubes.devices.DeviceAssignment(
+ self.assignment = DeviceAssignment(
backend_domain=self.device.backend_domain,
ident=self.device.ident,
- persistent=True
+ attach_automatically=True,
+ required=True,
)
+ def attach(self):
+ self.emitter.running = True
+ # device-attach event not implemented, so manipulate object manually
+ self.device.data['test_frontend_domain'] = self.emitter
+
+ def detach(self):
+ # device-detach event not implemented, so manipulate object manually
+ del self.device.data['test_frontend_domain']
+
def test_000_init(self):
self.assertFalse(self.collection._set)
def test_001_attach(self):
+ self.emitter.running = True
self.loop.run_until_complete(self.collection.attach(self.assignment))
self.assertEventFired(self.emitter, 'device-pre-attach:testclass')
self.assertEventFired(self.emitter, 'device-attach:testclass')
self.assertEventNotFired(self.emitter, 'device-pre-detach:testclass')
self.assertEventNotFired(self.emitter, 'device-detach:testclass')
- def test_002_detach(self):
- self.loop.run_until_complete(self.collection.attach(self.assignment))
+ def test_002_attach_to_halted(self):
+ with self.assertRaises(qubes.exc.QubesVMNotRunningError):
+ self.loop.run_until_complete(
+ self.collection.attach(self.assignment))
+
+ def test_003_detach(self):
+ self.attach()
self.loop.run_until_complete(self.collection.detach(self.assignment))
- self.assertEventFired(self.emitter, 'device-pre-attach:testclass')
- self.assertEventFired(self.emitter, 'device-attach:testclass')
self.assertEventFired(self.emitter, 'device-pre-detach:testclass')
self.assertEventFired(self.emitter, 'device-detach:testclass')
+ def test_004_detach_from_halted(self):
+ with self.assertRaises(LookupError):
+ self.loop.run_until_complete(
+ self.collection.detach(self.assignment))
+
def test_010_empty_detach(self):
+ self.emitter.running = True
with self.assertRaises(LookupError):
self.loop.run_until_complete(
self.collection.detach(self.assignment))
- def test_011_double_attach(self):
- self.loop.run_until_complete(self.collection.attach(self.assignment))
+ def test_011_empty_unassign(self):
+ for _ in range(2):
+ with self.assertRaises(LookupError):
+ self.loop.run_until_complete(
+ self.collection.unassign(self.assignment))
+ self.emitter.running = True
+ def test_012_double_attach(self):
+ self.attach()
with self.assertRaises(qubes.devices.DeviceAlreadyAttached):
self.loop.run_until_complete(
self.collection.attach(self.assignment))
- def test_012_double_detach(self):
- self.loop.run_until_complete(self.collection.attach(self.assignment))
+ def test_013_double_detach(self):
+ self.attach()
self.loop.run_until_complete(self.collection.detach(self.assignment))
+ self.detach()
- with self.assertRaises(qubes.devices.DeviceNotAttached):
+ with self.assertRaises(qubes.devices.DeviceNotAssigned):
self.loop.run_until_complete(
self.collection.detach(self.assignment))
- def test_013_list_attached_persistent(self):
- self.assertEqual(set([]), set(self.collection.persistent()))
- self.loop.run_until_complete(self.collection.attach(self.assignment))
- self.assertEventFired(self.emitter, 'device-list-attached:testclass')
- self.assertEqual({self.device}, set(self.collection.persistent()))
+ def test_014_double_assign(self):
+ self.loop.run_until_complete(self.collection.assign(self.assignment))
+
+ with self.assertRaises(qubes.devices.DeviceAlreadyAssigned):
+ self.loop.run_until_complete(
+ self.collection.assign(self.assignment))
+
+ def test_015_double_unassign(self):
+ self.loop.run_until_complete(self.collection.assign(self.assignment))
+ self.loop.run_until_complete(self.collection.unassign(self.assignment))
+
+ with self.assertRaises(qubes.devices.DeviceNotAssigned):
+ self.loop.run_until_complete(
+ self.collection.unassign(self.assignment))
+
+ def test_016_list_assigned(self):
+ self.assertEqual(set([]), set(self.collection.get_assigned_devices()))
+ self.loop.run_until_complete(self.collection.assign(self.assignment))
+ self.assertEqual({self.device},
+ set(self.collection.get_assigned_devices()))
self.assertEqual(set([]),
- set(self.collection.attached()))
+ set(self.collection.get_attached_devices()))
+ self.assertEqual({self.device},
+ set(self.collection.get_dedicated_devices()))
- def test_014_list_attached_non_persistent(self):
- self.assignment.persistent = False
- self.emitter.running = True
- self.loop.run_until_complete(self.collection.attach(self.assignment))
- # device-attach event not implemented, so manipulate object manually
- self.device.frontend_domain = self.emitter
+ def test_017_list_attached(self):
+ self.assignment.required = False
+ self.attach()
self.assertEqual({self.device},
- set(self.collection.attached()))
+ set(self.collection.get_attached_devices()))
self.assertEqual(set([]),
- set(self.collection.persistent()))
+ set(self.collection.get_assigned_devices()))
self.assertEqual({self.device},
- set(self.collection.attached()))
+ set(self.collection.get_dedicated_devices()))
self.assertEventFired(self.emitter, 'device-list-attached:testclass')
- def test_015_list_available(self):
+ def test_018_list_available(self):
self.assertEqual({self.device}, set(self.collection))
self.assertEventFired(self.emitter, 'device-list:testclass')
- def test_020_update_persistent_to_false(self):
- self.emitter.running = True
- self.assertEqual(set([]), set(self.collection.persistent()))
- self.loop.run_until_complete(self.collection.attach(self.assignment))
- # device-attach event not implemented, so manipulate object manually
- self.device.frontend_domain = self.emitter
- self.assertEqual({self.device}, set(self.collection.persistent()))
- self.assertEqual({self.device}, set(self.collection.attached()))
- self.assertEqual({self.device}, set(self.collection.persistent()))
- self.assertEqual({self.device}, set(self.collection.attached()))
- self.collection.update_persistent(self.device, False)
- self.assertEqual(set(), set(self.collection.persistent()))
- self.assertEqual({self.device}, set(self.collection.attached()))
-
- def test_021_update_persistent_to_true(self):
- self.assignment.persistent = False
- self.emitter.running = True
- self.assertEqual(set([]), set(self.collection.persistent()))
- self.loop.run_until_complete(self.collection.attach(self.assignment))
- # device-attach event not implemented, so manipulate object manually
- self.device.frontend_domain = self.emitter
- self.assertEqual(set(), set(self.collection.persistent()))
- self.assertEqual({self.device}, set(self.collection.attached()))
- self.assertEqual(set(), set(self.collection.persistent()))
- self.assertEqual({self.device}, set(self.collection.attached()))
- self.collection.update_persistent(self.device, True)
- self.assertEqual({self.device}, set(self.collection.persistent()))
- self.assertEqual({self.device}, set(self.collection.attached()))
-
- def test_022_update_persistent_reject_not_running(self):
- self.assertEqual(set([]), set(self.collection.persistent()))
- self.loop.run_until_complete(self.collection.attach(self.assignment))
- self.assertEqual({self.device}, set(self.collection.persistent()))
- self.assertEqual(set(), set(self.collection.attached()))
+ def test_020_update_required_to_false(self):
+ self.assertEqual(set([]), set(self.collection.get_assigned_devices()))
+ self.loop.run_until_complete(self.collection.assign(self.assignment))
+ self.attach()
+ self.assertEqual(
+ {self.device},
+ set(self.collection.get_assigned_devices(required_only=True)))
+ self.assertEqual(
+ {self.device}, set(self.collection.get_assigned_devices()))
+ self.loop.run_until_complete(
+ self.collection.update_required(self.device, False))
+ self.assertEqual(
+ {self.device}, set(self.collection.get_assigned_devices()))
+ self.assertEqual(
+ {self.device}, set(self.collection.get_attached_devices()))
+
+ def test_021_update_required_to_true(self):
+ self.assignment.required = False
+ self.attach()
+ self.assertEqual(set(), set(self.collection.get_assigned_devices()))
+ self.loop.run_until_complete(self.collection.assign(self.assignment))
+ self.assertEqual(
+ set(),
+ set(self.collection.get_assigned_devices(required_only=True)))
+ self.assertEqual({self.device},
+ set(self.collection.get_attached_devices()))
+ self.assertEqual({self.device}
+ , set(self.collection.get_assigned_devices()))
+ self.assertEqual({self.device},
+ set(self.collection.get_attached_devices()))
+ self.loop.run_until_complete(
+ self.collection.update_required(self.device, True))
+ self.assertEqual({self.device},
+ set(self.collection.get_assigned_devices()))
+ self.assertEqual({self.device},
+ set(self.collection.get_attached_devices()))
+
+ def test_022_update_required_reject_not_running(self):
+ self.assertEqual(set([]), set(self.collection.get_assigned_devices()))
+ self.loop.run_until_complete(self.collection.assign(self.assignment))
+ self.assertEqual({self.device},
+ set(self.collection.get_assigned_devices()))
+ self.assertEqual(set(), set(self.collection.get_attached_devices()))
with self.assertRaises(qubes.exc.QubesVMNotStartedError):
- self.collection.update_persistent(self.device, False)
+ self.loop.run_until_complete(
+ self.collection.update_required(self.device, False))
- def test_023_update_persistent_reject_not_attached(self):
- self.assertEqual(set(), set(self.collection.persistent()))
- self.assertEqual(set(), set(self.collection.attached()))
+ def test_023_update_required_reject_not_attached(self):
+ self.assertEqual(set(), set(self.collection.get_assigned_devices()))
+ self.assertEqual(set(), set(self.collection.get_attached_devices()))
self.emitter.running = True
with self.assertRaises(qubes.exc.QubesValueError):
- self.collection.update_persistent(self.device, True)
+ self.loop.run_until_complete(
+ self.collection.update_required(self.device, True))
with self.assertRaises(qubes.exc.QubesValueError):
- self.collection.update_persistent(self.device, False)
+ self.loop.run_until_complete(
+ self.collection.update_required(self.device, False))
+
+ def test_030_assign(self):
+ self.emitter.running = True
+ self.assignment.required = False
+ self.loop.run_until_complete(self.collection.assign(self.assignment))
+ self.assertEventFired(self.emitter, 'device-assign:testclass')
+ self.assertEventNotFired(self.emitter, 'device-unassign:testclass')
+
+ def test_031_assign_to_halted(self):
+ self.assignment.required = False
+ self.loop.run_until_complete(self.collection.assign(self.assignment))
+ self.assertEventFired(self.emitter, 'device-assign:testclass')
+ self.assertEventNotFired(self.emitter, 'device-unassign:testclass')
+
+ def test_032_assign_required(self):
+ self.emitter.running = True
+ self.loop.run_until_complete(self.collection.assign(self.assignment))
+ self.assertEventFired(self.emitter, 'device-assign:testclass')
+ self.assertEventNotFired(self.emitter, 'device-unassign:testclass')
+
+ def test_033_assign_required_to_halted(self):
+ self.loop.run_until_complete(self.collection.assign(self.assignment))
+ self.assertEventFired(self.emitter, 'device-assign:testclass')
+ self.assertEventNotFired(self.emitter, 'device-unassign:testclass')
+
+ def test_034_unassign_from_halted(self):
+ self.assignment.required = False
+ self.loop.run_until_complete(self.collection.assign(self.assignment))
+ self.loop.run_until_complete(self.collection.unassign(self.assignment))
+ self.assertEventFired(self.emitter, 'device-assign:testclass')
+ self.assertEventFired(self.emitter, 'device-unassign:testclass')
+
+ def test_035_unassign(self):
+ self.emitter.running = True
+ self.assignment.required = False
+ self.loop.run_until_complete(self.collection.assign(self.assignment))
+ self.loop.run_until_complete(self.collection.unassign(self.assignment))
+ self.assertEventFired(self.emitter, 'device-assign:testclass')
+ self.assertEventFired(self.emitter, 'device-unassign:testclass')
+
+ def test_040_detach_required(self):
+ self.loop.run_until_complete(self.collection.assign(self.assignment))
+ self.attach()
+ with self.assertRaises(qubes.exc.QubesVMNotHaltedError):
+ self.loop.run_until_complete(
+ self.collection.detach(self.assignment))
+
+ def test_041_detach_required_from_halted(self):
+ self.loop.run_until_complete(self.collection.assign(self.assignment))
+ with self.assertRaises(LookupError):
+ self.loop.run_until_complete(
+ self.collection.detach(self.assignment))
+
+ def test_042_unassign_required(self):
+ self.emitter.running = True
+ self.loop.run_until_complete(self.collection.assign(self.assignment))
+ self.loop.run_until_complete(self.collection.unassign(self.assignment))
+ self.assertEventFired(self.emitter, 'device-assign:testclass')
+ self.assertEventFired(self.emitter, 'device-unassign:testclass')
+
+ def test_043_detach_assigned(self):
+ self.assignment.required = False
+ self.loop.run_until_complete(self.collection.assign(self.assignment))
+ self.attach()
+ self.loop.run_until_complete(self.collection.detach(self.assignment))
+ self.assertEventFired(self.emitter, 'device-assign:testclass')
+ self.assertEventFired(self.emitter, 'device-pre-detach:testclass')
+ self.assertEventFired(self.emitter, 'device-detach:testclass')
class TC_01_DeviceManager(qubes.tests.QubesTestCase):
@@ -212,11 +340,311 @@ def test_000_init(self):
def test_001_missing(self):
device = TestDevice(self.emitter.app.domains['vm'], 'testdev')
- assignment = qubes.devices.DeviceAssignment(
+ assignment = DeviceAssignment(
backend_domain=device.backend_domain,
ident=device.ident,
- persistent=True)
+ attach_automatically=True, required=True)
self.loop.run_until_complete(
- self.manager['testclass'].attach(assignment))
- self.assertEventFired(self.emitter, 'device-attach:testclass')
+ self.manager['testclass'].assign(assignment))
+ self.assertEqual(
+ len(list(self.manager['testclass'].get_assigned_devices())), 1)
+
+class TC_02_DeviceInfo(qubes.tests.QubesTestCase):
+ def setUp(self):
+ super().setUp()
+ self.app = TestApp()
+ self.vm = TestVM(self.app, 'vm')
+
+ def test_010_serialize(self):
+ device = DeviceInfo(
+ backend_domain=self.vm,
+ ident="1-1.1.1",
+ devclass="bus",
+ vendor="ITL",
+ product="Qubes",
+ manufacturer="",
+ name="Some untrusted garbage",
+ serial=None,
+ interfaces=[DeviceInterface(" ******"),
+ DeviceInterface("u03**01")],
+ additional_info="",
+ date="06.12.23",
+ )
+ actual = device.serialize()
+ expected = (
+ b"manufacturer='unknown' self_identity='0000:0000::?******' "
+ b"serial='unknown' ident='1-1.1.1' product='Qubes' "
+ b"vendor='ITL' name='Some untrusted garbage' devclass='bus' "
+ b"backend_domain='vm' interfaces=' ******u03**01' "
+ b"_additional_info='' _date='06.12.23'")
+ expected = set(expected.replace(b"Some untrusted garbage",
+ b"Some_untrusted_garbage").split(b" "))
+ actual = set(actual.replace(b"Some untrusted garbage",
+ b"Some_untrusted_garbage").split(b" "))
+ self.assertEqual(actual, expected)
+
+ def test_011_serialize_with_parent(self):
+ device = DeviceInfo(
+ backend_domain=self.vm,
+ ident="1-1.1.1",
+ devclass="bus",
+ vendor="ITL",
+ product="Qubes",
+ manufacturer="",
+ name="Some untrusted garbage",
+ serial=None,
+ interfaces=[DeviceInterface(" ******"),
+ DeviceInterface("u03**01")],
+ additional_info="",
+ date="06.12.23",
+ parent=Device(self.vm, '1-1.1', 'pci')
+ )
+ actual = device.serialize()
+ expected = (
+ b"manufacturer='unknown' self_identity='0000:0000::?******' "
+ b"serial='unknown' ident='1-1.1.1' product='Qubes' "
+ b"vendor='ITL' name='Some untrusted garbage' devclass='bus' "
+ b"backend_domain='vm' interfaces=' ******u03**01' "
+ b"_additional_info='' _date='06.12.23' "
+ b"parent_ident='1-1.1' parent_devclass='pci'")
+ expected = set(expected.replace(b"Some untrusted garbage",
+ b"Some_untrusted_garbage").split(b" "))
+ actual = set(actual.replace(b"Some untrusted garbage",
+ b"Some_untrusted_garbage").split(b" "))
+ self.assertEqual(actual, expected)
+
+ def test_012_invalid_serialize(self):
+ device = DeviceInfo(
+ backend_domain=self.vm,
+ ident="1-1.1.1",
+ devclass="bus?",
+ vendor="malicious",
+ product="suspicious",
+ manufacturer="",
+ name="""Some='untrusted' garbage="5%\n" ?""",
+ )
+ with self.assertRaises(qubes.exc.ProtocolError):
+ _ = device.serialize()
+
+ def test_020_deserialize(self):
+ serialized = (
+ b"1-1.1.1 "
+ b"manufacturer='unknown' self_identity='0000:0000::?******' "
+ b"serial='unknown' ident='1-1.1.1' product='Qubes' "
+ b"vendor='ITL' name='Some untrusted garbage' devclass='bus' "
+ b"backend_domain='vm' interfaces=' ******u03**01' "
+ b"_additional_info='' _date='06.12.23' "
+ b"parent_ident='1-1.1' parent_devclass='None'")
+ actual = DeviceInfo.deserialize(serialized, self.vm)
+ expected = DeviceInfo(
+ backend_domain=self.vm,
+ ident="1-1.1.1",
+ devclass="bus",
+ vendor="ITL",
+ product="Qubes",
+ manufacturer="unknown",
+ name="Some untrusted garbage",
+ serial=None,
+ interfaces=[DeviceInterface(" ******"),
+ DeviceInterface("u03**01")],
+ additional_info="",
+ date="06.12.23",
+ )
+
+ self.assertEqual(actual.backend_domain, expected.backend_domain)
+ self.assertEqual(actual.ident, expected.ident)
+ self.assertEqual(actual.devclass, expected.devclass)
+ self.assertEqual(actual.vendor, expected.vendor)
+ self.assertEqual(actual.product, expected.product)
+ self.assertEqual(actual.manufacturer, expected.manufacturer)
+ self.assertEqual(actual.name, expected.name)
+ self.assertEqual(actual.serial, expected.serial)
+ self.assertEqual(repr(actual.interfaces), repr(expected.interfaces))
+ self.assertEqual(actual.self_identity, expected.self_identity)
+ self.assertEqual(actual.data, expected.data)
+
+ def test_021_invalid_deserialize(self):
+ serialized = (
+ b"1-1.1.1 "
+ b"manufacturer='unknown' self_identity='0000:0000::?******' "
+ b"serial='unknown' ident='1-1.1.1' product='Qubes' "
+ b"vendor='ITL' name='Some untrusted garbage' devclass='bus' "
+ b"backend_domain='vm' interfaces=' ******u03**01' "
+ b"_additional_info='' _date='06.12.23' "
+ b"parent_ident='1-1.1' parent_devclass='None' add'tional='info'")
+ actual = DeviceInfo.deserialize(serialized, self.vm)
+ self.assertIsInstance(actual, UnknownDevice)
+ self.assertEqual(actual.backend_domain, self.vm)
+ self.assertEqual(actual.ident, '1-1.1.1')
+ self.assertEqual(actual.devclass, 'peripheral')
+
+ def test_030_serialize_and_deserialize(self):
+ device = DeviceInfo(
+ backend_domain=self.vm,
+ ident="1-1.1.1",
+ devclass="bus?",
+ vendor="malicious",
+ product="suspicious",
+ manufacturer="",
+ name=r"""Some='untrusted' garbage="5%\n" ?""",
+ serial=None,
+ interfaces=[DeviceInterface(" 112233"),
+ DeviceInterface("&012345")],
+ **{"additional_info": "and='more'", "date": "06.12.23"}
+ )
+ serialized = device.serialize()
+ deserialized = DeviceInfo.deserialize(b'1-1.1.1 ' + serialized, self.vm)
+ self.assertEqual(deserialized.backend_domain, device.backend_domain)
+ self.assertEqual(deserialized.ident, device.ident)
+ self.assertEqual(deserialized.devclass, device.devclass)
+ self.assertEqual(deserialized.vendor, device.vendor)
+ self.assertEqual(deserialized.product, device.product)
+ self.assertEqual(deserialized.manufacturer, device.manufacturer)
+ self.assertEqual(deserialized.name, device.name)
+ self.assertEqual(deserialized.serial, device.serial)
+ self.assertEqual(deserialized.data, device.data)
+ self.assertEqual(deserialized.interfaces[0], device.interfaces[0])
+ self.assertEqual(deserialized.interfaces[1], device.interfaces[1])
+
+
+class TC_03_DeviceAssignment(qubes.tests.QubesTestCase):
+ def setUp(self):
+ super().setUp()
+ self.app = TestApp()
+ self.vm = TestVM(self.app, 'vm')
+
+ def test_010_serialize(self):
+ assignment = DeviceAssignment(
+ backend_domain=self.vm,
+ ident="1-1.1.1",
+ devclass="bus",
+ )
+ actual = assignment.serialize()
+ expected = (
+ b"ident='1-1.1.1' devclass='bus' "
+ b"backend_domain='vm' required='no' attach_automatically='no'")
+ expected = set(expected.split(b" "))
+ actual = set(actual.split(b" "))
+ self.assertEqual(actual, expected)
+
+ def test_011_serialize_required(self):
+ assignment = DeviceAssignment(
+ backend_domain=self.vm,
+ ident="1-1.1.1",
+ devclass="bus",
+ attach_automatically=True,
+ required=True,
+ )
+ actual = assignment.serialize()
+ expected = (
+ b"ident='1-1.1.1' devclass='bus' "
+ b"backend_domain='vm' required='yes' attach_automatically='yes'")
+ expected = set(expected.split(b" "))
+ actual = set(actual.split(b" "))
+ self.assertEqual(actual, expected)
+
+ def test_012_serialize_fronted(self):
+ assignment = DeviceAssignment(
+ backend_domain=self.vm,
+ ident="1-1.1.1",
+ devclass="bus",
+ frontend_domain=self.vm,
+ )
+ actual = assignment.serialize()
+ expected = (
+ b"ident='1-1.1.1' frontend_domain='vm' devclass='bus' "
+ b"backend_domain='vm' required='no' attach_automatically='no'")
+ expected = set(expected.split(b" "))
+ actual = set(actual.split(b" "))
+ self.assertEqual(actual, expected)
+
+ def test_013_serialize_options(self):
+ assignment = DeviceAssignment(
+ backend_domain=self.vm,
+ ident="1-1.1.1",
+ devclass="bus",
+ options={'read-only': 'yes'},
+ )
+ actual = assignment.serialize()
+ expected = (
+ b"ident='1-1.1.1' _read-only='yes' devclass='bus' "
+ b"backend_domain='vm' required='no' attach_automatically='no'")
+ expected = set(expected.split(b" "))
+ actual = set(actual.split(b" "))
+ self.assertEqual(actual, expected)
+
+ def test_014_invalid_serialize(self):
+ assignment = DeviceAssignment(
+ backend_domain=self.vm,
+ ident="1-1.1.1",
+ devclass="bus",
+ options={"read'only": 'yes'},
+ )
+ with self.assertRaises(qubes.exc.ProtocolError):
+ _ = assignment.serialize()
+
+ def test_020_deserialize(self):
+ serialized = (
+ b"ident='1-1.1.1' frontend_domain='vm' devclass='bus' "
+ b"backend_domain='vm' required='no' attach_automatically='yes' "
+ b"_read-only='yes'")
+ expected_device = Device(self.vm, '1-1.1.1', 'bus')
+ actual = DeviceAssignment.deserialize(serialized, expected_device)
+ expected = DeviceAssignment(
+ backend_domain=self.vm,
+ ident="1-1.1.1",
+ devclass="bus",
+ frontend_domain=self.vm,
+ attach_automatically=True,
+ required=False,
+ options={'read-only': 'yes'},
+ )
+
+ self.assertEqual(actual.backend_domain, expected.backend_domain)
+ self.assertEqual(actual.ident, expected.ident)
+ self.assertEqual(actual.devclass, expected.devclass)
+ self.assertEqual(actual.frontend_domain, expected.frontend_domain)
+ self.assertEqual(actual.attach_automatically, expected.attach_automatically)
+ self.assertEqual(actual.required, expected.required)
+ self.assertEqual(actual.options, expected.options)
+
+ def test_021_invalid_deserialize(self):
+ serialized = (
+ b"ident='1-1.1.1' frontend_domain='vm' devclass='bus' "
+ b"backend_domain='vm' required='no' attach_automatically='yes' "
+ b"_read'only='yes'")
+ expected_device = Device(self.vm, '1-1.1.1', 'bus')
+ with self.assertRaises(qubes.exc.ProtocolError):
+ _ = DeviceAssignment.deserialize(serialized, expected_device)
+
+ def test_022_invalid_deserialize_2(self):
+ serialized = (
+ b"ident='1-1.1.1' frontend_domain='vm' devclass='bus' "
+ b"backend_domain='vm' required='no' attach_automatically='yes' "
+ b"read-only='yes'")
+ expected_device = Device(self.vm, '1-1.1.1', 'bus')
+ with self.assertRaises(qubes.exc.ProtocolError):
+ _ = DeviceAssignment.deserialize(serialized, expected_device)
+
+ def test_030_serialize_and_deserialize(self):
+ expected = DeviceAssignment(
+ backend_domain=self.vm,
+ ident="1-1.1.1",
+ devclass="bus",
+ frontend_domain=self.vm,
+ attach_automatically=True,
+ required=False,
+ options={'read-only': 'yes'},
+ )
+ serialized = expected.serialize()
+ expected_device = Device(self.vm, '1-1.1.1', 'bus')
+ actual = DeviceAssignment.deserialize(serialized, expected_device)
+ self.assertEqual(actual.backend_domain, expected.backend_domain)
+ self.assertEqual(actual.ident, expected.ident)
+ self.assertEqual(actual.devclass, expected.devclass)
+ self.assertEqual(actual.frontend_domain, expected.frontend_domain)
+ self.assertEqual(actual.attach_automatically,
+ expected.attach_automatically)
+ self.assertEqual(actual.required, expected.required)
+ self.assertEqual(actual.options, expected.options)
diff --git a/qubes/tests/devices_block.py b/qubes/tests/devices_block.py
index 07daf211c..0d19eebb4 100644
--- a/qubes/tests/devices_block.py
+++ b/qubes/tests/devices_block.py
@@ -17,13 +17,15 @@
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, see .
-
+import asyncio
from unittest import mock
import jinja2
import qubes.tests
import qubes.ext.block
+from qubes.device_protocol import DeviceInterface, Device, DeviceInfo, \
+ DeviceAssignment
modules_disk = '''
@@ -102,6 +104,13 @@ def list(self, prefix):
class TestApp(object):
+ class Domains(dict):
+ def __init__(self):
+ super().__init__()
+
+ def __iter__(self):
+ return iter(self.values())
+
def __init__(self):
#: jinja2 environment for libvirt XML templates
self.env = jinja2.Environment(
@@ -112,11 +121,31 @@ def __init__(self):
]),
undefined=jinja2.StrictUndefined,
autoescape=True)
- self.domains = {}
+ self.domains = TestApp.Domains()
+ self.vmm = mock.Mock()
+
+
+class TestDeviceCollection(object):
+ def __init__(self, backend_vm, devclass):
+ self._exposed = []
+ self._assigned = []
+ self.backend_vm = backend_vm
+ self.devclass = devclass
+ def get_assigned_devices(self):
+ return self._assigned
-class TestVM(object):
- def __init__(self, qdb, domain_xml=None, running=True, name='test-vm'):
+ def __getitem__(self, ident):
+ for dev in self._exposed:
+ if dev.ident == ident:
+ return dev
+
+
+class TestVM(qubes.tests.TestEmitter):
+ def __init__(
+ self, qdb, domain_xml=None, running=True, name='test-vm',
+ *args, **kwargs):
+ super(TestVM, self).__init__(*args, **kwargs)
self.name = name
self.untrusted_qdb = TestQubesDB(qdb)
self.libvirt_domain = mock.Mock()
@@ -132,11 +161,17 @@ def __init__(self, qdb, domain_xml=None, running=True, name='test-vm'):
self.libvirt_domain.configure_mock(**{
'XMLDesc.return_value': domain_xml
})
+ self.devices = {
+ 'testclass': TestDeviceCollection(self, 'testclass')
+ }
def __eq__(self, other):
if isinstance(other, TestVM):
return self.name == other.name
+ def __str__(self):
+ return self.name
+
class TC_00_Block(qubes.tests.QubesTestCase):
@@ -147,25 +182,64 @@ def setUp(self):
def test_000_device_get(self):
vm = TestVM({
'/qubes-block-devices/sda': b'',
- '/qubes-block-devices/sda/desc': b'Test device',
+ '/qubes-block-devices/sda/desc': b'Test_ (device)',
'/qubes-block-devices/sda/size': b'1024000',
'/qubes-block-devices/sda/mode': b'w',
- })
+ '/qubes-block-devices/sda/parent': b'1-1.1:1.0',
+ }, domain_xml=domain_xml_template.format(""))
+ parent = DeviceInfo(vm, '1-1.1', devclass='usb')
+ vm.devices['usb'] = TestDeviceCollection(backend_vm=vm, devclass='usb')
+ vm.devices['usb']._exposed.append(parent)
+ vm.is_running = lambda: True
+
+ dom0 = TestVM({}, name='dom0',
+ domain_xml=domain_xml_template.format(""))
+
+ disk = '''
+
+
+
+
+
+
+
+ '''
+ front = TestVM({}, domain_xml=domain_xml_template.format(disk), name='front-vm')
+
+ vm.app.domains[0] = dom0
+ vm.app.domains['test-vm'] = vm
+ vm.app.domains['front-vm'] = front
+ front.app.domains = vm.app.domains
+ dom0.app.domains = vm.app.domains
+
device_info = self.ext.device_get(vm, 'sda')
self.assertIsInstance(device_info, qubes.ext.block.BlockDevice)
self.assertEqual(device_info.backend_domain, vm)
self.assertEqual(device_info.ident, 'sda')
- self.assertEqual(device_info.description, 'Test device')
- self.assertEqual(device_info._description, 'Test device')
+ self.assertEqual(device_info.name, 'device')
+ self.assertEqual(device_info._name, 'device')
+ self.assertEqual(device_info.serial, 'Test')
+ self.assertEqual(device_info._serial, 'Test')
self.assertEqual(device_info.size, 1024000)
self.assertEqual(device_info.mode, 'w')
- self.assertEqual(device_info.frontend_domain, None)
+ self.assertEqual(device_info.manufacturer,
+ 'sub-device of test-vm:1-1.1')
+ self.assertEqual(device_info.device_node, '/dev/sda')
+ self.assertEqual(device_info.interfaces,
+ [DeviceInterface("b******")])
+ self.assertEqual(device_info.parent_device,
+ Device(vm, '1-1.1', devclass='usb'))
+ self.assertEqual(device_info.attachment, front)
+ self.assertEqual(device_info.self_identity,
+ '1-1.1:0000:0000::?******:1.0')
+ self.assertEqual(
+ device_info.data.get('test_frontend_domain', None), None)
self.assertEqual(device_info.device_node, '/dev/sda')
def test_001_device_get_other_node(self):
vm = TestVM({
'/qubes-block-devices/mapper_dmroot': b'',
- '/qubes-block-devices/mapper_dmroot/desc': b'Test device',
+ '/qubes-block-devices/mapper_dmroot/desc': b'Test_device',
'/qubes-block-devices/mapper_dmroot/size': b'1024000',
'/qubes-block-devices/mapper_dmroot/mode': b'w',
})
@@ -173,22 +247,26 @@ def test_001_device_get_other_node(self):
self.assertIsInstance(device_info, qubes.ext.block.BlockDevice)
self.assertEqual(device_info.backend_domain, vm)
self.assertEqual(device_info.ident, 'mapper_dmroot')
- self.assertEqual(device_info.description, 'Test device')
- self.assertEqual(device_info._description, 'Test device')
+ self.assertEqual(device_info._name, None)
+ self.assertEqual(device_info.name, 'unknown')
+ self.assertEqual(device_info.serial, 'Test device')
+ self.assertEqual(device_info._serial, 'Test device')
self.assertEqual(device_info.size, 1024000)
self.assertEqual(device_info.mode, 'w')
- self.assertEqual(device_info.frontend_domain, None)
+ self.assertEqual(
+ device_info.data.get('test_frontend_domain', None), None)
self.assertEqual(device_info.device_node, '/dev/mapper/dmroot')
def test_002_device_get_invalid_desc(self):
vm = TestVM({
'/qubes-block-devices/sda': b'',
- '/qubes-block-devices/sda/desc': b'Test device<>za\xc4\x87abc',
+ '/qubes-block-devices/sda/desc': b'Test (device<>za\xc4\x87abc)',
'/qubes-block-devices/sda/size': b'1024000',
'/qubes-block-devices/sda/mode': b'w',
})
device_info = self.ext.device_get(vm, 'sda')
- self.assertEqual(device_info.description, 'Test device__za__abc')
+ self.assertEqual(device_info.serial, 'Test')
+ self.assertEqual(device_info.name, 'device zaabc')
def test_003_device_get_invalid_size(self):
vm = TestVM({
@@ -225,11 +303,11 @@ def test_005_device_get_none(self):
def test_010_devices_list(self):
vm = TestVM({
'/qubes-block-devices/sda': b'',
- '/qubes-block-devices/sda/desc': b'Test device',
+ '/qubes-block-devices/sda/desc': b'Test_device',
'/qubes-block-devices/sda/size': b'1024000',
'/qubes-block-devices/sda/mode': b'w',
'/qubes-block-devices/sdb': b'',
- '/qubes-block-devices/sdb/desc': b'Test device2',
+ '/qubes-block-devices/sdb/desc': b'Test_device (2)',
'/qubes-block-devices/sdb/size': b'2048000',
'/qubes-block-devices/sdb/mode': b'r',
})
@@ -237,12 +315,14 @@ def test_010_devices_list(self):
self.assertEqual(len(devices), 2)
self.assertEqual(devices[0].backend_domain, vm)
self.assertEqual(devices[0].ident, 'sda')
- self.assertEqual(devices[0].description, 'Test device')
+ self.assertEqual(devices[0].serial, 'Test device')
+ self.assertEqual(devices[0].name, 'unknown')
self.assertEqual(devices[0].size, 1024000)
self.assertEqual(devices[0].mode, 'w')
self.assertEqual(devices[1].backend_domain, vm)
self.assertEqual(devices[1].ident, 'sdb')
- self.assertEqual(devices[1].description, 'Test device2')
+ self.assertEqual(devices[1].serial, 'Test device')
+ self.assertEqual(devices[1].name, '2')
self.assertEqual(devices[1].size, 2048000)
self.assertEqual(devices[1].mode, 'r')
@@ -310,6 +390,7 @@ def test_031_list_attached(self):
options = devices[0][1]
self.assertEqual(dev.backend_domain, vm.app.domains['sys-usb'])
self.assertEqual(dev.ident, 'sda')
+ self.assertEqual(dev.attachment, None)
self.assertEqual(options['frontend-dev'], 'xvdi')
self.assertEqual(options['read-only'], 'yes')
@@ -569,17 +650,241 @@ def test_051_detach_not_attached(self):
'/qubes-block-devices/sda/size': b'1024000',
'/qubes-block-devices/sda/mode': b'r',
})
- device_xml = (
- '\n'
- ' \n'
- ' \n'
- ' \n'
- ' \n\n'
- ' \n'
- '')
vm = TestVM({}, domain_xml=domain_xml_template.format(''))
vm.app.domains['test-vm'] = vm
vm.app.domains['sys-usb'] = TestVM({}, name='sys-usb')
dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
self.ext.on_device_pre_detached_block(vm, '', dev)
self.assertFalse(vm.libvirt_domain.detachDevice.called)
+
+ def test_060_on_qdb_change_added(self):
+ back_vm = TestVM(name='sys-usb', qdb={
+ '/qubes-block-devices/sda': b'',
+ '/qubes-block-devices/sda/desc': b'Test device',
+ '/qubes-block-devices/sda/size': b'1024000',
+ '/qubes-block-devices/sda/mode': b'r',
+ }, domain_xml=domain_xml_template.format(""))
+ exp_dev = Device(back_vm, 'sda', 'block')
+
+ self.ext.on_qdb_change(back_vm, None, None)
+
+ self.assertEqual(self.ext.devices_cache, {'sys-usb': {'sda': None}})
+ self.assertEqual(
+ back_vm.fired_events[
+ ('device-added:block', frozenset({('device', exp_dev)}))],1)
+
+ def test_061_on_qdb_change_auto_attached(self):
+ back_vm = TestVM(name='sys-usb', qdb={
+ '/qubes-block-devices/sda': b'',
+ '/qubes-block-devices/sda/desc': b'Test device',
+ '/qubes-block-devices/sda/size': b'1024000',
+ '/qubes-block-devices/sda/mode': b'r',
+ }, domain_xml=domain_xml_template.format(""))
+ exp_dev = Device(back_vm, 'sda', 'block')
+ front = TestVM({}, domain_xml=domain_xml_template.format(""),
+ name='front-vm')
+ dom0 = TestVM({}, name='dom0',
+ domain_xml=domain_xml_template.format(""))
+ back_vm.app.domains['sys-usb'] = back_vm
+ back_vm.app.domains['front-vm'] = front
+ back_vm.app.domains[0] = dom0
+ front.app = back_vm.app
+ dom0.app = back_vm.app
+
+ back_vm.app.vmm.configure_mock(**{'offline_mode': False})
+ fire_event_async = mock.Mock()
+ front.fire_event_async = fire_event_async
+
+ back_vm.devices['block'] = TestDeviceCollection(
+ backend_vm=back_vm, devclass='block')
+ front.devices['block'] = TestDeviceCollection(
+ backend_vm=front, devclass='block')
+ dom0.devices['block'] = TestDeviceCollection(
+ backend_vm=dom0, devclass='block')
+
+ front.devices['block']._assigned.append(
+ DeviceAssignment.from_device(exp_dev))
+ back_vm.devices['block']._exposed.append(
+ qubes.ext.block.BlockDevice(back_vm, 'sda'))
+
+ # In the case of block devices it is the same,
+ # but notify_auto_attached is synchronous
+ self.ext.attach_and_notify = self.ext.notify_auto_attached
+ with mock.patch('asyncio.ensure_future'):
+ self.ext.on_qdb_change(back_vm, None, None)
+ self.assertEqual(self.ext.devices_cache, {'sys-usb': {'sda': front}})
+ fire_event_async.assert_called_once_with(
+ 'device-attach:block', device=exp_dev,
+ options={'read-only': 'yes', 'frontend-dev': 'xvdi'})
+
+ def test_062_on_qdb_change_attached(self):
+ # added
+ back_vm = TestVM(name='sys-usb', qdb={
+ '/qubes-block-devices/sda': b'',
+ '/qubes-block-devices/sda/desc': b'Test device',
+ '/qubes-block-devices/sda/size': b'1024000',
+ '/qubes-block-devices/sda/mode': b'r',
+ }, domain_xml=domain_xml_template.format(""))
+ exp_dev = Device(back_vm, 'sda', 'block')
+
+ self.ext.devices_cache = {'sys-usb': {'sda': None}}
+
+ # then attached
+ disk = '''
+
+
+
+
+
+
+
+ '''
+ front = TestVM({}, domain_xml=domain_xml_template.format(disk),
+ name='front-vm')
+ dom0 = TestVM({}, name='dom0',
+ domain_xml=domain_xml_template.format(""))
+ back_vm.app.domains['sys-usb'] = back_vm
+ back_vm.app.domains['front-vm'] = front
+ back_vm.app.domains[0] = dom0
+ front.app = back_vm.app
+ dom0.app = back_vm.app
+
+ back_vm.app.vmm.configure_mock(**{'offline_mode': False})
+ fire_event_async = mock.Mock()
+ front.fire_event_async = fire_event_async
+
+ back_vm.devices['block'] = TestDeviceCollection(
+ backend_vm=back_vm, devclass='block')
+ front.devices['block'] = TestDeviceCollection(
+ backend_vm=front, devclass='block')
+ dom0.devices['block'] = TestDeviceCollection(
+ backend_vm=dom0, devclass='block')
+
+ with mock.patch('asyncio.ensure_future'):
+ self.ext.on_qdb_change(back_vm, None, None)
+ self.assertEqual(self.ext.devices_cache, {'sys-usb': {'sda': front}})
+ fire_event_async.assert_called_once_with(
+ 'device-attach:block', device=exp_dev, options={})
+
+ def test_063_on_qdb_change_changed(self):
+ # attached to front-vm
+ back_vm = TestVM(name='sys-usb', qdb={
+ '/qubes-block-devices/sda': b'',
+ '/qubes-block-devices/sda/desc': b'Test device',
+ '/qubes-block-devices/sda/size': b'1024000',
+ '/qubes-block-devices/sda/mode': b'r',
+ }, domain_xml=domain_xml_template.format(""))
+ exp_dev = Device(back_vm, 'sda', 'block')
+
+ front = TestVM({}, name='front-vm')
+ dom0 = TestVM({}, name='dom0',
+ domain_xml=domain_xml_template.format(""))
+
+ self.ext.devices_cache = {'sys-usb': {'sda': front}}
+
+ disk = '''
+
+
+
+
+
+
+
+ '''
+ front_2 = TestVM({}, domain_xml=domain_xml_template.format(disk),
+ name='front-2')
+
+ back_vm.app.vmm.configure_mock(**{'offline_mode': False})
+ front.libvirt_domain.configure_mock(**{
+ 'XMLDesc.return_value': domain_xml_template.format("")
+ })
+
+ back_vm.app.domains['sys-usb'] = back_vm
+ back_vm.app.domains['front-vm'] = front
+ back_vm.app.domains['front-2'] = front_2
+ back_vm.app.domains[0] = dom0
+
+ front.app = back_vm.app
+ front_2.app = back_vm.app
+ dom0.app = back_vm.app
+
+ fire_event_async = mock.Mock()
+ front.fire_event_async = fire_event_async
+ fire_event_async_2 = mock.Mock()
+ front_2.fire_event_async = fire_event_async_2
+
+ back_vm.devices['block'] = TestDeviceCollection(
+ backend_vm=back_vm, devclass='block')
+ front.devices['block'] = TestDeviceCollection(
+ backend_vm=front, devclass='block')
+ dom0.devices['block'] = TestDeviceCollection(
+ backend_vm=dom0, devclass='block')
+ front_2.devices['block'] = TestDeviceCollection(
+ backend_vm=front_2, devclass='block')
+
+ with mock.patch('asyncio.ensure_future'):
+ self.ext.on_qdb_change(back_vm, None, None)
+
+ self.assertEqual(self.ext.devices_cache, {'sys-usb': {'sda': front_2}})
+ fire_event_async.assert_called_with(
+ 'device-detach:block', device=exp_dev)
+ fire_event_async_2.assert_called_once_with(
+ 'device-attach:block', device=exp_dev, options={})
+
+ def test_064_on_qdb_change_removed_attached(self):
+ # attached to front-vm
+ back_vm = TestVM(name='sys-usb', qdb={
+ '/qubes-block-devices/sda': b'',
+ '/qubes-block-devices/sda/desc': b'Test device',
+ '/qubes-block-devices/sda/size': b'1024000',
+ '/qubes-block-devices/sda/mode': b'r',
+ }, domain_xml=domain_xml_template.format(""))
+ dom0 = TestVM({}, name='dom0',
+ domain_xml=domain_xml_template.format(""))
+ exp_dev = Device(back_vm, 'sda', 'block')
+
+ disk = '''
+
+
+
+
+
+
+
+ '''
+ front = TestVM({}, domain_xml=domain_xml_template.format(disk),
+ name='front')
+ self.ext.devices_cache = {'sys-usb': {'sda': front}}
+
+ back_vm.app.vmm.configure_mock(**{'offline_mode': False})
+ front.libvirt_domain.configure_mock(**{
+ 'XMLDesc.return_value': domain_xml_template.format("")
+ })
+
+ back_vm.app.domains['sys-usb'] = back_vm
+ back_vm.app.domains['front-vm'] = front
+ back_vm.app.domains[0] = dom0
+
+ front.app = back_vm.app
+ dom0.app = back_vm.app
+
+ fire_event_async = mock.Mock()
+ front.fire_event_async = fire_event_async
+
+ back_vm.devices['block'] = TestDeviceCollection(
+ backend_vm=back_vm, devclass='block')
+ front.devices['block'] = TestDeviceCollection(
+ backend_vm=front, devclass='block')
+ dom0.devices['block'] = TestDeviceCollection(
+ backend_vm=dom0, devclass='block')
+
+ back_vm.untrusted_qdb = TestQubesDB({})
+ with mock.patch('asyncio.ensure_future'):
+ self.ext.on_qdb_change(back_vm, None, None)
+ self.assertEqual(self.ext.devices_cache, {'sys-usb': {}})
+ fire_event_async.assert_called_with(
+ 'device-detach:block', device=exp_dev)
+ self.assertEqual(
+ back_vm.fired_events[
+ ('device-removed:block', frozenset({('device', exp_dev)}))],
+ 1)
diff --git a/qubes/tests/devices_pci.py b/qubes/tests/devices_pci.py
index b653337e2..01011cadd 100644
--- a/qubes/tests/devices_pci.py
+++ b/qubes/tests/devices_pci.py
@@ -17,13 +17,11 @@
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, see .
-
from unittest import mock
-import jinja2
-
import qubes.tests
import qubes.ext.pci
+from qubes.device_protocol import DeviceInterface
class TestVM(object):
@@ -38,38 +36,10 @@ def __eq__(self, other):
if isinstance(other, TestVM):
return self.name == other.name
-class TC_00_Block(qubes.tests.QubesTestCase):
- def setUp(self):
- super().setUp()
- self.ext = qubes.ext.pci.PCIDeviceExtension()
- def test_000_unsupported_device(self):
- vm = TestVM()
- vm.app.configure_mock(**{
- 'vmm.libvirt_conn.listAllDevices.return_value':
- [mock.Mock(**{"XMLDesc.return_value": """
- pci_0000_00_14_0
- /sys/devices/pci0000:00/0000:00:14.0
- computer
-
- pciback
-
-
- 0x0c0330
- 0
- 0
- 20
- 0
- 9 Series Chipset Family USB xHCI Controller
- Intel Corporation
-
-
-""",
- "listCaps.return_value": ["pci"]
- }),
- mock.Mock(**{"XMLDesc.return_value": """
- pci_1000_00_14_0
- /sys/devices/pci1000:00/1000:00:14.0
+PCI_XML = """
+ pci_{}_00_14_0
+ /sys/devices/pci{}:00/{}:00:14.0
computer
pciback
@@ -84,11 +54,103 @@ def test_000_unsupported_device(self):
Intel Corporation
-""",
- "listCaps.return_value": ["pci"]
+"""
+
+
+def mock_file_open(filename: str, *_args, **_kwargs):
+ if filename == "/usr/share/hwdata/pci.ids":
+ # short version of pci.ids
+ content = """
+#
+# List of PCI ID's
+#
+# (...)
+#
+0001 SafeNet (wrong ID)
+0010 Allied Telesis, Inc (Wrong ID)
+# This is a relabelled RTL-8139
+ 8139 AT-2500TX V3 Ethernet
+# List of known device classes, subclasses and programming interfaces
+
+# Syntax:
+# C class class_name
+# subclass subclass_name <-- single tab
+# prog-if prog-if_name <-- two tabs
+
+C 00 Unclassified device
+\t00 Non-VGA unclassified device
+C 01 Mass storage controller
+\t01 IDE interface
+\t\t00 ISA Compatibility mode-only controller
+C 0c Serial bus controller
+\t00 FireWire (IEEE 1394)
+\t\t00 Generic
+\t\t10 OHCI
+\t01 ACCESS Bus
+\t02 SSA
+\t03 USB controller
+\t\t00 UHCI
+\t\t10 OHCI
+\t\t20 EHCI
+\t\t30 XHCI
+\t\t40 USB4 Host Interface
+\t\t80 Unspecified
+\t\tfe USB Device
+\t04 Fibre Channel
+\t05 SMBus
+\t06 InfiniBand
+\t07 IPMI Interface
+\t\t00 SMIC
+\t\t01 KCS
+\t\t02 BT (Block Transfer)
+\t08 SERCOS interface
+\t09 CANBUS
+\t80 Serial bus controller
+"""
+ elif filename.startswith("/sys/devices/pci"):
+ content = "0x0c0330"
+ else:
+ raise OSError()
+
+ file_object = mock.mock_open(read_data=content).return_value
+ file_object.__iter__.return_value = content
+ return file_object
+
+
+class TC_00_Block(qubes.tests.QubesTestCase):
+ def setUp(self):
+ super().setUp()
+ self.ext = qubes.ext.pci.PCIDeviceExtension()
+
+ @mock.patch('builtins.open', new=mock_file_open)
+ def test_000_unsupported_device(self):
+ vm = TestVM()
+ vm.app.configure_mock(**{
+ 'vmm.libvirt_conn.nodeDeviceLookupByName.return_value':
+ mock.Mock(**{"XMLDesc.return_value":
+ PCI_XML.format(*["0000"] * 3)
}),
- ]
+ 'vmm.libvirt_conn.listAllDevices.return_value':
+ [mock.Mock(**{"XMLDesc.return_value":
+ PCI_XML.format(*["0000"] * 3),
+ "listCaps.return_value": ["pci"]
+ }),
+ mock.Mock(**{"XMLDesc.return_value":
+ PCI_XML.format(*["1000"] * 3),
+ "listCaps.return_value": ["pci"]
+ }),
+ ]
})
devices = list(self.ext.on_device_list_pci(vm, 'device-list:pci'))
self.assertEqual(len(devices), 1)
self.assertEqual(devices[0].ident, "00_14.0")
+ self.assertEqual(devices[0].vendor, "Intel Corporation")
+ self.assertEqual(devices[0].product,
+ "9 Series Chipset Family USB xHCI Controller")
+ self.assertEqual(devices[0].interfaces, [DeviceInterface("p0c0330")])
+ self.assertEqual(devices[0].parent_device, None)
+ self.assertEqual(devices[0].libvirt_name, "pci_0000_00_14_0")
+ self.assertEqual(devices[0].description,
+ "USB controller: Intel Corporation 9 Series "
+ "Chipset Family USB xHCI Controller")
+ self.assertEqual(devices[0].self_identity, "0x8086:0x8cb1::p0c0330")
diff --git a/qubes/tests/integ/backup.py b/qubes/tests/integ/backup.py
index 42b40f02c..d62c6d3f7 100644
--- a/qubes/tests/integ/backup.py
+++ b/qubes/tests/integ/backup.py
@@ -303,7 +303,7 @@ def get_vms_info(self, vms):
vm_info['default'][prop] = vm.property_is_default(prop)
for dev_class in vm.devices.keys():
vm_info['devices'][dev_class] = {}
- for dev_ass in vm.devices[dev_class].assignments():
+ for dev_ass in vm.devices[dev_class].get_assigned_devices():
vm_info['devices'][dev_class][str(dev_ass.device)] = \
dev_ass.options
vms_info[vm.name] = vm_info
@@ -338,7 +338,7 @@ def assertCorrectlyRestored(self, vms_info, orig_hashes):
for dev in vm_info['devices'][dev_class]:
found = False
for restored_dev_ass in restored_vm.devices[
- dev_class].assignments():
+ dev_class].get_assigned_devices():
if str(restored_dev_ass.device) == dev:
found = True
self.assertEqual(vm_info['devices'][dev_class][dev],
diff --git a/qubes/tests/integ/devices_block.py b/qubes/tests/integ/devices_block.py
index 3897881a2..74e60b4ae 100644
--- a/qubes/tests/integ/devices_block.py
+++ b/qubes/tests/integ/devices_block.py
@@ -87,7 +87,7 @@ def test_000_list_loop(self):
dev_list = list(self.vm.devices['block'])
found = False
for dev in dev_list:
- if dev.description == self.img_path:
+ if dev.serial == self.img_path:
self.assertTrue(dev.ident.startswith('loop'))
self.assertEqual(dev.mode, 'w')
self.assertEqual(dev.size, 1024 * 1024 * 128)
@@ -113,7 +113,7 @@ def test_001_list_loop_mounted(self):
dev_list = list(self.vm.devices['block'])
for dev in dev_list:
- if dev.description == self.img_path:
+ if dev.serial == self.img_path:
self.fail(
'Device {} ({}) should not be listed because is mounted'
.format(dev, self.img_path))
@@ -132,11 +132,11 @@ def test_010_list_dm(self):
found = False
for dev in dev_list:
if dev.ident.startswith('loop'):
- self.assertNotEquals(dev.description, self.img_path,
+ self.assertNotEquals(dev.serial, self.img_path,
"Device {} ({}) should not be listed as it is used in "
"device-mapper".format(dev, self.img_path)
)
- elif dev.description == 'test-dm':
+ elif dev.serial == 'test-dm':
self.assertEqual(dev.mode, 'w')
self.assertEqual(dev.size, 1024 * 1024 * 128)
found = True
@@ -163,12 +163,12 @@ def test_011_list_dm_mounted(self):
dev_list = list(self.vm.devices['block'])
for dev in dev_list:
if dev.ident.startswith('loop'):
- self.assertNotEquals(dev.description, self.img_path,
+ self.assertNotEquals(dev.serial, self.img_path,
"Device {} ({}) should not be listed as it is used in "
"device-mapper".format(dev, self.img_path)
)
else:
- self.assertNotEquals(dev.description, 'test-dm',
+ self.assertNotEquals(dev.serial, 'test-dm',
"Device {} ({}) should not be listed as it is "
"mounted".format(dev, 'test-dm')
)
@@ -188,11 +188,11 @@ def test_012_list_dm_delayed(self):
found = False
for dev in dev_list:
if dev.ident.startswith('loop'):
- self.assertNotEquals(dev.description, self.img_path,
+ self.assertNotEquals(dev.serial, self.img_path,
"Device {} ({}) should not be listed as it is used in "
"device-mapper".format(dev, self.img_path)
)
- elif dev.description == 'test-dm':
+ elif dev.serial == 'test-dm':
self.assertEqual(dev.mode, 'w')
self.assertEqual(dev.size, 1024 * 1024 * 128)
found = True
@@ -218,7 +218,7 @@ def test_013_list_dm_removed(self):
dev_list = list(self.vm.devices['block'])
found = False
for dev in dev_list:
- if dev.description == self.img_path:
+ if dev.serial == self.img_path:
self.assertTrue(dev.ident.startswith('loop'))
self.assertEqual(dev.mode, 'w')
self.assertEqual(dev.size, 1024 * 1024 * 128)
@@ -242,7 +242,7 @@ def test_020_list_loop_partition(self):
dev_list = list(self.vm.devices['block'])
found = False
for dev in dev_list:
- if dev.description == self.img_path:
+ if dev.serial == self.img_path:
self.assertTrue(dev.ident.startswith('loop'))
self.assertEqual(dev.mode, 'w')
self.assertEqual(dev.size, 1024 * 1024 * 128)
@@ -271,7 +271,7 @@ def test_021_list_loop_partition_mounted(self):
dev_list = list(self.vm.devices['block'])
for dev in dev_list:
- if dev.description == self.img_path:
+ if dev.serial == self.img_path:
self.fail(
'Device {} ({}) should not be listed because its '
'partition is mounted'
@@ -318,16 +318,16 @@ def setUp(self):
"udevadm settle".format(path=self.img_path), user="root"))
dev_list = list(self.backend.devices['block'])
for dev in dev_list:
- if dev.description == self.img_path:
+ if dev.serial == self.img_path:
self.device = dev
self.device_ident = dev.ident
break
else:
self.fail('Device for {} in {} not found'.format(
- self.img_path, self.backend.name))
+ self.img_path, self.backend.serial))
def test_000_attach_reattach(self):
- ass = qubes.devices.DeviceAssignment(self.backend, self.device_ident)
+ ass = qubes.device_protocol.DeviceAssignment(self.backend, self.device_ident)
with self.subTest('attach'):
self.loop.run_until_complete(
self.frontend.devices['block'].attach(ass))
@@ -346,7 +346,8 @@ def test_000_attach_reattach(self):
self.loop.run_until_complete(
self.frontend.run_for_stdio('! ls /dev/xvdi'))
- self.assertIsNone(self.device.frontend_domain)
+ self.assertIsNone(
+ self.device.data.get('test_frontend_domain', None))
with self.subTest('reattach'):
self.loop.run_until_complete(
diff --git a/qubes/tests/integ/devices_pci.py b/qubes/tests/integ/devices_pci.py
index 290607e3b..7b1ea5579 100644
--- a/qubes/tests/integ/devices_pci.py
+++ b/qubes/tests/integ/devices_pci.py
@@ -37,11 +37,18 @@ def setUp(self):
if self._testMethodName not in ['test_000_list']:
pcidev = os.environ['QUBES_TEST_PCIDEV']
self.dev = self.app.domains[0].devices['pci'][pcidev]
- self.assignment = qubes.devices.DeviceAssignment(
+ self.assignment = qubes.device_protocol.DeviceAssignment(
backend_domain=self.dev.backend_domain,
ident=self.dev.ident,
- persistent=True)
- if isinstance(self.dev, qubes.devices.UnknownDevice):
+ attach_automatically=True,
+ )
+ self.required_assignment = qubes.device_protocol.DeviceAssignment(
+ backend_domain=self.dev.backend_domain,
+ ident=self.dev.ident,
+ attach_automatically=True,
+ required=True,
+ )
+ if isinstance(self.dev, qubes.device_protocol.UnknownDevice):
self.skipTest('Specified device {} does not exists'.format(pcidev))
self.init_default_template()
self.vm = self.app.add_new_vm(qubes.vm.appvm.AppVM,
@@ -73,27 +80,35 @@ def test_000_list(self):
self.fail('Not all devices listed, missing: {}'.format(
actual_devices))
- def assertDeviceNotInCollection(self, dev, dev_col):
- self.assertNotIn(dev, dev_col.attached())
- self.assertNotIn(dev, dev_col.persistent())
- self.assertNotIn(dev, dev_col.assignments())
- self.assertNotIn(dev, dev_col.assignments(persistent=True))
-
- def test_010_attach_offline_persistent(self):
+ def assertDeviceIs(
+ self, device, *, attached: bool, assigned: bool, required: bool
+ ):
dev_col = self.vm.devices['pci']
- self.assertDeviceNotInCollection(self.dev, dev_col)
- self.loop.run_until_complete(
- dev_col.attach(self.assignment))
+ if required:
+ assert assigned
+ self.assertTrue(attached == device in dev_col.get_attached_devices())
+ self.assertTrue(assigned == device in dev_col.get_assigned_devices())
+ self.assertTrue(
+ required == device in dev_col.get_assigned_devices(
+ required_only=True)
+ )
+ dedicated = assigned or attached
+ self.assertTrue(dedicated == device in dev_col.get_dedicated_devices())
+
+ def test_010_assign_offline(self): # TODO required
+ dev_col = self.vm.devices['pci']
+ self.assertDeviceIs(
+ self.dev, attached=False, assigned=False, required=False)
+
+ self.loop.run_until_complete(dev_col.assign(self.assignment))
self.app.save()
- self.assertNotIn(self.dev, dev_col.attached())
- self.assertIn(self.dev, dev_col.persistent())
- self.assertIn(self.dev, dev_col.assignments())
- self.assertIn(self.dev, dev_col.assignments(persistent=True))
- self.assertNotIn(self.dev, dev_col.assignments(persistent=False))
+ self.assertDeviceIs(
+ self.dev, attached=False, assigned=True, required=False)
self.loop.run_until_complete(self.vm.start())
+ self.assertDeviceIs(
+ self.dev, attached=True, assigned=False, required=False)
- self.assertIn(self.dev, dev_col.attached())
(stdout, _) = self.loop.run_until_complete(
self.vm.run_for_stdio('lspci'))
self.assertIn(self.dev.description, stdout.decode())
@@ -101,26 +116,24 @@ def test_010_attach_offline_persistent(self):
def test_011_attach_offline_temp_fail(self):
dev_col = self.vm.devices['pci']
- self.assertDeviceNotInCollection(self.dev, dev_col)
- self.assignment.persistent = False
+ self.assertDeviceIs(
+ self.dev, attached=False, assigned=False, required=False)
+ self.assignment.required = False
with self.assertRaises(qubes.exc.QubesVMNotRunningError):
self.loop.run_until_complete(
dev_col.attach(self.assignment))
-
- def test_020_attach_online_persistent(self):
+ def test_020_attach_online_persistent(self): # TODO: required
self.loop.run_until_complete(
self.vm.start())
dev_col = self.vm.devices['pci']
- self.assertDeviceNotInCollection(self.dev, dev_col)
+ self.assertDeviceIs(
+ self.dev, attached=False, assigned=False, required=False)
+
self.loop.run_until_complete(
dev_col.attach(self.assignment))
-
- self.assertIn(self.dev, dev_col.attached())
- self.assertIn(self.dev, dev_col.persistent())
- self.assertIn(self.dev, dev_col.assignments())
- self.assertIn(self.dev, dev_col.assignments(persistent=True))
- self.assertNotIn(self.dev, dev_col.assignments(persistent=False))
+ self.assertDeviceIs(
+ self.dev, attached=True, assigned=True, required=False)
# give VM kernel some time to discover new device
time.sleep(1)
@@ -131,7 +144,8 @@ def test_020_attach_online_persistent(self):
def test_021_persist_detach_online_fail(self):
dev_col = self.vm.devices['pci']
- self.assertDeviceNotInCollection(self.dev, dev_col)
+ self.assertDeviceIs(
+ self.dev, attached=False, assigned=False, required=False)
self.loop.run_until_complete(
dev_col.attach(self.assignment))
self.app.save()
@@ -141,37 +155,34 @@ def test_021_persist_detach_online_fail(self):
self.loop.run_until_complete(
self.vm.devices['pci'].detach(self.assignment))
- def test_030_persist_attach_detach_offline(self):
+ def test_030_persist_attach_detach_offline(self): # TODO: required
dev_col = self.vm.devices['pci']
- self.assertDeviceNotInCollection(self.dev, dev_col)
+ self.assertDeviceIs(
+ self.dev, attached=False, assigned=False, required=False)
+
self.loop.run_until_complete(
dev_col.attach(self.assignment))
self.app.save()
- self.assertNotIn(self.dev, dev_col.attached())
- self.assertIn(self.dev, dev_col.persistent())
- self.assertIn(self.dev, dev_col.assignments())
- self.assertIn(self.dev, dev_col.assignments(persistent=True))
- self.assertNotIn(self.dev, dev_col.assignments(persistent=False))
+ self.assertDeviceIs(
+ self.dev, attached=False, assigned=True, required=False)
+
self.loop.run_until_complete(
dev_col.detach(self.assignment))
- self.assertDeviceNotInCollection(self.dev, dev_col)
+ self.assertDeviceIs(
+ self.dev, attached=False, assigned=False, required=False)
- def test_031_attach_detach_online_temp(self):
+ def test_031_attach_detach_online_temp(self): # TODO: requiured
dev_col = self.vm.devices['pci']
self.loop.run_until_complete(
self.vm.start())
- self.assignment.persistent = False
- self.assertDeviceNotInCollection(self.dev, dev_col)
+ self.assignment.assigned = False
+ self.assertDeviceIs(
+ self.dev, attached=False, assigned=False, required=False)
+
self.loop.run_until_complete(
dev_col.attach(self.assignment))
-
- self.assertIn(self.dev, dev_col.attached())
- self.assertNotIn(self.dev, dev_col.persistent())
- self.assertIn(self.dev, dev_col.assignments())
- self.assertIn(self.dev, dev_col.assignments(persistent=False))
- self.assertNotIn(self.dev, dev_col.assignments(persistent=True))
- self.assertIn(self.dev, dev_col.assignments(persistent=False))
-
+ self.assertDeviceIs(
+ self.dev, attached=True, assigned=False, required=False)
# give VM kernel some time to discover new device
time.sleep(1)
@@ -181,7 +192,8 @@ def test_031_attach_detach_online_temp(self):
self.assertIn(self.dev.description, stdout.decode())
self.loop.run_until_complete(
dev_col.detach(self.assignment))
- self.assertDeviceNotInCollection(self.dev, dev_col)
+ self.assertDeviceIs(
+ self.dev, attached=False, assigned=False, required=False)
(stdout, _) = self.loop.run_until_complete(
self.vm.run_for_stdio('lspci'))
diff --git a/qubes/tests/integ/vm_qrexec_gui.py b/qubes/tests/integ/vm_qrexec_gui.py
index e55671963..011ca9cff 100644
--- a/qubes/tests/integ/vm_qrexec_gui.py
+++ b/qubes/tests/integ/vm_qrexec_gui.py
@@ -308,7 +308,7 @@ def common_audio_record_muted(self):
self.fail('VM recorded something, even though mic disabled')
def common_audio_record_unmuted(self):
- deva = qubes.devices.DeviceAssignment(self.app.domains[0], 'mic')
+ deva = qubes.device_protocol.DeviceAssignment(self.app.domains[0], 'mic')
self.loop.run_until_complete(
self.testvm1.devices['mic'].attach(deva))
# connect VM's recording source output monitor (instead of mic)
diff --git a/qubes/tests/vm/init.py b/qubes/tests/vm/init.py
index bf1f9ef21..761ff9953 100644
--- a/qubes/tests/vm/init.py
+++ b/qubes/tests/vm/init.py
@@ -110,13 +110,13 @@ def test_000_load(self):
})
self.assertCountEqual(vm.devices.keys(), ('pci',))
- self.assertCountEqual(list(vm.devices['pci'].persistent()),
- [qubes.ext.pci.PCIDevice(vm, '00_11.22')])
+ self.assertCountEqual(list(vm.devices['pci'].get_assigned_devices()),
+ [qubes.ext.pci.PCIDevice(vm, '00_11.22')])
- assignments = list(vm.devices['pci'].assignments())
+ assignments = list(vm.devices['pci'].get_assigned_devices())
self.assertEqual(len(assignments), 1)
self.assertEqual(assignments[0].options, {'no-strict-reset': 'True'})
- self.assertEqual(assignments[0].persistent, True)
+ self.assertEqual(assignments[0].attach_automatically, True)
self.assertXMLIsValid(vm.__xml__(), 'relaxng/domain.rng')
diff --git a/qubes/tests/vm/qubesvm.py b/qubes/tests/vm/qubesvm.py
index b07fdb2b0..6821942f7 100644
--- a/qubes/tests/vm/qubesvm.py
+++ b/qubes/tests/vm/qubesvm.py
@@ -79,9 +79,10 @@ class TestDeviceCollection(object):
def __init__(self):
self._list = []
- def persistent(self):
+ def get_assigned_devices(self, required_only=False):
return self._list
+
class TestQubesDB(object):
def __init__(self, data=None):
self.data = {}
@@ -214,7 +215,7 @@ def test_010_default_virt_mode(self):
self.vm.template = None
self.assertEqual(qubes.vm.qubesvm._default_virt_mode(self.vm),
'pvh')
- self.vm.devices['pci'].persistent().append('some-dev')
+ self.vm.devices['pci'].get_assigned_devices().append('some-dev')
self.assertEqual(qubes.vm.qubesvm._default_virt_mode(self.vm),
'hvm')
@@ -246,7 +247,7 @@ def test_021_default_maxmem_with_pcidevs(self):
self.vm.is_memory_balancing_possible = \
lambda: qubes.vm.qubesvm.QubesVM.is_memory_balancing_possible(
self.vm)
- self.vm.devices['pci'].persistent().append('00_00.0')
+ self.vm.devices['pci'].get_assigned_devices().append('00_00.0')
self.assertEqual(qubes.vm.qubesvm._default_maxmem(self.vm), 0)
def test_022_default_maxmem_linux(self):
@@ -1311,8 +1312,10 @@ def test_600_libvirt_xml_hvm_pcidev(self):
vm, # this is violation of API, but for PCI the argument
# is unused
'00_00.0',
- bus='pci',
- persistent=True)
+ devclass='pci',
+ attach_automatically=True,
+ required=True,
+ )
vm.devices['pci']._set.add(
assignment)
libvirt_xml = vm.create_config_file()
@@ -1391,12 +1394,12 @@ def test_600_libvirt_xml_hvm_pcidev_s0ix(self):
vm.kernel = None
# even with meminfo-writer enabled, should have memory==maxmem
vm.features['service.meminfo-writer'] = True
- assignment = qubes.devices.DeviceAssignment(
- vm, # this is violation of API, but for PCI the argument
+ assignment = qubes.device_protocol.DeviceAssignment(
+ vm, # this is a violation of API, but for PCI the argument
# is unused
'00_00.0',
- bus='pci',
- persistent=True)
+ devclass='pci',
+ attach_automatically=True, required=True)
vm.devices['pci']._set.add(
assignment)
libvirt_xml = vm.create_config_file()
@@ -1477,10 +1480,11 @@ def test_600_libvirt_xml_hvm_cdrom_boot(self):
vm.kernel = None
dom0.events_enabled = True
self.app.vmm.offline_mode = False
- dev = qubes.devices.DeviceAssignment(
+ dev = qubes.device_protocol.DeviceAssignment(
dom0, 'sda',
- {'devtype': 'cdrom', 'read-only': 'yes'}, persistent=True)
- self.loop.run_until_complete(vm.devices['block'].attach(dev))
+ {'devtype': 'cdrom', 'read-only': 'yes'},
+ attach_automatically=True, required=True)
+ self.loop.run_until_complete(vm.devices['block'].assign(dev))
libvirt_xml = vm.create_config_file()
self.assertXMLEqual(lxml.etree.XML(libvirt_xml),
lxml.etree.XML(expected))
@@ -1581,10 +1585,11 @@ def test_600_libvirt_xml_hvm_cdrom_dom0_kernel_boot(self):
})
dom0.events_enabled = True
self.app.vmm.offline_mode = False
- dev = qubes.devices.DeviceAssignment(
+ dev = qubes.device_protocol.DeviceAssignment(
dom0, 'sda',
- {'devtype': 'cdrom', 'read-only': 'yes'}, persistent=True)
- self.loop.run_until_complete(vm.devices['block'].attach(dev))
+ {'devtype': 'cdrom', 'read-only': 'yes'},
+ attach_automatically=True, required=True)
+ self.loop.run_until_complete(vm.devices['block'].assign(dev))
libvirt_xml = vm.create_config_file()
self.assertXMLEqual(lxml.etree.XML(libvirt_xml),
lxml.etree.XML(expected))
@@ -1886,10 +1891,11 @@ def test_615_libvirt_xml_block_devices(self):
'device.backend_domain.features.check_with_template.return_value': '4.2',
}),
]
- vm.devices['block'].assignments = lambda persistent: assignments
+ vm.devices['block'].get_assigned_devices = \
+ lambda required_only: assignments
libvirt_xml = vm.create_config_file()
- self.assertXMLEqual(lxml.etree.XML(libvirt_xml),
- lxml.etree.XML(expected))
+ self.assertXMLEqual(
+ lxml.etree.XML(libvirt_xml), lxml.etree.XML(expected))
@unittest.mock.patch('qubes.utils.get_timezone')
@unittest.mock.patch('qubes.utils.urandom')
diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py
index b3be66e92..1b0772b23 100644
--- a/qubes/vm/__init__.py
+++ b/qubes/vm/__init__.py
@@ -27,6 +27,7 @@
import re
import string
import uuid
+from typing import List
import lxml.etree
@@ -275,13 +276,16 @@ def load_extras(self):
options[option.get('name')] = str(option.text)
try:
- device_assignment = qubes.devices.DeviceAssignment(
+ device_assignment = qubes.device_protocol.DeviceAssignment(
self.app.domains[node.get('backend-domain')],
node.get('id'),
options,
- persistent=True
+ attach_automatically=True,
+ # backward compatibility: persistent~>required=True
+ required=qubes.property.bool(
+ None, None, node.get('required', 'yes')),
)
- self.devices[devclass].load_persistent(device_assignment)
+ self.devices[devclass].load_assignment(device_assignment)
except KeyError:
msg = "{}: Cannot find backend domain '{}' " \
"for device type {} '{}'".format(self.name, node.get(
@@ -295,15 +299,19 @@ def load_extras(self):
# SEE:1815 firewall, policy.
- def get_provided_assignments(self):
- '''List of persistent device assignments from this VM.'''
+ def get_provided_assignments(self, required_only: bool = False)\
+ -> List['qubes.device_protocol.DeviceAssignment']:
+ """
+ List device assignments from this VM.
+ """
assignments = []
for domain in self.app.domains:
if domain == self:
continue
for device_collection in domain.devices.values():
- for assignment in device_collection.persistent():
+ for assignment in device_collection.get_assigned_devices(
+ required_only):
if assignment.backend_domain == self:
assignments.append(assignment)
return assignments
@@ -329,10 +337,11 @@ def __xml__(self):
for devclass in self.devices:
devices = lxml.etree.Element('devices')
devices.set('class', devclass)
- for device in self.devices[devclass].assignments(persistent=True):
+ for device in self.devices[devclass].get_assigned_devices():
node = lxml.etree.Element('device')
node.set('backend-domain', device.backend_domain.name)
node.set('id', device.ident)
+ node.set('required', 'yes' if device.required else 'no')
for key, val in device.options.items():
option_node = lxml.etree.Element('option')
option_node.set('name', key)
diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py
index bab1d3979..a38d46432 100644
--- a/qubes/vm/qubesvm.py
+++ b/qubes/vm/qubesvm.py
@@ -132,10 +132,10 @@ def _setter_virt_mode(self, prop, value):
raise qubes.exc.QubesPropertyValueError(
self, prop, value,
'Invalid virtualization mode, supported values: hvm, pv, pvh')
- if value == 'pvh' and list(self.devices['pci'].persistent()):
+ if value == 'pvh' and list(self.devices['pci'].get_assigned_devices()):
raise qubes.exc.QubesPropertyValueError(
self, prop, value,
- "pvh mode can't be set if pci devices are attached")
+ "pvh mode can't be set if pci devices are assigned")
return value
@@ -169,7 +169,7 @@ def _setter_kbd_layout(self, prop, value):
def _default_virt_mode(self):
- if self.devices['pci'].persistent():
+ if list(self.devices['pci'].get_assigned_devices()):
return 'hvm'
try:
return self.template.virt_mode
@@ -215,7 +215,7 @@ def _default_maxmem(self):
def _default_kernelopts(self):
"""
Return default kernel options for the given kernel. If kernel directory
- contains 'default-kernelopts-{pci,nopci}.txt' file, use that. Otherwise
+ contains 'default-kernelopts-{pci,nopci}.txt' file, use that. Otherwise,
use built-in defaults.
For qubes without PCI devices, kernelopts of qube's template are
considered (for template-based qubes).
@@ -228,9 +228,9 @@ def _default_kernelopts(self):
kernels_dir = os.path.join(
qubes.config.system_path['qubes_kernels_base_dir'],
self.kernel)
- pci = bool(list(self.devices['pci'].persistent()))
+ any_pci_assigned = bool(list(self.devices['pci'].get_assigned_devices()))
extra_opts = ""
- if pci:
+ if any_pci_assigned:
path = os.path.join(kernels_dir, 'default-kernelopts-pci.txt')
if self.app.domains[0].features.get('suspend-s0ix', False):
extra_opts = " qubes_exp_pm_use_suspend=1"
@@ -244,8 +244,9 @@ def _default_kernelopts(self):
with open(path, encoding='ascii') as f_kernelopts:
return f_kernelopts.read().strip() + extra_opts
else:
- return (qubes.config.defaults['kernelopts_pcidevs'] if pci else
- qubes.config.defaults['kernelopts']) + extra_opts
+ return (qubes.config.defaults['kernelopts_pcidevs']
+ if any_pci_assigned else qubes.config.defaults['kernelopts']
+ ) + extra_opts
class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
@@ -1169,11 +1170,15 @@ async def start(self, start_guid=True, notify_function=None,
qmemman_client = None
try:
for devclass in self.devices:
- for dev in self.devices[devclass].persistent():
- if isinstance(dev, qubes.devices.UnknownDevice):
+ for ass in self.devices[devclass].get_assigned_devices():
+ if isinstance(
+ ass.device,
+ qubes.device_protocol.UnknownDevice) \
+ and ass.required:
raise qubes.exc.QubesException(
- '{} device {} not available'.format(
- devclass, dev))
+ f'{devclass.capitalize()} device {ass} '
+ f'not available'
+ )
if self.virt_mode == 'pvh' and not self.kernel:
raise qubes.exc.QubesException(
@@ -1216,8 +1221,9 @@ async def start(self, start_guid=True, notify_function=None,
except libvirt.libvirtError as exc:
# missing IOMMU?
if self.virt_mode == 'hvm' and \
- list(self.devices['pci'].persistent()) and \
- not self.app.host.is_iommu_supported():
+ list(self.devices['pci'].get_assigned_devices(
+ required_only=True)
+ ) and not self.app.host.is_iommu_supported():
exc = qubes.exc.QubesException(
'Failed to start an HVM qube with PCI devices assigned '
'- hardware does not support IOMMU/VT-d/AMD-Vi')
@@ -1623,12 +1629,12 @@ def is_memory_balancing_possible(self):
- have PCI devices
- balloon driver not present
- We don't have reliable way to detect the second point, but good
- heuristic is HVM virt_mode (PV and PVH require OS support and it does
- include balloon driver) and lack of qrexec/meminfo-writer service
+ We don't have a reliable way to detect the second point, but good
+ heuristic is HVM virt_mode (PV and PVH require OS support, and it does
+ include the balloon driver) and lack of qrexec/meminfo-writer service
support (no qubes tools installed).
"""
- if list(self.devices['pci'].persistent()):
+ if list(self.devices['pci'].get_assigned_devices()):
return False
if self.virt_mode == 'hvm':
# if VM announce any supported service
diff --git a/relaxng/qubes.rng b/relaxng/qubes.rng
index 39065e288..b286db21b 100644
--- a/relaxng/qubes.rng
+++ b/relaxng/qubes.rng
@@ -218,11 +218,21 @@ the parser will complain about missing combine= attribute on the second .
-
[0-9a-f]{2}_[0-9a-f]{2}.[0-9a-f]{2}
+
+
+
+ Flag: is the device required to start domain?
+ If not present assume 'yes'.
+
+
+ [yesno]+
+
+
+
diff --git a/rpm_spec/core-dom0.spec.in b/rpm_spec/core-dom0.spec.in
index 6ac367dff..3e092681b 100644
--- a/rpm_spec/core-dom0.spec.in
+++ b/rpm_spec/core-dom0.spec.in
@@ -254,16 +254,22 @@ admin.vm.Shutdown
admin.vm.Start
admin.vm.Stats
admin.vm.Unpause
+admin.vm.device.block.Assign
+admin.vm.device.block.Assigned
admin.vm.device.block.Attach
+admin.vm.device.block.Attached
admin.vm.device.block.Available
admin.vm.device.block.Detach
-admin.vm.device.block.List
-admin.vm.device.block.Set.persistent
+admin.vm.device.block.Set.required
+admin.vm.device.block.Unassign
+admin.vm.device.pci.Assign
+admin.vm.device.pci.Assigned
admin.vm.device.pci.Attach
+admin.vm.device.pci.Attached
admin.vm.device.pci.Available
admin.vm.device.pci.Detach
-admin.vm.device.pci.List
-admin.vm.device.pci.Set.persistent
+admin.vm.device.pci.Set.required
+admin.vm.device.pci.Unassign
admin.vm.feature.CheckWithAdminVM
admin.vm.feature.CheckWithNetvm
admin.vm.feature.CheckWithTemplate
@@ -366,6 +372,7 @@ done
%{python3_sitelib}/qubes/app.py
%{python3_sitelib}/qubes/backup.py
%{python3_sitelib}/qubes/config.py
+%{python3_sitelib}/qubes/device_protocol.py
%{python3_sitelib}/qubes/devices.py
%{python3_sitelib}/qubes/dochelpers.py
%{python3_sitelib}/qubes/events.py
@@ -438,6 +445,7 @@ done
%{python3_sitelib}/qubes/ext/r3compatibility.py
%{python3_sitelib}/qubes/ext/services.py
%{python3_sitelib}/qubes/ext/supported_features.py
+%{python3_sitelib}/qubes/ext/utils.py
%{python3_sitelib}/qubes/ext/windows.py
%{python3_sitelib}/qubes/ext/vm_config.py
diff --git a/templates/libvirt/xen.xml b/templates/libvirt/xen.xml
index 40af09f33..a9ce7c1db 100644
--- a/templates/libvirt/xen.xml
+++ b/templates/libvirt/xen.xml
@@ -7,7 +7,7 @@
{% block basic %}
{{ vm.name }}
{{ vm.uuid }}
- {% if ((vm.virt_mode == 'hvm' and vm.devices['pci'].persistent() | list)
+ {% if ((vm.virt_mode == 'hvm' and vm.devices['pci'].get_assigned_devices() | list)
or vm.maxmem == 0) -%}
{{ vm.memory }}
{% elif vm.use_memory_hotplug %}
@@ -79,7 +79,7 @@
{% endif %}
- {% if vm.devices['pci'].persistent() | list
+ {% if vm.devices['pci'].get_assigned_devices() | list
and vm.features.get('pci-e820-host', True) %}
@@ -156,7 +156,7 @@
{# start external devices from xvdi #}
{% set counter = {'i': 4} %}
- {% for assignment in vm.devices.block.assignments(True) %}
+ {% for assignment in vm.devices.block.get_assigned_devices(False) %}
{% set device = assignment.device %}
{% set options = assignment.options %}
{% include 'libvirt/devices/block.xml' %}
@@ -166,7 +166,7 @@
{% include 'libvirt/devices/net.xml' with context %}
{% endif %}
- {% for assignment in vm.devices.pci.assignments(True) %}
+ {% for assignment in vm.devices.pci.get_assigned_devices(True) %}
{% set device = assignment.device %}
{% set options = assignment.options %}
{% set power_mgmt =