diff --git a/Makefile b/Makefile index 7a410ade1..a52eb2e29 100644 --- a/Makefile +++ b/Makefile @@ -66,7 +66,7 @@ ADMIN_API_METHODS_SIMPLE = \ admin.vm.device.pci.Attached \ admin.vm.device.pci.Available \ admin.vm.device.pci.Detach \ - admin.vm.device.pci.Set.required \ + admin.vm.device.pci.Set.assignment \ admin.vm.device.pci.Unassign \ admin.vm.device.block.Assign \ admin.vm.device.block.Assigned \ @@ -74,7 +74,7 @@ ADMIN_API_METHODS_SIMPLE = \ admin.vm.device.block.Attached \ admin.vm.device.block.Available \ admin.vm.device.block.Detach \ - admin.vm.device.block.Set.required \ + admin.vm.device.block.Set.assignment \ admin.vm.device.block.Unassign \ admin.vm.device.usb.Assign \ admin.vm.device.usb.Assigned \ @@ -82,7 +82,7 @@ ADMIN_API_METHODS_SIMPLE = \ admin.vm.device.usb.Attached \ admin.vm.device.usb.Available \ admin.vm.device.usb.Detach \ - admin.vm.device.usb.Set.required \ + admin.vm.device.usb.Set.assignment \ admin.vm.device.usb.Unassign \ admin.vm.device.mic.Assign \ admin.vm.device.mic.Assigned \ @@ -90,7 +90,7 @@ ADMIN_API_METHODS_SIMPLE = \ admin.vm.device.mic.Attached \ admin.vm.device.mic.Available \ admin.vm.device.mic.Detach \ - admin.vm.device.mic.Set.required \ + admin.vm.device.mic.Set.assignment \ admin.vm.device.mic.Unassign \ admin.vm.feature.CheckWithNetvm \ admin.vm.feature.CheckWithTemplate \ @@ -227,7 +227,7 @@ endif 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.Set.assignment \ 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 e64811a3e..bb5c742bc 100644 --- a/doc/qubes-devices.rst +++ b/doc/qubes-devices.rst @@ -6,8 +6,8 @@ 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. +Devices are identified by pair of (backend domain, `port_id`), where `port_id` +is :py:class:`str` and can contain only characters from `[a-zA-Z0-9._-]` set. Device Assignment vs Attachment @@ -100,17 +100,17 @@ The microphone cannot be assigned (potentially) to any VM (attempting to attach Understanding Device Self Identity ---------------------------------- -It is important to understand that :py:class:`qubes.device_protocol.Device` does not +It is important to understand that :py:class:`qubes.device_protocol.Port` 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 +will be automatically attached. We can limit this using :py:meth:`qubes.device_protocol.DeviceInfo.device_id`, 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` +and encoded interfaces. In the case of block devices, `device_id` consists of the parent port to which the device is connected (if any), -the parent's `self_identity`, and the interface/partition number. +the parent's `device_id`, 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. diff --git a/qubes-rpc-policy/90-admin-default.policy.header b/qubes-rpc-policy/90-admin-default.policy.header index 864872ba4..bdd5c7065 100644 --- a/qubes-rpc-policy/90-admin-default.policy.header +++ b/qubes-rpc-policy/90-admin-default.policy.header @@ -26,7 +26,7 @@ !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.Set.required * include/admin-local-rwx +!include-service admin.vm.device.mic.Set.assignment * 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 @@ -34,6 +34,6 @@ !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.Set.required * include/admin-local-rwx +!include-service admin.vm.device.usb.Set.assignment * include/admin-local-rwx !include-service admin.vm.device.usb.Unassign * include/admin-local-rwx diff --git a/qubes/api/admin.py b/qubes/api/admin.py index caabed1bf..1318ec0ce 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -45,7 +45,8 @@ import qubes.vm import qubes.vm.adminvm import qubes.vm.qubesvm -from qubes.device_protocol import Device +from qubes.device_protocol import ( + VirtualDevice, UnknownDevice, DeviceAssignment, AssignmentMode) class QubesMgmtEventsDispatcher: @@ -1211,14 +1212,15 @@ async def vm_device_available(self, endpoint): raise qubes.exc.QubesException("qubesd shutdown in progress") raise if self.arg: - devices = [dev for dev in devices if dev.ident == self.arg] + devices = [dev for dev in devices if dev.port_id == self.arg] # 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 = {dev.ident: dev.serialize().decode() for dev in devices} - return ''.join('{} {}\n'.format(ident, dev_info[ident]) - for ident in sorted(dev_info)) + dev_info = {f'{dev.port_id}:{dev.device_id}': + dev.serialize().decode() for dev in devices} + return ''.join(f'{port_id} {dev_info[port_id]}\n' + for port_id in sorted(dev_info)) @qubes.api.method('admin.vm.device.{endpoint}.Assigned', endpoints=(ep.name for ep in importlib.metadata.entry_points(group='qubes.devices')), @@ -1237,7 +1239,7 @@ async def vm_device_list(self, endpoint): 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) + if (str(dev.backend_domain), dev.port_id) == (select_backend, select_ident)] # no duplicated devices, but device may not exist, in which case # the list is empty @@ -1246,12 +1248,13 @@ async def vm_device_list(self, endpoint): device_assignments, devclass=devclass) dev_info = { - f'{assignment.backend_domain}+{assignment.ident}': + (f'{assignment.backend_domain}' + f'+{assignment.port_id}:{assignment.device_id}'): 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)) + return ''.join('{} {}\n'.format(port_id, dev_info[port_id]) + for port_id in sorted(dev_info)) @qubes.api.method( 'admin.vm.device.{endpoint}.Attached', @@ -1272,21 +1275,24 @@ async def vm_device_attached(self, endpoint): 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) + if (str(dev.backend_domain), dev.port_id) == (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) + device_assignments = [ + a.clone(device=self.app.domains[a.backend_name + ].devices[devclass][a.port_id]) for a in device_assignments] + device_assignments = self.fire_event_for_filter( + device_assignments, devclass=devclass) dev_info = { - f'{assignment.backend_domain}+{assignment.ident}': + (f'{assignment.backend_domain}' + f'+{assignment.port_id}:{assignment.device_id}'): 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)) + return ''.join('{} {}\n'.format(port_id, dev_info[port_id]) + for port_id in sorted(dev_info)) # Assign/Unassign action can modify only persistent state of running VM. # For this reason, write=True @@ -1295,11 +1301,7 @@ async def vm_device_attached(self, endpoint): 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] + dev = self.load_device_info(devclass) assignment = qubes.device_protocol.DeviceAssignment.deserialize( untrusted_payload, expected_device=dev @@ -1315,6 +1317,21 @@ async def vm_device_assign(self, endpoint, untrusted_payload): await self.dest.devices[devclass].assign(assignment) self.app.save() + def load_device_info(self, devclass) -> VirtualDevice: + # qrexec already verified that no strange characters are in self.arg + _dev = VirtualDevice.from_qarg(self.arg, devclass, self.app.domains) + if _dev.port_id == '*' or _dev.device_id == '*': + return _dev + # load all info, may raise KeyError, either on domain or port_id + try: + dev = self.app.domains[ + _dev.backend_domain].devices[devclass][_dev.port_id] + if isinstance(dev, UnknownDevice): + return _dev + return dev + except KeyError: + return _dev + # Assign/Unassign action can modify only persistent state of running VM. # For this reason, write=True @qubes.api.method( @@ -1322,21 +1339,14 @@ async def vm_device_assign(self, endpoint, untrusted_payload): endpoints=( ep.name for ep in importlib.metadata.entry_points(group='qubes.devices')), - no_payload=True, scope='local', write=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] + dev = self.load_device_info(devclass) + assignment = DeviceAssignment(dev) 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() @@ -1350,20 +1360,13 @@ async def vm_device_unassign(self, endpoint): scope='local', execute=True) async def vm_device_attach(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 - ) + dev = self.load_device_info(devclass) + assignment = DeviceAssignment.deserialize( + untrusted_payload, expected_device=dev) self.fire_event_for_permission( - device=dev, devclass=devclass, - required=assignment.required, - attach_automatically=assignment.attach_automatically, + device=dev, + mode=assignment.mode.value, options=assignment.options ) @@ -1379,23 +1382,16 @@ async def vm_device_attach(self, endpoint, untrusted_payload): no_payload=True, scope='local', execute=True) async def vm_device_detach(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 device isn't found, it will be UnknownDevice - # instance - but allow it, otherwise it will be impossible to detach - # already removed device - dev = self.app.domains[backend_domain].devices[devclass][ident] + dev = self.load_device_info(devclass) self.fire_event_for_permission(device=dev, devclass=devclass) - assignment = qubes.device_protocol.DeviceAssignment( - dev.backend_domain, dev.ident, devclass) + assignment = qubes.device_protocol.DeviceAssignment(dev) await self.dest.devices[devclass].detach(assignment) # 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', + @qubes.api.method('admin.vm.device.{endpoint}.Set.assignment', endpoints=(ep.name for ep in importlib.metadata.entry_points(group='qubes.devices')), scope='local', write=True) @@ -1409,20 +1405,20 @@ async def vm_device_set_required(self, endpoint, untrusted_payload): """ devclass = endpoint - self.enforce(untrusted_payload in (b'True', b'False')) - # now is safe to eval, since the value of untrusted_payload is trusted - # pylint: disable=eval-used - assignment = eval(untrusted_payload) - del untrusted_payload + allowed_values = { + b'required': AssignmentMode.REQUIRED, + b'ask-to-attach': AssignmentMode.ASK, + b'auto-attach': AssignmentMode.AUTO} + try: + mode = allowed_values[untrusted_payload] + except KeyError: + raise qubes.exc.PermissionDenied() - # qrexec already verified that no strange characters are in self.arg - backend_domain_name, ident = self.arg.split('+', 1) - backend_domain = self.app.domains[backend_domain_name] - dev = Device(backend_domain, ident, devclass) + dev = VirtualDevice.from_qarg(self.arg, devclass, self.app.domains) - self.fire_event_for_permission(device=dev, assignment=assignment) + self.fire_event_for_permission(device=dev, mode=mode) - await self.dest.devices[devclass].update_required(dev, assignment) + await self.dest.devices[devclass].update_assignment(dev, mode) self.app.save() @qubes.api.method('admin.vm.firewall.Get', no_payload=True, diff --git a/qubes/app.py b/qubes/app.py index 2a00355b7..e9843ee2f 100644 --- a/qubes/app.py +++ b/qubes/app.py @@ -1521,7 +1521,7 @@ def on_domain_pre_deleted(self, event, vm): assignments = vm.get_provided_assignments() if assignments: desc = ', '.join( - assignment.ident for assignment in assignments) + assignment.port_id for assignment in assignments) raise qubes.exc.QubesVMInUseError( vm, 'VM has devices assigned to other VMs: ' + desc) diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index 3d8b9aaa6..f206afdcc 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -5,10 +5,10 @@ # 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) 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 @@ -32,7 +32,7 @@ import string import sys from enum import Enum -from typing import Optional, Dict, Any, List, Union, Tuple +from typing import Optional, Dict, Any, List, Union, Tuple, Callable import qubes.utils @@ -51,101 +51,21 @@ def qbool(value): return qubes.property.bool(None, None, value) -class Device: +class DeviceSerializer: """ - 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'). + Group of method for serialization of device properties. """ ALLOWED_CHARS_KEY = set( - string.digits + string.ascii_letters - + r"!#$%&()*+,-./:;<>?@[\]^_{|}~") + 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. + Unpacks basic port properties from a serialized encoded string. Returns: tuple: A tuple containing two dictionaries, properties and options, @@ -168,24 +88,24 @@ def unpack_properties( values = [] ut_key, _, ut_rest = ut_decoded.partition("='") - key = sanitize_str( - ut_key, cls.ALLOWED_CHARS_KEY, + key = cls.sanitize_str( + ut_key.strip(), 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, + value = cls.sanitize_str( + cls.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, + key = cls.sanitize_str( + ut_key.strip(), 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, + value = cls.sanitize_str( + cls.deserialize_str(ut_value), cls.ALLOWED_CHARS_PARAM, error_message='Invalid chars in property value: ') values.append(value) @@ -199,23 +119,25 @@ def unpack_properties( return properties, options @classmethod - def pack_property(cls, key: str, value: str): + def pack_property(cls, key: str, value: Optional[str]): """ Add property `key=value` to serialization. """ - key = sanitize_str( + if value is None: + return b'' + key = cls.sanitize_str( key, cls.ALLOWED_CHARS_KEY, error_message='Invalid chars in property name: ') - value = sanitize_str( - serialize_str(value), cls.ALLOWED_CHARS_PARAM, + value = cls.sanitize_str( + cls.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]): + def parse_basic_device_properties( + expected_device: 'VirtualDevice', properties: Dict[str, Any]): """ - Validates properties against an expected device configuration. + Validates properties against an expected port configuration. Modifies `properties`. @@ -223,27 +145,418 @@ def check_device_properties( UnexpectedDeviceProperty: If any property does not match the expected values. """ - expected = expected_device - exp_vm_name = expected.backend_domain.name + expected = expected_device.port + exp_vm_name = expected.backend_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 + properties.pop('backend_domain', None) - if properties.get('ident', expected.ident) != expected.ident: + if properties.get('port_id', expected.port_id) != expected.port_id: raise UnexpectedDeviceProperty( - f"Got device with id: {properties['ident']} " - f"when expected id: {expected.ident}.") - properties['ident'] = expected.ident + f"Got device from port: {properties['port_id']} " + f"when expected port: {expected.port_id}.") + properties.pop('port_id', None) + + if expected.devclass == 'peripheral': + expected = Port( + expected.backend_domain, + expected.port_id, + properties.get('devclass', None)) + if properties.get('devclass', expected.devclass) != expected.devclass: + raise UnexpectedDeviceProperty( + f"Got {properties['devclass']} device " + f"when expected {expected.devclass}.") + properties.pop('devclass', None) - if expected.devclass_is_set: - if (properties.get('devclass', expected.devclass) - != expected.devclass): + expected_devid = expected_device.device_id + # device id is optional + if expected_devid != '*': + if properties.get('device_id', expected_devid) != expected_devid: raise UnexpectedDeviceProperty( - f"Got {properties['devclass']} device " - f"when expected {expected.devclass}.") - properties['devclass'] = expected.devclass + f"Unrecognized device identity '{properties['device_id']}' " + f"expected '{expected_device.device_id}'" + ) + properties['device_id'] = properties.get('device_id', expected_devid) + + properties['port'] = expected + + @staticmethod + def serialize_str(value: str): + """ + Serialize python string to ensure consistency. + """ + return "'" + str(value).replace("'", r"\'") + "'" + + @staticmethod + def deserialize_str(value: str): + """ + Deserialize python string to ensure consistency. + """ + return value.replace(r"\'", "'") + + @staticmethod + 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 Port: + """ + Class of a *bus* device port with *port id* exposed by a *backend domain*. + + Attributes: + backend_domain (QubesVM): The domain which exposes devices, + e.g.`sys-usb`. + port_id (str): A unique (in backend domain) identifier for the port. + devclass (str): The class of the port (e.g., 'usb', 'pci'). + """ + def __init__(self, backend_domain, port_id, devclass): + self.__backend_domain = backend_domain + self.__port_id = port_id + self.__devclass = devclass + + def __hash__(self): + return hash((self.backend_name, self.port_id, self.devclass)) + + def __eq__(self, other): + if isinstance(other, Port): + return ( + self.backend_name == other.backend_name and + self.port_id == other.port_id and + self.devclass == other.devclass + ) + return False + + def __lt__(self, other): + if isinstance(other, Port): + return (self.backend_name, self.devclass, self.port_id) < \ + (self.backend_name, other.devclass, other.port_id) + raise TypeError(f"Comparing instances of 'Port' and '{type(other)}' " + "is not supported") + + def __repr__(self): + return f"{self.backend_name}+{self.port_id}" + + def __str__(self): + return f"{self.backend_name}:{self.port_id}" + + @property + def backend_name(self) -> str: + # pylint: disable=missing-function-docstring + if self.backend_domain not in (None, "*"): + return self.backend_domain.name + return "*" + + @classmethod + def from_qarg( + cls, representation: str, devclass, domains, blind=False + ) -> 'Port': + """ + Parse qrexec argument + to retrieve Port. + """ + if blind: + get_domain = domains.get_blind + else: + get_domain = domains.__getitem__ + return cls._parse(representation, devclass, get_domain, '+') + + @classmethod + def from_str( + cls, representation: str, devclass, domains, blind=False + ) -> 'Port': + """ + Parse string : to retrieve Port. + """ + if blind: + get_domain = domains.get_blind + else: + get_domain = domains.__getitem__ + return cls._parse(representation, devclass, get_domain, ':') + + @classmethod + def _parse( + cls, + representation: str, + devclass: str, + get_domain: Callable, + sep: str + ) -> 'Port': + """ + Parse string representation and return instance of Port. + """ + backend_name, port_id = representation.split(sep, 1) + backend = get_domain(backend_name) + return cls(backend_domain=backend, port_id=port_id, devclass=devclass) + + @property + def port_id(self) -> str: + """ + Immutable port identifier. + + Unique for given domain and devclass. + """ + if self.__port_id is not None: + return self.__port_id + return '*' + + @property + def backend_domain(self) -> Optional[QubesVM]: + """ Which domain exposed this port. (immutable)""" + return self.__backend_domain + + @property + def devclass(self) -> str: + """ Immutable port class such like: 'usb', 'pci' etc. + + For unknown classes "peripheral" is returned. + """ + if self.__devclass: + return self.__devclass + return "peripheral" + + +class VirtualDevice: + """ + Class of a device connected to *port*. + + Attributes: + port (Port): A unique identifier for the port within the backend domain. + device_id (str): A unique identifier for the device. + """ + def __init__( + self, + port: Optional[Port] = None, + device_id: Optional[str] = None, + ): + assert port is not None or device_id is not None + self.port: Optional[Port] = port + self._device_id = device_id + + def clone(self, **kwargs) -> 'VirtualDevice': + """ + Clone object and substitute attributes with explicitly given. + """ + attr = { + "port": self.port, + "device_id": self.device_id, + } + attr.update(kwargs) + return VirtualDevice(**attr) + + @property + def port(self) -> Union[Port, str]: + # pylint: disable=missing-function-docstring + return self._port + + @port.setter + def port(self, value: Union[Port, str, None]): + # pylint: disable=missing-function-docstring + self._port = value if value is not None else '*' + + @property + def device_id(self) -> str: + # pylint: disable=missing-function-docstring + if self._device_id is not None: + return self._device_id + return '*' + + @property + def is_device_id_set(self) -> bool: + """ + Check if `device_id` is explicitly set. + """ + return self._device_id is not None + + @property + def backend_domain(self) -> Union[QubesVM, str]: + # pylint: disable=missing-function-docstring + if self.port != '*' and self.port.backend_domain is not None: + return self.port.backend_domain + return '*' + + @property + def backend_name(self) -> str: + """ + Return backend domain name if any or `*`. + """ + if self.port != '*': + return self.port.backend_name + return '*' + + @property + def port_id(self) -> str: + # pylint: disable=missing-function-docstring + if self.port != '*' and self.port.port_id is not None: + return self.port.port_id + return '*' + + @property + def devclass(self) -> str: + # pylint: disable=missing-function-docstring + if self.port != '*' and self.port.devclass is not None: + return self.port.devclass + return '*' + + @property + def description(self) -> str: + """ + Return human-readable description of the device identity. + """ + if self.device_id == '*': + return 'any device' + return self.device_id + + def __hash__(self): + return hash((self.port, self.device_id)) + + def __eq__(self, other): + if isinstance(other, (VirtualDevice, DeviceAssignment)): + result = ( + self.port == other.port + and self.device_id == other.device_id + ) + return result + if isinstance(other, Port): + return ( + self.port == other + and self.device_id == '*' + ) + return super().__eq__(other) + + def __lt__(self, other): + """ + Desired order (important for auto-attachment): + + 1. : + 2. :* + 3. *: + 4. *:* + """ + if isinstance(other, (VirtualDevice, DeviceAssignment)): + if self.port == '*' and other.port != '*': + return True + if self.port != '*' and other.port == '*': + return False + reprs = {self: [self.port], other: [other.port]} + for obj, obj_repr in reprs.items(): + if obj.device_id != '*': + obj_repr.append(obj.device_id) + return reprs[self] < reprs[other] + if isinstance(other, Port): + _other = VirtualDevice(other, '*') + return self < _other + raise TypeError( + f"Comparing instances of {type(self)} and '{type(other)}' " + "is not supported") + + def __repr__(self): + return f"{self.port!r}:{self.device_id}" + + def __str__(self): + return f"{self.port}:{self.device_id}" + + @classmethod + def from_qarg( + cls, + representation: str, + devclass, + domains, + blind=False, + backend=None, + ) -> 'VirtualDevice': + """ + Parse qrexec argument +: to get device info + """ + if backend is None: + if blind: + get_domain = domains.get_blind + else: + get_domain = domains.__getitem__ + else: + get_domain = None + return cls._parse(representation, devclass, get_domain, backend, '+') + + @classmethod + def from_str( + cls, representation: str, devclass: Optional[str], domains, + blind=False, backend=None + ) -> 'VirtualDevice': + """ + Parse string +: to get device info + """ + if backend is None: + if blind: + get_domain = domains.get_blind + else: + get_domain = domains.__getitem__ + else: + get_domain = None + return cls._parse(representation, devclass, get_domain, backend, ':') + + @classmethod + def _parse( + cls, + representation: str, + devclass: Optional[str], + get_domain: Callable, + backend, + sep: str + ) -> 'VirtualDevice': + """ + Parse string representation and return instance of VirtualDevice. + """ + if backend is None: + backend_name, identity = representation.split(sep, 1) + if backend_name != '*': + backend = get_domain(backend_name) + else: + identity = representation + port_id, _, devid = identity.partition(':') + if devid == '': + devid = None + return cls( + Port(backend_domain=backend, port_id=port_id, devclass=devclass), + device_id=devid + ) + + def serialize(self) -> bytes: + """ + Serialize an object to be transmitted via Qubes API. + """ + properties = b' '.join( + DeviceSerializer.pack_property(key, value) + for key, value in ( + ('device_id', self.device_id), + ('port_id', self.port_id), + ('devclass', self.devclass))) + + properties += b' ' + DeviceSerializer.pack_property( + 'backend_domain', self.backend_name) + + return properties class DeviceCategory(Enum): @@ -262,8 +575,8 @@ class DeviceCategory(Enum): Mouse = ("u03**02", "p0902**") Printer = ("u07****",) Scanner = ("p0903**",) - # Multimedia = Audio, Video, Displays etc. Microphone = ("m******",) + # Multimedia = Audio, Video, Displays etc. Multimedia = ("u01****", "u0e****", "u06****", "u10****", "p03****", "p04****") Wireless = ("ue0****", "p0d****") @@ -368,7 +681,7 @@ def __eq__(self, other): def __str__(self): if self.devclass == "block": - return "Block device" + return "Block Device" if self.devclass in ("usb", "pci"): # try subclass first as in `lspci` result = self._load_classes(self.devclass).get( @@ -425,27 +738,46 @@ def _load_classes(bus: str): return result + def matches(self, other: 'DeviceInterface') -> bool: + """ + Check if this `DeviceInterface` (pattern) matches given one. + + The matching is done character by character using the string + representation (`repr`) of both objects. A wildcard character (`'*'`) + in the pattern (i.e., `self`) can match any character in the candidate + (i.e., `other`). + The two representations must be of the same length. + """ + pattern = repr(self) + candidate = repr(other) + if len(pattern) != len(candidate): + return False + for patt, cand in zip(pattern, candidate): + if patt == '*': + continue + if patt != cand: + return False + return True + -class DeviceInfo(Device): +class DeviceInfo(VirtualDevice): """ Holds all information about a device """ def __init__( self, - backend_domain: QubesVM, - ident: str, - devclass: Optional[str] = None, + port: Port, 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, + parent: Optional[Port] = None, attachment: Optional[QubesVM] = None, - self_identity: Optional[str] = None, + device_id: Optional[str] = None, **kwargs ): - super().__init__(backend_domain, ident, devclass) + super().__init__(port, device_id) self._vendor = vendor self._product = product @@ -455,7 +787,6 @@ def __init__( self._interfaces = interfaces self._parent = parent self._attachment = attachment - self._self_identity = self_identity self.data = kwargs @@ -552,8 +883,10 @@ def description(self) -> str: else: vendor = "unknown vendor" - main_interface = str(self.interfaces[0]) - return f"{main_interface}: {vendor} {prod}" + cat = self.interfaces[0].category.name + if cat == "Other": + cat = str(self.interfaces[0]) + return f"{cat}: {vendor} {prod}" @property def interfaces(self) -> List[DeviceInterface]: @@ -567,7 +900,7 @@ def interfaces(self) -> List[DeviceInterface]: return self._interfaces @property - def parent_device(self) -> Optional[Device]: + def parent_device(self) -> Optional[VirtualDevice]: """ The parent device, if any. @@ -577,7 +910,7 @@ def parent_device(self) -> Optional[Device]: return self._parent @property - def subdevices(self) -> List['DeviceInfo']: + def subdevices(self) -> List[VirtualDevice]: """ The list of children devices if any. @@ -585,7 +918,7 @@ def subdevices(self) -> List['DeviceInfo']: the subdevices id should be here. """ return [dev for dev in self.backend_domain.devices[self.devclass] - if dev.parent_device.ident == self.ident] + if dev.parent_device.port.port_id == self.port_id] @property def attachment(self) -> Optional[QubesVM]: @@ -598,35 +931,33 @@ def serialize(self) -> bytes: """ Serialize an object to be transmitted via Qubes API. """ - # 'backend_domain', 'attachment', 'interfaces', 'data', 'parent_device' + properties = VirtualDevice.serialize(self) + # '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) + default = DeviceInfo(self.port) + default_attrs = {'vendor', 'product', 'manufacturer', 'name', 'serial'} + properties += b' ' + b' '.join( + DeviceSerializer.pack_property(key, value) for key, value in ( + (key, getattr(self, key)) for key in default_attrs + if getattr(self, key) != getattr(default, key))) if self.attachment: - properties = self.pack_property( + properties += b' ' + DeviceSerializer.pack_property( 'attachment', self.attachment.name) - properties += b' ' + self.pack_property( + properties += b' ' + DeviceSerializer.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( + properties += b' ' + DeviceSerializer.pack_property( + 'parent_port_id', self.parent_device.port_id) + properties += b' ' + DeviceSerializer.pack_property( 'parent_devclass', self.parent_device.devclass) for key, value in self.data.items(): - properties += b' ' + self.pack_property("_" + key, value) + properties += b' ' + DeviceSerializer.pack_property( + "_" + key, value) return properties @@ -640,19 +971,17 @@ def deserialize( """ 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, - ) + head, _, rest = serialization.partition(b' ') + device = VirtualDevice.from_str( + head.decode('ascii', errors='ignore'), expected_devclass, + domains=None, backend=expected_backend_domain) try: device = cls._deserialize(rest, device) # pylint: disable=broad-exception-caught except Exception as exc: - print(exc, file=sys.stderr) + print(str(exc), file=sys.stderr) + device = UnknownDevice.from_device(device) return device @@ -660,15 +989,17 @@ def deserialize( def _deserialize( cls, untrusted_serialization: bytes, - expected_device: Device + expected_device: VirtualDevice ) -> 'DeviceInfo': """ Actually deserializes the object. """ - properties, options = cls.unpack_properties(untrusted_serialization) + properties, options = DeviceSerializer.unpack_properties( + untrusted_serialization) properties.update(options) - cls.check_device_properties(expected_device, properties) + DeviceSerializer.parse_basic_device_properties( + expected_device, properties) if 'attachment' not in properties or not properties['attachment']: properties['attachment'] = None @@ -677,12 +1008,6 @@ def _deserialize( 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 = [ @@ -691,9 +1016,9 @@ def _deserialize( properties['interfaces'] = interfaces if 'parent_ident' in properties: - properties['parent'] = Device( + properties['parent'] = Port( backend_domain=expected_device.backend_domain, - ident=properties['parent_ident'], + port_id=properties['parent_ident'], devclass=properties['parent_devclass'], ) del properties['parent_ident'] @@ -702,7 +1027,7 @@ def _deserialize( return cls(**properties) @property - def self_identity(self) -> str: + def device_id(self) -> str: """ Get additional identification of device presented by device itself. @@ -717,88 +1042,58 @@ def self_identity(self) -> str: 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: + if not self._device_id: return "0000:0000::?******" - return self._self_identity + return self._device_id + @device_id.setter + def device_id(self, value): + # Do not auto-override value like in super class + self._device_id = value -def serialize_str(value: str): - """ - Serialize python string to ensure consistency. - """ - return "'" + str(value).replace("'", r"\'") + "'" +class UnknownDevice(DeviceInfo): + # pylint: disable=too-few-public-methods + """Unknown device - for example, exposed by domain not running currently""" -def deserialize_str(value: str): - """ - Deserialize python string to ensure consistency. - """ - return value.replace(r"\'", "'") + @staticmethod + def from_device(device: VirtualDevice) -> 'UnknownDevice': + """ + Return `UnknownDevice` based on any virtual device. + """ + return UnknownDevice(device.port, device_id=device.device_id) -def sanitize_str( - untrusted_value: str, - allowed_chars: set, - replace_char: str = None, - error_message: str = "" -) -> str: +class AssignmentMode(Enum): """ - Sanitize given untrusted string. - - If `replace_char` is not None, ignore `error_message` and replace invalid - characters with the string. + Device assignment modes """ - 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 - + MANUAL = "manual" + ASK = "ask-to-attach" + AUTO = "auto-attach" + REQUIRED = "required" -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. +class DeviceAssignment: + """ + Maps a device to a frontend_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) + def __init__( + self, + device: VirtualDevice, + frontend_domain=None, + options=None, + mode: Union[str, AssignmentMode] = "manual", + ): + if isinstance(device, DeviceInfo): + device = VirtualDevice(device.port, device.device_id) + self.virtual_device = device self.__options = options or {} - if required: - assert attach_automatically - self.__required = required - self.__attach_automatically = attach_automatically + if isinstance(mode, AssignmentMode): + self.mode = mode + else: + self.mode = AssignmentMode(mode) self.frontend_domain = frontend_domain def clone(self, **kwargs): @@ -806,33 +1101,102 @@ def clone(self, **kwargs): Clone object and substitute attributes with explicitly given. """ attr = { - "backend_domain": self.backend_domain, - "ident": self.ident, + "device": self.virtual_device, "options": self.options, - "required": self.required, - "attach_automatically": self.attach_automatically, + "mode": self.mode, "frontend_domain": self.frontend_domain, - "devclass": self.devclass, } attr.update(kwargs) return self.__class__(**attr) - @classmethod - def from_device(cls, device: Device, **kwargs) -> 'DeviceAssignment': + def __repr__(self): + return f"{self.virtual_device!r}" + + def __str__(self): + return f"{self.virtual_device}" + + def __hash__(self): + return hash(self.virtual_device) + + def __eq__(self, other): + if isinstance(other, (VirtualDevice, DeviceAssignment)): + result = ( + self.port == other.port + and self.device_id == other.device_id + ) + return result + return False + + def __lt__(self, other): + if isinstance(other, DeviceAssignment): + return self.virtual_device < other.virtual_device + if isinstance(other, VirtualDevice): + return self.virtual_device < other + raise TypeError( + f"Comparing instances of {type(self)} and '{type(other)}' " + "is not supported") + + @property + def backend_domain(self) -> QubesVM: + # pylint: disable=missing-function-docstring + return self.virtual_device.backend_domain + + @property + def backend_name(self) -> str: + # pylint: disable=missing-function-docstring + return self.virtual_device.backend_name + + @property + def port_id(self) -> str: + # pylint: disable=missing-function-docstring + return self.virtual_device.port_id + + @property + def devclass(self) -> str: + # pylint: disable=missing-function-docstring + return self.virtual_device.devclass + + @property + def device_id(self) -> str: + # pylint: disable=missing-function-docstring + return self.virtual_device.device_id + + @property + def devices(self) -> List[DeviceInfo]: + """Get DeviceInfo objects corresponding to this DeviceAssignment""" + if self.port_id != '*': + # could return UnknownDevice + return [self.backend_domain.devices[self.devclass][self.port_id]] + result = [] + if self.device_id == "0000:0000::?******": + return result + for dev in self.backend_domain.devices[self.devclass]: + if dev.device_id == self.device_id: + result.append(dev) + return result + + @property + def device(self) -> DeviceInfo: """ - Get assignment of the device. + Get single DeviceInfo object or raise an error. + + If port id is set we have exactly one device + since we can attach ony one device to one port. + If assignment is more general we can get 0 or many devices. """ - return cls( - backend_domain=device.backend_domain, - ident=device.ident, - devclass=device.devclass, - **kwargs - ) + devices = self.devices + if len(devices) == 1: + return devices[0] + if len(devices) > 1: + raise ProtocolError("Too many devices matches to assignment") + raise ProtocolError("Any devices matches to assignment") @property - def device(self) -> DeviceInfo: - """Get DeviceInfo object corresponding to this DeviceAssignment""" - return self.backend_domain.devices[self.devclass][self.ident] + def port(self) -> Port: + """ + Device port visible in Qubes. + """ + return Port(self.backend_domain, self.port_id, self.devclass) @property def frontend_domain(self) -> Optional[QubesVM]: @@ -855,7 +1219,10 @@ def attached(self) -> bool: Returns False if device is attached to different domain """ - return self.device.attachment == self.frontend_domain + for device in self.devices: + if device.attachment and device.attachment == self.frontend_domain: + return True + return False @property def required(self) -> bool: @@ -863,11 +1230,7 @@ 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 + return self.mode == AssignmentMode.REQUIRED @property def attach_automatically(self) -> bool: @@ -875,11 +1238,11 @@ 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 + return self.mode in ( + AssignmentMode.AUTO, + AssignmentMode.ASK, + AssignmentMode.REQUIRED + ) @property def options(self) -> Dict[str, Any]: @@ -895,24 +1258,16 @@ 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) - + properties = self.virtual_device.serialize() + properties += b' ' + DeviceSerializer.pack_property( + 'mode', self.mode.value) if self.frontend_domain is not None: - properties += b' ' + self.pack_property( + properties += b' ' + DeviceSerializer.pack_property( 'frontend_domain', self.frontend_domain.name) for key, value in self.options.items(): - properties += b' ' + self.pack_property("_" + key, value) + properties += b' ' + DeviceSerializer.pack_property( + "_" + key, value) return properties @@ -920,7 +1275,7 @@ def serialize(self) -> bytes: def deserialize( cls, serialization: bytes, - expected_device: Device, + expected_device: VirtualDevice, ) -> 'DeviceAssignment': """ Recovers a serialized object, see: :py:meth:`serialize`. @@ -928,25 +1283,44 @@ def deserialize( try: result = cls._deserialize(serialization, expected_device) except Exception as exc: - raise ProtocolError() from exc + raise ProtocolError(str(exc)) from exc return result @classmethod def _deserialize( cls, untrusted_serialization: bytes, - expected_device: Device, + expected_device: VirtualDevice, ) -> 'DeviceAssignment': """ Actually deserializes the object. """ - properties, options = cls.unpack_properties(untrusted_serialization) + properties, options = DeviceSerializer.unpack_properties( + untrusted_serialization) properties['options'] = options - cls.check_device_properties(expected_device, properties) + DeviceSerializer.parse_basic_device_properties( + expected_device, properties) - properties['attach_automatically'] = qbool( - properties.get('attach_automatically', 'no')) - properties['required'] = qbool(properties.get('required', 'no')) + expected_device = expected_device.clone( + device_id=properties['device_id']) + # we do not need port, we need device + del properties['port'] + properties.pop('device_id', None) + properties['device'] = expected_device return cls(**properties) + + def matches(self, device: VirtualDevice) -> bool: + """ + Checks if the given device matches the assignment. + """ + if self.devclass != device.devclass: + return False + if self.backend_domain != device.backend_domain: + return False + if self.port_id not in ('*', device.port_id): + return False + if self.device_id not in ('*', device.device_id): + return False + return True diff --git a/qubes/devices.py b/qubes/devices.py index c073ef759..61890e4ef 100644 --- a/qubes/devices.py +++ b/qubes/devices.py @@ -28,8 +28,8 @@ 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 -:py:class:`str` and can contain only characters from `[a-zA-Z0-9._-]` set. +Devices are identified by pair of (backend domain, `port_id`), where `port_id` +is :py:class:`str` and can contain only characters from `[a-zA-Z0-9._-]` set. Such extension should: - provide `qubes.devices` endpoint - a class descendant from @@ -63,10 +63,13 @@ import qubes.exc import qubes.utils -from qubes.device_protocol import (Device, DeviceInfo, UnknownDevice, - DeviceAssignment) +from qubes.device_protocol import (Port, DeviceInfo, UnknownDevice, + DeviceAssignment, VirtualDevice, + AssignmentMode) +DEVICE_DENY_LIST = "/etc/qubes/device-deny.list" + class DeviceNotAssigned(qubes.exc.QubesException, KeyError): """ Trying to unassign not assigned device. @@ -126,7 +129,7 @@ class DeviceCollection: :param device: :py:class:`DeviceInfo` object to be attached - .. event:: device-detach: (device) + .. event:: device-detach: (port) Fired when device is detached from a VM. @@ -134,13 +137,13 @@ class DeviceCollection: :param device: :py:class:`DeviceInfo` object to be attached - .. event:: device-pre-detach: (device) + .. event:: device-pre-detach: (port) Fired before device is detached from a VM Handler for this event can be asynchronous (a coroutine). - :param device: :py:class:`DeviceInfo` object to be attached + :param port: :py:class:`Port` object from which device be detached .. event:: device-assign: (device, options) @@ -165,9 +168,9 @@ class DeviceCollection: event should return a list of py:class:`DeviceInfo` objects (or appropriate class specific descendant) - .. event:: device-get: (ident) + .. event:: device-get: (port_id) - Fired to get a single device, given by the `ident` parameter. + Fired to get a single device, given by the `port_id` parameter. Handlers of this event should either return appropriate object of :py:class:`DeviceInfo`, or :py:obj:`None`. Especially should not raise :py:class:`exceptions.KeyError`. @@ -193,9 +196,7 @@ async def attach(self, assignment: DeviceAssignment): Attach device to domain. """ - if not assignment.devclass_is_set: - assignment.devclass = self._bus - elif assignment.devclass != self._bus: + if assignment.devclass != self._bus: raise ValueError( f'Trying to attach {assignment.devclass} device ' f'when {self._bus} device expected.') @@ -205,8 +206,12 @@ async def attach(self, assignment: DeviceAssignment): self._vm,"VM not running, cannot attach device," " do you mean `assign`?") + if len(assignment.devices) != 1: + raise ValueError( + f'Cannot attach ambiguous {assignment.devclass} device.') + device = assignment.device - if device in self.get_attached_devices(): + if device in [ass.device for ass in self.get_attached_devices()]: raise DeviceAlreadyAttached( 'device {!s} of class {} already attached to {!s}'.format( device, self._bus, self._vm)) @@ -223,18 +228,19 @@ 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: + if assignment.devclass != self._bus: raise ValueError( - f'Trying to attach {assignment.devclass} device ' + f'Trying to assign {assignment.devclass} device ' f'when {self._bus} device expected.') - device = assignment.device - if device in self.get_assigned_devices(): + device = assignment.virtual_device + if assignment in self.get_assigned_devices(): raise DeviceAlreadyAssigned( - 'device {!s} of class {} already assigned to {!s}'.format( - device, self._bus, self._vm)) + f'{self._bus} device {device!s} ' + f'already assigned to {self._vm!s}') + + if not assignment.attach_automatically: + raise ValueError('Only auto-attachable devices can be assigned.') self._set.add(assignment) @@ -250,17 +256,16 @@ def load_assignment(self, device_assignment: DeviceAssignment): """ assert not self._vm.events_enabled assert device_assignment.attach_automatically - device_assignment.devclass = self._bus self._set.add(device_assignment) - async def update_required(self, device: Device, required: bool): + async def update_assignment( + self, device: VirtualDevice, mode: AssignmentMode + ): """ 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 + :param VirtualDevice device: device for which change required flag + :param AssignmentMode mode: new assignment mode """ if self._vm.is_halted(): raise qubes.exc.QubesVMNotStartedError( @@ -268,7 +273,7 @@ async def update_required(self, device: Device, required: bool): 'VM must be running to modify device assignment' ) assignments = [a for a in self.get_assigned_devices() - if a == device] + if a.virtual_device == device] if not assignments: raise qubes.exc.QubesValueError( f'Device {device} not assigned to {self._vm.name}') @@ -277,25 +282,27 @@ async def update_required(self, device: Device, required: bool): # be careful to use already present assignment, not the provided one # - to not change options as a side effect - if assignment.required == required: + if assignment.mode == mode: return - assignment.required = required + new_assignment = assignment.clone(mode=mode) + self._set.discard(assignment) + self._set.add(new_assignment) await self._vm.fire_event_async( 'device-assignment-changed:' + self._bus, device=device) - async def detach(self, device: Device): + async def detach(self, port: Port): """ Detach device from domain. """ for assign in self.get_attached_devices(): - if device == assign: + if port.port_id == assign.port_id: # load all options assignment = assign break else: raise DeviceNotAssigned( - f'device {device.ident!s} of class {self._bus} not ' + f'device {port.port_id!s} of class {self._bus} not ' f'attached to {self._vm!s}') if assignment.required and not self._vm.is_halted(): @@ -304,33 +311,33 @@ async def detach(self, device: Device): "Can not detach a required device from a non halted qube. " "You need to unassign device first.") - # use the local object - device = assignment.device + # use the local object, only one device can match + port = assignment.device.port await self._vm.fire_event_async( - 'device-pre-detach:' + self._bus, pre_event=True, device=device) + 'device-pre-detach:' + self._bus, pre_event=True, port=port) await self._vm.fire_event_async( - 'device-detach:' + self._bus, device=device) + 'device-detach:' + self._bus, port=port) - async def unassign(self, device_assignment: DeviceAssignment): + async def unassign(self, assignment: DeviceAssignment): """ Unassign device from domain. """ - for assignment in self.get_assigned_devices(): - if device_assignment == assignment: + all_ass = [] + for assign in self.get_assigned_devices(): + all_ass.append(assign) + if assignment == assign: # load all options - device_assignment = assignment + assignment = assign break else: raise DeviceNotAssigned( - f'device {device_assignment.ident!s} of class {self._bus} not ' - f'assigned to {self._vm!s}') + f'{self._bus} device {assignment} not assigned to {self._vm!s}') self._set.discard(assignment) - device = device_assignment.device await self._vm.fire_event_async( - 'device-unassign:' + self._bus, device=device) + 'device-unassign:' + self._bus, device=assignment.virtual_device) def get_dedicated_devices(self) -> Iterable[DeviceAssignment]: """ @@ -351,13 +358,10 @@ def get_attached_devices(self) -> Iterable[DeviceAssignment]: break else: yield DeviceAssignment( - backend_domain=dev.backend_domain, - ident=dev.ident, - options=options, + dev, frontend_domain=self._vm, - devclass=dev.devclass, - attach_automatically=False, - required=False, + options=options, + mode='manual', ) def get_assigned_devices( @@ -368,10 +372,10 @@ def get_assigned_devices( Safe to access before libvirt bootstrap. """ - for dev in self._set: - if required_only and not dev.required: + for ass in self._set: + if required_only and not ass.required: continue - yield dev + yield ass def get_exposed_devices(self) -> Iterable[DeviceInfo]: """ @@ -381,8 +385,8 @@ def get_exposed_devices(self) -> Iterable[DeviceInfo]: __iter__ = get_exposed_devices - def __getitem__(self, ident): - '''Get device object with given ident. + def __getitem__(self, port_id): + """Get device object with given port id. :returns: py:class:`DeviceInfo` @@ -391,15 +395,15 @@ def __getitem__(self, ident): devices - otherwise it will be impossible to detach already disconnected device. - :raises AssertionError: when multiple devices with the same ident are + :raises AssertionError: when multiple devices with the same port_id are found - ''' - dev = self._vm.fire_event('device-get:' + self._bus, ident=ident) + """ + dev = self._vm.fire_event('device-get:' + self._bus, port_id=port_id) if dev: assert len(dev) == 1 return dev[0] - return UnknownDevice(self._vm, ident, devclass=self._bus) + return UnknownDevice(Port(self._vm, port_id, devclass=self._bus)) class DeviceManager(dict): @@ -430,8 +434,9 @@ def add(self, assignment: DeviceAssignment): """ Add assignment to collection """ assert assignment.attach_automatically vm = assignment.backend_domain - ident = assignment.ident - key = (vm, ident) + port_id = assignment.port_id + dev_id = assignment.device_id + key = (vm, port_id, dev_id) assert key not in self._dict self._dict[key] = assignment @@ -442,20 +447,23 @@ def discard(self, assignment: DeviceAssignment): """ assert assignment.attach_automatically vm = assignment.backend_domain - ident = assignment.ident - key = (vm, ident) + port_id = assignment.port_id + dev_id = assignment.device_id + key = (vm, port_id, dev_id) if key not in self._dict: raise KeyError del self._dict[key] def __contains__(self, device) -> bool: - return (device.backend_domain, device.ident) in self._dict + key = (device.backend_domain, device.port_id, device.device_id) + return key in self._dict def get(self, device: DeviceInfo) -> DeviceAssignment: """ Returns the corresponding `DeviceAssignment` for the device. """ - return self._dict[(device.backend_domain, device.ident)] + key = (device.backend_domain, device.port_id, device.device_id) + return self._dict[key] def __iter__(self): return self._dict.values().__iter__() diff --git a/qubes/ext/admin.py b/qubes/ext/admin.py index 4abc04418..6fe1a2108 100644 --- a/qubes/ext/admin.py +++ b/qubes/ext/admin.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, see . +import importlib import qubes.api import qubes.api.internal @@ -24,6 +25,9 @@ import qubes.vm.adminvm from qrexec.policy import utils, parser +from qubes.device_protocol import DeviceInterface +from qubes.devices import DEVICE_DENY_LIST + class JustEvaluateAskResolution(parser.AskResolution): async def execute(self): @@ -160,3 +164,37 @@ def on_tag_add(self, vm, event, tag, **kwargs): tag_with = created_by.features.get('tag-created-vm-with', '') for tag_with_single in tag_with.split(): vm.tags.add(tag_with_single) + + @qubes.ext.handler(*(f'admin-permission:admin.vm.device.{ep.name}.Attach' + for ep in importlib.metadata.entry_points(group='qubes.devices'))) + def on_device_attach( + self, vm, event, dest, arg, device, mode, options, **kwargs + ): + # pylint: disable=unused-argument + # ignore auto-attachment + if mode != 'manual': + return + + # load device deny list + deny = {} + try: + with open(DEVICE_DENY_LIST, 'r', encoding="utf-8") as file: + for line in file: + line = line.strip() + + if line: + name, *values = line.split() + + values = ' '.join(values).replace(',', ' ').split() + values = {v for v in values if len(v) > 0} + + deny[name] = deny.get(name, set()).union(set(values)) + except IOError: + pass + + # check if any presented interface is on deny list + for interface in deny.get(dest.name, set()): + pattern = DeviceInterface(interface) + for devint in device.interfaces: + if pattern.matches(devint): + raise qubes.exc.PermissionDenied() diff --git a/qubes/ext/block.py b/qubes/ext/block.py index 268e6ddcd..243fc5dc5 100644 --- a/qubes/ext/block.py +++ b/qubes/ext/block.py @@ -32,7 +32,7 @@ import qubes.device_protocol import qubes.devices import qubes.ext -from qubes.ext.utils import device_list_change +from qubes.ext.utils import device_list_change, confirm_device_attachment from qubes.storage import Storage name_re = re.compile(r"\A[a-z0-9-]{1,12}\Z") @@ -47,9 +47,10 @@ class BlockDevice(qubes.device_protocol.DeviceInfo): - def __init__(self, backend_domain, ident): - super().__init__( - backend_domain=backend_domain, ident=ident, devclass="block") + def __init__(self, backend_domain, port_id): + port = qubes.device_protocol.Port( + backend_domain=backend_domain, port_id=port_id, devclass="block") + super().__init__(port) # lazy loading self._mode = None @@ -86,7 +87,7 @@ 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') + f'/qubes-block-devices/{self.port_id}/desc') if not untrusted_desc: return "unknown", "unknown" desc = BlockDevice._sanitize( @@ -107,7 +108,7 @@ def _load_lazily_name_and_serial(self): @property def manufacturer(self) -> str: if self.parent_device: - return f"sub-device of {self.parent_device}" + return f"sub-device of {self.parent_device.port}" return f"hosted by {self.backend_domain!s}" @property @@ -117,12 +118,12 @@ def mode(self): if not self.backend_domain.is_running(): return 'w' untrusted_mode = self.backend_domain.untrusted_qdb.read( - '/qubes-block-devices/{}/mode'.format(self.ident)) + '/qubes-block-devices/{}/mode'.format(self.port_id)) if untrusted_mode is None: self._mode = 'w' elif untrusted_mode not in (b'w', b'r'): self.backend_domain.log.warning( - 'Device {} has invalid mode'.format(self.ident)) + 'Device {} has invalid mode'.format(self.port_id)) self._mode = 'w' else: self._mode = untrusted_mode.decode() @@ -135,12 +136,12 @@ def size(self): if not self.backend_domain.is_running(): return None untrusted_size = self.backend_domain.untrusted_qdb.read( - '/qubes-block-devices/{}/size'.format(self.ident)) + '/qubes-block-devices/{}/size'.format(self.port_id)) if untrusted_size is None: self._size = 0 elif not untrusted_size.isdigit(): self.backend_domain.log.warning( - 'Device {} has invalid size'.format(self.ident)) + 'Device {} has invalid size'.format(self.port_id)) self._size = 0 else: self._size = int(untrusted_size) @@ -149,7 +150,7 @@ def size(self): @property def device_node(self): """Device node in backend domain""" - return '/dev/' + self.ident.replace('_', '/') + return '/dev/' + self.port_id.replace('_', '/') @property def interfaces(self) -> List[qubes.device_protocol.DeviceInterface]: @@ -161,7 +162,7 @@ def interfaces(self) -> List[qubes.device_protocol.DeviceInterface]: return [qubes.device_protocol.DeviceInterface("******", "block")] @property - def parent_device(self) -> Optional[qubes.device_protocol.Device]: + def parent_device(self) -> Optional[qubes.device_protocol.Port]: """ The parent device, if any. @@ -172,7 +173,7 @@ def parent_device(self) -> Optional[qubes.device_protocol.Device]: 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') + f'/qubes-block-devices/{self.port_id}/parent') if untrusted_parent_info is None: return None # '4-4.1:1.0' -> parent_ident='4-4.1', interface_num='1.0' @@ -187,7 +188,8 @@ def parent_device(self) -> Optional[qubes.device_protocol.Device]: self.backend_domain.devices)[devclass][parent_ident] except KeyError: self._parent = qubes.device_protocol.UnknownDevice( - self.backend_domain, parent_ident, devclass=devclass) + qubes.device_protocol.Port( + self.backend_domain, parent_ident, devclass=devclass)) self._interface_num = interface_num return self._parent @@ -213,27 +215,27 @@ def _is_attached_to(self, vm): info = _try_get_block_device_info(vm.app, disk) if not info: continue - backend_domain, ident = info + backend_domain, port_id = info if backend_domain.name != self.backend_domain.name: continue - if self.ident == ident: + if self.port_id == port_id: return True return False @property - def self_identity(self) -> str: + def device_id(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 + p_info = p.backend_domain.devices[p.devclass][p.port_id] + parent_identity = p_info.device_id if p.devclass == 'usb': - parent_identity = f'{p.ident}:{parent_identity}' + parent_identity = f'{p.port_id}:{parent_identity}' if self._interface_num: # device interface number (not partition) self_id = self._interface_num @@ -248,7 +250,7 @@ def _get_possible_partition_number(self) -> Optional[int]: The behavior is undefined for the rest block devices. """ # partition number: 'xxxxx12' -> '12' (partition) - numbers = re.findall(r'\d+$', self.ident) + numbers = re.findall(r'\d+$', self.port_id) return int(numbers[-1]) if numbers else None @staticmethod @@ -281,13 +283,13 @@ def _try_get_block_device_info(app, disk): dev_path = dev_path_node.get('dev') if dev_path.startswith('/dev/'): - ident = dev_path[len('/dev/'):] + port_id = dev_path[len('/dev/'):] else: - ident = dev_path + port_id = dev_path - ident = ident.replace('/', '_') + port_id = port_id.replace('/', '_') - return backend_domain, ident + return backend_domain, port_id class BlockDeviceExtension(qubes.ext.Extension): @@ -308,7 +310,7 @@ def on_domain_init_load(self, vm, event): # and definitely isn't running yet device_attachments = self.get_device_attachments(vm) current_devices = dict( - (dev.ident, device_attachments.get(dev.ident, None)) + (dev.port_id, device_attachments.get(dev.port_id, None)) for dev in self.on_device_list_block(vm, None)) self.devices_cache[vm.name] = current_devices else: @@ -320,7 +322,7 @@ def on_qdb_change(self, vm, event, path): # pylint: disable=unused-argument device_attachments = self.get_device_attachments(vm) current_devices = dict( - (dev.ident, device_attachments.get(dev.ident, None)) + (dev.port_id, device_attachments.get(dev.port_id, None)) for dev in self.on_device_list_block(vm, None)) device_list_change(self, current_devices, vm, path, BlockDevice) @@ -340,26 +342,26 @@ def get_device_attachments(vm_): info = _try_get_block_device_info(vm.app, disk) if not info: continue - _backend_domain, ident = info + _backend_domain, port_id = info - result[ident] = vm + result[port_id] = vm return result @staticmethod - def device_get(vm, ident): + def device_get(vm, port_id): """ Read information about a device from QubesDB :param vm: backend VM object - :param ident: device identifier + :param port_id: port identifier :returns BlockDevice """ untrusted_qubes_device_attrs = vm.untrusted_qdb.list( - '/qubes-block-devices/{}/'.format(ident)) + '/qubes-block-devices/{}/'.format(port_id)) if not untrusted_qubes_device_attrs: return None - return BlockDevice(vm, ident) + return BlockDevice(vm, port_id) @qubes.ext.handler('device-list:block') def on_device_list_block(self, vm, event): @@ -377,19 +379,19 @@ def on_device_list_block(self, vm, event): vm.log.warning(msg % vm.name) continue - ident = untrusted_ident + port_id = untrusted_ident - device_info = self.device_get(vm, ident) + device_info = self.device_get(vm, port_id) if device_info: yield device_info @qubes.ext.handler('device-get:block') - def on_device_get_block(self, vm, event, ident): + def on_device_get_block(self, vm, event, port_id): # pylint: disable=unused-argument if not vm.is_running(): return if not vm.app.vmm.offline_mode: - device_info = self.device_get(vm, ident) + device_info = self.device_get(vm, port_id) if device_info: yield device_info @@ -439,13 +441,13 @@ def on_device_list_attached(self, vm, event, **kwargs): options['devtype'] = disk.get('device') if dev_path.startswith('/dev/'): - ident = dev_path[len('/dev/'):] + port_id = dev_path[len('/dev/'):] else: - ident = dev_path + port_id = dev_path - ident = ident.replace('/', '_') + port_id = port_id.replace('/', '_') - yield (BlockDevice(backend_domain, ident), options) + yield (BlockDevice(backend_domain, port_id), options) @staticmethod def find_unused_frontend(vm, devtype='disk'): @@ -497,15 +499,6 @@ def pre_attachment_internal( 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)) @@ -539,7 +532,7 @@ def pre_attachment_internal( 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 + self.devices_cache[device.backend_domain.name][device.port_id] = vm if 'frontend-dev' not in options: options['frontend-dev'] = self.find_unused_frontend( @@ -548,20 +541,40 @@ def pre_attachment_internal( @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): + to_attach = {} + assignments = vm.devices['block'].get_assigned_devices() + # the most specific assignments first + for assignment in reversed(sorted(assignments)): + if assignment.required: + # already attached + continue + for device in assignment.devices: + if isinstance(device, qubes.device_protocol.UnknownDevice): + continue + if device.attachment: + continue + if not assignment.matches(device): + print( + "Unrecognized identity, skipping attachment of device " + f"from the port {assignment}", file=sys.stderr) + continue + # chose first assignment (the most specific) and ignore rest + if device not in to_attach: + # make it unique + to_attach[device] = assignment.clone(device=device) + for assignment in to_attach.values(): + asyncio.ensure_future(self.attach_and_notify(vm, assignment)) + + async def attach_and_notify(self, vm, assignment): # bypass DeviceCollection logic preventing double attach - # we expected that these devices are already attached to this vm - self.notify_auto_attached(vm, device, options) + device = assignment.device + if assignment.mode.value == "ask-to-attach": + if vm.name != confirm_device_attachment(device, {vm: assignment}): + return + self.on_device_pre_attached_block( + vm, 'device-pre-attach:block', device, assignment.options) + await vm.fire_event_async( + 'device-attach:block', device=device, options=assignment.options) @qubes.ext.handler('domain-shutdown') async def on_domain_shutdown(self, vm, event, **_kwargs): @@ -584,7 +597,7 @@ async def on_domain_shutdown(self, vm, event, **_kwargs): if front_vm == vm: dev = BlockDevice(vm, dev_id) asyncio.ensure_future(front_vm.fire_event_async( - 'device-detach:block', device=dev)) + 'device-detach:block', port=dev.port)) else: new_cache[domain.name][dev_id] = front_vm self.devices_cache = new_cache.copy() @@ -592,9 +605,9 @@ async def on_domain_shutdown(self, vm, event, **_kwargs): 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) + vm, 'device-pre-detach:block', device.port) await vm.fire_event_async( - 'device-detach:block', device=device, options=options) + 'device-detach:block', port=device.port, options=options) @qubes.ext.handler('qubes-close', system=True) def on_qubes_close(self, app, event): @@ -602,7 +615,7 @@ def on_qubes_close(self, app, event): self.devices_cache.clear() @qubes.ext.handler('device-pre-detach:block') - def on_device_pre_detached_block(self, vm, event, device): + def on_device_pre_detached_block(self, vm, event, port): # pylint: disable=unused-argument if not vm.is_running(): return @@ -610,10 +623,10 @@ def on_device_pre_detached_block(self, vm, event, device): # 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 + if attached_device.port == port: + self.devices_cache[port.backend_domain.name][ + port.port_id] = None vm.libvirt_domain.detachDevice( vm.app.env.get_template('libvirt/devices/block.xml').render( - device=device, vm=vm, options=options)) + device=attached_device, vm=vm, options=options)) break diff --git a/qubes/ext/pci.py b/qubes/ext/pci.py index 1535a7f9d..eaaffe2a8 100644 --- a/qubes/ext/pci.py +++ b/qubes/ext/pci.py @@ -161,20 +161,21 @@ class PCIDevice(qubes.device_protocol.DeviceInfo): r'\Apci_0000_(?P[0-9a-f]+)_(?P[0-9a-f]+)_' r'(?P[0-9a-f]+)\Z') - def __init__(self, backend_domain, ident, libvirt_name=None): + def __init__(self, backend_domain, port_id, libvirt_name=None): if libvirt_name: dev_match = self._libvirt_regex.match(libvirt_name) if not dev_match: raise UnsupportedDevice(libvirt_name) - ident = '{bus}_{device}.{function}'.format(**dev_match.groupdict()) + port_id = '{bus}_{device}.{function}'.format( + **dev_match.groupdict()) - super().__init__( - backend_domain=backend_domain, ident=ident, devclass="pci") + port = qubes.device_protocol.Port( + backend_domain=backend_domain, port_id=port_id, devclass="pci") + super().__init__(port) - dev_match = self.regex.match(ident) + dev_match = self.regex.match(port_id) if not dev_match: - raise ValueError('Invalid device identifier: {!r}'.format( - ident)) + raise ValueError('Invalid device identifier: {!r}'.format(port_id)) for group in self.regex.groupindex: setattr(self, group, dev_match.group(group)) @@ -222,6 +223,10 @@ def interfaces(self) -> List[qubes.device_protocol.DeviceInterface]: Every device should have at least one interface. """ if self._interfaces is None: + if self.backend_domain.app.vmm.offline_mode: + # don't cache this value + return [qubes.device_protocol.DeviceInterface( + '******', devclass='pci')] hostdev_details = \ self.backend_domain.app.vmm.libvirt_conn.nodeDeviceLookupByName( self.libvirt_name @@ -259,7 +264,7 @@ def description(self): return self._description @property - def self_identity(self) -> str: + def device_id(self) -> str: """ Get identification of the device not related to port. """ @@ -286,7 +291,9 @@ def _load_desc(self) -> Dict[str, str]: "manufacturer": unknown, "name": unknown, "serial": unknown} - if not self.backend_domain.is_running(): + if (not self.backend_domain.is_running() + or self.backend_domain.app.vmm.offline_mode + ): # don't cache these values return result hostdev_details = \ @@ -310,7 +317,7 @@ def _load_desc(self) -> Dict[str, str]: def frontend_domain(self): # TODO: cache this all_attached = attached_devices(self.backend_domain.app) - return all_attached.get(self.ident, None) + return all_attached.get(self.port_id, None) class PCIDeviceExtension(qubes.ext.Extension): @@ -340,10 +347,10 @@ def on_device_list_pci(self, vm, event): unsupported_devices_warned.add(libvirt_name) @qubes.ext.handler('device-get:pci') - def on_device_get_pci(self, vm, event, ident): + def on_device_get_pci(self, vm, event, port_id): # pylint: disable=unused-argument if not vm.app.vmm.offline_mode: - yield _cache_get(vm, ident) + yield _cache_get(vm, port_id) @qubes.ext.handler('device-list-attached:pci') def on_device_list_attached(self, vm, event, **kwargs): @@ -360,20 +367,20 @@ def on_device_list_attached(self, vm, event, **kwargs): device = address.get('slot')[2:] function = address.get('function')[2:] - ident = '{bus}_{device}.{function}'.format( + port_id = '{bus}_{device}.{function}'.format( bus=bus, device=device, function=function, ) - yield (PCIDevice(vm.app.domains[0], ident), {}) + yield (PCIDevice(vm.app.domains[0], port_id), {}) @qubes.ext.handler('device-pre-attach:pci') def on_device_pre_attached_pci(self, vm, event, device, options): # pylint: disable=unused-argument if not os.path.exists('/sys/bus/pci/devices/0000:{}'.format( - device.ident.replace('_', ':'))): + device.port_id.replace('_', ':'))): raise qubes.exc.QubesException( - 'Invalid PCI device: {}'.format(device.ident)) + 'Invalid PCI device: {}'.format(device.port_id)) if isinstance(vm, qubes.vm.adminvm.AdminVM): raise qubes.exc.QubesException("Can't attach PCI device to dom0") @@ -386,7 +393,7 @@ def on_device_pre_attached_pci(self, vm, event, device, options): return try: - device = _cache_get(device.backend_domain, device.ident) + device = _cache_get(device.backend_domain, device.port_id) self.bind_pci_to_pciback(vm.app, device) vm.libvirt_domain.attachDevice( vm.app.env.get_template('libvirt/devices/pci.xml').render( @@ -396,10 +403,10 @@ def on_device_pre_attached_pci(self, vm, event, device, options): except subprocess.CalledProcessError as e: vm.log.exception('Failed to attach PCI device {!r} on the fly,' ' changes will be seen after VM restart.'.format( - device.ident), e) + device.port_id), e) @qubes.ext.handler('device-pre-detach:pci') - def on_device_pre_detached_pci(self, vm, event, device): + def on_device_pre_detached_pci(self, vm, event, port): # pylint: disable=unused-argument if not vm.is_running(): return @@ -408,16 +415,16 @@ def on_device_pre_detached_pci(self, vm, event, device): # provision in libvirt for extracting device-side BDF; we need it for # qubes.DetachPciDevice, which unbinds driver, not to oops the kernel - device = _cache_get(device.backend_domain, device.ident) + device = _cache_get(port.backend_domain, port.port_id) with subprocess.Popen(['xl', 'pci-list', str(vm.xid)], stdout=subprocess.PIPE) as p: result = p.communicate()[0].decode() - m = re.search(r'^(\d+.\d+)\s+0000:{}$'.format(device.ident.replace( + m = re.search(r'^(\d+.\d+)\s+0000:{}$'.format(device.port_id.replace( '_', ':')), result, flags=re.MULTILINE) if not m: - vm.log.error('Device %s already detached', device.ident) + vm.log.error('Device %s already detached', device.port_id) return vmdev = m.group(1) try: @@ -431,14 +438,14 @@ def on_device_pre_detached_pci(self, vm, event, device): except (subprocess.CalledProcessError, libvirt.libvirtError) as e: vm.log.exception('Failed to detach PCI device {!r} on the fly,' ' changes will be seen after VM restart.'.format( - device.ident), e) + device.port_id), e) raise @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'].get_assigned_devices(): - device = _cache_get(assignment.backend_domain, assignment.ident) + device = _cache_get(assignment.backend_domain, assignment.port_id) self.bind_pci_to_pciback(vm.app, device) @staticmethod @@ -476,6 +483,6 @@ def on_app_close(self, app, event): @functools.lru_cache(maxsize=None) -def _cache_get(vm, ident): - ''' Caching wrapper around `PCIDevice(vm, ident)`. ''' - return PCIDevice(vm, ident) +def _cache_get(vm, port_id): + """ Caching wrapper around `PCIDevice(vm, port_id)`. """ + return PCIDevice(vm, port_id) diff --git a/qubes/ext/utils.py b/qubes/ext/utils.py index 1c7831922..4bf6ec2b7 100644 --- a/qubes/ext/utils.py +++ b/qubes/ext/utils.py @@ -19,13 +19,20 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA. import asyncio +import subprocess +import sys import qubes +from typing import Type + +from qubes import device_protocol +from qubes.device_protocol import VirtualDevice + def device_list_change( ext: qubes.ext.Extension, current_devices, - vm, path, device_class: qubes.device_protocol.DeviceInfo + vm, path, device_class: Type[qubes.device_protocol.DeviceInfo] ): devclass = device_class.__name__[:-len('Device')].lower() @@ -36,15 +43,15 @@ def device_list_change( 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) + for port_id, front_vm in detached.items(): + dev = device_class(vm, port_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) + f'device-detach:{devclass}', port=dev.port)) + for port_id in removed: + device = device_class(vm, port_id) + vm.fire_event(f'device-removed:{devclass}', port=device.port) + for port_id in added: + device = device_class(vm, port_id) vm.fire_event(f'device-added:{devclass}', device=device) for dev_ident, front_vm in attached.items(): dev = device_class(vm, dev_ident) @@ -54,24 +61,55 @@ def device_list_change( ext.devices_cache[vm.name] = current_devices + to_attach = {} 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)) + for assignment in reversed(sorted( + front_vm.devices[devclass].get_assigned_devices())): + for device in assignment.devices: + if (assignment.matches(device) + and device.port_id in added + and device.port_id not in attached + ): + frontends = to_attach.get(device.port_id, {}) + # make it unique + ass = assignment.clone( + device=VirtualDevice(device.port, device.device_id)) + curr = frontends.get(front_vm, None) + if curr is None or curr < ass: + # chose the most specific assignment + frontends[front_vm] = ass + to_attach[device.port_id] = frontends + + for port_id, frontends in to_attach.items(): + if len(frontends) > 1: + # unique + device = tuple(frontends.values())[0].device + target_name = confirm_device_attachment(device, frontends) + for front in frontends: + if front.name == target_name: + target = front + assignment = frontends[front] + # already asked + if assignment.mode.value == "ask-to-attach": + assignment.mode = device_protocol.AssignmentMode.AUTO + break + else: + return + else: + target = tuple(frontends.keys())[0] + assignment = frontends[target] + + asyncio.ensure_future(ext.attach_and_notify(target, assignment)) 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) + # - newly appeared devices (port_id) + # - devices attached from a vm to frontend vm (port_id: frontend_vm) + # - devices detached from frontend vm (port_id: frontend_vm) + # - disappeared devices, e.g., plugged out (port_id) added = set() attached = {} detached = {} @@ -100,3 +138,19 @@ def compare_device_cache(vm, devices_cache, current_devices): if cached_front is not None: detached[dev_id] = cached_front return added, attached, detached, removed + + +def confirm_device_attachment(device, frontends) -> str: + try: + # pylint: disable=consider-using-with + proc = subprocess.Popen( + ["attach-confirm", device.backend_domain.name, + device.port_id, device.description, + *[f.name for f in frontends.keys()]], + stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + (target_name, _) = proc.communicate() + return target_name.decode() + except Exception as exc: + print("attach-confirm", exc, file=sys.stderr) + return "" diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index ed0a04156..13b22f3a4 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -41,6 +41,9 @@ import qubes.tests import qubes.storage +from qubes.device_protocol import (DeviceInfo, VirtualDevice, Port, + DeviceAssignment) + # properties defined in API volume_properties = [ 'pool', 'vid', 'size', 'usage', 'rw', 'source', 'path', @@ -1656,7 +1659,6 @@ def test_346_vm_create_in_pool_duplicate_pool(self, storage_mock): self.assertNotIn('test-vm2', self.app.domains) self.assertFalse(self.app.save.called) - def test_400_property_list(self): # actual function tested for admin.vm.property.* already # this test is kind of stupid, but at least check if appropriate @@ -1701,12 +1703,12 @@ def test_450_property_reset(self): def device_list_testclass(self, vm, event): if vm is not self.vm: return - dev = qubes.device_protocol.DeviceInfo( - self.vm, '1234', product='Some device') + dev = DeviceInfo(Port( + self.vm, '1234', 'testclass'), product='Some device') dev.extra_prop = 'xx' yield dev - dev = qubes.device_protocol.DeviceInfo( - self.vm, '4321', product='Some other device') + dev = DeviceInfo(Port( + self.vm, '4321', 'testclass'), product='Some other device') yield dev def assertSerializedEqual(self, actual, expected): @@ -1725,14 +1727,14 @@ def test_460_vm_device_available(self): 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' " + "1234:0000:0000::?****** " + "device_id='0000:0000::?******' " + "devclass='testclass' product='Some_device' port_id='1234' " + "backend_domain='test-vm1' interfaces='?******'\n" + "4321:0000:0000::?****** " + "device_id='0000:0000::?******' " + "devclass='testclass' product='Some_other_device' " + "port_id='4321' backend_domain='test-vm1' " "interfaces='?******'\n") self.assertFalse(self.app.save.called) @@ -1742,10 +1744,10 @@ def test_461_vm_device_available_specific(self): b'test-vm1', b'4321') 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' " + "4321:0000:0000::?****** " + "device_id='0000:0000::?******' " + "devclass='testclass' product='Some_other_device' " + "port_id='4321' backend_domain='test-vm1' " "interfaces='?******'\n") self.assertFalse(self.app.save.called) @@ -1757,40 +1759,41 @@ def test_462_vm_device_available_invalid(self): self.assertFalse(self.app.save.called) def test_470_vm_device_list_assigned(self): - assignment = qubes.device_protocol.DeviceAssignment(self.vm, '1234', - attach_automatically=True, required=True) + assignment = DeviceAssignment(VirtualDevice(Port( + self.vm, '1234', 'testclass')), mode='required') 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') self.assertEqual(value, - "test-vm1+1234 required='yes' attach_automatically='yes' " - "ident='1234' devclass='testclass' backend_domain='test-vm1'\n") + "test-vm1+1234:* device_id='*' port_id='1234' " + "devclass='testclass' backend_domain='test-vm1' mode='required'\n") self.assertFalse(self.app.save.called) 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'}) + assignment = DeviceAssignment(VirtualDevice(Port( + self.vm, '1234', 'testclass')), + mode='required', options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) - assignment = qubes.device_protocol.DeviceAssignment(self.vm, '4321', - attach_automatically=True, required=True) + assignment = DeviceAssignment(VirtualDevice(Port( + self.vm, '4321', 'testclass')), mode='required') 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') self.assertEqual(value, - "test-vm1+1234 required='yes' attach_automatically='yes' " - "ident='1234' devclass='testclass' backend_domain='test-vm1' " + "test-vm1+1234:* device_id='*' port_id='1234' " + "devclass='testclass' backend_domain='test-vm1' mode='required' " "_opt1='value'\n" - "test-vm1+4321 required='yes' attach_automatically='yes' " - "ident='4321' devclass='testclass' backend_domain='test-vm1'\n") + "test-vm1+4321:* device_id='*' port_id='4321' " + "devclass='testclass' backend_domain='test-vm1' mode='required'\n") self.assertFalse(self.app.save.called) def device_list_single_attached_testclass(self, vm, event, **kwargs): if vm is not self.vm: return - dev = qubes.device_protocol.DeviceInfo(self.vm, '1234', 'testclass') + dev = qubes.device_protocol.DeviceInfo(Port(self.vm, '1234', 'testclass')) yield (dev, {'attach_opt': 'value'}) def test_472_vm_device_list_attached(self): @@ -1799,33 +1802,36 @@ def test_472_vm_device_list_attached(self): value = self.call_mgmt_func(b'admin.vm.device.testclass.Attached', b'test-vm1') self.assertEqual(value, - "test-vm1+1234 required='no' attach_automatically='no' " - "ident='1234' devclass='testclass' backend_domain='test-vm1' " + "test-vm1+1234:0000:0000::?****** " + "device_id='0000:0000::?******' port_id='1234' " + "devclass='testclass' backend_domain='test-vm1' mode='manual' " "frontend_domain='test-vm1' _attach_opt='value'\n") self.assertFalse(self.app.save.called) def test_473_vm_device_list_assigned_specific(self): - assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', attach_automatically=True, required=True) + assignment = DeviceAssignment(VirtualDevice(Port( + self.vm, '1234', 'testclass')), mode='required') self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) - assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '4321', attach_automatically=True, required=True) + assignment = DeviceAssignment(VirtualDevice(Port( + self.vm, '4321', 'testclass')), mode='required') 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 required='yes' attach_automatically='yes' " - "ident='1234' devclass='testclass' backend_domain='test-vm1'\n") + "test-vm1+1234:* device_id='*' port_id='1234' " + "devclass='testclass' backend_domain='test-vm1' mode='required'\n") self.assertFalse(self.app.save.called) 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') + dev = qubes.device_protocol.DeviceInfo( + Port(self.vm, '1234', 'testclass')) yield (dev, {'attach_opt': 'value'}) - dev = qubes.device_protocol.DeviceInfo(self.vm, '4321', 'testclass') + dev = qubes.device_protocol.DeviceInfo( + Port(self.vm, '4321', 'testclass')) yield (dev, {'attach_opt': 'value'}) def test_474_vm_device_list_attached_specific(self): @@ -1834,9 +1840,10 @@ def test_474_vm_device_list_attached_specific(self): value = self.call_mgmt_func(b'admin.vm.device.testclass.Attached', b'test-vm1', b'test-vm1+1234') self.assertEqual(value, - "test-vm1+1234 required='no' attach_automatically='no' " - "ident='1234' devclass='testclass' backend_domain='test-vm1' " - "frontend_domain='test-vm1' _attach_opt='value'\n") + "test-vm1+1234:0000:0000::?****** " + "device_id='0000:0000::?******' port_id='1234' " + "devclass='testclass' backend_domain='test-vm1' " + "mode='manual' frontend_domain='test-vm1' _attach_opt='value'\n") self.assertFalse(self.app.save.called) def test_480_vm_device_attach(self): @@ -1848,7 +1855,7 @@ def test_480_vm_device_attach(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'test-vm1', b'test-vm1+1234:0000:0000::?******') self.assertIsNone(value) mock_action.assert_called_once_with( self.vm, f'device-attach:testclass', @@ -1869,8 +1876,8 @@ def test_481_vm_device_assign(self): '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'") + b'test-vm1', b'test-vm1+1234:0000:0000::?******', + b"mode='auto-attach'") self.assertIsNone(value) mock_action.assert_called_once_with( self.vm, f'device-assign:testclass', @@ -1891,8 +1898,8 @@ def test_483_vm_device_assign_required(self): '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' required='yes'") + b'test-vm1', b'test-vm1+1234:0000:0000::?******', + b"mode='required'") self.assertIsNone(value) mock_action.assert_called_once_with( self.vm, f'device-assign:testclass', @@ -1910,7 +1917,7 @@ def test_484_vm_device_attach_not_running(self): 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') + b'test-vm1', b'test-vm1+1234:0000:0000::?******') self.assertFalse(mock_action.called) self.assertEqual( len(list(self.vm.devices['testclass'].get_assigned_devices())), 0) @@ -1925,8 +1932,8 @@ def test_485_vm_device_assign_not_running(self): 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'") + b'test-vm1', b'test-vm1+1234:0000:0000::?******', + b"mode='auto-attach'") mock_action.assert_called_once_with( self.vm, f'device-assign:testclass', device=self.vm.devices['testclass']['1234'], @@ -1944,8 +1951,8 @@ def test_486_vm_device_assign_required_not_running(self): 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'") + b'test-vm1', b'test-vm1+1234:0000:0000::?******', + b"mode='required'") mock_action.assert_called_once_with( self.vm, f'device-assign:testclass', device=self.vm.devices['testclass']['1234'], @@ -1963,7 +1970,8 @@ def test_487_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:0000:0000::?******', + b"_option1='value2'") self.assertIsNone(value) dev = self.vm.devices['testclass']['1234'] mock_attach.assert_called_once_with( @@ -1982,8 +1990,8 @@ def test_488_vm_device_assign_options(self): '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'") + b'test-vm1', b'test-vm1+1234:0000:0000::?******', + b"mode='auto-attach' _option1='value2'") self.assertIsNone(value) dev = self.vm.devices['testclass']['1234'] mock_attach.assert_called_once_with( @@ -1991,10 +1999,84 @@ def test_488_vm_device_assign_options(self): options={'option1': 'value2'}) self.app.save.assert_called_once_with() + def test_489_vm_multiple_device_one_port_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): + self.call_mgmt_func( + b'admin.vm.device.testclass.Assign', + b'test-vm1', b'test-vm1+1234:dead', + b"mode='auto-attach'") + mock_action.assert_called_with( + self.vm, f'device-assign:testclass', + device=VirtualDevice(Port(self.vm, '1234', 'testclass'), + device_id='dead'), + options={}) + self.call_mgmt_func( + b'admin.vm.device.testclass.Assign', + b'test-vm1', b'test-vm1+1234:beef', + b"mode='auto-attach'") + mock_action.assert_called_with( + self.vm, f'device-assign:testclass', + device=VirtualDevice(Port(self.vm, '1234', 'testclass'), + device_id='beef'), + options={}) + + self.assertEqual( + len(list(self.vm.devices['testclass'].get_assigned_devices())), + 2) + self.app.save.assert_called() + + def test_4890_vm_overlapping_assignments(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): + self.call_mgmt_func( + b'admin.vm.device.testclass.Assign', + b'test-vm1', b'test-vm1+1234:dead', + b"mode='auto-attach'") + mock_action.assert_called_with( + self.vm, f'device-assign:testclass', + device=VirtualDevice(Port(self.vm, '1234', 'testclass'), + device_id='dead'), + options={}) + self.call_mgmt_func( + b'admin.vm.device.testclass.Assign', + b'test-vm1', b'test-vm1+1234:*', + b"mode='auto-attach'") + mock_action.assert_called_with( + self.vm, f'device-assign:testclass', + device=VirtualDevice(Port(self.vm, '1234', 'testclass'), + device_id='*'), + options={}) + self.call_mgmt_func( + b'admin.vm.device.testclass.Assign', + b'test-vm1', b'test-vm1+*:dead', + b"mode='auto-attach'") + mock_action.assert_called_with( + self.vm, f'device-assign:testclass', + device=VirtualDevice(Port(self.vm, '*', 'testclass'), + device_id='dead'), + options={}) + + self.assertEqual( + len(list(self.vm.devices['testclass'].get_assigned_devices())), + 3) + self.app.save.assert_called() + 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'}) + assignment = DeviceAssignment(VirtualDevice(Port( + self.vm, '1234', 'testclass'), + device_id='dead:beef:cafe'), + mode='auto-attach', options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) mock_action = unittest.mock.Mock() @@ -2004,17 +2086,18 @@ def test_490_vm_device_unassign_from_running(self): 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') + b'test-vm1', b'test-vm1+1234:dead:beef:cafe') self.assertIsNone(value) mock_action.assert_called_once_with( self.vm, 'device-unassign:testclass', - device=self.vm.devices['testclass']['1234']) + device=assignment.virtual_device) 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'}) + assignment = DeviceAssignment(VirtualDevice(Port( + self.vm, '1234', 'testclass'), + device_id='dead:beef:cafe'), + mode='required', options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) mock_action = unittest.mock.Mock() @@ -2025,17 +2108,18 @@ def test_491_vm_device_unassign_required_from_running(self): 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') + b'test-vm1', b'test-vm1+1234:dead:beef:cafe') self.assertIsNone(value) mock_action.assert_called_once_with( self.vm, 'device-unassign:testclass', - device=self.vm.devices['testclass']['1234']) + device=assignment.virtual_device) 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'}) + assignment = DeviceAssignment(VirtualDevice(Port( + self.vm, '1234', 'testclass'), + device_id='dead:beef:cafe'), + mode='required', options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) mock_action = unittest.mock.Mock() @@ -2044,16 +2128,17 @@ def test_492_vm_device_unassign_from_halted(self): 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') + b'test-vm1', b'test-vm1+1234:dead:beef:cafe') mock_action.assert_called_once_with( self.vm, 'device-unassign:testclass', - device=self.vm.devices['testclass']['1234']) + device=assignment.virtual_device) self.app.save.assert_called_once_with() 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'}) + assignment = DeviceAssignment(VirtualDevice(Port( + self.vm, '1234', 'testclass'), + device_id='dead:beef:cafe'), + mode='required', options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) mock_action = unittest.mock.Mock() @@ -2062,19 +2147,20 @@ def test_493_vm_device_unassign_required_from_halted(self): 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') + b'test-vm1', b'test-vm1+1234:dead:beef:cafe') mock_action.assert_called_once_with( self.vm, 'device-unassign:testclass', - device=self.vm.devices['testclass']['1234']) + device=assignment.virtual_device) 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_single_attached_testclass) - assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', attach_automatically=True, required=False, - options={'opt1': 'value'}) + assignment = DeviceAssignment(VirtualDevice(Port( + self.vm, '1234', 'testclass'), + device_id='dead:beef:cafe'), + mode='auto-attach', options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) mock_action = unittest.mock.Mock() @@ -2083,10 +2169,10 @@ def test_494_vm_device_unassign_attached(self): 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') + b'test-vm1', b'test-vm1+1234:dead:beef:cafe') mock_action.assert_called_once_with( self.vm, 'device-unassign:testclass', - device=self.vm.devices['testclass']['1234']) + device=assignment.virtual_device) self.app.save.assert_called_once_with() def test_495_vm_device_unassign_not_assigned(self): @@ -2098,7 +2184,7 @@ def test_495_vm_device_unassign_not_assigned(self): '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') + b'test-vm1', b'test-vm1+1234:*') self.assertFalse(mock_detach.called) self.assertFalse(self.app.save.called) @@ -2113,10 +2199,10 @@ def test_496_vm_device_detach(self): with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM, 'is_halted', lambda _: False): value = self.call_mgmt_func(b'admin.vm.device.testclass.Detach', - b'test-vm1', b'test-vm1+1234') + b'test-vm1', b'test-vm1+1234:*') self.assertIsNone(value) mock_detach.assert_called_once_with(self.vm, 'device-detach:testclass', - device=self.vm.devices['testclass']['1234']) + port=self.vm.devices['testclass']['1234'].port) self.assertFalse(self.app.save.called) def test_497_vm_device_detach_not_attached(self): @@ -2159,8 +2245,9 @@ 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.device_protocol.DeviceAssignment( - self.vm, '1234', attach_automatically=True, required=True) + assignment = DeviceAssignment(VirtualDevice(Port( + self.vm, '1234', 'testclass')), + mode='required') self.loop.run_until_complete( self.vm2.devices['testclass'].assign(assignment)) @@ -2857,10 +2944,10 @@ 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_required_true(self): - assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', attach_automatically=True, required=False, - options={'opt1': 'value'}) + def test_650_vm_device_set_mode_required(self): + assignment = DeviceAssignment(VirtualDevice(Port( + self.vm, '1234', 'testclass'), device_id='bee'), + mode='auto-attach', options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) mock_action = unittest.mock.Mock() @@ -2871,26 +2958,28 @@ def test_650_vm_device_set_required_true(self): with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM, 'is_halted', lambda _: False): value = self.call_mgmt_func( - b'admin.vm.device.testclass.Set.required', - b'test-vm1', b'test-vm1+1234', b'True') + b'admin.vm.device.testclass.Set.assignment', + b'test-vm1', b'test-vm1+1234:bee', b'required') self.assertIsNone(value) - dev = qubes.device_protocol.DeviceInfo(self.vm, '1234') - required = self.vm.devices['testclass'].get_assigned_devices( - required_only=True) + dev = DeviceInfo(Port( + self.vm, '1234', 'testclass'), device_id='bee') + required = list(self.vm.devices['testclass'].get_assigned_devices( + required_only=True)) self.assertIn(dev, required) + self.assertEqual(required[0].mode.value, "required") self.assertEventFired( self.emitter, - 'admin-permission:admin.vm.device.testclass.Set.required') + 'admin-permission:admin.vm.device.testclass.Set.assignment') mock_action.assert_called_once_with( self.vm, f'device-assignment-changed:testclass', - device=self.vm.devices['testclass']['1234']) + device=assignment.virtual_device) self.app.save.assert_called_once_with() - 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'}) + def test_651_vm_device_set_mode_ask(self): + assignment = DeviceAssignment(VirtualDevice(Port( + self.vm, '1234', 'testclass'), device_id='bee'), + mode='required', options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) mock_action = unittest.mock.Mock() @@ -2901,82 +2990,101 @@ def test_651_vm_device_set_required_false(self): with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM, 'is_halted', lambda _: False): value = self.call_mgmt_func( - b'admin.vm.device.testclass.Set.required', - b'test-vm1', b'test-vm1+1234', b'False') + b'admin.vm.device.testclass.Set.assignment', + b'test-vm1', b'test-vm1+1234:bee', b'ask-to-attach') self.assertIsNone(value) - dev = qubes.device_protocol.DeviceInfo(self.vm, '1234') + dev = DeviceInfo(Port(self.vm, '1234', 'testclass'), + device_id='bee') required = self.vm.devices['testclass'].get_assigned_devices( required_only=True) self.assertNotIn(dev, required) + assignments = list(self.vm.devices['testclass'].get_assigned_devices()) + self.assertEqual(assignments[0].mode.value, "ask-to-attach") self.assertEventFired( self.emitter, - 'admin-permission:admin.vm.device.testclass.Set.required') + 'admin-permission:admin.vm.device.testclass.Set.assignment') mock_action.assert_called_once_with( self.vm, f'device-assignment-changed:testclass', - device=self.vm.devices['testclass']['1234']) + device=assignment.virtual_device) self.app.save.assert_called_once_with() - 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'}) + def test_652_vm_device_set_mode_auto(self): + assignment = DeviceAssignment(VirtualDevice(Port( + self.vm, '1234', 'testclass'), device_id='bee'), + mode='required', 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.required', - b'test-vm1', b'test-vm1+1234', b'True') + b'admin.vm.device.testclass.Set.assignment', + b'test-vm1', b'test-vm1+1234:bee', b'auto-attach') + self.assertIsNone(value) - dev = qubes.device_protocol.DeviceInfo(self.vm, '1234') + dev = DeviceInfo(Port(self.vm, '1234', 'testclass'), + device_id='bee') required = self.vm.devices['testclass'].get_assigned_devices( required_only=True) - self.assertIn(dev, required) + self.assertNotIn(dev, required) + assignments = list(self.vm.devices['testclass'].get_assigned_devices()) + self.assertEqual(assignments[0].mode.value, "auto-attach") + self.assertEventFired( + self.emitter, + 'admin-permission:admin.vm.device.testclass.Set.assignment') + mock_action.assert_called_once_with( + self.vm, f'device-assignment-changed:testclass', + device=assignment.virtual_device) self.app.save.assert_called_once_with() - 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'}) + def test_653_vm_device_set_mode_unchanged(self): + assignment = DeviceAssignment(VirtualDevice(Port( + self.vm, '1234', 'testclass'), device_id='bee'), + mode='required', options={'opt1': 'value'}) self.loop.run_until_complete( 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.required', - b'test-vm1', b'test-vm1+1234', b'False') + b'admin.vm.device.testclass.Set.assignment', + b'test-vm1', b'test-vm1+1234:bee', b'required') self.assertIsNone(value) - dev = qubes.device_protocol.DeviceInfo(self.vm, '1234') + dev = DeviceInfo(Port(self.vm, '1234', 'testclass'), + device_id='bee') required = self.vm.devices['testclass'].get_assigned_devices( required_only=True) - self.assertNotIn(dev, required) + self.assertIn(dev, required) self.app.save.assert_called_once_with() - def test_654_vm_device_set_persistent_not_assigned(self): + def test_654_vm_device_set_mode_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.exc.QubesValueError): self.call_mgmt_func( - b'admin.vm.device.testclass.Set.required', - b'test-vm1', b'test-vm1+1234', b'True') - dev = qubes.device_protocol.DeviceInfo(self.vm, '1234') + b'admin.vm.device.testclass.Set.assignment', + b'test-vm1', b'test-vm1+1234', b'required') + dev = qubes.device_protocol.DeviceInfo(Port(self.vm, '1234', 'testclass')) 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): + def test_655_vm_device_set_mode_invalid_value(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.exc.PermissionDenied): self.call_mgmt_func( - b'admin.vm.device.testclass.Set.required', - b'test-vm1', b'test-vm1+1234', b'maybe') - dev = qubes.device_protocol.DeviceInfo(self.vm, '1234') + b'admin.vm.device.testclass.Set.assignment', + b'test-vm1', b'test-vm1+1234', b'True') + dev = qubes.device_protocol.DeviceInfo(Port(self.vm, '1234', 'testclass')) self.assertNotIn(dev, self.vm.devices['testclass'].get_assigned_devices()) self.assertFalse(self.app.save.called) diff --git a/qubes/tests/app.py b/qubes/tests/app.py index d9779e541..6f0de5111 100644 --- a/qubes/tests/app.py +++ b/qubes/tests/app.py @@ -815,7 +815,7 @@ def test_206_remove_attached(self): # See also qubes.tests.api_admin. vm = self.app.add_new_vm( 'AppVM', name='test-vm', template=self.template, label='red') - assignment = mock.Mock(ident='1234') + assignment = mock.Mock(port_id='1234') vm.get_provided_assignments = lambda: [assignment] with self.assertRaises(qubes.exc.QubesVMInUseError): del self.app.domains[vm] diff --git a/qubes/tests/devices.py b/qubes/tests/devices.py index 22db41c35..72d2d2fd3 100644 --- a/qubes/tests/devices.py +++ b/qubes/tests/devices.py @@ -1,4 +1,5 @@ # pylint: disable=protected-access,pointless-statement +import sys # # The Qubes OS Project, https://www.qubes-os.org/ @@ -21,8 +22,9 @@ # import qubes.devices -from qubes.device_protocol import (Device, DeviceInfo, DeviceAssignment, - DeviceInterface, UnknownDevice) +from qubes.device_protocol import (Port, DeviceInfo, DeviceAssignment, + DeviceInterface, UnknownDevice, + VirtualDevice, AssignmentMode) import qubes.tests @@ -48,7 +50,8 @@ def __init__(self, app, name, *args, **kwargs): super(TestVM, self).__init__(*args, **kwargs) self.app = app self.name = name - self.device = TestDevice(self, 'testdev', 'testclass') + self.device = TestDevice( + Port(self, 'testport', 'testclass'), device_id='testdev') self.events_enabled = True self.devices = { 'testclass': qubes.devices.DeviceCollection(self, 'testclass') @@ -90,12 +93,7 @@ def setUp(self): self.app.domains['vm'] = self.emitter self.device = self.emitter.device self.collection = self.emitter.devices['testclass'] - self.assignment = DeviceAssignment( - backend_domain=self.device.backend_domain, - ident=self.device.ident, - attach_automatically=True, - required=True, - ) + self.assignment = DeviceAssignment(self.device, mode='required') def attach(self): self.emitter.running = True @@ -124,20 +122,21 @@ def test_002_attach_to_halted(self): def test_003_detach(self): self.attach() - self.loop.run_until_complete(self.collection.detach(self.assignment)) + self.loop.run_until_complete(self.collection.detach( + self.assignment.port)) 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)) + self.collection.detach(self.assignment.port)) def test_010_empty_detach(self): self.emitter.running = True with self.assertRaises(LookupError): self.loop.run_until_complete( - self.collection.detach(self.assignment)) + self.collection.detach(self.assignment.port)) def test_011_empty_unassign(self): for _ in range(2): @@ -154,12 +153,13 @@ def test_012_double_attach(self): def test_013_double_detach(self): self.attach() - self.loop.run_until_complete(self.collection.detach(self.assignment)) + self.loop.run_until_complete(self.collection.detach( + self.assignment.port)) self.detach() with self.assertRaises(qubes.devices.DeviceNotAssigned): self.loop.run_until_complete( - self.collection.detach(self.assignment)) + self.collection.detach(self.assignment.port)) def test_014_double_assign(self): self.loop.run_until_complete(self.collection.assign(self.assignment)) @@ -179,95 +179,126 @@ def test_015_double_unassign(self): 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}, + self.assertEqual({self.assignment}, set(self.collection.get_assigned_devices())) self.assertEqual(set([]), set(self.collection.get_attached_devices())) - self.assertEqual({self.device}, + self.assertEqual({self.assignment}, set(self.collection.get_dedicated_devices())) def test_017_list_attached(self): - self.assignment.required = False + self.assignment = self.assignment.clone(mode='auto-attach') self.attach() - self.assertEqual({self.device}, + self.assertEqual({self.assignment}, set(self.collection.get_attached_devices())) self.assertEqual(set([]), set(self.collection.get_assigned_devices())) - self.assertEqual({self.device}, + self.assertEqual({self.assignment}, set(self.collection.get_dedicated_devices())) self.assertEventFired(self.emitter, 'device-list-attached:testclass') def test_018_list_available(self): - self.assertEqual({self.device}, set(self.collection)) + self.assertEqual({self.assignment}, set(self.collection)) self.assertEventFired(self.emitter, 'device-list:testclass') - def test_020_update_required_to_false(self): + def test_020_update_mode_to_auto(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}, + {self.assignment}, set(self.collection.get_assigned_devices(required_only=True))) self.assertEqual( - {self.device}, set(self.collection.get_assigned_devices())) + {self.assignment}, set(self.collection.get_assigned_devices())) self.loop.run_until_complete( - self.collection.update_required(self.device, False)) + self.collection.update_assignment(self.device, AssignmentMode.AUTO)) + self.assertEqual( + set(), + set(self.collection.get_assigned_devices(required_only=True))) self.assertEqual( - {self.device}, set(self.collection.get_assigned_devices())) + {self.assignment}, set(self.collection.get_assigned_devices())) self.assertEqual( - {self.device}, set(self.collection.get_attached_devices())) + {self.assignment}, set(self.collection.get_attached_devices())) - def test_021_update_required_to_true(self): - self.assignment.required = False + def test_021_update_mode_to_ask(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.assignment}, + set(self.collection.get_assigned_devices(required_only=True))) + self.assertEqual( + {self.assignment}, set(self.collection.get_assigned_devices())) + self.loop.run_until_complete( + self.collection.update_assignment(self.device, AssignmentMode.ASK)) + self.assertEqual( + set(), + set(self.collection.get_assigned_devices(required_only=True))) + self.assertEqual( + {self.assignment}, set(self.collection.get_assigned_devices())) + self.assertEqual( + {self.assignment}, set(self.collection.get_attached_devices())) + + def test_022_update_mode_to_required(self): + self.assignment = self.assignment.clone(mode='auto-attach') 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}, + self.assertEqual({self.assignment}, set(self.collection.get_attached_devices())) - self.assertEqual({self.device} + self.assertEqual({self.assignment} , set(self.collection.get_assigned_devices())) - self.assertEqual({self.device}, + self.assertEqual({self.assignment}, 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())) + self.collection.update_assignment( + self.device, AssignmentMode.REQUIRED)) + self.assertEqual({self.assignment}, + set(self.collection.get_assigned_devices( + required_only=True))) + self.assertEqual( + {self.assignment}, set(self.collection.get_attached_devices())) - def test_022_update_required_reject_not_running(self): + def test_023_update_mode_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}, + self.assertEqual({self.assignment}, set(self.collection.get_assigned_devices())) self.assertEqual(set(), set(self.collection.get_attached_devices())) with self.assertRaises(qubes.exc.QubesVMNotStartedError): self.loop.run_until_complete( - self.collection.update_required(self.device, False)) + self.collection.update_assignment( + self.device, AssignmentMode.ASK)) - def test_023_update_required_reject_not_attached(self): + def test_024_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.loop.run_until_complete( - self.collection.update_required(self.device, True)) + self.collection.update_assignment( + self.device, AssignmentMode.REQUIRED)) + with self.assertRaises(qubes.exc.QubesValueError): + self.loop.run_until_complete( + self.collection.update_assignment( + self.device, AssignmentMode.ASK)) with self.assertRaises(qubes.exc.QubesValueError): self.loop.run_until_complete( - self.collection.update_required(self.device, False)) + self.collection.update_assignment( + self.device, AssignmentMode.AUTO)) def test_030_assign(self): self.emitter.running = True - self.assignment.required = False + self.assignment = self.assignment.clone(mode='auto-attach') 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.assignment = self.assignment.clone(mode='auto-attach') self.loop.run_until_complete(self.collection.assign(self.assignment)) self.assertEventFired(self.emitter, 'device-assign:testclass') self.assertEventNotFired(self.emitter, 'device-unassign:testclass') @@ -284,7 +315,7 @@ def test_033_assign_required_to_halted(self): self.assertEventNotFired(self.emitter, 'device-unassign:testclass') def test_034_unassign_from_halted(self): - self.assignment.required = False + self.assignment = self.assignment.clone(mode='auto-attach') 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') @@ -292,7 +323,30 @@ def test_034_unassign_from_halted(self): def test_035_unassign(self): self.emitter.running = True - self.assignment.required = False + self.assignment = self.assignment.clone(mode='auto-attach') + 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_036_assign_unassign_port(self): + self.emitter.running = True + device = self.assignment.virtual_device + device = device.clone(port=Port( + device.backend_domain, '*', device.devclass)) + self.assignment = self.assignment.clone( + mode='ask-to-attach', device=device) + 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_037_assign_unassign_device(self): + self.emitter.running = True + device = self.assignment.virtual_device + device = device.clone(device_id="*") + self.assignment = self.assignment.clone( + mode='ask-to-attach', device=device) 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') @@ -303,13 +357,13 @@ def test_040_detach_required(self): self.attach() with self.assertRaises(qubes.exc.QubesVMNotHaltedError): self.loop.run_until_complete( - self.collection.detach(self.assignment)) + self.collection.detach(self.assignment.port)) 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)) + self.collection.detach(self.assignment.port)) def test_042_unassign_required(self): self.emitter.running = True @@ -319,10 +373,11 @@ def test_042_unassign_required(self): self.assertEventFired(self.emitter, 'device-unassign:testclass') def test_043_detach_assigned(self): - self.assignment.required = False + self.assignment = self.assignment.clone(mode='auto-attach') self.loop.run_until_complete(self.collection.assign(self.assignment)) self.attach() - self.loop.run_until_complete(self.collection.detach(self.assignment)) + self.loop.run_until_complete(self.collection.detach( + self.assignment.port)) self.assertEventFired(self.emitter, 'device-assign:testclass') self.assertEventFired(self.emitter, 'device-pre-detach:testclass') self.assertEventFired(self.emitter, 'device-detach:testclass') @@ -339,11 +394,9 @@ def test_000_init(self): self.assertEqual(self.manager, {}) def test_001_missing(self): - device = TestDevice(self.emitter.app.domains['vm'], 'testdev') - assignment = DeviceAssignment( - backend_domain=device.backend_domain, - ident=device.ident, - attach_automatically=True, required=True) + device = TestDevice( + Port(self.emitter.app.domains['vm'], 'testdev', 'testclass')) + assignment = DeviceAssignment(device, mode='required') self.loop.run_until_complete( self.manager['testclass'].assign(assignment)) self.assertEqual( @@ -358,9 +411,9 @@ def setUp(self): def test_010_serialize(self): device = DeviceInfo( - backend_domain=self.vm, - ident="1-1.1.1", - devclass="bus", + Port(backend_domain=self.vm, + port_id="1-1.1.1", + devclass="bus"), vendor="ITL", product="Qubes", manufacturer="", @@ -370,11 +423,11 @@ def test_010_serialize(self): DeviceInterface("u03**01")], additional_info="", date="06.12.23", + device_id='0000:0000::?******', ) actual = device.serialize() expected = ( - b"manufacturer='unknown' self_identity='0000:0000::?******' " - b"serial='unknown' ident='1-1.1.1' product='Qubes' " + b"device_id='0000:0000::?******' port_id='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'") @@ -386,9 +439,9 @@ def test_010_serialize(self): def test_011_serialize_with_parent(self): device = DeviceInfo( - backend_domain=self.vm, - ident="1-1.1.1", - devclass="bus", + Port(backend_domain=self.vm, + port_id="1-1.1.1", + devclass="bus"), vendor="ITL", product="Qubes", manufacturer="", @@ -398,16 +451,16 @@ def test_011_serialize_with_parent(self): DeviceInterface("u03**01")], additional_info="", date="06.12.23", - parent=Device(self.vm, '1-1.1', 'pci') + parent=Port(self.vm, '1-1.1', 'pci'), + device_id='0000:0000::?******', ) actual = device.serialize() expected = ( - b"manufacturer='unknown' self_identity='0000:0000::?******' " - b"serial='unknown' ident='1-1.1.1' product='Qubes' " + b"device_id='0000:0000::?******' port_id='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'") + b"parent_port_id='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", @@ -416,9 +469,9 @@ def test_011_serialize_with_parent(self): def test_012_invalid_serialize(self): device = DeviceInfo( - backend_domain=self.vm, - ident="1-1.1.1", - devclass="bus?", + Port(backend_domain=self.vm, + port_id="1-1.1.1", + devclass="testclass"), vendor="malicious", product="suspicious", manufacturer="", @@ -430,17 +483,16 @@ def test_012_invalid_serialize(self): 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"device_id='0000:0000::?******' port_id='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", + Port(backend_domain=self.vm, + port_id="1-1.1.1", + devclass="bus"), vendor="ITL", product="Qubes", manufacturer="unknown", @@ -450,10 +502,11 @@ def test_020_deserialize(self): DeviceInterface("u03**01")], additional_info="", date="06.12.23", + device_id='0000:0000::?******', ) self.assertEqual(actual.backend_domain, expected.backend_domain) - self.assertEqual(actual.ident, expected.ident) + self.assertEqual(actual.port_id, expected.port_id) self.assertEqual(actual.devclass, expected.devclass) self.assertEqual(actual.vendor, expected.vendor) self.assertEqual(actual.product, expected.product) @@ -461,14 +514,14 @@ def test_020_deserialize(self): 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.device_id, expected.device_id) 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"manufacturer='unknown' device_id='0000:0000::?******' " + b"serial='unknown' port_id='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' " @@ -476,14 +529,14 @@ def test_021_invalid_deserialize(self): 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.port_id, '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?", + Port(backend_domain=self.vm, + port_id="1-1.1.1", + devclass="testclass"), vendor="malicious", product="suspicious", manufacturer="", @@ -496,7 +549,7 @@ def test_030_serialize_and_deserialize(self): 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.port_id, device.port_id) self.assertEqual(deserialized.devclass, device.devclass) self.assertEqual(deserialized.vendor, device.vendor) self.assertEqual(deserialized.product, device.product) @@ -515,70 +568,77 @@ def setUp(self): self.vm = TestVM(self.app, 'vm') def test_010_serialize(self): - assignment = DeviceAssignment( + assignment = DeviceAssignment(VirtualDevice(Port( backend_domain=self.vm, - ident="1-1.1.1", + port_id="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'") + b"device_id='*' port_id='1-1.1.1' devclass='bus' " + b"backend_domain='vm' mode='manual'") 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, + VirtualDevice(Port( + backend_domain=self.vm, + port_id="1-1.1.1", + devclass="bus", + )), + mode='required', ) actual = assignment.serialize() expected = ( - b"ident='1-1.1.1' devclass='bus' " - b"backend_domain='vm' required='yes' attach_automatically='yes'") + b"device_id='*' port_id='1-1.1.1' devclass='bus' " + b"backend_domain='vm' mode='required'") 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", + VirtualDevice(Port( + backend_domain=self.vm, + port_id="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'") + b"device_id='*' port_id='1-1.1.1' frontend_domain='vm' " + b"devclass='bus' backend_domain='vm' mode='manual'") 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", + VirtualDevice(Port( + backend_domain=self.vm, + port_id="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'") + b"device_id='*' port_id='1-1.1.1' _read-only='yes' devclass='bus' " + b"backend_domain='vm' mode='manual'") 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", + VirtualDevice(Port( + backend_domain=self.vm, + port_id="1-1.1.1", + devclass="bus", + )), options={"read'only": 'yes'}, ) with self.assertRaises(qubes.exc.ProtocolError): @@ -586,23 +646,24 @@ def test_014_invalid_serialize(self): 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"device_id='*' port_id='1-1.1.1' frontend_domain='vm' " + b"devclass='bus' backend_domain='vm' mode='auto-attach' " b"_read-only='yes'") - expected_device = Device(self.vm, '1-1.1.1', 'bus') + expected_device = VirtualDevice(Port(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", + VirtualDevice(Port( + backend_domain=self.vm, + port_id="1-1.1.1", + devclass="bus", + )), frontend_domain=self.vm, - attach_automatically=True, - required=False, + mode='auto-attach', options={'read-only': 'yes'}, ) self.assertEqual(actual.backend_domain, expected.backend_domain) - self.assertEqual(actual.ident, expected.ident) + self.assertEqual(actual.port_id, expected.port_id) self.assertEqual(actual.devclass, expected.devclass) self.assertEqual(actual.frontend_domain, expected.frontend_domain) self.assertEqual(actual.attach_automatically, expected.attach_automatically) @@ -611,37 +672,34 @@ def test_020_deserialize(self): 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"device_id='*' port_id='1-1.1.1' frontend_domain='vm' " + b"devclass='bus' backend_domain='vm' mode='auto-attach' " b"_read'only='yes'") - expected_device = Device(self.vm, '1-1.1.1', 'bus') + expected_device = VirtualDevice(Port(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"device_id='*' port_id='1-1.1.1' frontend_domain='vm' " + b"devclass='bus' backend_domain='vm' mode='auto-attach' " b"read-only='yes'") - expected_device = Device(self.vm, '1-1.1.1', 'bus') + expected_device = VirtualDevice(Port(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_device = VirtualDevice(Port(self.vm, '1-1.1.1', 'bus')) expected = DeviceAssignment( - backend_domain=self.vm, - ident="1-1.1.1", - devclass="bus", + expected_device, frontend_domain=self.vm, - attach_automatically=True, - required=False, + mode='auto-attach', 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.port_id, expected.port_id) self.assertEqual(actual.devclass, expected.devclass) self.assertEqual(actual.frontend_domain, expected.frontend_domain) self.assertEqual(actual.attach_automatically, diff --git a/qubes/tests/devices_block.py b/qubes/tests/devices_block.py index 0d19eebb4..298da302c 100644 --- a/qubes/tests/devices_block.py +++ b/qubes/tests/devices_block.py @@ -18,14 +18,25 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, see . import asyncio +import unittest from unittest import mock +from unittest.mock import Mock import jinja2 import qubes.tests import qubes.ext.block -from qubes.device_protocol import DeviceInterface, Device, DeviceInfo, \ - DeviceAssignment +from qubes.device_protocol import DeviceInterface, Port, DeviceInfo, \ + DeviceAssignment, VirtualDevice + + +def async_test(f): + def wrapper(*args, **kwargs): + coro = asyncio.coroutine(f) + future = coro(*args, **kwargs) + loop = asyncio.get_event_loop() + loop.run_until_complete(future) + return wrapper modules_disk = ''' @@ -135,9 +146,14 @@ def __init__(self, backend_vm, devclass): def get_assigned_devices(self): return self._assigned - def __getitem__(self, ident): + def get_exposed_devices(self): + yield from self._exposed + + __iter__ = get_exposed_devices + + def __getitem__(self, port_id): for dev in self._exposed: - if dev.ident == ident: + if dev.port_id == port_id: return dev @@ -165,6 +181,9 @@ def __init__( 'testclass': TestDeviceCollection(self, 'testclass') } + def __hash__(self): + return hash(self.name) + def __eq__(self, other): if isinstance(other, TestVM): return self.name == other.name @@ -173,6 +192,16 @@ def __str__(self): return self.name +def get_qdb(mode): + result = { + '/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': mode.encode(), + } + return result + + class TC_00_Block(qubes.tests.QubesTestCase): def setUp(self): @@ -187,7 +216,8 @@ def test_000_device_get(self): '/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') + parent = DeviceInfo(Port(vm, '1-1.1', devclass='usb'), + device_id='0000:0000::?******') vm.devices['usb'] = TestDeviceCollection(backend_vm=vm, devclass='usb') vm.devices['usb']._exposed.append(parent) vm.is_running = lambda: True @@ -215,7 +245,7 @@ def test_000_device_get(self): 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.port_id, 'sda') self.assertEqual(device_info.name, 'device') self.assertEqual(device_info._name, 'device') self.assertEqual(device_info.serial, 'Test') @@ -227,10 +257,9 @@ def test_000_device_get(self): 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.parent_device, parent) self.assertEqual(device_info.attachment, front) - self.assertEqual(device_info.self_identity, + self.assertEqual(device_info.device_id, '1-1.1:0000:0000::?******:1.0') self.assertEqual( device_info.data.get('test_frontend_domain', None), None) @@ -246,7 +275,7 @@ def test_001_device_get_other_node(self): device_info = self.ext.device_get(vm, 'mapper_dmroot') 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.port_id, 'mapper_dmroot') self.assertEqual(device_info._name, None) self.assertEqual(device_info.name, 'unknown') self.assertEqual(device_info.serial, 'Test device') @@ -314,13 +343,13 @@ def test_010_devices_list(self): devices = sorted(list(self.ext.on_device_list_block(vm, ''))) self.assertEqual(len(devices), 2) self.assertEqual(devices[0].backend_domain, vm) - self.assertEqual(devices[0].ident, 'sda') + self.assertEqual(devices[0].port_id, 'sda') 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].port_id, 'sdb') self.assertEqual(devices[1].serial, 'Test device') self.assertEqual(devices[1].name, '2') self.assertEqual(devices[1].size, 2048000) @@ -333,8 +362,8 @@ def test_011_devices_list_empty(self): def test_012_devices_list_invalid_ident(self): vm = TestVM({ - '/qubes-block-devices/invalid ident': b'', - '/qubes-block-devices/invalid+ident': b'', + '/qubes-block-devices/invalid port_id': b'', + '/qubes-block-devices/invalid+port_id': b'', '/qubes-block-devices/invalid#': b'', }) devices = sorted(list(self.ext.on_device_list_block(vm, ''))) @@ -389,7 +418,7 @@ def test_031_list_attached(self): dev = devices[0][0] options = devices[0][1] self.assertEqual(dev.backend_domain, vm.app.domains['sys-usb']) - self.assertEqual(dev.ident, 'sda') + self.assertEqual(dev.port_id, 'sda') self.assertEqual(dev.attachment, None) self.assertEqual(options['frontend-dev'], 'xvdi') self.assertEqual(options['read-only'], 'yes') @@ -412,7 +441,7 @@ def test_032_list_attached_dom0(self): dev = devices[0][0] options = devices[0][1] self.assertEqual(dev.backend_domain, vm.app.domains['dom0']) - self.assertEqual(dev.ident, 'sda') + self.assertEqual(dev.port_id, 'sda') self.assertEqual(options['frontend-dev'], 'xvdi') self.assertEqual(options['read-only'], 'no') @@ -434,18 +463,13 @@ def test_033_list_attached_cdrom(self): dev = devices[0][0] options = devices[0][1] self.assertEqual(dev.backend_domain, vm.app.domains['sys-usb']) - self.assertEqual(dev.ident, 'sr0') + self.assertEqual(dev.port_id, 'sr0') self.assertEqual(options['frontend-dev'], 'xvdi') self.assertEqual(options['read-only'], 'yes') self.assertEqual(options['devtype'], 'cdrom') def test_040_attach(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'w', - }) + back_vm = TestVM(name='sys-usb', qdb=get_qdb(mode='w')) vm = TestVM({}, domain_xml=domain_xml_template.format('')) dev = qubes.ext.block.BlockDevice(back_vm, 'sda') self.ext.on_device_pre_attached_block(vm, '', dev, {}) @@ -460,12 +484,7 @@ def test_040_attach(self): vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml) def test_041_attach_frontend(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'w', - }) + back_vm = TestVM(name='sys-usb', qdb=get_qdb(mode='w')) vm = TestVM({}, domain_xml=domain_xml_template.format('')) dev = qubes.ext.block.BlockDevice(back_vm, 'sda') self.ext.on_device_pre_attached_block(vm, '', dev, @@ -481,12 +500,7 @@ def test_041_attach_frontend(self): vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml) def test_042_attach_read_only(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'w', - }) + back_vm = TestVM(name='sys-usb', qdb=get_qdb(mode='w')) vm = TestVM({}, domain_xml=domain_xml_template.format('')) dev = qubes.ext.block.BlockDevice(back_vm, 'sda') self.ext.on_device_pre_attached_block(vm, '', dev, @@ -503,12 +517,7 @@ def test_042_attach_read_only(self): vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml) def test_043_attach_invalid_option(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'w', - }) + back_vm = TestVM(name='sys-usb', qdb=get_qdb(mode='w')) vm = TestVM({}, domain_xml=domain_xml_template.format('')) dev = qubes.ext.block.BlockDevice(back_vm, 'sda') with self.assertRaises(qubes.exc.QubesValueError): @@ -517,12 +526,7 @@ def test_043_attach_invalid_option(self): self.assertFalse(vm.libvirt_domain.attachDevice.called) def test_044_attach_invalid_option2(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'w', - }) + back_vm = TestVM(name='sys-usb', qdb=get_qdb(mode='w')) vm = TestVM({}, domain_xml=domain_xml_template.format('')) dev = qubes.ext.block.BlockDevice(back_vm, 'sda') with self.assertRaises(qubes.exc.QubesValueError): @@ -531,12 +535,7 @@ def test_044_attach_invalid_option2(self): self.assertFalse(vm.libvirt_domain.attachDevice.called) def test_045_attach_backend_not_running(self): - back_vm = TestVM(name='sys-usb', running=False, 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'w', - }) + back_vm = TestVM(name='sys-usb', running=False, qdb=get_qdb(mode='w')) vm = TestVM({}, domain_xml=domain_xml_template.format('')) dev = qubes.ext.block.BlockDevice(back_vm, 'sda') with self.assertRaises(qubes.exc.QubesVMNotRunningError): @@ -544,12 +543,7 @@ def test_045_attach_backend_not_running(self): self.assertFalse(vm.libvirt_domain.attachDevice.called) def test_046_attach_ro_dev_rw(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', - }) + back_vm = TestVM(name='sys-usb', qdb=get_qdb(mode='r')) vm = TestVM({}, domain_xml=domain_xml_template.format('')) dev = qubes.ext.block.BlockDevice(back_vm, 'sda') with self.assertRaises(qubes.exc.QubesValueError): @@ -558,12 +552,7 @@ def test_046_attach_ro_dev_rw(self): self.assertFalse(vm.libvirt_domain.attachDevice.called) def test_047_attach_read_only_auto(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', - }) + back_vm = TestVM(name='sys-usb', qdb=get_qdb(mode='r')) vm = TestVM({}, domain_xml=domain_xml_template.format('')) dev = qubes.ext.block.BlockDevice(back_vm, 'sda') self.ext.on_device_pre_attached_block(vm, '', dev, {}) @@ -579,12 +568,7 @@ def test_047_attach_read_only_auto(self): vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml) def test_048_attach_cdrom_xvdi(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', - }) + back_vm = TestVM(name='sys-usb', qdb=get_qdb(mode='r')) vm = TestVM({}, domain_xml=domain_xml_template.format(modules_disk)) dev = qubes.ext.block.BlockDevice(back_vm, 'sda') self.ext.on_device_pre_attached_block(vm, '', dev, {'devtype': 'cdrom'}) @@ -600,12 +584,7 @@ def test_048_attach_cdrom_xvdi(self): vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml) def test_048_attach_cdrom_xvdd(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', - }) + back_vm = TestVM(name='sys-usb', qdb=get_qdb(mode='r')) vm = TestVM({}, domain_xml=domain_xml_template.format('')) dev = qubes.ext.block.BlockDevice(back_vm, 'sda') self.ext.on_device_pre_attached_block(vm, '', dev, {'devtype': 'cdrom'}) @@ -621,12 +600,7 @@ def test_048_attach_cdrom_xvdd(self): vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml) def test_050_detach(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', - }) + back_vm = TestVM(name='sys-usb', qdb=get_qdb(mode='r')) device_xml = ( '\n' ' \n' @@ -640,48 +614,36 @@ def test_050_detach(self): 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.ext.on_device_pre_detached_block(vm, '', dev.port) vm.libvirt_domain.detachDevice.assert_called_once_with(device_xml) def test_051_detach_not_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', - }) + back_vm = TestVM(name='sys-usb', qdb=get_qdb(mode='r')) 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.ext.on_device_pre_detached_block(vm, '', dev.port) 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') + back_vm = TestVM(name='sys-usb', qdb=get_qdb(mode='r'), + domain_xml=domain_xml_template.format("")) + exp_dev = qubes.ext.block.BlockDevice(back_vm, 'sda') 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(""), + ('device-added:block', frozenset({('device', exp_dev)}))], 1) + + @staticmethod + def added_assign_setup(attached_device=""): + back_vm = TestVM(name='sys-usb', qdb=get_qdb(mode='r'), + domain_xml=domain_xml_template.format("")) + front = TestVM({}, domain_xml=domain_xml_template.format( + attached_device), name='front-vm') dom0 = TestVM({}, name='dom0', domain_xml=domain_xml_template.format("")) @@ -702,30 +664,132 @@ def test_061_on_qdb_change_auto_attached(self): 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')) + return back_vm, front + + def test_061_on_qdb_change_required(self): + back, front = self.added_assign_setup() - # 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 + exp_dev = qubes.ext.block.BlockDevice(back, 'sda') + assignment = DeviceAssignment(exp_dev, mode='required') + front.devices['block']._assigned.append(assignment) + back.devices['block']._exposed.append( + qubes.ext.block.BlockDevice(back, 'sda')) + + self.ext.attach_and_notify = Mock() 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'}) + self.ext.on_qdb_change(back, None, None) + self.ext.attach_and_notify.assert_called_once_with( + front, assignment) + + def test_062_on_qdb_change_auto_attached(self): + back, front = self.added_assign_setup() + + exp_dev = qubes.ext.block.BlockDevice(back, 'sda') + assignment = DeviceAssignment(exp_dev) + front.devices['block']._assigned.append(assignment) + back.devices['block']._exposed.append( + qubes.ext.block.BlockDevice(back, 'sda')) + + self.ext.attach_and_notify = Mock() + with mock.patch('asyncio.ensure_future'): + self.ext.on_qdb_change(back, None, None) + self.ext.attach_and_notify.assert_called_once_with( + front, assignment) + + def test_063_on_qdb_change_ask_to_attached(self): + back, front = self.added_assign_setup() + + exp_dev = qubes.ext.block.BlockDevice(back, 'sda') + assignment = DeviceAssignment(exp_dev, mode='ask-to-attach') + front.devices['block']._assigned.append(assignment) + back.devices['block']._exposed.append( + qubes.ext.block.BlockDevice(back, 'sda')) + + self.ext.attach_and_notify = Mock() + with mock.patch('asyncio.ensure_future'): + self.ext.on_qdb_change(back, None, None) + self.ext.attach_and_notify.assert_called_once_with( + front, assignment) + + def test_064_on_qdb_change_multiple_assignments_including_full(self): + back, front = self.added_assign_setup() + + exp_dev = qubes.ext.block.BlockDevice(back, 'sda') + full_assig = DeviceAssignment(VirtualDevice( + exp_dev.port, exp_dev.device_id), mode='auto-attach', + options={'pid': 'did'}) + port_assign = DeviceAssignment(VirtualDevice( + exp_dev.port, '*'), mode='auto-attach', + options={'pid': 'any'}) + dev_assign = DeviceAssignment(VirtualDevice(Port( + exp_dev.backend_domain, '*', 'block'), + exp_dev.device_id), mode='auto-attach', + options={'any': 'did'}) + + front.devices['block']._assigned.append(dev_assign) + front.devices['block']._assigned.append(port_assign) + front.devices['block']._assigned.append(full_assig) + back.devices['block']._exposed.append( + qubes.ext.block.BlockDevice(back, 'sda')) + + self.ext.attach_and_notify = Mock() + with mock.patch('asyncio.ensure_future'): + self.ext.on_qdb_change(back, None, None) + self.assertEqual(self.ext.attach_and_notify.call_args[0][1].options, + {'pid': 'did'}) + + def test_065_on_qdb_change_multiple_assignments_port_vs_dev(self): + back, front = self.added_assign_setup() + + exp_dev = qubes.ext.block.BlockDevice(back, 'sda') + port_assign = DeviceAssignment(VirtualDevice( + exp_dev.port, '*'), mode='auto-attach', + options={'pid': 'any'}) + dev_assign = DeviceAssignment(VirtualDevice(Port( + exp_dev.backend_domain, '*', 'block'), + exp_dev.device_id), mode='auto-attach', + options={'any': 'did'}) + + front.devices['block']._assigned.append(dev_assign) + front.devices['block']._assigned.append(port_assign) + back.devices['block']._exposed.append( + qubes.ext.block.BlockDevice(back, 'sda')) + + self.ext.attach_and_notify = Mock() + with mock.patch('asyncio.ensure_future'): + self.ext.on_qdb_change(back, None, None) + self.assertEqual(self.ext.attach_and_notify.call_args[0][1].options, + {'pid': 'any'}) + + def test_066_on_qdb_change_multiple_assignments_dev(self): + back, front = self.added_assign_setup() + + exp_dev = qubes.ext.block.BlockDevice(back, 'sda') + port_assign = DeviceAssignment(VirtualDevice(Port( + exp_dev.backend_domain, 'other', 'block'), + '*'), mode='auto-attach', + options={'pid': 'any'}) + dev_assign = DeviceAssignment(VirtualDevice(Port( + exp_dev.backend_domain, '*', 'block'), + exp_dev.device_id), mode='auto-attach', options={'any': 'did'}) + + front.devices['block']._assigned.append(dev_assign) + front.devices['block']._assigned.append(port_assign) + back.devices['block']._exposed.append( + qubes.ext.block.BlockDevice(back, 'sda')) + back.devices['block']._exposed.append( + qubes.ext.block.BlockDevice(back, 'other')) + + self.ext.attach_and_notify = Mock() + with mock.patch('asyncio.ensure_future'): + self.ext.on_qdb_change(back, None, None) + self.assertEqual(self.ext.attach_and_notify.call_args[0][1].options, + {'any': 'did'}) - def test_062_on_qdb_change_attached(self): + def test_067_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') + back_vm = TestVM(name='sys-usb', qdb=get_qdb(mode='r'), domain_xml=domain_xml_template.format("")) + exp_dev = qubes.ext.block.BlockDevice(back_vm, 'sda') self.ext.devices_cache = {'sys-usb': {'sda': None}} @@ -766,15 +830,10 @@ def test_062_on_qdb_change_attached(self): fire_event_async.assert_called_once_with( 'device-attach:block', device=exp_dev, options={}) - def test_063_on_qdb_change_changed(self): + def test_068_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') + back_vm = TestVM(name='sys-usb', qdb=get_qdb(mode='r'), domain_xml=domain_xml_template.format("")) + exp_dev = qubes.ext.block.BlockDevice(back_vm, 'sda') front = TestVM({}, name='front-vm') dom0 = TestVM({}, name='dom0', @@ -827,21 +886,16 @@ def test_063_on_qdb_change_changed(self): self.assertEqual(self.ext.devices_cache, {'sys-usb': {'sda': front_2}}) fire_event_async.assert_called_with( - 'device-detach:block', device=exp_dev) + 'device-detach:block', port=exp_dev.port) fire_event_async_2.assert_called_once_with( 'device-attach:block', device=exp_dev, options={}) - def test_064_on_qdb_change_removed_attached(self): + def test_069_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("")) + back_vm = TestVM(name='sys-usb', qdb=get_qdb(mode='r'), domain_xml=domain_xml_template.format("")) dom0 = TestVM({}, name='dom0', domain_xml=domain_xml_template.format("")) - exp_dev = Device(back_vm, 'sda', 'block') + exp_dev = qubes.ext.block.BlockDevice(back_vm, 'sda') disk = ''' @@ -853,7 +907,7 @@ def test_064_on_qdb_change_removed_attached(self): ''' front = TestVM({}, domain_xml=domain_xml_template.format(disk), - name='front') + name='front') self.ext.devices_cache = {'sys-usb': {'sda': front}} back_vm.app.vmm.configure_mock(**{'offline_mode': False}) @@ -883,8 +937,172 @@ def test_064_on_qdb_change_removed_attached(self): 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) + 'device-detach:block', port=exp_dev.port) self.assertEqual( back_vm.fired_events[ - ('device-removed:block', frozenset({('device', exp_dev)}))], + ('device-removed:block', frozenset({('port', exp_dev.port)}))], 1) + + @unittest.mock.patch('subprocess.Popen') + def test_070_on_qdb_change_two_fronts_failed(self, mock_confirm): + back, front = self.added_assign_setup() + + exp_dev = qubes.ext.block.BlockDevice(back, 'sda') + assign = DeviceAssignment(exp_dev, mode='auto-attach') + + front.devices['block']._assigned.append(assign) + back.devices['block']._assigned.append(assign) + back.devices['block']._exposed.append(exp_dev) + + proc = Mock() + proc.communicate = Mock() + proc.communicate.return_value = (b'nonsense', b'') + mock_confirm.return_value = proc + self.ext.attach_and_notify = Mock() + with mock.patch('asyncio.ensure_future'): + self.ext.on_qdb_change(back, None, None) + proc.communicate.assert_called_once() + self.ext.attach_and_notify.assert_not_called() + + @unittest.mock.patch('subprocess.Popen') + def test_071_on_qdb_change_two_fronts(self, mock_confirm): + back, front = self.added_assign_setup() + + exp_dev = qubes.ext.block.BlockDevice(back, 'sda') + assign = DeviceAssignment(exp_dev, mode='ask-to-attach') + + front.devices['block']._assigned.append(assign) + back.devices['block']._assigned.append(assign) + back.devices['block']._exposed.append(exp_dev) + + proc = Mock() + proc.communicate = Mock() + proc.communicate.return_value = (b'front-vm', b'') + mock_confirm.return_value = proc + self.ext.attach_and_notify = Mock() + with mock.patch('asyncio.ensure_future'): + self.ext.on_qdb_change(back, None, None) + proc.communicate.assert_called_once() + self.ext.attach_and_notify.assert_called_once_with( + front, assign) + # don't ask again + self.assertEqual(self.ext.attach_and_notify.call_args[0][1].mode.value, + 'auto-attach') + + def test_072_on_qdb_change_ask(self): + back, front = self.added_assign_setup() + + exp_dev = qubes.ext.block.BlockDevice(back, 'sda') + assign = DeviceAssignment(exp_dev, mode='ask-to-attach') + + front.devices['block']._assigned.append(assign) + back.devices['block']._exposed.append(exp_dev) + + self.ext.attach_and_notify = Mock() + with mock.patch('asyncio.ensure_future'): + self.ext.on_qdb_change(back, None, None) + self.assertEqual(self.ext.attach_and_notify.call_args[0][1].mode.value, + 'ask-to-attach') + + def test_080_on_startup_multiple_assignments_including_full(self): + back, front = self.added_assign_setup() + + exp_dev = qubes.ext.block.BlockDevice(back, 'sda') + full_assig = DeviceAssignment(VirtualDevice( + exp_dev.port, exp_dev.device_id), mode='auto-attach', + options={'pid': 'did'}) + port_assign = DeviceAssignment(VirtualDevice( + exp_dev.port, '*'), mode='auto-attach', + options={'pid': 'any'}) + dev_assign = DeviceAssignment(VirtualDevice(Port( + exp_dev.backend_domain, '*', 'block'), + exp_dev.device_id), mode='auto-attach', + options={'any': 'did'}) + + front.devices['block']._assigned.append(dev_assign) + front.devices['block']._assigned.append(port_assign) + front.devices['block']._assigned.append(full_assig) + back.devices['block']._exposed.append( + qubes.ext.block.BlockDevice(back, 'sda')) + + self.ext.attach_and_notify = Mock() + loop = asyncio.get_event_loop() + with mock.patch('asyncio.ensure_future'): + loop.run_until_complete(self.ext.on_domain_start(front, None)) + self.assertEqual(self.ext.attach_and_notify.call_args[0][1].options, + {'pid': 'did'}) + + def test_081_on_startup_multiple_assignments_port_vs_dev(self): + back, front = self.added_assign_setup() + + exp_dev = qubes.ext.block.BlockDevice(back, 'sda') + port_assign = DeviceAssignment(VirtualDevice( + exp_dev.port, '*'), mode='auto-attach', + options={'pid': 'any'}) + dev_assign = DeviceAssignment(VirtualDevice(Port( + exp_dev.backend_domain, '*', 'block'), + exp_dev.device_id), mode='auto-attach', + options={'any': 'did'}) + + front.devices['block']._assigned.append(dev_assign) + front.devices['block']._assigned.append(port_assign) + back.devices['block']._exposed.append( + qubes.ext.block.BlockDevice(back, 'sda')) + + self.ext.attach_and_notify = Mock() + loop = asyncio.get_event_loop() + with mock.patch('asyncio.ensure_future'): + loop.run_until_complete(self.ext.on_domain_start(front, None)) + self.assertEqual(self.ext.attach_and_notify.call_args[0][1].options, + {'pid': 'any'}) + + def test_082_on_startup_multiple_assignments_dev(self): + back, front = self.added_assign_setup() + + exp_dev = qubes.ext.block.BlockDevice(back, 'sda') + port_assign = DeviceAssignment(VirtualDevice(Port( + exp_dev.backend_domain, 'other', 'block'), + '*'), mode='auto-attach', + options={'pid': 'any'}) + dev_assign = DeviceAssignment(VirtualDevice(Port( + exp_dev.backend_domain, '*', 'block'), + exp_dev.device_id), mode='auto-attach', options={'any': 'did'}) + + front.devices['block']._assigned.append(dev_assign) + front.devices['block']._assigned.append(port_assign) + back.devices['block']._exposed.append( + qubes.ext.block.BlockDevice(back, 'sda')) + back.devices['block']._exposed.append( + qubes.ext.block.BlockDevice(back, 'other')) + + self.ext.attach_and_notify = Mock() + loop = asyncio.get_event_loop() + with mock.patch('asyncio.ensure_future'): + loop.run_until_complete(self.ext.on_domain_start(front, None)) + self.assertEqual(self.ext.attach_and_notify.call_args[0][1].options, + {'any': 'did'}) + + def test_083_on_startup_already_attached(self): + disk = ''' + + + + + + + + ''' + back, front = self.added_assign_setup(disk) + + exp_dev = qubes.ext.block.BlockDevice(back, 'sda') + assign = DeviceAssignment(VirtualDevice( + exp_dev.port, exp_dev.device_id), mode='auto-attach') + + front.devices['block']._assigned.append(assign) + back.devices['block']._exposed.append(exp_dev) + + self.ext.attach_and_notify = Mock() + loop = asyncio.get_event_loop() + with mock.patch('asyncio.ensure_future'): + loop.run_until_complete(self.ext.on_domain_start(front, None)) + self.ext.attach_and_notify.assert_not_called() diff --git a/qubes/tests/devices_pci.py b/qubes/tests/devices_pci.py index 01011cadd..57344d87d 100644 --- a/qubes/tests/devices_pci.py +++ b/qubes/tests/devices_pci.py @@ -126,6 +126,7 @@ def setUp(self): def test_000_unsupported_device(self): vm = TestVM() vm.app.configure_mock(**{ + 'vmm.offline_mode': False, 'vmm.libvirt_conn.nodeDeviceLookupByName.return_value': mock.Mock(**{"XMLDesc.return_value": PCI_XML.format(*["0000"] * 3) @@ -143,7 +144,7 @@ def test_000_unsupported_device(self): }) 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].port_id, "00_14.0") self.assertEqual(devices[0].vendor, "Intel Corporation") self.assertEqual(devices[0].product, "9 Series Chipset Family USB xHCI Controller") @@ -153,4 +154,4 @@ def test_000_unsupported_device(self): 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") + self.assertEqual(devices[0].device_id, "0x8086:0x8cb1::p0c0330") diff --git a/qubes/tests/integ/audio.py b/qubes/tests/integ/audio.py index 63f03b1b7..336de9f33 100644 --- a/qubes/tests/integ/audio.py +++ b/qubes/tests/integ/audio.py @@ -271,14 +271,18 @@ async def _check_audio_input_status(vm, status): await asyncio.sleep(0.5) def attach_mic(self): - deva = qubes.device_protocol.DeviceAssignment(self.app.domains[0], 'mic') + deva = qubes.device_protocol.DeviceAssignment( + qubes.device_protocol.VirtualDevice( + qubes.device_protocol.Port(self.app.domains[0], 'mic', 'mic'))) self.loop.run_until_complete( self.testvm1.devices['mic'].attach(deva) ) self.loop.run_until_complete(self.retrieve_audio_input(self.testvm1, b"1")) def detach_mic(self): - deva = qubes.device_protocol.DeviceAssignment(self.app.domains[0], 'mic') + deva = qubes.device_protocol.DeviceAssignment( + qubes.device_protocol.VirtualDevice( + qubes.device_protocol.Port(self.app.domains[0], 'mic', 'mic'))) self.loop.run_until_complete( self.testvm1.devices['mic'].detach(deva) ) diff --git a/qubes/tests/integ/backup.py b/qubes/tests/integ/backup.py index d62c6d3f7..ef32b1c71 100644 --- a/qubes/tests/integ/backup.py +++ b/qubes/tests/integ/backup.py @@ -303,9 +303,8 @@ 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].get_assigned_devices(): - vm_info['devices'][dev_class][str(dev_ass.device)] = \ - dev_ass.options + for ass in vm.devices[dev_class].get_assigned_devices(): + vm_info['devices'][dev_class][str(ass)] = ass.options vms_info[vm.name] = vm_info return vms_info @@ -339,7 +338,7 @@ def assertCorrectlyRestored(self, vms_info, orig_hashes): found = False for restored_dev_ass in restored_vm.devices[ dev_class].get_assigned_devices(): - if str(restored_dev_ass.device) == dev: + if str(restored_dev_ass) == dev: found = True self.assertEqual(vm_info['devices'][dev_class][dev], restored_dev_ass.options, diff --git a/qubes/tests/integ/devices_block.py b/qubes/tests/integ/devices_block.py index e3d17b73b..1642278b5 100644 --- a/qubes/tests/integ/devices_block.py +++ b/qubes/tests/integ/devices_block.py @@ -88,7 +88,7 @@ def test_000_list_loop(self): found = False for dev in dev_list: if dev.serial == self.img_path: - self.assertTrue(dev.ident.startswith('loop')) + self.assertTrue(dev.port_id.startswith('loop')) self.assertEqual(dev.mode, 'w') self.assertEqual(dev.size, 1024 * 1024 * 128) found = True @@ -131,7 +131,7 @@ def test_010_list_dm(self): dev_list = list(self.vm.devices['block']) found = False for dev in dev_list: - if dev.ident.startswith('loop'): + if dev.port_id.startswith('loop'): self.assertNotEqual(dev.serial, self.img_path, "Device {} ({}) should not be listed as it is used in " "device-mapper".format(dev, self.img_path) @@ -162,7 +162,7 @@ def test_011_list_dm_mounted(self): dev_list = list(self.vm.devices['block']) for dev in dev_list: - if dev.ident.startswith('loop'): + if dev.port_id.startswith('loop'): self.assertNotEqual(dev.serial, self.img_path, "Device {} ({}) should not be listed as it is used in " "device-mapper".format(dev, self.img_path) @@ -187,7 +187,7 @@ def test_012_list_dm_delayed(self): dev_list = list(self.vm.devices['block']) found = False for dev in dev_list: - if dev.ident.startswith('loop'): + if dev.port_id.startswith('loop'): self.assertNotEqual(dev.serial, self.img_path, "Device {} ({}) should not be listed as it is used in " "device-mapper".format(dev, self.img_path) @@ -219,7 +219,7 @@ def test_013_list_dm_removed(self): found = False for dev in dev_list: if dev.serial == self.img_path: - self.assertTrue(dev.ident.startswith('loop')) + self.assertTrue(dev.port_id.startswith('loop')) self.assertEqual(dev.mode, 'w') self.assertEqual(dev.size, 1024 * 1024 * 128) found = True @@ -243,10 +243,10 @@ def test_020_list_loop_partition(self): found = False for dev in dev_list: if dev.serial == self.img_path: - self.assertTrue(dev.ident.startswith('loop')) + self.assertTrue(dev.port_id.startswith('loop')) self.assertEqual(dev.mode, 'w') self.assertEqual(dev.size, 1024 * 1024 * 128) - self.assertIn(dev.ident + 'p1', [d.ident for d in dev_list]) + self.assertIn(dev.port_id + 'p1', [d.port_id for d in dev_list]) found = True if not found: @@ -276,7 +276,7 @@ def test_021_list_loop_partition_mounted(self): 'Device {} ({}) should not be listed because its ' 'partition is mounted' .format(dev, self.img_path)) - elif dev.ident.startswith('loop') and dev.ident.endswith('p1'): + elif dev.port_id.startswith('loop') and dev.port_id.endswith('p1'): # FIXME: risky assumption that only tests create partitioned # loop devices self.fail( @@ -320,14 +320,18 @@ def setUp(self): for dev in dev_list: if dev.serial == self.img_path: self.device = dev - self.device_ident = dev.ident + self.device_ident = dev.port_id break else: self.fail('Device for {} in {} not found'.format( self.img_path, self.backend.serial)) def test_000_attach_reattach(self): - ass = qubes.device_protocol.DeviceAssignment(self.backend, self.device_ident) + ass = qubes.device_protocol.DeviceAssignment( + qubes.device_protocol.VirtualDevice( + qubes.device_protocol.Port( + self.backend, self.device_ident, 'block') + )) with self.subTest('attach'): self.loop.run_until_complete( self.frontend.devices['block'].attach(ass)) diff --git a/qubes/tests/integ/devices_pci.py b/qubes/tests/integ/devices_pci.py index 7b1ea5579..459123d5d 100644 --- a/qubes/tests/integ/devices_pci.py +++ b/qubes/tests/integ/devices_pci.py @@ -28,6 +28,7 @@ import qubes.devices import qubes.ext.pci import qubes.tests +from qubes.device_protocol import DeviceAssignment @qubes.tests.skipUnlessEnv('QUBES_TEST_PCIDEV') @@ -37,16 +38,8 @@ 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.device_protocol.DeviceAssignment( - backend_domain=self.dev.backend_domain, - ident=self.dev.ident, - 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, + self.assignment = DeviceAssignment( + self.dev, mode='required' ) if isinstance(self.dev, qubes.device_protocol.UnknownDevice): self.skipTest('Specified device {} does not exists'.format(pcidev)) @@ -69,7 +62,7 @@ def test_000_list(self): l.split(' (')[0].split(' ', 1) for l in p.communicate()[0].decode().splitlines()) for dev in self.app.domains[0].devices['pci']: - lspci_ident = dev.ident.replace('_', ':') + lspci_ident = dev.port_id.replace('_', ':') self.assertIsInstance(dev, qubes.ext.pci.PCIDevice) self.assertEqual(dev.backend_domain, self.app.domains[0]) self.assertIn(lspci_ident, actual_devices) @@ -95,7 +88,7 @@ def assertDeviceIs( dedicated = assigned or attached self.assertTrue(dedicated == device in dev_col.get_dedicated_devices()) - def test_010_assign_offline(self): # TODO required + def test_010_assign_offline(self): dev_col = self.vm.devices['pci'] self.assertDeviceIs( self.dev, attached=False, assigned=False, required=False) @@ -103,11 +96,11 @@ def test_010_assign_offline(self): # TODO required self.loop.run_until_complete(dev_col.assign(self.assignment)) self.app.save() self.assertDeviceIs( - self.dev, attached=False, assigned=True, required=False) + self.dev, attached=False, assigned=True, required=True) self.loop.run_until_complete(self.vm.start()) self.assertDeviceIs( - self.dev, attached=True, assigned=False, required=False) + self.dev, attached=True, assigned=False, required=True) (stdout, _) = self.loop.run_until_complete( self.vm.run_for_stdio('lspci')) @@ -123,7 +116,7 @@ def test_011_attach_offline_temp_fail(self): self.loop.run_until_complete( dev_col.attach(self.assignment)) - def test_020_attach_online_persistent(self): # TODO: required + def test_020_attach_online_persistent(self): self.loop.run_until_complete( self.vm.start()) dev_col = self.vm.devices['pci'] @@ -133,7 +126,7 @@ def test_020_attach_online_persistent(self): # TODO: required self.loop.run_until_complete( dev_col.attach(self.assignment)) self.assertDeviceIs( - self.dev, attached=True, assigned=True, required=False) + self.dev, attached=True, assigned=True, required=True) # give VM kernel some time to discover new device time.sleep(1) @@ -155,7 +148,7 @@ 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): # TODO: required + def test_030_persist_attach_detach_offline(self): dev_col = self.vm.devices['pci'] self.assertDeviceIs( self.dev, attached=False, assigned=False, required=False) @@ -164,14 +157,14 @@ def test_030_persist_attach_detach_offline(self): # TODO: required dev_col.attach(self.assignment)) self.app.save() self.assertDeviceIs( - self.dev, attached=False, assigned=True, required=False) + self.dev, attached=False, assigned=True, required=True) self.loop.run_until_complete( dev_col.detach(self.assignment)) self.assertDeviceIs( self.dev, attached=False, assigned=False, required=False) - def test_031_attach_detach_online_temp(self): # TODO: requiured + def test_031_attach_detach_online_temp(self): dev_col = self.vm.devices['pci'] self.loop.run_until_complete( self.vm.start()) diff --git a/qubes/tests/vm/init.py b/qubes/tests/vm/init.py index 761ff9953..9138fa840 100644 --- a/qubes/tests/vm/init.py +++ b/qubes/tests/vm/init.py @@ -1,4 +1,5 @@ # pylint: disable=protected-access +import sys # # The Qubes OS Project, https://www.qubes-os.org/ @@ -48,6 +49,10 @@ class TestVM(qubes.vm.BaseVM): testlabel = qubes.property('testlabel') defaultprop = qubes.property('defaultprop', default='defaultvalue') + def is_running(self): + return False + + class TC_10_BaseVM(qubes.tests.QubesTestCase): def setUp(self): super().setUp() @@ -110,8 +115,10 @@ def test_000_load(self): }) self.assertCountEqual(vm.devices.keys(), ('pci',)) - self.assertCountEqual(list(vm.devices['pci'].get_assigned_devices()), - [qubes.ext.pci.PCIDevice(vm, '00_11.22')]) + + self.assertTrue( + list(vm.devices['pci'].get_assigned_devices())[0].matches( + qubes.ext.pci.PCIDevice(vm, '00_11.22', ))) assignments = list(vm.devices['pci'].get_assigned_devices()) self.assertEqual(len(assignments), 1) diff --git a/qubes/tests/vm/qubesvm.py b/qubes/tests/vm/qubesvm.py index 6821942f7..744b7e5d5 100644 --- a/qubes/tests/vm/qubesvm.py +++ b/qubes/tests/vm/qubesvm.py @@ -1308,13 +1308,16 @@ def test_600_libvirt_xml_hvm_pcidev(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 - # is unused - '00_00.0', - devclass='pci', - attach_automatically=True, - required=True, + assignment = qubes.device_protocol.DeviceAssignment( + qubes.device_protocol.VirtualDevice( + qubes.device_protocol.Port( + backend_domain=vm, # this is violation of API, + # but for PCI the argument is unused + port_id='00_00.0', + devclass="pci", + ) + ), + mode='required', ) vm.devices['pci']._set.add( assignment) @@ -1395,11 +1398,15 @@ def test_600_libvirt_xml_hvm_pcidev_s0ix(self): # even with meminfo-writer enabled, should have memory==maxmem vm.features['service.meminfo-writer'] = True assignment = qubes.device_protocol.DeviceAssignment( - vm, # this is a violation of API, but for PCI the argument - # is unused - '00_00.0', - devclass='pci', - attach_automatically=True, required=True) + qubes.device_protocol.VirtualDevice( + qubes.device_protocol.Port( + backend_domain=vm, # this is violation of API, + # but for PCI the argument is unused + port_id='00_00.0', + devclass="pci", + ), + ), + mode='required') vm.devices['pci']._set.add( assignment) libvirt_xml = vm.create_config_file() @@ -1481,9 +1488,15 @@ def test_600_libvirt_xml_hvm_cdrom_boot(self): dom0.events_enabled = True self.app.vmm.offline_mode = False dev = qubes.device_protocol.DeviceAssignment( - dom0, 'sda', - {'devtype': 'cdrom', 'read-only': 'yes'}, - attach_automatically=True, required=True) + qubes.device_protocol.VirtualDevice( + qubes.device_protocol.Port( + backend_domain=dom0, + port_id='sda', + devclass="block", + ) + ), + options={'devtype': 'cdrom', 'read-only': 'yes'}, + mode='required') self.loop.run_until_complete(vm.devices['block'].assign(dev)) libvirt_xml = vm.create_config_file() self.assertXMLEqual(lxml.etree.XML(libvirt_xml), @@ -1586,9 +1599,15 @@ def test_600_libvirt_xml_hvm_cdrom_dom0_kernel_boot(self): dom0.events_enabled = True self.app.vmm.offline_mode = False dev = qubes.device_protocol.DeviceAssignment( - dom0, 'sda', - {'devtype': 'cdrom', 'read-only': 'yes'}, - attach_automatically=True, required=True) + qubes.device_protocol.VirtualDevice( + qubes.device_protocol.Port( + backend_domain=dom0, + port_id='sda', + devclass="block", + ) + ), + options={'devtype': 'cdrom', 'read-only': 'yes'}, + mode='required') self.loop.run_until_complete(vm.devices['block'].assign(dev)) libvirt_xml = vm.create_config_file() self.assertXMLEqual(lxml.etree.XML(libvirt_xml), @@ -1872,23 +1891,37 @@ def test_615_libvirt_xml_block_devices(self): 'options': {'frontend-dev': 'xvdl'}, 'device.device_node': '/dev/sdb', 'device.backend_domain.name': 'dom0', + 'devices': [unittest.mock.Mock(**{ + 'device_node': '/dev/sdb', + 'backend_domain.name': 'dom0',})] }), unittest.mock.Mock(**{ 'options': {'devtype': 'cdrom'}, 'device.device_node': '/dev/sda', 'device.backend_domain.name': 'dom0', + 'devices': [unittest.mock.Mock(**{ + 'device_node': '/dev/sda', + 'backend_domain.name': 'dom0', })] }), unittest.mock.Mock(**{ 'options': {'read-only': True}, 'device.device_node': '/dev/loop0', 'device.backend_domain.name': 'backend0', 'device.backend_domain.features.check_with_template.return_value': '4.2', + 'devices': [unittest.mock.Mock(**{ + 'device_node': '/dev/loop0', + 'backend_domain.name': 'backend0', + 'backend_domain.features.check_with_template.return_value': '4.2'})] }), unittest.mock.Mock(**{ 'options': {}, 'device.device_node': '/dev/loop0', 'device.backend_domain.name': 'backend1', 'device.backend_domain.features.check_with_template.return_value': '4.2', + 'devices': [unittest.mock.Mock(**{ + 'device_node': '/dev/loop0', + 'backend_domain.name': 'backend1', + 'backend_domain.features.check_with_template.return_value': '4.2'})] }), ] vm.devices['block'].get_assigned_devices = \ diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index accee85a5..87e3fa389 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -33,6 +33,7 @@ import qubes import qubes.devices +import qubes.device_protocol import qubes.events import qubes.features import qubes.log @@ -284,14 +285,45 @@ def load_extras(self): options[option.get('name')] = str(option.text) try: + # backward compatibility: persistent~>required=True + legacy_required = node.get('required', 'absent') + if legacy_required == 'absent': + mode_str = node.get('mode', 'required') + try: + mode = (qubes.device_protocol. + AssignmentMode(mode_str)) + except ValueError: + self.log.error( + "Unrecognized assignment mode, ignoring.") + continue + else: + required = qubes.property.bool( + None, None, legacy_required) + if required: + mode = (qubes.device_protocol. + AssignmentMode.REQUIRED) + else: + mode = (qubes.device_protocol. + AssignmentMode.AUTO) + if 'identity' in options: + identity = options.get('identity') + del options['identity'] + else: + identity = node.get('identity', '*') + backend_name = node.get('backend-domain', None) + backend = self.app.domains[backend_name] \ + if backend_name else None device_assignment = qubes.device_protocol.DeviceAssignment( - self.app.domains[node.get('backend-domain')], - node.get('id'), - options, - attach_automatically=True, - # backward compatibility: persistent~>required=True - required=qubes.property.bool( - None, None, node.get('required', 'yes')), + qubes.device_protocol.VirtualDevice( + qubes.device_protocol.Port( + backend_domain=backend, + port_id=node.get('id', '*'), + devclass=devclass, + ), + device_id=identity, + ), + options=options, + mode=mode, ) self.devices[devclass].load_assignment(device_assignment) except KeyError: @@ -345,12 +377,14 @@ def __xml__(self): for devclass in self.devices: devices = lxml.etree.Element('devices') devices.set('class', devclass) - for device in self.devices[devclass].get_assigned_devices(): + for assignment 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(): + node.set('backend-domain', str(assignment.backend_name)) + node.set('id', assignment.port_id) + node.set('mode', assignment.mode.value) + identity = assignment.device_id or '*' + node.set('identity', identity) + for key, val in assignment.options.items(): option_node = lxml.etree.Element('option') option_node.set('name', key) option_node.text = val @@ -425,8 +459,8 @@ def _qdb_watch_reader(self, loop): if watched_path == path or ( watched_path.endswith('/') and path.startswith(watched_path)): - self.fire_event('domain-qdb-change:' + watched_path, - path=path) + self.fire_event( + 'domain-qdb-change:' + watched_path, path=path) except qubesdb.DisconnectedError: loop.remove_reader(self._qdb_connection_watch.watch_fd()) self._qdb_connection_watch.close() diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index fa7fd3ec6..c9cba58c6 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -1184,10 +1184,14 @@ async def start(self, start_guid=True, notify_function=None, try: for devclass in self.devices: for ass in self.devices[devclass].get_assigned_devices(): - if isinstance( - ass.device, - qubes.device_protocol.UnknownDevice) \ - and ass.required: + if not ass.required: + continue + for device in ass.devices: + if not isinstance( + device, qubes.device_protocol.UnknownDevice + ): + break + else: raise qubes.exc.QubesException( f'{devclass.capitalize()} device {ass} ' f'not available' diff --git a/relaxng/qubes.rng b/relaxng/qubes.rng index b286db21b..110c77172 100644 --- a/relaxng/qubes.rng +++ b/relaxng/qubes.rng @@ -205,7 +205,7 @@ the parser will complain about missing combine= attribute on the second . - One device. It's identified by by a pair of + One device. It's identified by a pair of backend domain and some identifier (device class dependant). @@ -222,6 +222,27 @@ the parser will complain about missing combine= attribute on the second . [0-9a-f]{2}_[0-9a-f]{2}.[0-9a-f]{2} + + + + Device presented identity. + + + [a-z0-9:*_-]+ + + + + + + + Available values: 'required', 'auto-attach', 'ask-to-attach'. + If not present: 'required' is assumed. + + + [a-z_-]+ + + + diff --git a/rpm_spec/core-dom0.spec.in b/rpm_spec/core-dom0.spec.in index b7f907dc8..01c027177 100644 --- a/rpm_spec/core-dom0.spec.in +++ b/rpm_spec/core-dom0.spec.in @@ -260,7 +260,7 @@ admin.vm.device.block.Attach admin.vm.device.block.Attached admin.vm.device.block.Available admin.vm.device.block.Detach -admin.vm.device.block.Set.required +admin.vm.device.block.Set.assignment admin.vm.device.block.Unassign admin.vm.device.pci.Assign admin.vm.device.pci.Assigned @@ -268,7 +268,7 @@ admin.vm.device.pci.Attach admin.vm.device.pci.Attached admin.vm.device.pci.Available admin.vm.device.pci.Detach -admin.vm.device.pci.Set.required +admin.vm.device.pci.Set.assignment admin.vm.device.pci.Unassign admin.vm.feature.CheckWithAdminVM admin.vm.feature.CheckWithNetvm diff --git a/templates/libvirt/xen.xml b/templates/libvirt/xen.xml index a9ce7c1db..80acf3397 100644 --- a/templates/libvirt/xen.xml +++ b/templates/libvirt/xen.xml @@ -156,10 +156,11 @@ {# start external devices from xvdi #} {% set counter = {'i': 4} %} - {% for assignment in vm.devices.block.get_assigned_devices(False) %} - {% set device = assignment.device %} - {% set options = assignment.options %} - {% include 'libvirt/devices/block.xml' %} + {% for assignment in vm.devices.block.get_assigned_devices(True) %} + {% for device in assignment.devices %} + {% set options = assignment.options %} + {% include 'libvirt/devices/block.xml' %} + {% endfor %} {% endfor %} {% if vm.netvm %} @@ -167,11 +168,12 @@ {% endif %} {% for assignment in vm.devices.pci.get_assigned_devices(True) %} - {% set device = assignment.device %} - {% set options = assignment.options %} - {% set power_mgmt = - vm.app.domains[0].features.get('suspend-s0ix', False) %} - {% include 'libvirt/devices/pci.xml' %} + {% for device in assignment.devices %} + {% set options = assignment.options %} + {% set power_mgmt = + vm.app.domains[0].features.get('suspend-s0ix', False) %} + {% include 'libvirt/devices/pci.xml' %} + {% endfor %} {% endfor %} {% if vm.virt_mode == 'hvm' %}