From 52c71c9a304eb6c6869657ae7208ed6d22e7e307 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Thu, 4 Jul 2024 13:54:22 +0200 Subject: [PATCH 01/35] q-dev: port --- doc/qubes-devices.rst | 2 +- qubes/api/admin.py | 8 +- qubes/device_protocol.py | 150 +++++++++++++++---------------- qubes/devices.py | 25 +++--- qubes/ext/block.py | 5 +- qubes/ext/pci.py | 3 +- qubes/tests/api_admin.py | 55 ++++++------ qubes/tests/devices.py | 53 ++++++----- qubes/tests/devices_block.py | 16 ++-- qubes/tests/integ/devices_pci.py | 12 ++- qubes/vm/__init__.py | 3 +- 11 files changed, 161 insertions(+), 171 deletions(-) diff --git a/doc/qubes-devices.rst b/doc/qubes-devices.rst index e64811a3e..be6d705fc 100644 --- a/doc/qubes-devices.rst +++ b/doc/qubes-devices.rst @@ -100,7 +100,7 @@ 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 diff --git a/qubes/api/admin.py b/qubes/api/admin.py index caabed1bf..98101f4dd 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -45,7 +45,7 @@ import qubes.vm import qubes.vm.adminvm import qubes.vm.qubesvm -from qubes.device_protocol import Device +from qubes.device_protocol import Port class QubesMgmtEventsDispatcher: @@ -1302,7 +1302,7 @@ async def vm_device_assign(self, endpoint, untrusted_payload): dev = self.app.domains[backend_domain].devices[devclass][ident] assignment = qubes.device_protocol.DeviceAssignment.deserialize( - untrusted_payload, expected_device=dev + untrusted_payload, expected_port=dev ) self.fire_event_for_permission( @@ -1357,7 +1357,7 @@ async def vm_device_attach(self, endpoint, untrusted_payload): dev = self.app.domains[backend_domain].devices[devclass][ident] assignment = qubes.device_protocol.DeviceAssignment.deserialize( - untrusted_payload, expected_device=dev + untrusted_payload, expected_port=dev ) self.fire_event_for_permission( @@ -1418,7 +1418,7 @@ async def vm_device_set_required(self, endpoint, untrusted_payload): # 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 = Port(backend_domain, ident, devclass) self.fire_event_for_permission(device=dev, assignment=assignment) diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index 3d8b9aaa6..76a1ff2ef 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 @@ -51,101 +51,82 @@ def qbool(value): return qubes.property.bool(None, None, value) -class Device: +class Port: """ - Basic class of a *bus* device with *ident* exposed by a *backend domain*. + Class of a *bus* device port 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'). + ident (str): A unique identifier for the port within the backend domain. + devclass (str): The class of the port (e.g., 'usb', 'pci'). """ ALLOWED_CHARS_KEY = set( string.digits + string.ascii_letters + r"!#$%&()*+,-./:;<>?@[\]^_{|}~") ALLOWED_CHARS_PARAM = ALLOWED_CHARS_KEY.union(set(string.punctuation + ' ')) - def __init__(self, backend_domain, ident, devclass=None): + def __init__(self, backend_domain, ident, devclass): self.__backend_domain = backend_domain self.__ident = ident - self.__bus = devclass + self.__devclass = devclass def __hash__(self): - return hash((str(self.backend_domain), self.ident)) + return hash((self.backend_domain.name, self.ident, self.devclass)) def __eq__(self, other): - if isinstance(other, Device): + if isinstance(other, Port): return ( self.backend_domain == other.backend_domain and - self.ident == other.ident + self.ident == other.ident and + self.devclass == other.devclass ) - raise TypeError(f"Comparing instances of 'Device' and '{type(other)}' " + raise TypeError(f"Comparing instances of 'Port' 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)}' " + if isinstance(other, Port): + return (self.backend_domain.name, self.devclass, self.ident) < \ + (other.backend_domain.name, other.devclass, other.ident) + raise TypeError(f"Comparing instances of 'Port' and '{type(other)}' " "is not supported") def __repr__(self): - return "[%s]:%s" % (self.backend_domain, self.ident) + return f"[{self.backend_domain.name}]:{self.devclass}:{self.ident}" def __str__(self): - return '{!s}:{!s}'.format(self.backend_domain, self.ident) + return f"{self.backend_domain.name}:{self.ident}" @property def ident(self) -> str: """ - Immutable device identifier. + Immutable port identifier. - Unique for given domain and device type. + Unique for given domain and devclass. """ return self.__ident @property def backend_domain(self) -> QubesVM: - """ Which domain provides this device. (immutable)""" + """ Which domain exposed this port. (immutable)""" return self.__backend_domain @property def devclass(self) -> str: - """ Immutable* Device class such like: 'usb', 'pci' etc. + """ Immutable port class such like: 'usb', 'pci' etc. - For unknown devices "peripheral" is returned. - - *see `@devclass.setter` + For unknown classes "peripheral" is returned. """ - if self.__bus: - return self.__bus + if self.__devclass: + return self.__devclass 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, @@ -213,9 +194,9 @@ def pack_property(cls, key: str, value: str): @staticmethod def check_device_properties( - expected_device: 'Device', properties: Dict[str, Any]): + expected_port: 'Port', properties: Dict[str, Any]): """ - Validates properties against an expected device configuration. + Validates properties against an expected port configuration. Modifies `properties`. @@ -223,7 +204,7 @@ def check_device_properties( UnexpectedDeviceProperty: If any property does not match the expected values. """ - expected = expected_device + expected = expected_port exp_vm_name = expected.backend_domain.name if properties.get('backend_domain', exp_vm_name) != exp_vm_name: raise UnexpectedDeviceProperty( @@ -237,13 +218,11 @@ def check_device_properties( f"when expected id: {expected.ident}.") properties['ident'] = expected.ident - if expected.devclass_is_set: - if (properties.get('devclass', expected.devclass) - != expected.devclass): - raise UnexpectedDeviceProperty( - f"Got {properties['devclass']} device " - f"when expected {expected.devclass}.") - properties['devclass'] = expected.devclass + if properties.get('devclass', expected.devclass) != expected.devclass: + raise UnexpectedDeviceProperty( + f"Got {properties['devclass']} device " + f"when expected {expected.devclass}.") + properties['devclass'] = expected.devclass class DeviceCategory(Enum): @@ -426,26 +405,24 @@ def _load_classes(bus: str): return result -class DeviceInfo(Device): +class DeviceInfo(Port): """ 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, **kwargs ): - super().__init__(backend_domain, ident, devclass) + super().__init__(port.backend_domain, port.ident, port.devclass) self._vendor = vendor self._product = product @@ -459,6 +436,13 @@ def __init__( self.data = kwargs + @property + def port(self) -> Port: + """ + Device port visible in Qubes. + """ + return Port(self.backend_domain, self.ident, self.devclass) + @property def vendor(self) -> str: """ @@ -567,7 +551,7 @@ def interfaces(self) -> List[DeviceInterface]: return self._interfaces @property - def parent_device(self) -> Optional[Device]: + def parent_device(self) -> Optional[Port]: """ The parent device, if any. @@ -660,7 +644,7 @@ def deserialize( def _deserialize( cls, untrusted_serialization: bytes, - expected_device: Device + expected_port: Port ) -> 'DeviceInfo': """ Actually deserializes the object. @@ -668,20 +652,19 @@ def _deserialize( properties, options = cls.unpack_properties(untrusted_serialization) properties.update(options) - cls.check_device_properties(expected_device, properties) + cls.check_device_properties(expected_port, properties) if 'attachment' not in properties or not properties['attachment']: properties['attachment'] = None else: - app = expected_device.backend_domain.app + app = expected_port.backend_domain.app properties['attachment'] = app.domains.get_blind( properties['attachment']) - if (expected_device.devclass_is_set - and properties['devclass'] != expected_device.devclass): + if properties['devclass'] != expected_port.devclass: raise UnexpectedDeviceProperty( f"Got {properties['devclass']} device " - f"when expected {expected_device.devclass}.") + f"when expected {expected_port.devclass}.") if 'interfaces' in properties: interfaces = properties['interfaces'] @@ -691,15 +674,23 @@ def _deserialize( properties['interfaces'] = interfaces if 'parent_ident' in properties: - properties['parent'] = Device( - backend_domain=expected_device.backend_domain, + properties['parent'] = Port( + backend_domain=expected_port.backend_domain, ident=properties['parent_ident'], devclass=properties['parent_devclass'], ) del properties['parent_ident'] del properties['parent_devclass'] - return cls(**properties) + port = Port( + properties['backend_domain'], + properties['ident'], + properties['devclass']) + del properties['backend_domain'] + del properties['ident'] + del properties['devclass'] + + return cls(port, **properties) @property def self_identity(self) -> str: @@ -767,10 +758,11 @@ class UnknownDevice(DeviceInfo): """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) + port = Port(backend_domain, ident, devclass) + super().__init__(port, **kwargs) -class DeviceAssignment(Device): +class DeviceAssignment(Port): """ Maps a device to a frontend_domain. There are 3 flags `attached`, `automatically_attached` and `required`. @@ -818,7 +810,7 @@ def clone(self, **kwargs): return self.__class__(**attr) @classmethod - def from_device(cls, device: Device, **kwargs) -> 'DeviceAssignment': + def from_device(cls, device: Port, **kwargs) -> 'DeviceAssignment': """ Get assignment of the device. """ @@ -920,13 +912,13 @@ def serialize(self) -> bytes: def deserialize( cls, serialization: bytes, - expected_device: Device, + expected_port: Port, ) -> 'DeviceAssignment': """ Recovers a serialized object, see: :py:meth:`serialize`. """ try: - result = cls._deserialize(serialization, expected_device) + result = cls._deserialize(serialization, expected_port) except Exception as exc: raise ProtocolError() from exc return result @@ -935,7 +927,7 @@ def deserialize( def _deserialize( cls, untrusted_serialization: bytes, - expected_device: Device, + expected_port: Port, ) -> 'DeviceAssignment': """ Actually deserializes the object. @@ -943,7 +935,7 @@ def _deserialize( properties, options = cls.unpack_properties(untrusted_serialization) properties['options'] = options - cls.check_device_properties(expected_device, properties) + cls.check_device_properties(expected_port, properties) properties['attach_automatically'] = qbool( properties.get('attach_automatically', 'no')) diff --git a/qubes/devices.py b/qubes/devices.py index c073ef759..058dc9ae5 100644 --- a/qubes/devices.py +++ b/qubes/devices.py @@ -63,7 +63,7 @@ import qubes.exc import qubes.utils -from qubes.device_protocol import (Device, DeviceInfo, UnknownDevice, +from qubes.device_protocol import (Port, DeviceInfo, UnknownDevice, DeviceAssignment) @@ -193,9 +193,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.') @@ -223,9 +221,7 @@ 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'when {self._bus} device expected.') @@ -250,10 +246,9 @@ 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_required(self, device: Port, required: bool): """ Update `required` flag of an already attached device. @@ -284,7 +279,7 @@ async def update_required(self, device: Device, required: bool): await self._vm.fire_event_async( 'device-assignment-changed:' + self._bus, device=device) - async def detach(self, device: Device): + async def detach(self, device: Port): """ Detach device from domain. """ @@ -316,15 +311,17 @@ async def unassign(self, device_assignment: DeviceAssignment): """ Unassign device from domain. """ + all_ass = [] for assignment in self.get_assigned_devices(): + all_ass.append(assignment.devclass) if device_assignment == assignment: # load all options device_assignment = assignment 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 at port {device_assignment}' + f'not assigned to {self._vm!s} | {all_ass} vs {device_assignment.devclass}') self._set.discard(assignment) @@ -353,9 +350,9 @@ def get_attached_devices(self) -> Iterable[DeviceAssignment]: yield DeviceAssignment( backend_domain=dev.backend_domain, ident=dev.ident, - options=options, - frontend_domain=self._vm, devclass=dev.devclass, + frontend_domain=self._vm, + options=options, attach_automatically=False, required=False, ) diff --git a/qubes/ext/block.py b/qubes/ext/block.py index 268e6ddcd..34db054a4 100644 --- a/qubes/ext/block.py +++ b/qubes/ext/block.py @@ -48,8 +48,9 @@ class BlockDevice(qubes.device_protocol.DeviceInfo): def __init__(self, backend_domain, ident): - super().__init__( + port = qubes.device_protocol.Port( backend_domain=backend_domain, ident=ident, devclass="block") + super().__init__(port) # lazy loading self._mode = None @@ -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. diff --git a/qubes/ext/pci.py b/qubes/ext/pci.py index 1535a7f9d..3ca8c0ba6 100644 --- a/qubes/ext/pci.py +++ b/qubes/ext/pci.py @@ -168,8 +168,9 @@ def __init__(self, backend_domain, ident, libvirt_name=None): raise UnsupportedDevice(libvirt_name) ident = '{bus}_{device}.{function}'.format(**dev_match.groupdict()) - super().__init__( + port = qubes.device_protocol.Port( backend_domain=backend_domain, ident=ident, devclass="pci") + super().__init__(port) dev_match = self.regex.match(ident) if not dev_match: diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index ed0a04156..520811381 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -1702,11 +1702,11 @@ 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') + 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') + Port(self.vm, '4321', 'testclass'), product='Some other device') yield dev def assertSerializedEqual(self, actual, expected): @@ -1757,7 +1757,8 @@ 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', + assignment = qubes.device_protocol.DeviceAssignment( + self.vm, '1234', 'test', attach_automatically=True, required=True) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -1769,11 +1770,13 @@ def test_470_vm_device_list_assigned(self): self.assertFalse(self.app.save.called) def test_471_vm_device_list_assigned_options(self): - assignment = qubes.device_protocol.DeviceAssignment(self.vm, '1234', + assignment = qubes.device_protocol.DeviceAssignment( + self.vm, '1234', 'test', attach_automatically=True, required=True, options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) - assignment = qubes.device_protocol.DeviceAssignment(self.vm, '4321', + assignment = qubes.device_protocol.DeviceAssignment( + self.vm, '4321', 'test', attach_automatically=True, required=True) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -1790,7 +1793,7 @@ def test_471_vm_device_list_assigned_options(self): 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): @@ -1806,11 +1809,11 @@ def test_472_vm_device_list_attached(self): def test_473_vm_device_list_assigned_specific(self): assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', attach_automatically=True, required=True) + self.vm, '1234', 'test', attach_automatically=True, required=True) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '4321', attach_automatically=True, required=True) + self.vm, '4321', 'test', attach_automatically=True, required=True) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) value = self.call_mgmt_func(b'admin.vm.device.testclass.Assigned', @@ -1823,9 +1826,9 @@ def test_473_vm_device_list_assigned_specific(self): def device_list_multiple_attached_testclass(self, vm, event, **kwargs): if vm is not self.vm: return - dev = qubes.device_protocol.DeviceInfo(self.vm, '1234', 'testclass') + 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, '1234', 'testclass')) yield (dev, {'attach_opt': 'value'}) def test_474_vm_device_list_attached_specific(self): @@ -1993,7 +1996,7 @@ def test_488_vm_device_assign_options(self): def test_490_vm_device_unassign_from_running(self): assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', attach_automatically=True, required=False, + self.vm, '1234', 'test', attach_automatically=True, required=False, options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -2013,7 +2016,7 @@ def test_490_vm_device_unassign_from_running(self): def test_491_vm_device_unassign_required_from_running(self): assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', attach_automatically=True, required=True, + self.vm, '1234', 'test', attach_automatically=True, required=True, options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -2034,7 +2037,7 @@ def test_491_vm_device_unassign_required_from_running(self): def test_492_vm_device_unassign_from_halted(self): assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', attach_automatically=True, required=False, + self.vm, '1234', 'test', attach_automatically=True, required=False, options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -2052,7 +2055,7 @@ def test_492_vm_device_unassign_from_halted(self): def test_493_vm_device_unassign_required_from_halted(self): assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', attach_automatically=True, required=True, + self.vm, '1234', 'test', attach_automatically=True, required=True, options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -2073,7 +2076,7 @@ def test_494_vm_device_unassign_attached(self): 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, + self.vm, '1234', 'test', attach_automatically=True, required=False, options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -2160,7 +2163,7 @@ def test_501_vm_remove_running(self, mock_rmtree, mock_remove): 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) + self.vm, '1234', 'test', attach_automatically=True, required=True) self.loop.run_until_complete( self.vm2.devices['testclass'].assign(assignment)) @@ -2859,7 +2862,7 @@ def test_642_vm_create_disposable_not_allowed(self, storage_mock): def test_650_vm_device_set_required_true(self): assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', attach_automatically=True, required=False, + self.vm, '1234', 'test', attach_automatically=True, required=False, options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -2875,7 +2878,7 @@ def test_650_vm_device_set_required_true(self): b'test-vm1', b'test-vm1+1234', b'True') self.assertIsNone(value) - dev = qubes.device_protocol.DeviceInfo(self.vm, '1234') + dev = qubes.device_protocol.DeviceInfo(Port(self.vm, '1234', 'testclass')) required = self.vm.devices['testclass'].get_assigned_devices( required_only=True) self.assertIn(dev, required) @@ -2889,7 +2892,7 @@ def test_650_vm_device_set_required_true(self): def test_651_vm_device_set_required_false(self): assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', attach_automatically=True, required=True, + self.vm, '1234', 'test', attach_automatically=True, required=True, options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -2905,7 +2908,7 @@ def test_651_vm_device_set_required_false(self): b'test-vm1', b'test-vm1+1234', b'False') self.assertIsNone(value) - dev = qubes.device_protocol.DeviceInfo(self.vm, '1234') + dev = qubes.device_protocol.DeviceInfo(Port(self.vm, '1234', 'testclass')) required = self.vm.devices['testclass'].get_assigned_devices( required_only=True) self.assertNotIn(dev, required) @@ -2919,7 +2922,7 @@ def test_651_vm_device_set_required_false(self): def test_652_vm_device_set_required_true_unchanged(self): assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', attach_automatically=True, required=True, + self.vm, '1234', 'test', attach_automatically=True, required=True, options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -2929,7 +2932,7 @@ def test_652_vm_device_set_required_true_unchanged(self): b'admin.vm.device.testclass.Set.required', b'test-vm1', b'test-vm1+1234', b'True') self.assertIsNone(value) - dev = qubes.device_protocol.DeviceInfo(self.vm, '1234') + dev = qubes.device_protocol.DeviceInfo(Port(self.vm, '1234', 'testclass')) required = self.vm.devices['testclass'].get_assigned_devices( required_only=True) self.assertIn(dev, required) @@ -2937,7 +2940,7 @@ def test_652_vm_device_set_required_true_unchanged(self): def test_653_vm_device_set_required_false_unchanged(self): assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', attach_automatically=True, required=False, + self.vm, '1234', 'test', attach_automatically=True, required=False, options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -2947,7 +2950,7 @@ def test_653_vm_device_set_required_false_unchanged(self): b'admin.vm.device.testclass.Set.required', b'test-vm1', b'test-vm1+1234', b'False') self.assertIsNone(value) - dev = qubes.device_protocol.DeviceInfo(self.vm, '1234') + dev = qubes.device_protocol.DeviceInfo(Port(self.vm, '1234', 'testclass')) required = self.vm.devices['testclass'].get_assigned_devices( required_only=True) self.assertNotIn(dev, required) @@ -2962,7 +2965,7 @@ def test_654_vm_device_set_persistent_not_assigned(self): 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') + 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) @@ -2976,7 +2979,7 @@ def test_655_vm_device_set_persistent_invalid_value(self): 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') + 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/devices.py b/qubes/tests/devices.py index 22db41c35..67a3c945a 100644 --- a/qubes/tests/devices.py +++ b/qubes/tests/devices.py @@ -21,7 +21,7 @@ # import qubes.devices -from qubes.device_protocol import (Device, DeviceInfo, DeviceAssignment, +from qubes.device_protocol import (Port, DeviceInfo, DeviceAssignment, DeviceInterface, UnknownDevice) import qubes.tests @@ -90,9 +90,8 @@ 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, + self.assignment = DeviceAssignment.from_device( + self.device, attach_automatically=True, required=True, ) @@ -340,10 +339,8 @@ def test_000_init(self): 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) + assignment = DeviceAssignment.from_device( + device, attach_automatically=True, required=True) self.loop.run_until_complete( self.manager['testclass'].assign(assignment)) self.assertEqual( @@ -358,9 +355,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, + ident="1-1.1.1", + devclass="bus"), vendor="ITL", product="Qubes", manufacturer="", @@ -386,9 +383,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, + ident="1-1.1.1", + devclass="bus"), vendor="ITL", product="Qubes", manufacturer="", @@ -398,7 +395,7 @@ 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') ) actual = device.serialize() expected = ( @@ -416,9 +413,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, + ident="1-1.1.1", + devclass="bus?"), vendor="malicious", product="suspicious", manufacturer="", @@ -438,9 +435,9 @@ def test_020_deserialize(self): 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, + ident="1-1.1.1", + devclass="bus"), vendor="ITL", product="Qubes", manufacturer="unknown", @@ -481,9 +478,9 @@ def test_021_invalid_deserialize(self): 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, + ident="1-1.1.1", + devclass="bus?"), vendor="malicious", product="suspicious", manufacturer="", @@ -589,7 +586,7 @@ def test_020_deserialize(self): b"ident='1-1.1.1' frontend_domain='vm' devclass='bus' " b"backend_domain='vm' required='no' attach_automatically='yes' " b"_read-only='yes'") - expected_device = Device(self.vm, '1-1.1.1', 'bus') + expected_device = Port(self.vm, '1-1.1.1', 'bus') actual = DeviceAssignment.deserialize(serialized, expected_device) expected = DeviceAssignment( backend_domain=self.vm, @@ -614,7 +611,7 @@ def test_021_invalid_deserialize(self): b"ident='1-1.1.1' frontend_domain='vm' devclass='bus' " b"backend_domain='vm' required='no' attach_automatically='yes' " b"_read'only='yes'") - expected_device = Device(self.vm, '1-1.1.1', 'bus') + expected_device = Port(self.vm, '1-1.1.1', 'bus') with self.assertRaises(qubes.exc.ProtocolError): _ = DeviceAssignment.deserialize(serialized, expected_device) @@ -623,7 +620,7 @@ def test_022_invalid_deserialize_2(self): b"ident='1-1.1.1' frontend_domain='vm' devclass='bus' " b"backend_domain='vm' required='no' attach_automatically='yes' " b"read-only='yes'") - expected_device = Device(self.vm, '1-1.1.1', 'bus') + expected_device = Port(self.vm, '1-1.1.1', 'bus') with self.assertRaises(qubes.exc.ProtocolError): _ = DeviceAssignment.deserialize(serialized, expected_device) @@ -638,7 +635,7 @@ def test_030_serialize_and_deserialize(self): options={'read-only': 'yes'}, ) serialized = expected.serialize() - expected_device = Device(self.vm, '1-1.1.1', 'bus') + expected_device = Port(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) diff --git a/qubes/tests/devices_block.py b/qubes/tests/devices_block.py index 0d19eebb4..1f5744680 100644 --- a/qubes/tests/devices_block.py +++ b/qubes/tests/devices_block.py @@ -24,7 +24,7 @@ import qubes.tests import qubes.ext.block -from qubes.device_protocol import DeviceInterface, Device, DeviceInfo, \ +from qubes.device_protocol import DeviceInterface, Port, DeviceInfo, \ DeviceAssignment modules_disk = ''' @@ -187,7 +187,7 @@ 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')) vm.devices['usb'] = TestDeviceCollection(backend_vm=vm, devclass='usb') vm.devices['usb']._exposed.append(parent) vm.is_running = lambda: True @@ -228,7 +228,7 @@ def test_000_device_get(self): self.assertEqual(device_info.interfaces, [DeviceInterface("b******")]) self.assertEqual(device_info.parent_device, - Device(vm, '1-1.1', devclass='usb')) + Port(vm, '1-1.1', devclass='usb')) self.assertEqual(device_info.attachment, front) self.assertEqual(device_info.self_identity, '1-1.1:0000:0000::?******:1.0') @@ -664,7 +664,7 @@ def test_060_on_qdb_change_added(self): '/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') + exp_dev = Port(back_vm, 'sda', 'block') self.ext.on_qdb_change(back_vm, None, None) @@ -680,7 +680,7 @@ def test_061_on_qdb_change_auto_attached(self): '/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') + exp_dev = Port(back_vm, 'sda', 'block') front = TestVM({}, domain_xml=domain_xml_template.format(""), name='front-vm') dom0 = TestVM({}, name='dom0', @@ -725,7 +725,7 @@ def test_062_on_qdb_change_attached(self): '/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') + exp_dev = Port(back_vm, 'sda', 'block') self.ext.devices_cache = {'sys-usb': {'sda': None}} @@ -774,7 +774,7 @@ def test_063_on_qdb_change_changed(self): '/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') + exp_dev = Port(back_vm, 'sda', 'block') front = TestVM({}, name='front-vm') dom0 = TestVM({}, name='dom0', @@ -841,7 +841,7 @@ def test_064_on_qdb_change_removed_attached(self): }, 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 = Port(back_vm, 'sda', 'block') disk = ''' diff --git a/qubes/tests/integ/devices_pci.py b/qubes/tests/integ/devices_pci.py index 7b1ea5579..f3136e9b8 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,14 +38,11 @@ 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.assignment = DeviceAssignment.from_device( + self.dev, attach_automatically=True ) - self.required_assignment = qubes.device_protocol.DeviceAssignment( - backend_domain=self.dev.backend_domain, - ident=self.dev.ident, + self.required_assignment = DeviceAssignment.from_device( + self.dev, attach_automatically=True, required=True, ) diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index accee85a5..8c152d850 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -287,7 +287,8 @@ def load_extras(self): device_assignment = qubes.device_protocol.DeviceAssignment( self.app.domains[node.get('backend-domain')], node.get('id'), - options, + devclass=devclass, + options=options, attach_automatically=True, # backward compatibility: persistent~>required=True required=qubes.property.bool( From f14f844c24371d264a1f9a1b60b244ab1564abd3 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Tue, 27 Aug 2024 12:04:30 +0200 Subject: [PATCH 02/35] q-dev: attachment confirmation PoC --- qubes/ext/utils.py | 44 +++++++++++++++++++++++++++++++++++++++++--- qubes/vm/__init__.py | 4 ++-- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/qubes/ext/utils.py b/qubes/ext/utils.py index 1c7831922..796378913 100644 --- a/qubes/ext/utils.py +++ b/qubes/ext/utils.py @@ -18,14 +18,24 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA. +import itertools import asyncio +import subprocess +import sys + +import gbulb import qubes +from typing import Type + +from qubes.ext.attachment_confirm import confirm_device_attachment +from qrexec import server + 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() @@ -54,6 +64,7 @@ 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 @@ -62,8 +73,35 @@ def device_list_change( and assignment.ident in added and assignment.ident not in attached ): - asyncio.ensure_future(ext.attach_and_notify( - front_vm, assignment.device, assignment.options)) + frontends = to_attach.get(assignment.ident, {}) + frontends[front_vm] = assignment + to_attach[assignment.ident] = frontends + + for ident, frontends in to_attach.items(): + if len(frontends) > 1: + guivm = 'dom0' # TODO + + assignment = tuple(frontends.values())[0] + + proc = subprocess.Popen( + ["/home/user/devel/test.py", guivm, + assignment.backend_domain.name, assignment.ident, + *[f.name for f in frontends.keys()]], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (target_name, _) = proc.communicate() + target_name = target_name.decode() + for front in frontends: + if front.name == target_name: + target = front + break + else: + print("Something really goes bad :/", file=sys.stderr) + return + else: + target = tuple(frontends.keys())[0] + assignment = frontends[target] + asyncio.ensure_future(ext.attach_and_notify( + target, assignment.device, assignment.options)) def compare_device_cache(vm, devices_cache, current_devices): diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index 8c152d850..8c4fb11e6 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -426,8 +426,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() From a7ea7fd663bc6731e3ca2320844505ce1e80ef25 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Wed, 31 Jul 2024 11:17:17 +0200 Subject: [PATCH 03/35] q-dev: port --- qubes/api/admin.py | 4 +- qubes/device_protocol.py | 70 +++++++++++++++++------------- qubes/devices.py | 8 ++-- qubes/tests/api_admin.py | 40 +++++++++++------ qubes/tests/devices.py | 56 ++++++++++++++---------- qubes/tests/devices_block.py | 3 +- qubes/tests/integ/devices_block.py | 3 +- qubes/tests/integ/devices_pci.py | 4 +- qubes/tests/vm/qubesvm.py | 34 ++++++++++----- qubes/vm/__init__.py | 9 ++-- 10 files changed, 140 insertions(+), 91 deletions(-) diff --git a/qubes/api/admin.py b/qubes/api/admin.py index 98101f4dd..55fb5ae28 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -1336,7 +1336,7 @@ async def vm_device_unassign(self, endpoint): self.fire_event_for_permission(device=dev, devclass=devclass) assignment = qubes.device_protocol.DeviceAssignment( - dev.backend_domain, dev.ident, devclass) + qubes.device_protocol.Port(dev.backend_domain, dev.ident, devclass)) await self.dest.devices[devclass].unassign(assignment) self.app.save() @@ -1390,7 +1390,7 @@ async def vm_device_detach(self, endpoint): self.fire_event_for_permission(device=dev, devclass=devclass) assignment = qubes.device_protocol.DeviceAssignment( - dev.backend_domain, dev.ident, devclass) + qubes.device_protocol.Port(dev.backend_domain, dev.ident, devclass)) await self.dest.devices[devclass].detach(assignment) # Assign/Unassign action can modify only a persistent state of running VM. diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index 76a1ff2ef..c1d25ab28 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -241,8 +241,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****") @@ -347,7 +347,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( @@ -536,8 +536,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]: @@ -782,15 +784,29 @@ class DeviceAssignment(Port): and required to start domain. """ - def __init__(self, backend_domain, ident, options=None, - frontend_domain=None, devclass=None, - required=False, attach_automatically=False): - super().__init__(backend_domain, ident, devclass) + class AssignmentType(Enum): + MANUAL = 0 + ASK = 1 + AUTO = 2 + REQUIRED = 3 + + def __init__( + self, + port: Port, + frontend_domain=None, + options=None, + required=False, + attach_automatically=False + ): + super().__init__(port.backend_domain, port.ident, port.devclass) self.__options = options or {} if required: assert attach_automatically - self.__required = required - self.__attach_automatically = attach_automatically + self.type = DeviceAssignment.AssignmentType.REQUIRED + elif attach_automatically: + self.type = DeviceAssignment.AssignmentType.AUTO + else: + self.type = DeviceAssignment.AssignmentType.MANUAL self.frontend_domain = frontend_domain def clone(self, **kwargs): @@ -798,28 +814,14 @@ def clone(self, **kwargs): Clone object and substitute attributes with explicitly given. """ attr = { - "backend_domain": self.backend_domain, - "ident": self.ident, "options": self.options, "required": self.required, "attach_automatically": self.attach_automatically, "frontend_domain": self.frontend_domain, - "devclass": self.devclass, } attr.update(kwargs) - return self.__class__(**attr) - - @classmethod - def from_device(cls, device: Port, **kwargs) -> 'DeviceAssignment': - """ - Get assignment of the device. - """ - return cls( - backend_domain=device.backend_domain, - ident=device.ident, - devclass=device.devclass, - **kwargs - ) + return self.__class__( + Port(self.backend_domain, self.ident, self.devclass), **attr) @property def device(self) -> DeviceInfo: @@ -855,11 +857,11 @@ 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 + return self.type == DeviceAssignment.AssignmentType.REQUIRED @required.setter def required(self, required: bool): - self.__required = required + self.type = DeviceAssignment.AssignmentType.REQUIRED @property def attach_automatically(self) -> bool: @@ -867,11 +869,14 @@ 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 + return self.type in ( + DeviceAssignment.AssignmentType.AUTO, + DeviceAssignment.AssignmentType.REQUIRED + ) @attach_automatically.setter def attach_automatically(self, attach_automatically: bool): - self.__attach_automatically = attach_automatically + self.type = DeviceAssignment.AssignmentType.AUTO @property def options(self) -> Dict[str, Any]: @@ -936,9 +941,12 @@ def _deserialize( properties['options'] = options cls.check_device_properties(expected_port, properties) + del properties['backend_domain'] + del properties['ident'] + del properties['devclass'] properties['attach_automatically'] = qbool( properties.get('attach_automatically', 'no')) properties['required'] = qbool(properties.get('required', 'no')) - return cls(**properties) + return cls(expected_port, **properties) diff --git a/qubes/devices.py b/qubes/devices.py index 058dc9ae5..28bed46c2 100644 --- a/qubes/devices.py +++ b/qubes/devices.py @@ -348,9 +348,11 @@ def get_attached_devices(self) -> Iterable[DeviceAssignment]: break else: yield DeviceAssignment( - backend_domain=dev.backend_domain, - ident=dev.ident, - devclass=dev.devclass, + Port( + backend_domain=dev.backend_domain, + ident=dev.ident, + devclass=dev.devclass, + ), frontend_domain=self._vm, options=options, attach_automatically=False, diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index 520811381..806f4e8d9 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -1758,7 +1758,7 @@ def test_462_vm_device_available_invalid(self): def test_470_vm_device_list_assigned(self): assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', 'test', + qubes.device_protocol.Port(self.vm, '1234', 'test'), attach_automatically=True, required=True) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -1771,7 +1771,7 @@ def test_470_vm_device_list_assigned(self): def test_471_vm_device_list_assigned_options(self): assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', 'test', + qubes.device_protocol.Port(self.vm, '1234', 'test'), attach_automatically=True, required=True, options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -1809,11 +1809,13 @@ def test_472_vm_device_list_attached(self): def test_473_vm_device_list_assigned_specific(self): assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', 'test', attach_automatically=True, required=True) + qubes.device_protocol.Port(self.vm, '1234', 'test'), + attach_automatically=True, required=True) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '4321', 'test', attach_automatically=True, required=True) + qubes.device_protocol.Port(self.vm, '4321', 'test'), + attach_automatically=True, required=True) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) value = self.call_mgmt_func(b'admin.vm.device.testclass.Assigned', @@ -1996,7 +1998,8 @@ def test_488_vm_device_assign_options(self): def test_490_vm_device_unassign_from_running(self): assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', 'test', attach_automatically=True, required=False, + qubes.device_protocol.Port(self.vm, '1234', 'test'), + attach_automatically=True, required=False, options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -2016,7 +2019,8 @@ def test_490_vm_device_unassign_from_running(self): def test_491_vm_device_unassign_required_from_running(self): assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', 'test', attach_automatically=True, required=True, + qubes.device_protocol.Port(self.vm, '1234', 'test'), + attach_automatically=True, required=True, options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -2037,7 +2041,8 @@ def test_491_vm_device_unassign_required_from_running(self): def test_492_vm_device_unassign_from_halted(self): assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', 'test', attach_automatically=True, required=False, + qubes.device_protocol.Port(self.vm, '1234', 'test'), + attach_automatically=True, required=False, options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -2055,7 +2060,8 @@ def test_492_vm_device_unassign_from_halted(self): def test_493_vm_device_unassign_required_from_halted(self): assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', 'test', attach_automatically=True, required=True, + qubes.device_protocol.Port(self.vm, '1234', 'test'), + attach_automatically=True, required=True, options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -2076,7 +2082,8 @@ def test_494_vm_device_unassign_attached(self): self.vm.add_handler('device-list-attached:testclass', self.device_list_single_attached_testclass) assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', 'test', attach_automatically=True, required=False, + qubes.device_protocol.Port(self.vm, '1234', 'test'), + attach_automatically=True, required=False, options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -2163,7 +2170,8 @@ def test_501_vm_remove_running(self, mock_rmtree, mock_remove): def test_502_vm_remove_attached(self, mock_rmtree, mock_remove): self.setup_for_clone() assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', 'test', attach_automatically=True, required=True) + qubes.device_protocol.Port(self.vm, '1234', 'test'), + attach_automatically=True, required=True) self.loop.run_until_complete( self.vm2.devices['testclass'].assign(assignment)) @@ -2862,7 +2870,8 @@ def test_642_vm_create_disposable_not_allowed(self, storage_mock): def test_650_vm_device_set_required_true(self): assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', 'test', attach_automatically=True, required=False, + qubes.device_protocol.Port(self.vm, '1234', 'test'), + attach_automatically=True, required=False, options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -2892,7 +2901,8 @@ def test_650_vm_device_set_required_true(self): def test_651_vm_device_set_required_false(self): assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', 'test', attach_automatically=True, required=True, + qubes.device_protocol.Port(self.vm, '1234', 'test'), + attach_automatically=True, required=True, options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -2922,7 +2932,8 @@ def test_651_vm_device_set_required_false(self): def test_652_vm_device_set_required_true_unchanged(self): assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', 'test', attach_automatically=True, required=True, + qubes.device_protocol.Port(self.vm, '1234', 'test'), + attach_automatically=True, required=True, options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) @@ -2940,7 +2951,8 @@ def test_652_vm_device_set_required_true_unchanged(self): def test_653_vm_device_set_required_false_unchanged(self): assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '1234', 'test', attach_automatically=True, required=False, + qubes.device_protocol.Port(self.vm, '1234', 'test'), + attach_automatically=True, required=False, options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) diff --git a/qubes/tests/devices.py b/qubes/tests/devices.py index 67a3c945a..9fff18e6a 100644 --- a/qubes/tests/devices.py +++ b/qubes/tests/devices.py @@ -90,7 +90,7 @@ def setUp(self): self.app.domains['vm'] = self.emitter self.device = self.emitter.device self.collection = self.emitter.devices['testclass'] - self.assignment = DeviceAssignment.from_device( + self.assignment = DeviceAssignment( self.device, attach_automatically=True, required=True, @@ -339,7 +339,7 @@ def test_000_init(self): def test_001_missing(self): device = TestDevice(self.emitter.app.domains['vm'], 'testdev') - assignment = DeviceAssignment.from_device( + assignment = DeviceAssignment( device, attach_automatically=True, required=True) self.loop.run_until_complete( self.manager['testclass'].assign(assignment)) @@ -512,11 +512,11 @@ def setUp(self): self.vm = TestVM(self.app, 'vm') def test_010_serialize(self): - assignment = DeviceAssignment( + assignment = DeviceAssignment(Port( backend_domain=self.vm, ident="1-1.1.1", devclass="bus", - ) + )) actual = assignment.serialize() expected = ( b"ident='1-1.1.1' devclass='bus' " @@ -527,9 +527,11 @@ def test_010_serialize(self): def test_011_serialize_required(self): assignment = DeviceAssignment( - backend_domain=self.vm, - ident="1-1.1.1", - devclass="bus", + Port( + backend_domain=self.vm, + ident="1-1.1.1", + devclass="bus", + ), attach_automatically=True, required=True, ) @@ -543,9 +545,11 @@ def test_011_serialize_required(self): def test_012_serialize_fronted(self): assignment = DeviceAssignment( - backend_domain=self.vm, - ident="1-1.1.1", - devclass="bus", + Port( + backend_domain=self.vm, + ident="1-1.1.1", + devclass="bus", + ), frontend_domain=self.vm, ) actual = assignment.serialize() @@ -558,9 +562,11 @@ def test_012_serialize_fronted(self): def test_013_serialize_options(self): assignment = DeviceAssignment( - backend_domain=self.vm, - ident="1-1.1.1", - devclass="bus", + Port( + backend_domain=self.vm, + ident="1-1.1.1", + devclass="bus", + ), options={'read-only': 'yes'}, ) actual = assignment.serialize() @@ -573,9 +579,11 @@ def test_013_serialize_options(self): def test_014_invalid_serialize(self): assignment = DeviceAssignment( - backend_domain=self.vm, - ident="1-1.1.1", - devclass="bus", + Port( + backend_domain=self.vm, + ident="1-1.1.1", + devclass="bus", + ), options={"read'only": 'yes'}, ) with self.assertRaises(qubes.exc.ProtocolError): @@ -589,9 +597,11 @@ def test_020_deserialize(self): expected_device = 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", + Port( + backend_domain=self.vm, + ident="1-1.1.1", + devclass="bus", + ), frontend_domain=self.vm, attach_automatically=True, required=False, @@ -626,9 +636,11 @@ def test_022_invalid_deserialize_2(self): def test_030_serialize_and_deserialize(self): expected = DeviceAssignment( - backend_domain=self.vm, - ident="1-1.1.1", - devclass="bus", + Port( + backend_domain=self.vm, + ident="1-1.1.1", + devclass="bus", + ), frontend_domain=self.vm, attach_automatically=True, required=False, diff --git a/qubes/tests/devices_block.py b/qubes/tests/devices_block.py index 1f5744680..8bc261854 100644 --- a/qubes/tests/devices_block.py +++ b/qubes/tests/devices_block.py @@ -702,8 +702,7 @@ 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)) + front.devices['block']._assigned.append(DeviceAssignment(exp_dev)) back_vm.devices['block']._exposed.append( qubes.ext.block.BlockDevice(back_vm, 'sda')) diff --git a/qubes/tests/integ/devices_block.py b/qubes/tests/integ/devices_block.py index e3d17b73b..eb982430f 100644 --- a/qubes/tests/integ/devices_block.py +++ b/qubes/tests/integ/devices_block.py @@ -327,7 +327,8 @@ def setUp(self): 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.Port(self.backend, self.device_ident, 'test')) 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 f3136e9b8..af456e0b2 100644 --- a/qubes/tests/integ/devices_pci.py +++ b/qubes/tests/integ/devices_pci.py @@ -38,10 +38,10 @@ 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 = DeviceAssignment.from_device( + self.assignment = DeviceAssignment( self.dev, attach_automatically=True ) - self.required_assignment = DeviceAssignment.from_device( + self.required_assignment = DeviceAssignment( self.dev, attach_automatically=True, required=True, diff --git a/qubes/tests/vm/qubesvm.py b/qubes/tests/vm/qubesvm.py index 6821942f7..e75013be4 100644 --- a/qubes/tests/vm/qubesvm.py +++ b/qubes/tests/vm/qubesvm.py @@ -1308,11 +1308,13 @@ 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', + assignment = qubes.device_protocol.DeviceAssignment( + qubes.device_protocol.Port( + backend_domain=vm, # this is violation of API, + # but for PCI the argument is unused + ident='00_00.0', + devclass="pci", + ), attach_automatically=True, required=True, ) @@ -1395,10 +1397,12 @@ 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', + qubes.device_protocol.Port( + backend_domain=vm, # this is violation of API, + # but for PCI the argument is unused + ident='00_00.0', + devclass="pci", + ), attach_automatically=True, required=True) vm.devices['pci']._set.add( assignment) @@ -1481,7 +1485,11 @@ 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', + qubes.device_protocol.Port( + backend_domain=dom0, + ident='sda', + devclass="block", + ), {'devtype': 'cdrom', 'read-only': 'yes'}, attach_automatically=True, required=True) self.loop.run_until_complete(vm.devices['block'].assign(dev)) @@ -1586,7 +1594,11 @@ 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', + qubes.device_protocol.Port( + backend_domain=dom0, + ident='sda', + devclass="block", + ), {'devtype': 'cdrom', 'read-only': 'yes'}, attach_automatically=True, required=True) self.loop.run_until_complete(vm.devices['block'].assign(dev)) diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index 8c4fb11e6..6eb39365b 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -285,9 +285,12 @@ def load_extras(self): try: device_assignment = qubes.device_protocol.DeviceAssignment( - self.app.domains[node.get('backend-domain')], - node.get('id'), - devclass=devclass, + qubes.device_protocol.Port( + backend_domain=self.app.domains[ + node.get('backend-domain')], + ident=node.get('id'), + devclass=devclass, + ), options=options, attach_automatically=True, # backward compatibility: persistent~>required=True From b63d801fd1ee36c82e14e85af06dd10bd40fee4c Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Thu, 1 Aug 2024 22:43:24 +0200 Subject: [PATCH 04/35] q-dev: comparison --- qubes/device_protocol.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index c1d25ab28..5b21c8281 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -436,6 +436,32 @@ def __init__( self.data = kwargs + def __hash__(self): + return hash(self.port)# self.self_identity)) + + def __eq__(self, other): + if isinstance(other, DeviceInfo): + return ( + self.port == other.port + # and self.self_identity == other.self_identity + ) + else: + return super().__lt__(other) + + def __lt__(self, other): + if isinstance(other, DeviceInfo): + # return (self.port, self.self_identity) < \ + # (other.port, other.self_identity) + return self.port < other.port + else: + return super().__lt__(other) + + def __repr__(self): + return f"{self.port!r}"#:{self.self_identity}" + + def __str__(self): + return f"{self.port}"#:{self.self_identity}" + @property def port(self) -> Port: """ From 29f45c57754fa5f538843b6f786cc9022363f9d1 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Fri, 2 Aug 2024 08:24:12 +0200 Subject: [PATCH 05/35] q-dev: assignment --- qubes/device_protocol.py | 46 +++++++++++++------------------- qubes/devices.py | 7 +++-- qubes/tests/api_admin.py | 40 +++++++++++---------------- qubes/tests/devices.py | 18 ++++--------- qubes/tests/integ/devices_pci.py | 6 ++--- qubes/tests/vm/qubesvm.py | 9 +++---- qubes/vm/__init__.py | 37 ++++++++++++++++++------- 7 files changed, 76 insertions(+), 87 deletions(-) diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index 5b21c8281..7d20f0ed9 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -790,6 +790,13 @@ def __init__(self, backend_domain, ident, *, devclass, **kwargs): super().__init__(port, **kwargs) +class AssignmentMode(Enum): + MANUAL = "manual" + ASK = "ask-to-attach" + AUTO = "auto-attach" + REQUIRED = "required" + + class DeviceAssignment(Port): """ Maps a device to a frontend_domain. @@ -810,29 +817,20 @@ class DeviceAssignment(Port): and required to start domain. """ - class AssignmentType(Enum): - MANUAL = 0 - ASK = 1 - AUTO = 2 - REQUIRED = 3 - def __init__( self, port: Port, + device_id=None, frontend_domain=None, options=None, - required=False, - attach_automatically=False + mode: Union[str, AssignmentMode] = "manual", ): super().__init__(port.backend_domain, port.ident, port.devclass) self.__options = options or {} - if required: - assert attach_automatically - self.type = DeviceAssignment.AssignmentType.REQUIRED - elif attach_automatically: - self.type = DeviceAssignment.AssignmentType.AUTO + if isinstance(mode, AssignmentMode): + self.mode = mode else: - self.type = DeviceAssignment.AssignmentType.MANUAL + self.mode = AssignmentMode(mode) self.frontend_domain = frontend_domain def clone(self, **kwargs): @@ -883,11 +881,11 @@ 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.type == DeviceAssignment.AssignmentType.REQUIRED + return self.mode == AssignmentMode.REQUIRED @required.setter def required(self, required: bool): - self.type = DeviceAssignment.AssignmentType.REQUIRED + self.mode = AssignmentMode.REQUIRED @property def attach_automatically(self) -> bool: @@ -895,14 +893,14 @@ def attach_automatically(self) -> bool: Should this device automatically connect to the frontend domain when available and not connected to other qubes? """ - return self.type in ( - DeviceAssignment.AssignmentType.AUTO, - DeviceAssignment.AssignmentType.REQUIRED + return self.mode in ( + AssignmentMode.AUTO, + AssignmentMode.REQUIRED ) @attach_automatically.setter def attach_automatically(self, attach_automatically: bool): - self.type = DeviceAssignment.AssignmentType.AUTO + self.mode = AssignmentMode.AUTO @property def options(self) -> Dict[str, Any]: @@ -921,9 +919,7 @@ def serialize(self) -> bytes: 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'), + ('mode', self.mode.value), ('ident', self.ident), ('devclass', self.devclass))) @@ -971,8 +967,4 @@ def _deserialize( del properties['ident'] del properties['devclass'] - properties['attach_automatically'] = qbool( - properties.get('attach_automatically', 'no')) - properties['required'] = qbool(properties.get('required', 'no')) - return cls(expected_port, **properties) diff --git a/qubes/devices.py b/qubes/devices.py index 28bed46c2..75fa54ca2 100644 --- a/qubes/devices.py +++ b/qubes/devices.py @@ -229,8 +229,8 @@ async def assign(self, assignment: DeviceAssignment): device = assignment.device if device in self.get_assigned_devices(): raise DeviceAlreadyAssigned( - 'device {!s} of class {} already assigned to {!s}'.format( - device, self._bus, self._vm)) + '{} device {!s} already assigned to {!s}'.format( + self._bus, device, self._vm)) self._set.add(assignment) @@ -355,8 +355,7 @@ def get_attached_devices(self) -> Iterable[DeviceAssignment]: ), frontend_domain=self._vm, options=options, - attach_automatically=False, - required=False, + mode='manual', ) def get_assigned_devices( diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index 806f4e8d9..02d29b312 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -1759,7 +1759,7 @@ def test_462_vm_device_available_invalid(self): def test_470_vm_device_list_assigned(self): assignment = qubes.device_protocol.DeviceAssignment( qubes.device_protocol.Port(self.vm, '1234', 'test'), - attach_automatically=True, required=True) + mode='required') self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) value = self.call_mgmt_func(b'admin.vm.device.testclass.Assigned', @@ -1772,12 +1772,11 @@ def test_470_vm_device_list_assigned(self): def test_471_vm_device_list_assigned_options(self): assignment = qubes.device_protocol.DeviceAssignment( qubes.device_protocol.Port(self.vm, '1234', 'test'), - attach_automatically=True, required=True, options={'opt1': 'value'}) + mode='required', options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) assignment = qubes.device_protocol.DeviceAssignment( - self.vm, '4321', 'test', - attach_automatically=True, required=True) + self.vm, '4321', 'test', mode='required') self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) value = self.call_mgmt_func(b'admin.vm.device.testclass.Assigned', @@ -1810,12 +1809,12 @@ def test_472_vm_device_list_attached(self): def test_473_vm_device_list_assigned_specific(self): assignment = qubes.device_protocol.DeviceAssignment( qubes.device_protocol.Port(self.vm, '1234', 'test'), - attach_automatically=True, required=True) + mode='required') self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) assignment = qubes.device_protocol.DeviceAssignment( qubes.device_protocol.Port(self.vm, '4321', 'test'), - attach_automatically=True, required=True) + mode='required') self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) value = self.call_mgmt_func(b'admin.vm.device.testclass.Assigned', @@ -1999,8 +1998,7 @@ def test_488_vm_device_assign_options(self): def test_490_vm_device_unassign_from_running(self): assignment = qubes.device_protocol.DeviceAssignment( qubes.device_protocol.Port(self.vm, '1234', 'test'), - attach_automatically=True, required=False, - options={'opt1': 'value'}) + mode='auto-attach', options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) mock_action = unittest.mock.Mock() @@ -2020,8 +2018,7 @@ def test_490_vm_device_unassign_from_running(self): def test_491_vm_device_unassign_required_from_running(self): assignment = qubes.device_protocol.DeviceAssignment( qubes.device_protocol.Port(self.vm, '1234', 'test'), - attach_automatically=True, required=True, - options={'opt1': 'value'}) + mode='required', options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) mock_action = unittest.mock.Mock() @@ -2042,8 +2039,7 @@ def test_491_vm_device_unassign_required_from_running(self): def test_492_vm_device_unassign_from_halted(self): assignment = qubes.device_protocol.DeviceAssignment( qubes.device_protocol.Port(self.vm, '1234', 'test'), - attach_automatically=True, required=False, - options={'opt1': 'value'}) + mode='required', options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) mock_action = unittest.mock.Mock() @@ -2061,8 +2057,7 @@ def test_492_vm_device_unassign_from_halted(self): def test_493_vm_device_unassign_required_from_halted(self): assignment = qubes.device_protocol.DeviceAssignment( qubes.device_protocol.Port(self.vm, '1234', 'test'), - attach_automatically=True, required=True, - options={'opt1': 'value'}) + mode='required', options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) mock_action = unittest.mock.Mock() @@ -2083,8 +2078,7 @@ def test_494_vm_device_unassign_attached(self): self.device_list_single_attached_testclass) assignment = qubes.device_protocol.DeviceAssignment( qubes.device_protocol.Port(self.vm, '1234', 'test'), - attach_automatically=True, required=False, - options={'opt1': 'value'}) + mode='auto-attach', options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) mock_action = unittest.mock.Mock() @@ -2171,7 +2165,7 @@ def test_502_vm_remove_attached(self, mock_rmtree, mock_remove): self.setup_for_clone() assignment = qubes.device_protocol.DeviceAssignment( qubes.device_protocol.Port(self.vm, '1234', 'test'), - attach_automatically=True, required=True) + mode='required') self.loop.run_until_complete( self.vm2.devices['testclass'].assign(assignment)) @@ -2871,8 +2865,7 @@ def test_642_vm_create_disposable_not_allowed(self, storage_mock): def test_650_vm_device_set_required_true(self): assignment = qubes.device_protocol.DeviceAssignment( qubes.device_protocol.Port(self.vm, '1234', 'test'), - attach_automatically=True, required=False, - options={'opt1': 'value'}) + mode='auto-attach', options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) mock_action = unittest.mock.Mock() @@ -2902,8 +2895,7 @@ def test_650_vm_device_set_required_true(self): def test_651_vm_device_set_required_false(self): assignment = qubes.device_protocol.DeviceAssignment( qubes.device_protocol.Port(self.vm, '1234', 'test'), - attach_automatically=True, required=True, - options={'opt1': 'value'}) + mode='required', options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) mock_action = unittest.mock.Mock() @@ -2933,8 +2925,7 @@ def test_651_vm_device_set_required_false(self): def test_652_vm_device_set_required_true_unchanged(self): assignment = qubes.device_protocol.DeviceAssignment( qubes.device_protocol.Port(self.vm, '1234', 'test'), - attach_automatically=True, required=True, - options={'opt1': 'value'}) + 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, @@ -2952,8 +2943,7 @@ def test_652_vm_device_set_required_true_unchanged(self): def test_653_vm_device_set_required_false_unchanged(self): assignment = qubes.device_protocol.DeviceAssignment( qubes.device_protocol.Port(self.vm, '1234', 'test'), - attach_automatically=True, required=False, - options={'opt1': 'value'}) + mode='auto-attach', options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM, diff --git a/qubes/tests/devices.py b/qubes/tests/devices.py index 9fff18e6a..9b2e0ab14 100644 --- a/qubes/tests/devices.py +++ b/qubes/tests/devices.py @@ -90,11 +90,7 @@ def setUp(self): self.app.domains['vm'] = self.emitter self.device = self.emitter.device self.collection = self.emitter.devices['testclass'] - self.assignment = DeviceAssignment( - self.device, - attach_automatically=True, - required=True, - ) + self.assignment = DeviceAssignment(self.device, mode='required') def attach(self): self.emitter.running = True @@ -339,8 +335,7 @@ def test_000_init(self): def test_001_missing(self): device = TestDevice(self.emitter.app.domains['vm'], 'testdev') - assignment = DeviceAssignment( - device, attach_automatically=True, required=True) + assignment = DeviceAssignment(device, mode='required') self.loop.run_until_complete( self.manager['testclass'].assign(assignment)) self.assertEqual( @@ -532,8 +527,7 @@ def test_011_serialize_required(self): ident="1-1.1.1", devclass="bus", ), - attach_automatically=True, - required=True, + mode='required', ) actual = assignment.serialize() expected = ( @@ -603,8 +597,7 @@ def test_020_deserialize(self): devclass="bus", ), frontend_domain=self.vm, - attach_automatically=True, - required=False, + mode='auto-attach', options={'read-only': 'yes'}, ) @@ -642,8 +635,7 @@ def test_030_serialize_and_deserialize(self): devclass="bus", ), frontend_domain=self.vm, - attach_automatically=True, - required=False, + mode='auto-attach', options={'read-only': 'yes'}, ) serialized = expected.serialize() diff --git a/qubes/tests/integ/devices_pci.py b/qubes/tests/integ/devices_pci.py index af456e0b2..880bc241a 100644 --- a/qubes/tests/integ/devices_pci.py +++ b/qubes/tests/integ/devices_pci.py @@ -39,12 +39,10 @@ def setUp(self): pcidev = os.environ['QUBES_TEST_PCIDEV'] self.dev = self.app.domains[0].devices['pci'][pcidev] self.assignment = DeviceAssignment( - self.dev, attach_automatically=True + self.dev, mode='auto-attach' ) self.required_assignment = DeviceAssignment( - self.dev, - attach_automatically=True, - required=True, + self.dev, mode='required', ) if isinstance(self.dev, qubes.device_protocol.UnknownDevice): self.skipTest('Specified device {} does not exists'.format(pcidev)) diff --git a/qubes/tests/vm/qubesvm.py b/qubes/tests/vm/qubesvm.py index e75013be4..b5136aaa6 100644 --- a/qubes/tests/vm/qubesvm.py +++ b/qubes/tests/vm/qubesvm.py @@ -1315,8 +1315,7 @@ def test_600_libvirt_xml_hvm_pcidev(self): ident='00_00.0', devclass="pci", ), - attach_automatically=True, - required=True, + mode='required', ) vm.devices['pci']._set.add( assignment) @@ -1403,7 +1402,7 @@ def test_600_libvirt_xml_hvm_pcidev_s0ix(self): ident='00_00.0', devclass="pci", ), - attach_automatically=True, required=True) + mode='required') vm.devices['pci']._set.add( assignment) libvirt_xml = vm.create_config_file() @@ -1491,7 +1490,7 @@ def test_600_libvirt_xml_hvm_cdrom_boot(self): devclass="block", ), {'devtype': 'cdrom', 'read-only': 'yes'}, - attach_automatically=True, required=True) + 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), @@ -1600,7 +1599,7 @@ def test_600_libvirt_xml_hvm_cdrom_dom0_kernel_boot(self): devclass="block", ), {'devtype': 'cdrom', 'read-only': 'yes'}, - attach_automatically=True, required=True) + 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), diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index 6eb39365b..a118e2606 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,6 +285,27 @@ 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) + device_assignment = qubes.device_protocol.DeviceAssignment( qubes.device_protocol.Port( backend_domain=self.app.domains[ @@ -292,10 +314,7 @@ def load_extras(self): devclass=devclass, ), options=options, - attach_automatically=True, - # backward compatibility: persistent~>required=True - required=qubes.property.bool( - None, None, node.get('required', 'yes')), + mode=mode, ) self.devices[devclass].load_assignment(device_assignment) except KeyError: @@ -349,12 +368,12 @@ 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', assignment.backend_domain.name) + node.set('id', assignment.ident) + node.set('mode', assignment.mode.value) + for key, val in assignment.options.items(): option_node = lxml.etree.Element('option') option_node.set('name', key) option_node.text = val From f0e8c850943dca896a083036ef39a9e756f555d9 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Tue, 27 Aug 2024 12:06:27 +0200 Subject: [PATCH 06/35] q-dev: ask-to-attach is attach_automatically --- qubes/device_protocol.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index 7d20f0ed9..3dafcb4ce 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -895,7 +895,8 @@ def attach_automatically(self) -> bool: """ return self.mode in ( AssignmentMode.AUTO, - AssignmentMode.REQUIRED + AssignmentMode.ASK, + AssignmentMode.REQUIRED, ) @attach_automatically.setter From 7700e1f433c797d1b29cb0ab7ee11559dd6656a6 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Mon, 5 Aug 2024 08:07:23 +0200 Subject: [PATCH 07/35] q-dev: add device_identity to device assignment --- qubes/device_protocol.py | 26 ++++++++++++++++++++------ qubes/ext/block.py | 18 +++++++++--------- qubes/vm/__init__.py | 9 ++++++++- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index 3dafcb4ce..ba91f28a5 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -820,7 +820,7 @@ class DeviceAssignment(Port): def __init__( self, port: Port, - device_id=None, + device_identity=None, frontend_domain=None, options=None, mode: Union[str, AssignmentMode] = "manual", @@ -832,25 +832,38 @@ def __init__( else: self.mode = AssignmentMode(mode) self.frontend_domain = frontend_domain + if device_identity == 'any': + device_identity = None + self.device_identity = device_identity def clone(self, **kwargs): """ Clone object and substitute attributes with explicitly given. """ + port = kwargs.get( + "port", Port(self.backend_domain, self.ident, self.devclass)) attr = { "options": self.options, - "required": self.required, - "attach_automatically": self.attach_automatically, + "mode": self.mode, + "device_identity": self.device_identity, "frontend_domain": self.frontend_domain, } attr.update(kwargs) - return self.__class__( - Port(self.backend_domain, self.ident, self.devclass), **attr) + return self.__class__(port, **attr) @property def device(self) -> DeviceInfo: """Get DeviceInfo object corresponding to this DeviceAssignment""" - return self.backend_domain.devices[self.devclass][self.ident] + dev = self.backend_domain.devices[self.devclass][self.ident] + if (self.device_identity is not None + and self.device_identity != dev.self_identity): + # raise ProtocolError( + # "Device identity does not match, expected " + # f"'{self.device_identity}' got '{dev.self_identity}'") + # TODO + return UnknownDevice( + self.backend_domain, self.ident, devclass=self.devclass) + return dev @property def frontend_domain(self) -> Optional[QubesVM]: @@ -921,6 +934,7 @@ def serialize(self) -> bytes: self.pack_property(key, value) for key, value in ( ('mode', self.mode.value), + ('device_identity', self.device_identity), ('ident', self.ident), ('devclass', self.devclass))) diff --git a/qubes/ext/block.py b/qubes/ext/block.py index 34db054a4..9fd856e83 100644 --- a/qubes/ext/block.py +++ b/qubes/ext/block.py @@ -498,19 +498,19 @@ 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)) + # identity = value # TODO! + # 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}" + # ) + if 'read-only' not in options: options['read-only'] = 'yes' if device.mode == 'r' else 'no' if options.get('read-only', 'no') == 'no' and device.mode == 'r': diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index a118e2606..5260ec8f7 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -305,7 +305,11 @@ def load_extras(self): else: mode = (qubes.device_protocol. AssignmentMode.AUTO) - + if 'identity' in options: + identity = options.get('identity') + del options['identity'] + else: + identity = node.get('identity', 'any') device_assignment = qubes.device_protocol.DeviceAssignment( qubes.device_protocol.Port( backend_domain=self.app.domains[ @@ -313,6 +317,7 @@ def load_extras(self): ident=node.get('id'), devclass=devclass, ), + device_identity=identity, options=options, mode=mode, ) @@ -373,6 +378,8 @@ def __xml__(self): node.set('backend-domain', assignment.backend_domain.name) node.set('id', assignment.ident) node.set('mode', assignment.mode.value) + identity = assignment.device_identity or 'any' + node.set('identity', identity) for key, val in assignment.options.items(): option_node = lxml.etree.Element('option') option_node.set('name', key) From 686c806ca61f5c5293d821a84f29f622d8445589 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Mon, 5 Aug 2024 12:19:30 +0200 Subject: [PATCH 08/35] q-dev: check identity --- qubes/device_protocol.py | 9 +++++---- qubes/ext/block.py | 38 ++++++++++++++++++++++---------------- qubes/ext/utils.py | 3 +-- qubes/vm/__init__.py | 8 ++++++++ 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index ba91f28a5..4951892b9 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -855,14 +855,15 @@ def clone(self, **kwargs): def device(self) -> DeviceInfo: """Get DeviceInfo object corresponding to this DeviceAssignment""" dev = self.backend_domain.devices[self.devclass][self.ident] - if (self.device_identity is not None - and self.device_identity != dev.self_identity): + # TODO: device identity could not match + # if (self.device_identity is not None + # and self.device_identity != dev.self_identity): # raise ProtocolError( # "Device identity does not match, expected " # f"'{self.device_identity}' got '{dev.self_identity}'") # TODO - return UnknownDevice( - self.backend_domain, self.ident, devclass=self.devclass) + # return UnknownDevice( + # self.backend_domain, self.ident, devclass=self.devclass) return dev @property diff --git a/qubes/ext/block.py b/qubes/ext/block.py index 9fd856e83..6094a8420 100644 --- a/qubes/ext/block.py +++ b/qubes/ext/block.py @@ -502,15 +502,6 @@ def pre_attachment_internal( raise qubes.exc.QubesValueError( 'Unsupported option {}'.format(option)) - # identity = value # TODO! - # 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}" - # ) - if 'read-only' not in options: options['read-only'] = 'yes' if device.mode == 'r' else 'no' if options.get('read-only', 'no') == 'no' and device.mode == 'r': @@ -550,19 +541,34 @@ def pre_attachment_internal( 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) + if assignment.mode == qubes.device_protocol.AssignmentMode.ASK: + pass + self.notify_auto_attached(vm, assignment) + + def notify_auto_attached(self, vm, assignment): + identity = assignment.device_ientity + device = assignment.device + if identity not in ('any', device.self_identity): + print("Unrecognized identity, skipping attachment of device in port" + f" {assignment}", file=sys.stderr) + raise qubes.devices.UnrecognizedDevice( + f"Device presented identity {device.self_identity} " + f"does not match expected {identity}" + ) - def notify_auto_attached(self, vm, device, options): self.pre_attachment_internal( - vm, device, options, expected_attachment=vm) + vm, device, assignment.options, expected_attachment=vm) + asyncio.ensure_future(vm.fire_event_async( - 'device-attach:block', device=device, options=options)) + 'device-attach:block', + device=device, + options=assignment.options, + )) - async def attach_and_notify(self, vm, device, options): + 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) + self.notify_auto_attached(vm, assignment) @qubes.ext.handler('domain-shutdown') async def on_domain_shutdown(self, vm, event, **_kwargs): diff --git a/qubes/ext/utils.py b/qubes/ext/utils.py index 796378913..048946ef8 100644 --- a/qubes/ext/utils.py +++ b/qubes/ext/utils.py @@ -100,8 +100,7 @@ def device_list_change( else: target = tuple(frontends.keys())[0] assignment = frontends[target] - asyncio.ensure_future(ext.attach_and_notify( - target, assignment.device, assignment.options)) + asyncio.ensure_future(ext.attach_and_notify(target, assignment)) def compare_device_cache(vm, devices_cache, current_devices): diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index 5260ec8f7..637b2972d 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -285,13 +285,17 @@ def load_extras(self): options[option.get('name')] = str(option.text) try: + import sys # TODO + print(f'{self.name=}', file=sys.stderr) # TODO # backward compatibility: persistent~>required=True legacy_required = node.get('required', 'absent') + print(f'{legacy_required=}', file=sys.stderr) # TODO if legacy_required == 'absent': mode_str = node.get('mode', 'required') try: mode = (qubes.device_protocol. AssignmentMode(mode_str)) + print(f'{mode=}', file=sys.stderr) # TODO except ValueError: self.log.error( "Unrecognized assignment mode, ignoring.") @@ -299,17 +303,21 @@ def load_extras(self): else: required = qubes.property.bool( None, None, legacy_required) + print(f'{required=}', file=sys.stderr) # TODO if required: mode = (qubes.device_protocol. AssignmentMode.REQUIRED) + print(f'{mode=}', file=sys.stderr) # TODO else: mode = (qubes.device_protocol. AssignmentMode.AUTO) + print(f'{mode=}', file=sys.stderr) # TODO if 'identity' in options: identity = options.get('identity') del options['identity'] else: identity = node.get('identity', 'any') + print(mode.value, file=sys.stderr) # TODO device_assignment = qubes.device_protocol.DeviceAssignment( qubes.device_protocol.Port( backend_domain=self.app.domains[ From 083cb8cb75fc5cdaed08931bc9f41673d804af3e Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Tue, 27 Aug 2024 12:14:32 +0200 Subject: [PATCH 09/35] q-dev: implementation of attachment confirmation --- qubes/ext/utils.py | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/qubes/ext/utils.py b/qubes/ext/utils.py index 048946ef8..efe78269e 100644 --- a/qubes/ext/utils.py +++ b/qubes/ext/utils.py @@ -18,19 +18,15 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA. -import itertools +import importlib import asyncio import subprocess -import sys - -import gbulb import qubes from typing import Type -from qubes.ext.attachment_confirm import confirm_device_attachment -from qrexec import server +from qubes import device_protocol def device_list_change( @@ -70,6 +66,8 @@ def device_list_change( continue for assignment in front_vm.devices[devclass].get_assigned_devices(): if (assignment.backend_domain == vm + and assignment.device_identity + == assignment.device.self_identity and assignment.ident in added and assignment.ident not in attached ): @@ -79,27 +77,22 @@ def device_list_change( for ident, frontends in to_attach.items(): if len(frontends) > 1: - guivm = 'dom0' # TODO - - assignment = tuple(frontends.values())[0] - - proc = subprocess.Popen( - ["/home/user/devel/test.py", guivm, - assignment.backend_domain.name, assignment.ident, - *[f.name for f in frontends.keys()]], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - (target_name, _) = proc.communicate() - target_name = target_name.decode() + 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: - print("Something really goes bad :/", file=sys.stderr) return else: target = tuple(frontends.keys())[0] - assignment = frontends[target] + assignment = frontends[target] + asyncio.ensure_future(ext.attach_and_notify(target, assignment)) @@ -137,3 +130,17 @@ 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: + guivm = 'dom0' # TODO + # TODO: guivm rpc? + + proc = subprocess.Popen( + ["attach-confirm", guivm, + device.backend_domain.name, device.ident, + device.description, + *[f.name for f in frontends.keys()]], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (target_name, _) = proc.communicate() + return target_name.decode() From e14a620fe022fa89eea181d34c949c4cc4d7c0d4 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Wed, 7 Aug 2024 07:40:23 +0200 Subject: [PATCH 10/35] q-dev: auto-attach only required block devices before vm start we might need to ask to attach rest --- qubes/ext/block.py | 8 +++++--- templates/libvirt/xen.xml | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/qubes/ext/block.py b/qubes/ext/block.py index 6094a8420..7611a9862 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") @@ -541,8 +541,6 @@ def pre_attachment_internal( async def on_domain_start(self, vm, _event, **_kwargs): # pylint: disable=unused-argument for assignment in vm.devices['block'].get_assigned_devices(): - if assignment.mode == qubes.device_protocol.AssignmentMode.ASK: - pass self.notify_auto_attached(vm, assignment) def notify_auto_attached(self, vm, assignment): @@ -556,6 +554,10 @@ def notify_auto_attached(self, vm, assignment): f"does not match expected {identity}" ) + if assignment.mode.value == "ask-to-attach": + if vm.name != confirm_device_attachment(device, {vm: assignment}): + return + self.pre_attachment_internal( vm, device, assignment.options, expected_attachment=vm) diff --git a/templates/libvirt/xen.xml b/templates/libvirt/xen.xml index a9ce7c1db..95b59a0b6 100644 --- a/templates/libvirt/xen.xml +++ b/templates/libvirt/xen.xml @@ -156,7 +156,7 @@ {# start external devices from xvdi #} {% set counter = {'i': 4} %} - {% for assignment in vm.devices.block.get_assigned_devices(False) %} + {% for assignment in vm.devices.block.get_assigned_devices(True) %} {% set device = assignment.device %} {% set options = assignment.options %} {% include 'libvirt/devices/block.xml' %} From b910c7f8b14ef3688845071107509ae008e47c5e Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Wed, 7 Aug 2024 10:55:16 +0200 Subject: [PATCH 11/35] q-dev: fix attribute name --- qubes/ext/block.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qubes/ext/block.py b/qubes/ext/block.py index 7611a9862..56e1ecc7a 100644 --- a/qubes/ext/block.py +++ b/qubes/ext/block.py @@ -544,7 +544,7 @@ async def on_domain_start(self, vm, _event, **_kwargs): self.notify_auto_attached(vm, assignment) def notify_auto_attached(self, vm, assignment): - identity = assignment.device_ientity + identity = assignment.device_identity device = assignment.device if identity not in ('any', device.self_identity): print("Unrecognized identity, skipping attachment of device in port" From 53cfe74b668bb1cae172491a7c57a6c335541d65 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Wed, 7 Aug 2024 11:22:21 +0200 Subject: [PATCH 12/35] q-dev: backward compatible device_protocol --- qubes/device_protocol.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index 4951892b9..2446fab42 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -410,7 +410,10 @@ class DeviceInfo(Port): def __init__( self, - port: Port, + port: Optional[Port] = None, + backend_domain: Optional = None, + ident: Optional = None, + devclass: Optional = None, vendor: Optional[str] = None, product: Optional[str] = None, manufacturer: Optional[str] = None, @@ -422,6 +425,8 @@ def __init__( self_identity: Optional[str] = None, **kwargs ): + if port is None: + port = Port(backend_domain, ident, devclass) super().__init__(port.backend_domain, port.ident, port.devclass) self._vendor = vendor @@ -819,12 +824,17 @@ class DeviceAssignment(Port): def __init__( self, - port: Port, + port: Optional[Port] = None, + backend_domain: Optional = None, + ident: Optional = None, + devclass: Optional = None, device_identity=None, frontend_domain=None, options=None, mode: Union[str, AssignmentMode] = "manual", ): + if port is None: + port = Port(backend_domain, ident, devclass) super().__init__(port.backend_domain, port.ident, port.devclass) self.__options = options or {} if isinstance(mode, AssignmentMode): @@ -832,8 +842,6 @@ def __init__( else: self.mode = AssignmentMode(mode) self.frontend_domain = frontend_domain - if device_identity == 'any': - device_identity = None self.device_identity = device_identity def clone(self, **kwargs): @@ -910,7 +918,7 @@ def attach_automatically(self) -> bool: return self.mode in ( AssignmentMode.AUTO, AssignmentMode.ASK, - AssignmentMode.REQUIRED, + AssignmentMode.REQUIRED ) @attach_automatically.setter From b97240b64888d14f719dd05271e6c975782f57c9 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Thu, 8 Aug 2024 11:26:08 +0200 Subject: [PATCH 13/35] q-dev: add self_identity do device identity --- qubes/api/admin.py | 9 ++++++--- qubes/device_protocol.py | 16 ++++++++++++---- qubes/devices.py | 12 ++++++------ 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/qubes/api/admin.py b/qubes/api/admin.py index 55fb5ae28..8fd9e4c10 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -1216,7 +1216,8 @@ async def vm_device_available(self, endpoint): # 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} + dev_info = {f'{dev.ident}:{dev.self_identity}': + dev.serialize().decode() for dev in devices} return ''.join('{} {}\n'.format(ident, dev_info[ident]) for ident in sorted(dev_info)) @@ -1246,7 +1247,8 @@ 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.ident}:{assignment.device_identity}'): assignment.serialize().decode('ascii', errors="ignore") for assignment in device_assignments} @@ -1281,7 +1283,8 @@ async def vm_device_attached(self, endpoint): devclass=devclass) dev_info = { - f'{assignment.backend_domain}+{assignment.ident}': + (f'{assignment.backend_domain}' + f'+{assignment.ident}:{assignment.device_identity}'): assignment.serialize().decode('ascii', errors="ignore") for assignment in device_assignments} diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index 2446fab42..b270c042d 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -214,8 +214,8 @@ def check_device_properties( if properties.get('ident', expected.ident) != expected.ident: raise UnexpectedDeviceProperty( - f"Got device with id: {properties['ident']} " - f"when expected id: {expected.ident}.") + f"Got device from port: {properties['ident']} " + f"when expected port: {expected.ident}.") properties['ident'] = expected.ident if properties.get('devclass', expected.devclass) != expected.devclass: @@ -964,12 +964,14 @@ def deserialize( cls, serialization: bytes, expected_port: Port, + expected_identity: Optional[str], ) -> 'DeviceAssignment': """ Recovers a serialized object, see: :py:meth:`serialize`. """ try: - result = cls._deserialize(serialization, expected_port) + result = cls._deserialize( + serialization, expected_port, expected_identity) except Exception as exc: raise ProtocolError() from exc return result @@ -979,6 +981,7 @@ def _deserialize( cls, untrusted_serialization: bytes, expected_port: Port, + expected_identity: Optional[str], ) -> 'DeviceAssignment': """ Actually deserializes the object. @@ -991,4 +994,9 @@ def _deserialize( del properties['ident'] del properties['devclass'] - return cls(expected_port, **properties) + assignment = cls(expected_port, **properties) + if assignment.device.self_identity != expected_identity: + raise UnexpectedDeviceProperty( + f"Got device with identity {assignment.device.self_identity}" + f"when expected devices with identity {expected_identity}.") + return assignment diff --git a/qubes/devices.py b/qubes/devices.py index 75fa54ca2..617251ef3 100644 --- a/qubes/devices.py +++ b/qubes/devices.py @@ -223,14 +223,14 @@ async def assign(self, assignment: DeviceAssignment): """ 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(): raise DeviceAlreadyAssigned( - '{} device {!s} already assigned to {!s}'.format( - self._bus, device, self._vm)) + f'{self._bus} device {device!s} ' + f'already assigned to {self._vm!s}') self._set.add(assignment) @@ -366,10 +366,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]: """ From 69ff16ca93fc34ce2d03e42b8203472e926e85ba Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Mon, 12 Aug 2024 18:16:32 +0200 Subject: [PATCH 14/35] q-dev: refactor device_protocol.py --- doc/qubes-devices.rst | 10 +- qubes/api/admin.py | 76 ++- qubes/app.py | 2 +- qubes/device_protocol.py | 762 ++++++++++++++++++----------- qubes/devices.py | 71 +-- qubes/ext/block.py | 97 ++-- qubes/ext/pci.py | 52 +- qubes/ext/utils.py | 23 +- qubes/tests/api_admin.py | 24 +- qubes/tests/app.py | 2 +- qubes/tests/devices.py | 202 ++++---- qubes/tests/devices_block.py | 57 ++- qubes/tests/devices_pci.py | 4 +- qubes/tests/integ/audio.py | 8 +- qubes/tests/integ/devices_block.py | 23 +- qubes/tests/integ/devices_pci.py | 2 +- qubes/tests/vm/qubesvm.py | 44 +- qubes/vm/__init__.py | 26 +- 18 files changed, 838 insertions(+), 647 deletions(-) diff --git a/doc/qubes-devices.rst b/doc/qubes-devices.rst index be6d705fc..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 @@ -106,11 +106,11 @@ 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/api/admin.py b/qubes/api/admin.py index 8fd9e4c10..4baec9c43 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -45,7 +45,7 @@ import qubes.vm import qubes.vm.adminvm import qubes.vm.qubesvm -from qubes.device_protocol import Port +from qubes.device_protocol import Port, Device, DeviceInfo class QubesMgmtEventsDispatcher: @@ -1211,15 +1211,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 = {f'{dev.ident}:{dev.self_identity}': + dev_info = {f'{dev.port_id}:{dev.device_id}': dev.serialize().decode() for dev in devices} - 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}.Assigned', endpoints=(ep.name for ep in importlib.metadata.entry_points(group='qubes.devices')), @@ -1238,7 +1238,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 @@ -1248,12 +1248,12 @@ async def vm_device_list(self, endpoint): dev_info = { (f'{assignment.backend_domain}' - f'+{assignment.ident}:{assignment.device_identity}'): + 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', @@ -1274,7 +1274,7 @@ 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 @@ -1284,12 +1284,12 @@ async def vm_device_attached(self, endpoint): dev_info = { (f'{assignment.backend_domain}' - f'+{assignment.ident}:{assignment.device_identity}'): + 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 @@ -1298,14 +1298,10 @@ 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_port=dev + untrusted_payload, expected_device=dev ) self.fire_event_for_permission( @@ -1318,6 +1314,13 @@ 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) -> DeviceInfo: + # qrexec already verified that no strange characters are in self.arg + _dev = Device.from_qarg(self.arg, devclass, self.app.domains) + # load all info, may raise KeyError, either on domain or port_id + return self.app.domains[ + _dev.backend_domain].devices[devclass][_dev.port_id] + # Assign/Unassign action can modify only persistent state of running VM. # For this reason, write=True @qubes.api.method( @@ -1328,18 +1331,11 @@ async def vm_device_assign(self, endpoint, untrusted_payload): 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) self.fire_event_for_permission(device=dev, devclass=devclass) - assignment = qubes.device_protocol.DeviceAssignment( - qubes.device_protocol.Port(dev.backend_domain, dev.ident, devclass)) + assignment = qubes.device_protocol.DeviceAssignment(dev) await self.dest.devices[devclass].unassign(assignment) self.app.save() @@ -1353,14 +1349,10 @@ 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] + dev = self.load_device_info(devclass) assignment = qubes.device_protocol.DeviceAssignment.deserialize( - untrusted_payload, expected_port=dev + untrusted_payload, expected_device=dev ) self.fire_event_for_permission( @@ -1382,18 +1374,11 @@ 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( - qubes.device_protocol.Port(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. @@ -1418,10 +1403,7 @@ async def vm_device_set_required(self, endpoint, untrusted_payload): assignment = eval(untrusted_payload) del untrusted_payload - # 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 = Port(backend_domain, ident, devclass) + dev = Device.from_qarg(self.arg, devclass, self.app.domains) self.fire_event_for_permission(device=dev, assignment=assignment) 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 b270c042d..0bbb70bce 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -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,76 +51,12 @@ def qbool(value): return qubes.property.bool(None, None, value) -class Port: - """ - Class of a *bus* device port 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 port within the backend domain. - devclass (str): The class of the port (e.g., 'usb', 'pci'). - """ +class DeviceSerializer: 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): - self.__backend_domain = backend_domain - self.__ident = ident - self.__devclass = devclass - - def __hash__(self): - return hash((self.backend_domain.name, self.ident, self.devclass)) - - def __eq__(self, other): - if isinstance(other, Port): - return ( - self.backend_domain == other.backend_domain and - self.ident == other.ident and - self.devclass == other.devclass - ) - raise TypeError(f"Comparing instances of 'Port' and '{type(other)}' " - "is not supported") - - def __lt__(self, other): - if isinstance(other, Port): - return (self.backend_domain.name, self.devclass, self.ident) < \ - (other.backend_domain.name, other.devclass, other.ident) - raise TypeError(f"Comparing instances of 'Port' and '{type(other)}' " - "is not supported") - - def __repr__(self): - return f"[{self.backend_domain.name}]:{self.devclass}:{self.ident}" - - def __str__(self): - return f"{self.backend_domain.name}:{self.ident}" - - @property - def ident(self) -> str: - """ - Immutable port identifier. - - Unique for given domain and devclass. - """ - return self.__ident - - @property - def backend_domain(self) -> 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" - @classmethod def unpack_properties( cls, untrusted_serialization: bytes @@ -149,24 +85,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) @@ -180,21 +116,23 @@ 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_port: 'Port', properties: Dict[str, Any]): + def parse_basic_device_properties( + expected_device: 'Device', properties: Dict[str, Any]): """ Validates properties against an expected port configuration. @@ -204,25 +142,345 @@ def check_device_properties( UnexpectedDeviceProperty: If any property does not match the expected values. """ - expected = expected_port + expected = expected_device.port exp_vm_name = expected.backend_domain.name if properties.get('backend_domain', exp_vm_name) != exp_vm_name: raise UnexpectedDeviceProperty( f"Got device exposed by {properties['backend_domain']}" f"when expected devices from {exp_vm_name}.") - properties['backend_domain'] = expected.backend_domain + 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 from port: {properties['ident']} " - f"when expected port: {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['devclass'] = expected.devclass + properties.pop('devclass', None) + + 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"Unrecognized device identity '{properties['device_id']}' " + f"expected '{expected_device.device_id}'" + ) + + 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 identifier for the port within the backend domain. + 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_domain.name, self.port_id, self.devclass)) + + def __eq__(self, other): + if isinstance(other, Port): + return ( + self.backend_domain == other.backend_domain 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_domain.name, self.devclass, self.port_id) < \ + (other.backend_domain.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_domain.name}+{self.port_id}" + + def __str__(self): + return f"{self.backend_domain.name}:{self.port_id}" + + @classmethod + def from_qarg( + cls, representation: str, devclass, domains, blind=False + ) -> '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) -> 'Port': + get_domain = domains.get + return cls._parse(representation, devclass, get_domain, ':') + + @classmethod + def _parse( + cls, + representation: str, + devclass: str, + get_domain: Callable, + sep: str + ) -> '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. + """ + return self.__port_id + + @property + def backend_domain(self) -> 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 Device: + def __init__( + self, + port: Optional[Port] = None, + device_id: Optional[str] = None, + ): + self.port: Optional[Port] = port + self._device_id = device_id if device_id else '*' + + def clone(self, **kwargs): + """ + Clone object and substitute attributes with explicitly given. + """ + attr = { + "port": self.port, + "device_id": self.device_id, + } + attr.update(kwargs) + return self.__class__(**attr) + + @property + def port(self): + return self._port + + @port.setter + def port(self, value): + self._port = value if value is not None else '*' + + @property + def device_id(self): + return self._device_id + + @device_id.setter + def device_id(self, value): + self._device_id = value if value else '*' + + @property + def backend_domain(self): + if self.port != '*': + return self.port.backend_domain + return None + + @property + def port_id(self): + if self.port != '*': + return self.port.port_id + return None + + @property + def devclass(self): + if self.port != '*': + return self.port.devclass + return None + + def __hash__(self): + return hash((self.port, self.device_id)) + + def __eq__(self, other): + if isinstance(other, (Device, 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, (Device, 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 in reprs: + if obj.device_id != '*': + reprs[obj].append(obj.device_id) + return reprs[self] < reprs[other] + elif isinstance(other, Port): + _other = Device(other, '*') + return self < _other + else: + 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, + ) -> 'Device': + 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, + backend=None + ) -> 'Device': + if backend is None: + get_domain = domains.get + 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 + ) -> 'Device': + if backend is None: + backend_name, identity = representation.split(sep, 1) + backend = get_domain(backend_name) + else: + identity = representation + port_id, _, devid = identity.partition(':') + if devid in ('', '*'): + devid = '*' + 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_domain.name) + + return properties class DeviceCategory(Enum): @@ -405,15 +663,12 @@ def _load_classes(bus: str): return result -class DeviceInfo(Port): +class DeviceInfo(Device): """ Holds all information about a device """ def __init__( self, - port: Optional[Port] = None, - backend_domain: Optional = None, - ident: Optional = None, - devclass: Optional = None, + port: Port, vendor: Optional[str] = None, product: Optional[str] = None, manufacturer: Optional[str] = None, @@ -422,12 +677,10 @@ def __init__( interfaces: Optional[List[DeviceInterface]] = None, parent: Optional[Port] = None, attachment: Optional[QubesVM] = None, - self_identity: Optional[str] = None, + device_id: Optional[str] = None, **kwargs ): - if port is None: - port = Port(backend_domain, ident, devclass) - super().__init__(port.backend_domain, port.ident, port.devclass) + super().__init__(port, device_id) self._vendor = vendor self._product = product @@ -437,43 +690,9 @@ def __init__( self._interfaces = interfaces self._parent = parent self._attachment = attachment - self._self_identity = self_identity self.data = kwargs - def __hash__(self): - return hash(self.port)# self.self_identity)) - - def __eq__(self, other): - if isinstance(other, DeviceInfo): - return ( - self.port == other.port - # and self.self_identity == other.self_identity - ) - else: - return super().__lt__(other) - - def __lt__(self, other): - if isinstance(other, DeviceInfo): - # return (self.port, self.self_identity) < \ - # (other.port, other.self_identity) - return self.port < other.port - else: - return super().__lt__(other) - - def __repr__(self): - return f"{self.port!r}"#:{self.self_identity}" - - def __str__(self): - return f"{self.port}"#:{self.self_identity}" - - @property - def port(self) -> Port: - """ - Device port visible in Qubes. - """ - return Port(self.backend_domain, self.ident, self.devclass) - @property def vendor(self) -> str: """ @@ -584,7 +803,7 @@ def interfaces(self) -> List[DeviceInterface]: return self._interfaces @property - def parent_device(self) -> Optional[Port]: + def parent_device(self) -> Optional[Device]: """ The parent device, if any. @@ -594,7 +813,7 @@ def parent_device(self) -> Optional[Port]: return self._parent @property - def subdevices(self) -> List['DeviceInfo']: + def subdevices(self) -> List[Device]: """ The list of children devices if any. @@ -602,7 +821,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]: @@ -615,35 +834,32 @@ def serialize(self) -> bytes: """ Serialize an object to be transmitted via Qubes API. """ - # 'backend_domain', 'attachment', 'interfaces', 'data', 'parent_device' + properties = Device.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 = 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 @@ -657,19 +873,16 @@ 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 = Device.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) + device = UnknownDevice.from_device(device) return device @@ -677,28 +890,25 @@ def deserialize( def _deserialize( cls, untrusted_serialization: bytes, - expected_port: Port + expected_device: Device ) -> '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_port, properties) + DeviceSerializer.parse_basic_device_properties( + expected_device, properties) if 'attachment' not in properties or not properties['attachment']: properties['attachment'] = None else: - app = expected_port.backend_domain.app + app = expected_device.backend_domain.app properties['attachment'] = app.domains.get_blind( properties['attachment']) - if properties['devclass'] != expected_port.devclass: - raise UnexpectedDeviceProperty( - f"Got {properties['devclass']} device " - f"when expected {expected_port.devclass}.") - if 'interfaces' in properties: interfaces = properties['interfaces'] interfaces = [ @@ -708,25 +918,17 @@ def _deserialize( if 'parent_ident' in properties: properties['parent'] = Port( - backend_domain=expected_port.backend_domain, - ident=properties['parent_ident'], + backend_domain=expected_device.backend_domain, + port_id=properties['parent_ident'], devclass=properties['parent_devclass'], ) del properties['parent_ident'] del properties['parent_devclass'] - port = Port( - properties['backend_domain'], - properties['ident'], - properties['devclass']) - del properties['backend_domain'] - del properties['ident'] - del properties['devclass'] - - return cls(port, **properties) + return cls(**properties) @property - def self_identity(self) -> str: + def device_id(self) -> str: """ Get additional identification of device presented by device itself. @@ -741,58 +943,22 @@ 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 - -def serialize_str(value: str): - """ - Serialize python string to ensure consistency. - """ - return "'" + str(value).replace("'", r"\'") + "'" - - -def deserialize_str(value: str): - """ - Deserialize python string to ensure consistency. - """ - return value.replace(r"\'", "'") - - -def sanitize_str( - untrusted_value: str, - allowed_chars: set, - replace_char: str = None, - error_message: str = "" -) -> str: - """ - Sanitize given untrusted string. - - If `replace_char` is not None, ignore `error_message` and replace invalid - characters with the string. - """ - if replace_char is None: - not_allowed_chars = set(untrusted_value) - allowed_chars - if not_allowed_chars: - raise ProtocolError(error_message + repr(not_allowed_chars)) - return untrusted_value - result = "" - for char in untrusted_value: - if char in allowed_chars: - result += char - else: - result += replace_char - return result + @device_id.setter + def device_id(self, value): + # Do not auto-override value like in super class + self._device_id = value 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): - port = Port(backend_domain, ident, devclass) - super().__init__(port, **kwargs) + @staticmethod + def from_device(device) -> 'UnknownDevice': + return UnknownDevice(device.port, device_id=device.device_id) class AssignmentMode(Enum): @@ -802,7 +968,7 @@ class AssignmentMode(Enum): REQUIRED = "required" -class DeviceAssignment(Port): +class DeviceAssignment: """ Maps a device to a frontend_domain. There are 3 flags `attached`, `automatically_attached` and `required`. @@ -824,56 +990,95 @@ class DeviceAssignment(Port): def __init__( self, - port: Optional[Port] = None, - backend_domain: Optional = None, - ident: Optional = None, - devclass: Optional = None, - device_identity=None, + device: Device, frontend_domain=None, options=None, mode: Union[str, AssignmentMode] = "manual", ): - if port is None: - port = Port(backend_domain, ident, devclass) - super().__init__(port.backend_domain, port.ident, port.devclass) + if isinstance(device, DeviceInfo): + device = Device(device.port, device.device_id) + self._device_ident = device self.__options = options or {} if isinstance(mode, AssignmentMode): self.mode = mode else: self.mode = AssignmentMode(mode) self.frontend_domain = frontend_domain - self.device_identity = device_identity def clone(self, **kwargs): """ Clone object and substitute attributes with explicitly given. """ - port = kwargs.get( - "port", Port(self.backend_domain, self.ident, self.devclass)) + kwargs["device"] = kwargs.get( + "device", Device( + Port(self.backend_domain, self.port_id, self.devclass), + self.device_id + )) attr = { "options": self.options, "mode": self.mode, - "device_identity": self.device_identity, "frontend_domain": self.frontend_domain, } attr.update(kwargs) - return self.__class__(port, **attr) + return self.__class__(**attr) + + def __repr__(self): + return f"{self._device_ident!r}" + + def __str__(self): + return f"{self._device_ident}" + + def __hash__(self): + return hash(self._device_ident) + + def __eq__(self, other): + if isinstance(other, (Device, 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._device_ident < other._device_ident + if isinstance(other, Device): + return self._device_ident < other + raise TypeError( + f"Comparing instances of {type(self)} and '{type(other)}' " + "is not supported") + + @property + def backend_domain(self): + return self._device_ident.port.backend_domain + + @property + def port_id(self): + return self._device_ident.port.port_id + + @property + def devclass(self): + return self._device_ident.port.devclass + + @property + def device_id(self): + return self._device_ident.device_id @property def device(self) -> DeviceInfo: """Get DeviceInfo object corresponding to this DeviceAssignment""" - dev = self.backend_domain.devices[self.devclass][self.ident] + dev = self.backend_domain.devices[self.devclass][self.port_id] # TODO: device identity could not match - # if (self.device_identity is not None - # and self.device_identity != dev.self_identity): - # raise ProtocolError( - # "Device identity does not match, expected " - # f"'{self.device_identity}' got '{dev.self_identity}'") - # TODO - # return UnknownDevice( - # self.backend_domain, self.ident, devclass=self.devclass) return dev + @property + 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]: """ Which domain the device is attached/assigned to. """ @@ -905,10 +1110,6 @@ def required(self) -> bool: """ return self.mode == AssignmentMode.REQUIRED - @required.setter - def required(self, required: bool): - self.mode = AssignmentMode.REQUIRED - @property def attach_automatically(self) -> bool: """ @@ -921,10 +1122,6 @@ def attach_automatically(self) -> bool: AssignmentMode.REQUIRED ) - @attach_automatically.setter - def attach_automatically(self, attach_automatically: bool): - self.mode = AssignmentMode.AUTO - @property def options(self) -> Dict[str, Any]: """ Device options (same as in the legacy API). """ @@ -939,23 +1136,15 @@ 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 ( - ('mode', self.mode.value), - ('device_identity', self.device_identity), - ('ident', self.ident), - ('devclass', self.devclass))) - - properties += b' ' + self.pack_property( - 'backend_domain', self.backend_domain.name) - + properties = self._device_ident.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 @@ -963,40 +1152,35 @@ def serialize(self) -> bytes: def deserialize( cls, serialization: bytes, - expected_port: Port, - expected_identity: Optional[str], + expected_device: Device, ) -> 'DeviceAssignment': """ Recovers a serialized object, see: :py:meth:`serialize`. """ try: - result = cls._deserialize( - serialization, expected_port, expected_identity) + 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_port: Port, - expected_identity: Optional[str], + expected_device: Device, ) -> '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_port, properties) - del properties['backend_domain'] - del properties['ident'] - del properties['devclass'] + DeviceSerializer.parse_basic_device_properties( + expected_device, properties) + # we do not need port, we need device + del properties['port'] + properties.pop('device_id', None) + properties['device'] = expected_device - assignment = cls(expected_port, **properties) - if assignment.device.self_identity != expected_identity: - raise UnexpectedDeviceProperty( - f"Got device with identity {assignment.device.self_identity}" - f"when expected devices with identity {expected_identity}.") - return assignment + return cls(**properties) diff --git a/qubes/devices.py b/qubes/devices.py index 617251ef3..b41a84467 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 @@ -59,12 +59,13 @@ `device-list-change:class` event. """ import itertools +import sys from typing import Iterable import qubes.exc import qubes.utils from qubes.device_protocol import (Port, DeviceInfo, UnknownDevice, - DeviceAssignment) + DeviceAssignment, Device) class DeviceNotAssigned(qubes.exc.QubesException, KeyError): @@ -134,13 +135,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 +166,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`. @@ -227,7 +228,7 @@ async def assign(self, assignment: DeviceAssignment): f'when {self._bus} device expected.') device = assignment.device - if device in self.get_assigned_devices(): + if assignment in self.get_assigned_devices(): raise DeviceAlreadyAssigned( f'{self._bus} device {device!s} ' f'already assigned to {self._vm!s}') @@ -248,7 +249,7 @@ def load_assignment(self, device_assignment: DeviceAssignment): assert device_assignment.attach_automatically self._set.add(device_assignment) - async def update_required(self, device: Port, required: bool): + async def update_required(self, device: Device, required: bool): """ Update `required` flag of an already attached device. @@ -263,7 +264,7 @@ async def update_required(self, device: Port, required: bool): 'VM must be running to modify device assignment' ) assignments = [a for a in self.get_assigned_devices() - if a == device] + if a.device == device] if not assignments: raise qubes.exc.QubesValueError( f'Device {device} not assigned to {self._vm.name}') @@ -275,22 +276,23 @@ async def update_required(self, device: Port, required: bool): if assignment.required == required: return - assignment.required = required + assignments[0] = assignment.clone( + mode='required' if required else 'auto-attach') await self._vm.fire_event_async( 'device-assignment-changed:' + self._bus, device=device) - async def detach(self, device: Port): + 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(): @@ -300,12 +302,12 @@ async def detach(self, device: Port): "You need to unassign device first.") # use the local object - device = assignment.device + 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): """ @@ -344,15 +346,12 @@ def get_attached_devices(self) -> Iterable[DeviceAssignment]: for dev, options in attached: for assignment in self._set: if dev == assignment: + print("ok", file=sys.stderr) yield assignment break else: yield DeviceAssignment( - Port( - backend_domain=dev.backend_domain, - ident=dev.ident, - devclass=dev.devclass, - ), + dev, frontend_domain=self._vm, options=options, mode='manual', @@ -379,8 +378,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` @@ -389,15 +388,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): @@ -428,8 +427,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 @@ -440,20 +440,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/block.py b/qubes/ext/block.py index 56e1ecc7a..46c5f8001 100644 --- a/qubes/ext/block.py +++ b/qubes/ext/block.py @@ -47,9 +47,9 @@ class BlockDevice(qubes.device_protocol.DeviceInfo): - def __init__(self, backend_domain, ident): + def __init__(self, backend_domain, port_id): port = qubes.device_protocol.Port( - backend_domain=backend_domain, ident=ident, devclass="block") + backend_domain=backend_domain, port_id=port_id, devclass="block") super().__init__(port) # lazy loading @@ -87,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( @@ -108,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 @@ -118,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() @@ -136,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) @@ -150,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]: @@ -173,7 +173,7 @@ def parent_device(self) -> Optional[qubes.device_protocol.Port]: 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' @@ -188,7 +188,8 @@ def parent_device(self) -> Optional[qubes.device_protocol.Port]: 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 @@ -214,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 @@ -249,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 @@ -282,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): @@ -309,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: @@ -321,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) @@ -341,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): @@ -378,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 @@ -440,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'): @@ -531,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( @@ -544,13 +545,13 @@ async def on_domain_start(self, vm, _event, **_kwargs): self.notify_auto_attached(vm, assignment) def notify_auto_attached(self, vm, assignment): - identity = assignment.device_identity + identity = assignment.device_id device = assignment.device - if identity not in ('any', device.self_identity): + if identity not in ('*', device.device_id): print("Unrecognized identity, skipping attachment of device in port" f" {assignment}", file=sys.stderr) raise qubes.devices.UnrecognizedDevice( - f"Device presented identity {device.self_identity} " + f"Device presented identity {device.device_id} " f"does not match expected {identity}" ) @@ -601,7 +602,7 @@ 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) @@ -611,7 +612,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 @@ -619,10 +620,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 3ca8c0ba6..0fef28838 100644 --- a/qubes/ext/pci.py +++ b/qubes/ext/pci.py @@ -161,21 +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()) port = qubes.device_protocol.Port( - backend_domain=backend_domain, ident=ident, devclass="pci") + 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)) @@ -260,7 +260,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. """ @@ -311,7 +311,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): @@ -341,10 +341,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): @@ -361,20 +361,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") @@ -387,7 +387,8 @@ def on_device_pre_attached_pci(self, vm, event, device, options): return try: - device = _cache_get(device.backend_domain, device.ident) + # TODO? + 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( @@ -397,10 +398,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 @@ -409,16 +410,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: @@ -432,14 +433,15 @@ 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) + # TODO? + device = _cache_get(assignment.backend_domain, assignment.port_id) self.bind_pci_to_pciback(vm.app, device) @staticmethod @@ -477,6 +479,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 efe78269e..c6cda9800 100644 --- a/qubes/ext/utils.py +++ b/qubes/ext/utils.py @@ -66,16 +66,15 @@ def device_list_change( continue for assignment in front_vm.devices[devclass].get_assigned_devices(): if (assignment.backend_domain == vm - and assignment.device_identity - == assignment.device.self_identity - and assignment.ident in added - and assignment.ident not in attached + and assignment.device_id == assignment.device.device_id + and assignment.port_id in added + and assignment.port_id not in attached ): - frontends = to_attach.get(assignment.ident, {}) + frontends = to_attach.get(assignment.port_id, {}) frontends[front_vm] = assignment - to_attach[assignment.ident] = frontends + to_attach[assignment.port_id] = frontends - for ident, frontends in to_attach.items(): + for port_id, frontends in to_attach.items(): if len(frontends) > 1: device = tuple(frontends.values())[0].device target_name = confirm_device_attachment(device, frontends) @@ -98,10 +97,10 @@ def device_list_change( 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 = {} @@ -138,7 +137,7 @@ def confirm_device_attachment(device, frontends) -> str: proc = subprocess.Popen( ["attach-confirm", guivm, - device.backend_domain.name, device.ident, + device.backend_domain.name, device.port_id, device.description, *[f.name for f in frontends.keys()]], stdout=subprocess.PIPE, stderr=subprocess.PIPE) diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index 02d29b312..114312cad 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -1726,13 +1726,13 @@ def test_460_vm_device_available(self): 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' " + "device_id='0000:0000::?******' vendor='unknown' " + "devclass='peripheral' product='Some_device' port_id='1234' " "name='unknown' backend_domain='test-vm1' interfaces='?******'\n" "4321 serial='unknown' manufacturer='unknown' " - "self_identity='0000:0000::?******' vendor='unknown' " + "device_id='0000:0000::?******' vendor='unknown' " "devclass='peripheral' product='Some_other_device' " - "ident='4321' name='unknown' backend_domain='test-vm1' " + "port_id='4321' name='unknown' backend_domain='test-vm1' " "interfaces='?******'\n") self.assertFalse(self.app.save.called) @@ -1743,9 +1743,9 @@ def test_461_vm_device_available_specific(self): value = value.replace("'Some other device'", "'Some_other_device'") self.assertSerializedEqual(value, "4321 serial='unknown' manufacturer='unknown' " - "self_identity='0000:0000::?******' vendor='unknown' " + "device_id='0000:0000::?******' vendor='unknown' " "devclass='peripheral' product='Some_other_device' " - "ident='4321' name='unknown' backend_domain='test-vm1' " + "port_id='4321' name='unknown' backend_domain='test-vm1' " "interfaces='?******'\n") self.assertFalse(self.app.save.called) @@ -1766,7 +1766,7 @@ def test_470_vm_device_list_assigned(self): b'test-vm1') self.assertEqual(value, "test-vm1+1234 required='yes' attach_automatically='yes' " - "ident='1234' devclass='testclass' backend_domain='test-vm1'\n") + "port_id='1234' devclass='testclass' backend_domain='test-vm1'\n") self.assertFalse(self.app.save.called) def test_471_vm_device_list_assigned_options(self): @@ -1783,10 +1783,10 @@ def test_471_vm_device_list_assigned_options(self): b'test-vm1') self.assertEqual(value, "test-vm1+1234 required='yes' attach_automatically='yes' " - "ident='1234' devclass='testclass' backend_domain='test-vm1' " + "port_id='1234' devclass='testclass' backend_domain='test-vm1' " "_opt1='value'\n" "test-vm1+4321 required='yes' attach_automatically='yes' " - "ident='4321' devclass='testclass' backend_domain='test-vm1'\n") + "port_id='4321' devclass='testclass' backend_domain='test-vm1'\n") self.assertFalse(self.app.save.called) def device_list_single_attached_testclass(self, vm, event, **kwargs): @@ -1802,7 +1802,7 @@ def test_472_vm_device_list_attached(self): b'test-vm1') self.assertEqual(value, "test-vm1+1234 required='no' attach_automatically='no' " - "ident='1234' devclass='testclass' backend_domain='test-vm1' " + "port_id='1234' devclass='testclass' backend_domain='test-vm1' " "frontend_domain='test-vm1' _attach_opt='value'\n") self.assertFalse(self.app.save.called) @@ -1821,7 +1821,7 @@ def test_473_vm_device_list_assigned_specific(self): 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") + "port_id='1234' devclass='testclass' backend_domain='test-vm1'\n") self.assertFalse(self.app.save.called) def device_list_multiple_attached_testclass(self, vm, event, **kwargs): @@ -1839,7 +1839,7 @@ def test_474_vm_device_list_attached_specific(self): 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' " + "port_id='1234' devclass='testclass' backend_domain='test-vm1' " "frontend_domain='test-vm1' _attach_opt='value'\n") 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 9b2e0ab14..0098f7d47 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/ @@ -22,7 +23,7 @@ import qubes.devices from qubes.device_protocol import (Port, DeviceInfo, DeviceAssignment, - DeviceInterface, UnknownDevice) + DeviceInterface, UnknownDevice, Device) import qubes.tests @@ -48,7 +49,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'), 'testdev') self.events_enabled = True self.devices = { 'testclass': qubes.devices.DeviceCollection(self, 'testclass') @@ -119,20 +121,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): @@ -144,17 +147,20 @@ def test_011_empty_unassign(self): def test_012_double_attach(self): self.attach() with self.assertRaises(qubes.devices.DeviceAlreadyAttached): + print(self.assignment, file=sys.stderr) # TODO + print(self.assignment.device, file=sys.stderr) # TODO self.loop.run_until_complete( self.collection.attach(self.assignment)) def test_013_double_detach(self): self.attach() - self.loop.run_until_complete(self.collection.detach(self.assignment)) + self.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)) @@ -174,26 +180,26 @@ 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): @@ -201,42 +207,42 @@ def test_020_update_required_to_false(self): 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_required(self.device.port, False)) 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 + 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}, + self.collection.update_required(self.device.port, True)) + self.assertEqual({self.assignment}, set(self.collection.get_assigned_devices())) - self.assertEqual({self.device}, + self.assertEqual({self.assignment}, set(self.collection.get_attached_devices())) def test_022_update_required_reject_not_running(self): self.assertEqual(set([]), set(self.collection.get_assigned_devices())) self.loop.run_until_complete(self.collection.assign(self.assignment)) - self.assertEqual({self.device}, + 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): @@ -256,13 +262,13 @@ def test_023_update_required_reject_not_attached(self): 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') @@ -279,7 +285,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') @@ -287,7 +293,7 @@ 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') @@ -298,13 +304,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 @@ -314,10 +320,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') @@ -334,7 +341,8 @@ def test_000_init(self): self.assertEqual(self.manager, {}) def test_001_missing(self): - device = TestDevice(self.emitter.app.domains['vm'], 'testdev') + 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)) @@ -351,7 +359,7 @@ def setUp(self): def test_010_serialize(self): device = DeviceInfo( Port(backend_domain=self.vm, - ident="1-1.1.1", + port_id="1-1.1.1", devclass="bus"), vendor="ITL", product="Qubes", @@ -362,11 +370,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'") @@ -379,7 +387,7 @@ def test_010_serialize(self): def test_011_serialize_with_parent(self): device = DeviceInfo( Port(backend_domain=self.vm, - ident="1-1.1.1", + port_id="1-1.1.1", devclass="bus"), vendor="ITL", product="Qubes", @@ -390,16 +398,16 @@ def test_011_serialize_with_parent(self): DeviceInterface("u03**01")], additional_info="", date="06.12.23", - parent=Port(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", @@ -409,8 +417,8 @@ def test_011_serialize_with_parent(self): def test_012_invalid_serialize(self): device = DeviceInfo( Port(backend_domain=self.vm, - ident="1-1.1.1", - devclass="bus?"), + port_id="1-1.1.1", + devclass="testclass"), vendor="malicious", product="suspicious", manufacturer="", @@ -422,8 +430,7 @@ 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' " @@ -431,7 +438,7 @@ def test_020_deserialize(self): actual = DeviceInfo.deserialize(serialized, self.vm) expected = DeviceInfo( Port(backend_domain=self.vm, - ident="1-1.1.1", + port_id="1-1.1.1", devclass="bus"), vendor="ITL", product="Qubes", @@ -442,10 +449,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) @@ -453,14 +461,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' " @@ -468,14 +476,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( Port(backend_domain=self.vm, - ident="1-1.1.1", - devclass="bus?"), + port_id="1-1.1.1", + devclass="testclass"), vendor="malicious", product="suspicious", manufacturer="", @@ -488,7 +496,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) @@ -507,77 +515,77 @@ def setUp(self): self.vm = TestVM(self.app, 'vm') def test_010_serialize(self): - assignment = DeviceAssignment(Port( + assignment = DeviceAssignment(Device(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( - Port( + Device(Port( backend_domain=self.vm, - ident="1-1.1.1", + 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( - Port( + Device(Port( backend_domain=self.vm, - ident="1-1.1.1", + 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( - Port( + Device(Port( backend_domain=self.vm, - ident="1-1.1.1", + 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( - Port( + Device(Port( backend_domain=self.vm, - ident="1-1.1.1", + port_id="1-1.1.1", devclass="bus", - ), + )), options={"read'only": 'yes'}, ) with self.assertRaises(qubes.exc.ProtocolError): @@ -585,24 +593,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 = Port(self.vm, '1-1.1.1', 'bus') + expected_device = Device(Port(self.vm, '1-1.1.1', 'bus')) actual = DeviceAssignment.deserialize(serialized, expected_device) expected = DeviceAssignment( - Port( + Device(Port( backend_domain=self.vm, - ident="1-1.1.1", + port_id="1-1.1.1", devclass="bus", - ), + )), frontend_domain=self.vm, 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,38 +619,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 = Port(self.vm, '1-1.1.1', 'bus') + expected_device = Device(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 = Port(self.vm, '1-1.1.1', 'bus') + expected_device = Device(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 = Device(Port(self.vm, '1-1.1.1', 'bus')) expected = DeviceAssignment( - Port( - backend_domain=self.vm, - ident="1-1.1.1", - devclass="bus", - ), + expected_device, frontend_domain=self.vm, mode='auto-attach', options={'read-only': 'yes'}, ) serialized = expected.serialize() - expected_device = Port(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 8bc261854..2041838e6 100644 --- a/qubes/tests/devices_block.py +++ b/qubes/tests/devices_block.py @@ -18,6 +18,7 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, see . import asyncio +import sys from unittest import mock import jinja2 @@ -25,7 +26,7 @@ import qubes.tests import qubes.ext.block from qubes.device_protocol import DeviceInterface, Port, DeviceInfo, \ - DeviceAssignment + DeviceAssignment, Device modules_disk = ''' @@ -135,9 +136,9 @@ def __init__(self, backend_vm, devclass): def get_assigned_devices(self): return self._assigned - def __getitem__(self, ident): + def __getitem__(self, port_id): for dev in self._exposed: - if dev.ident == ident: + if dev.port_id == port_id: return dev @@ -165,6 +166,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 @@ -187,7 +191,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(Port(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 +220,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 +232,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, - Port(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 +250,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 +318,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 +337,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 +393,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 +416,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,7 +438,7 @@ 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') @@ -640,7 +644,7 @@ 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): @@ -654,7 +658,7 @@ def test_051_detach_not_attached(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) self.assertFalse(vm.libvirt_domain.detachDevice.called) def test_060_on_qdb_change_added(self): @@ -664,14 +668,16 @@ def test_060_on_qdb_change_added(self): '/qubes-block-devices/sda/size': b'1024000', '/qubes-block-devices/sda/mode': b'r', }, domain_xml=domain_xml_template.format("")) - exp_dev = Port(back_vm, 'sda', 'block') + 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}}) + print(back_vm.fired_events, file=sys.stderr) # TODO + print(exp_dev, file=sys.stderr) # TODO self.assertEqual( back_vm.fired_events[ - ('device-added:block', frozenset({('device', exp_dev)}))],1) + ('device-added:block', frozenset({('device', exp_dev)}))], 1) def test_061_on_qdb_change_auto_attached(self): back_vm = TestVM(name='sys-usb', qdb={ @@ -680,7 +686,7 @@ def test_061_on_qdb_change_auto_attached(self): '/qubes-block-devices/sda/size': b'1024000', '/qubes-block-devices/sda/mode': b'r', }, domain_xml=domain_xml_template.format("")) - exp_dev = Port(back_vm, 'sda', 'block') + exp_dev = qubes.ext.block.BlockDevice(back_vm, 'sda') front = TestVM({}, domain_xml=domain_xml_template.format(""), name='front-vm') dom0 = TestVM({}, name='dom0', @@ -702,7 +708,8 @@ def test_061_on_qdb_change_auto_attached(self): dom0.devices['block'] = TestDeviceCollection( backend_vm=dom0, devclass='block') - front.devices['block']._assigned.append(DeviceAssignment(exp_dev)) + front.devices['block']._assigned.append( + DeviceAssignment(exp_dev)) back_vm.devices['block']._exposed.append( qubes.ext.block.BlockDevice(back_vm, 'sda')) @@ -724,7 +731,7 @@ def test_062_on_qdb_change_attached(self): '/qubes-block-devices/sda/size': b'1024000', '/qubes-block-devices/sda/mode': b'r', }, domain_xml=domain_xml_template.format("")) - exp_dev = Port(back_vm, 'sda', 'block') + exp_dev = qubes.ext.block.BlockDevice(back_vm, 'sda') self.ext.devices_cache = {'sys-usb': {'sda': None}} @@ -773,7 +780,7 @@ def test_063_on_qdb_change_changed(self): '/qubes-block-devices/sda/size': b'1024000', '/qubes-block-devices/sda/mode': b'r', }, domain_xml=domain_xml_template.format("")) - exp_dev = Port(back_vm, 'sda', 'block') + exp_dev = qubes.ext.block.BlockDevice(back_vm, 'sda') front = TestVM({}, name='front-vm') dom0 = TestVM({}, name='dom0', @@ -840,7 +847,7 @@ def test_064_on_qdb_change_removed_attached(self): }, domain_xml=domain_xml_template.format("")) dom0 = TestVM({}, name='dom0', domain_xml=domain_xml_template.format("")) - exp_dev = Port(back_vm, 'sda', 'block') + exp_dev = qubes.ext.block.BlockDevice(back_vm, 'sda') disk = ''' diff --git a/qubes/tests/devices_pci.py b/qubes/tests/devices_pci.py index 01011cadd..6f68cb89c 100644 --- a/qubes/tests/devices_pci.py +++ b/qubes/tests/devices_pci.py @@ -143,7 +143,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 +153,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..bf13c4d0a 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.Device( + 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.Device( + 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/devices_block.py b/qubes/tests/integ/devices_block.py index eb982430f..c72bb42ce 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,7 +320,7 @@ 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( @@ -328,7 +328,10 @@ def setUp(self): def test_000_attach_reattach(self): ass = qubes.device_protocol.DeviceAssignment( - qubes.device_protocol.Port(self.backend, self.device_ident, 'test')) + qubes.device_protocol.Device( + 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 880bc241a..03f75af78 100644 --- a/qubes/tests/integ/devices_pci.py +++ b/qubes/tests/integ/devices_pci.py @@ -65,7 +65,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) diff --git a/qubes/tests/vm/qubesvm.py b/qubes/tests/vm/qubesvm.py index b5136aaa6..8071c2bcd 100644 --- a/qubes/tests/vm/qubesvm.py +++ b/qubes/tests/vm/qubesvm.py @@ -1309,11 +1309,13 @@ def test_600_libvirt_xml_hvm_pcidev(self): # even with meminfo-writer enabled, should have memory==maxmem vm.features['service.meminfo-writer'] = True assignment = qubes.device_protocol.DeviceAssignment( - qubes.device_protocol.Port( - backend_domain=vm, # this is violation of API, - # but for PCI the argument is unused - ident='00_00.0', - devclass="pci", + qubes.device_protocol.Device( + 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', ) @@ -1396,11 +1398,13 @@ 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( - qubes.device_protocol.Port( - backend_domain=vm, # this is violation of API, - # but for PCI the argument is unused - ident='00_00.0', - devclass="pci", + qubes.device_protocol.Device( + 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( @@ -1484,10 +1488,12 @@ def test_600_libvirt_xml_hvm_cdrom_boot(self): dom0.events_enabled = True self.app.vmm.offline_mode = False dev = qubes.device_protocol.DeviceAssignment( - qubes.device_protocol.Port( - backend_domain=dom0, - ident='sda', - devclass="block", + qubes.device_protocol.Device( + qubes.device_protocol.Port( + backend_domain=dom0, + port_id='sda', + devclass="block", + ) ), {'devtype': 'cdrom', 'read-only': 'yes'}, mode='required') @@ -1593,10 +1599,12 @@ 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( - qubes.device_protocol.Port( - backend_domain=dom0, - ident='sda', - devclass="block", + qubes.device_protocol.Device( + qubes.device_protocol.Port( + backend_domain=dom0, + port_id='sda', + devclass="block", + ) ), {'devtype': 'cdrom', 'read-only': 'yes'}, mode='required') diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index 637b2972d..352fb65d5 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -285,17 +285,13 @@ def load_extras(self): options[option.get('name')] = str(option.text) try: - import sys # TODO - print(f'{self.name=}', file=sys.stderr) # TODO # backward compatibility: persistent~>required=True legacy_required = node.get('required', 'absent') - print(f'{legacy_required=}', file=sys.stderr) # TODO if legacy_required == 'absent': mode_str = node.get('mode', 'required') try: mode = (qubes.device_protocol. AssignmentMode(mode_str)) - print(f'{mode=}', file=sys.stderr) # TODO except ValueError: self.log.error( "Unrecognized assignment mode, ignoring.") @@ -303,29 +299,27 @@ def load_extras(self): else: required = qubes.property.bool( None, None, legacy_required) - print(f'{required=}', file=sys.stderr) # TODO if required: mode = (qubes.device_protocol. AssignmentMode.REQUIRED) - print(f'{mode=}', file=sys.stderr) # TODO else: mode = (qubes.device_protocol. AssignmentMode.AUTO) - print(f'{mode=}', file=sys.stderr) # TODO if 'identity' in options: identity = options.get('identity') del options['identity'] else: identity = node.get('identity', 'any') - print(mode.value, file=sys.stderr) # TODO device_assignment = qubes.device_protocol.DeviceAssignment( - qubes.device_protocol.Port( - backend_domain=self.app.domains[ - node.get('backend-domain')], - ident=node.get('id'), - devclass=devclass, + qubes.device_protocol.Device( + qubes.device_protocol.Port( + backend_domain=self.app.domains[ + node.get('backend-domain')], + port_id=node.get('id'), + devclass=devclass, + ), + device_id=identity, ), - device_identity=identity, options=options, mode=mode, ) @@ -384,9 +378,9 @@ def __xml__(self): for assignment in self.devices[devclass].get_assigned_devices(): node = lxml.etree.Element('device') node.set('backend-domain', assignment.backend_domain.name) - node.set('id', assignment.ident) + node.set('id', assignment.port_id) node.set('mode', assignment.mode.value) - identity = assignment.device_identity or 'any' + identity = assignment.device_id or 'any' node.set('identity', identity) for key, val in assignment.options.items(): option_node = lxml.etree.Element('option') From f3c3caeb4147d04fdec693768c2d3ed912137511 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Thu, 15 Aug 2024 12:19:29 +0200 Subject: [PATCH 15/35] q-dev: fix events --- qubes/devices.py | 2 +- qubes/ext/block.py | 8 ++++---- qubes/ext/utils.py | 2 +- qubes/tests/api_admin.py | 2 +- qubes/tests/devices_block.py | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/qubes/devices.py b/qubes/devices.py index b41a84467..4f78270b0 100644 --- a/qubes/devices.py +++ b/qubes/devices.py @@ -127,7 +127,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. diff --git a/qubes/ext/block.py b/qubes/ext/block.py index 46c5f8001..0780876e6 100644 --- a/qubes/ext/block.py +++ b/qubes/ext/block.py @@ -548,8 +548,8 @@ def notify_auto_attached(self, vm, assignment): identity = assignment.device_id device = assignment.device if identity not in ('*', device.device_id): - print("Unrecognized identity, skipping attachment of device in port" - f" {assignment}", file=sys.stderr) + print("Unrecognized identity, skipping attachment of device " + f"from the port {assignment}", file=sys.stderr) raise qubes.devices.UnrecognizedDevice( f"Device presented identity {device.device_id} " f"does not match expected {identity}" @@ -594,7 +594,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)) else: new_cache[domain.name][dev_id] = front_vm self.devices_cache = new_cache.copy() @@ -604,7 +604,7 @@ async def _detach_and_notify(self, vm, device, options): self.on_device_pre_detached_block( vm, 'device-pre-detach:block', device.port) await vm.fire_event_async( - 'device-detach:block', device=device, options=options) + 'device-detach:block', port=device, options=options) @qubes.ext.handler('qubes-close', system=True) def on_qubes_close(self, app, event): diff --git a/qubes/ext/utils.py b/qubes/ext/utils.py index c6cda9800..3505293d1 100644 --- a/qubes/ext/utils.py +++ b/qubes/ext/utils.py @@ -45,7 +45,7 @@ def device_list_change( for dev_id, front_vm in detached.items(): dev = device_class(vm, dev_id) asyncio.ensure_future(front_vm.fire_event_async( - f'device-detach:{devclass}', device=dev)) + f'device-detach:{devclass}', port=dev)) for dev_id in removed: device = device_class(vm, dev_id) vm.fire_event(f'device-removed:{devclass}', device=device) diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index 114312cad..fa4366e59 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -2120,7 +2120,7 @@ def test_496_vm_device_detach(self): 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']) self.assertFalse(self.app.save.called) def test_497_vm_device_detach_not_attached(self): diff --git a/qubes/tests/devices_block.py b/qubes/tests/devices_block.py index 2041838e6..79bc1e203 100644 --- a/qubes/tests/devices_block.py +++ b/qubes/tests/devices_block.py @@ -833,9 +833,9 @@ 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) fire_event_async_2.assert_called_once_with( - 'device-attach:block', device=exp_dev, options={}) + 'device-attach:block', port=exp_dev, options={}) def test_064_on_qdb_change_removed_attached(self): # attached to front-vm @@ -889,7 +889,7 @@ 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) self.assertEqual( back_vm.fired_events[ ('device-removed:block', frozenset({('device', exp_dev)}))], From 8e3822e8d690b077b979607bcfbeac310bd5109c Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Thu, 15 Aug 2024 13:34:27 +0200 Subject: [PATCH 16/35] q-dev: unify protocol --- qubes/device_protocol.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index 0bbb70bce..266257333 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -270,8 +270,13 @@ def from_qarg( return cls._parse(representation, devclass, get_domain, '+') @classmethod - def from_str(cls, representation: str, devclass, domains) -> 'Port': - get_domain = domains.get + def from_str( + cls, representation: str, devclass, domains, blind=False + ) -> 'Port': + if blind: + get_domain = domains.get_blind + else: + get_domain = domains.__getitem__ return cls._parse(representation, devclass, get_domain, ':') @classmethod @@ -436,10 +441,13 @@ def from_qarg( @classmethod def from_str( cls, representation: str, devclass: Optional[str], domains, - backend=None + blind=False, backend=None ) -> 'Device': if backend is None: - get_domain = domains.get + if blind: + get_domain = domains.get_blind + else: + get_domain = domains.__getitem__ else: get_domain = None return cls._parse(representation, devclass, get_domain, backend, ':') From 7d22664ff9b2e1b02db48b6e8c8ccccb944f739b Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Thu, 15 Aug 2024 13:42:33 +0200 Subject: [PATCH 17/35] q-dev: fix test --- qubes/ext/block.py | 4 ++-- qubes/ext/utils.py | 2 +- qubes/tests/devices_block.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/qubes/ext/block.py b/qubes/ext/block.py index 0780876e6..af16ae048 100644 --- a/qubes/ext/block.py +++ b/qubes/ext/block.py @@ -594,7 +594,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', port=dev)) + 'device-detach:block', port=dev.port)) else: new_cache[domain.name][dev_id] = front_vm self.devices_cache = new_cache.copy() @@ -604,7 +604,7 @@ async def _detach_and_notify(self, vm, device, options): self.on_device_pre_detached_block( vm, 'device-pre-detach:block', device.port) await vm.fire_event_async( - 'device-detach:block', port=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): diff --git a/qubes/ext/utils.py b/qubes/ext/utils.py index 3505293d1..3ff46b96c 100644 --- a/qubes/ext/utils.py +++ b/qubes/ext/utils.py @@ -45,7 +45,7 @@ def device_list_change( for dev_id, front_vm in detached.items(): dev = device_class(vm, dev_id) asyncio.ensure_future(front_vm.fire_event_async( - f'device-detach:{devclass}', port=dev)) + f'device-detach:{devclass}', port=dev.port)) for dev_id in removed: device = device_class(vm, dev_id) vm.fire_event(f'device-removed:{devclass}', device=device) diff --git a/qubes/tests/devices_block.py b/qubes/tests/devices_block.py index 79bc1e203..0ba70907b 100644 --- a/qubes/tests/devices_block.py +++ b/qubes/tests/devices_block.py @@ -833,9 +833,9 @@ 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', port=exp_dev) + 'device-detach:block', port=exp_dev.port) fire_event_async_2.assert_called_once_with( - 'device-attach:block', port=exp_dev, options={}) + 'device-attach:block', device=exp_dev, options={}) def test_064_on_qdb_change_removed_attached(self): # attached to front-vm @@ -889,7 +889,7 @@ 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', port=exp_dev) + 'device-detach:block', port=exp_dev.port) self.assertEqual( back_vm.fired_events[ ('device-removed:block', frozenset({('device', exp_dev)}))], From a4e16216749d355644458e84e1e890216cf467b7 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Thu, 15 Aug 2024 15:33:14 +0200 Subject: [PATCH 18/35] q-dev: virtual device --- qubes/api/admin.py | 6 ++-- qubes/device_protocol.py | 45 +++++++++++++++--------------- qubes/devices.py | 6 ++-- qubes/tests/devices.py | 22 +++++++-------- qubes/tests/devices_block.py | 2 +- qubes/tests/integ/audio.py | 4 +-- qubes/tests/integ/devices_block.py | 2 +- qubes/tests/vm/qubesvm.py | 8 +++--- qubes/vm/__init__.py | 8 +++--- 9 files changed, 52 insertions(+), 51 deletions(-) diff --git a/qubes/api/admin.py b/qubes/api/admin.py index 4baec9c43..3f257d7a3 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -45,7 +45,7 @@ import qubes.vm import qubes.vm.adminvm import qubes.vm.qubesvm -from qubes.device_protocol import Port, Device, DeviceInfo +from qubes.device_protocol import Port, VirtualDevice, DeviceInfo class QubesMgmtEventsDispatcher: @@ -1316,7 +1316,7 @@ async def vm_device_assign(self, endpoint, untrusted_payload): def load_device_info(self, devclass) -> DeviceInfo: # qrexec already verified that no strange characters are in self.arg - _dev = Device.from_qarg(self.arg, devclass, self.app.domains) + _dev = VirtualDevice.from_qarg(self.arg, devclass, self.app.domains) # load all info, may raise KeyError, either on domain or port_id return self.app.domains[ _dev.backend_domain].devices[devclass][_dev.port_id] @@ -1403,7 +1403,7 @@ async def vm_device_set_required(self, endpoint, untrusted_payload): assignment = eval(untrusted_payload) del untrusted_payload - dev = Device.from_qarg(self.arg, devclass, self.app.domains) + dev = VirtualDevice.from_qarg(self.arg, devclass, self.app.domains) self.fire_event_for_permission(device=dev, assignment=assignment) diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index 266257333..b45b10b25 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -132,7 +132,7 @@ def pack_property(cls, key: str, value: Optional[str]): @staticmethod def parse_basic_device_properties( - expected_device: 'Device', properties: Dict[str, Any]): + expected_device: 'VirtualDevice', properties: Dict[str, Any]): """ Validates properties against an expected port configuration. @@ -316,7 +316,7 @@ def devclass(self) -> str: return "peripheral" -class Device: +class VirtualDevice: def __init__( self, port: Optional[Port] = None, @@ -374,7 +374,7 @@ def __hash__(self): return hash((self.port, self.device_id)) def __eq__(self, other): - if isinstance(other, (Device, DeviceAssignment)): + if isinstance(other, (VirtualDevice, DeviceAssignment)): result = ( self.port == other.port and self.device_id == other.device_id @@ -396,7 +396,7 @@ def __lt__(self, other): 3. *: 4. *:* """ - if isinstance(other, (Device, DeviceAssignment)): + if isinstance(other, (VirtualDevice, DeviceAssignment)): if self.port == '*' and other.port != '*': return True if self.port != '*' and other.port == '*': @@ -407,7 +407,7 @@ def __lt__(self, other): reprs[obj].append(obj.device_id) return reprs[self] < reprs[other] elif isinstance(other, Port): - _other = Device(other, '*') + _other = VirtualDevice(other, '*') return self < _other else: raise TypeError( @@ -428,7 +428,7 @@ def from_qarg( domains, blind=False, backend=None, - ) -> 'Device': + ) -> 'VirtualDevice': if backend is None: if blind: get_domain = domains.get_blind @@ -442,7 +442,7 @@ def from_qarg( def from_str( cls, representation: str, devclass: Optional[str], domains, blind=False, backend=None - ) -> 'Device': + ) -> 'VirtualDevice': if backend is None: if blind: get_domain = domains.get_blind @@ -460,7 +460,7 @@ def _parse( get_domain: Callable, backend, sep: str - ) -> 'Device': + ) -> 'VirtualDevice': if backend is None: backend_name, identity = representation.split(sep, 1) backend = get_domain(backend_name) @@ -671,7 +671,7 @@ def _load_classes(bus: str): return result -class DeviceInfo(Device): +class DeviceInfo(VirtualDevice): """ Holds all information about a device """ def __init__( @@ -811,7 +811,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. @@ -821,7 +821,7 @@ def parent_device(self) -> Optional[Device]: return self._parent @property - def subdevices(self) -> List[Device]: + def subdevices(self) -> List[VirtualDevice]: """ The list of children devices if any. @@ -842,7 +842,7 @@ def serialize(self) -> bytes: """ Serialize an object to be transmitted via Qubes API. """ - properties = Device.serialize(self) + properties = VirtualDevice.serialize(self) # 'attachment', 'interfaces', 'data', 'parent_device' # are not string, so they need special treatment default = DeviceInfo(self.port) @@ -882,7 +882,7 @@ def deserialize( Recovers a serialized object, see: :py:meth:`serialize`. """ head, _, rest = serialization.partition(b' ') - device = Device.from_str( + device = VirtualDevice.from_str( head.decode('ascii', errors='ignore'), expected_devclass, domains=None, backend=expected_backend_domain) @@ -898,7 +898,7 @@ def deserialize( def _deserialize( cls, untrusted_serialization: bytes, - expected_device: Device + expected_device: VirtualDevice ) -> 'DeviceInfo': """ Actually deserializes the object. @@ -998,13 +998,13 @@ class DeviceAssignment: def __init__( self, - device: Device, + device: VirtualDevice, frontend_domain=None, options=None, mode: Union[str, AssignmentMode] = "manual", ): if isinstance(device, DeviceInfo): - device = Device(device.port, device.device_id) + device = VirtualDevice(device.port, device.device_id) self._device_ident = device self.__options = options or {} if isinstance(mode, AssignmentMode): @@ -1018,7 +1018,7 @@ def clone(self, **kwargs): Clone object and substitute attributes with explicitly given. """ kwargs["device"] = kwargs.get( - "device", Device( + "device", VirtualDevice( Port(self.backend_domain, self.port_id, self.devclass), self.device_id )) @@ -1040,7 +1040,7 @@ def __hash__(self): return hash(self._device_ident) def __eq__(self, other): - if isinstance(other, (Device, DeviceAssignment)): + if isinstance(other, (VirtualDevice, DeviceAssignment)): result = ( self.port == other.port and self.device_id == other.device_id @@ -1051,7 +1051,7 @@ def __eq__(self, other): def __lt__(self, other): if isinstance(other, DeviceAssignment): return self._device_ident < other._device_ident - if isinstance(other, Device): + if isinstance(other, VirtualDevice): return self._device_ident < other raise TypeError( f"Comparing instances of {type(self)} and '{type(other)}' " @@ -1076,7 +1076,8 @@ def device_id(self): @property def device(self) -> DeviceInfo: """Get DeviceInfo object corresponding to this DeviceAssignment""" - dev = self.backend_domain.devices[self.devclass][self.port_id] + if self.port_id: + dev = self.backend_domain.devices[self.devclass][self.port_id] # TODO: device identity could not match return dev @@ -1160,7 +1161,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`. @@ -1175,7 +1176,7 @@ def deserialize( def _deserialize( cls, untrusted_serialization: bytes, - expected_device: Device, + expected_device: VirtualDevice, ) -> 'DeviceAssignment': """ Actually deserializes the object. diff --git a/qubes/devices.py b/qubes/devices.py index 4f78270b0..54177b2d8 100644 --- a/qubes/devices.py +++ b/qubes/devices.py @@ -65,7 +65,7 @@ import qubes.exc import qubes.utils from qubes.device_protocol import (Port, DeviceInfo, UnknownDevice, - DeviceAssignment, Device) + DeviceAssignment, VirtualDevice) class DeviceNotAssigned(qubes.exc.QubesException, KeyError): @@ -249,11 +249,11 @@ def load_assignment(self, device_assignment: DeviceAssignment): assert device_assignment.attach_automatically self._set.add(device_assignment) - async def update_required(self, device: Device, required: bool): + async def update_required(self, device: VirtualDevice, required: bool): """ Update `required` flag of an already attached device. - :param Device device: device for which change required flag + :param VirtualDevice 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 diff --git a/qubes/tests/devices.py b/qubes/tests/devices.py index 0098f7d47..944ab95d1 100644 --- a/qubes/tests/devices.py +++ b/qubes/tests/devices.py @@ -23,7 +23,7 @@ import qubes.devices from qubes.device_protocol import (Port, DeviceInfo, DeviceAssignment, - DeviceInterface, UnknownDevice, Device) + DeviceInterface, UnknownDevice, VirtualDevice) import qubes.tests @@ -515,7 +515,7 @@ def setUp(self): self.vm = TestVM(self.app, 'vm') def test_010_serialize(self): - assignment = DeviceAssignment(Device(Port( + assignment = DeviceAssignment(VirtualDevice(Port( backend_domain=self.vm, port_id="1-1.1.1", devclass="bus", @@ -530,7 +530,7 @@ def test_010_serialize(self): def test_011_serialize_required(self): assignment = DeviceAssignment( - Device(Port( + VirtualDevice(Port( backend_domain=self.vm, port_id="1-1.1.1", devclass="bus", @@ -547,7 +547,7 @@ def test_011_serialize_required(self): def test_012_serialize_fronted(self): assignment = DeviceAssignment( - Device(Port( + VirtualDevice(Port( backend_domain=self.vm, port_id="1-1.1.1", devclass="bus", @@ -564,7 +564,7 @@ def test_012_serialize_fronted(self): def test_013_serialize_options(self): assignment = DeviceAssignment( - Device(Port( + VirtualDevice(Port( backend_domain=self.vm, port_id="1-1.1.1", devclass="bus", @@ -581,7 +581,7 @@ def test_013_serialize_options(self): def test_014_invalid_serialize(self): assignment = DeviceAssignment( - Device(Port( + VirtualDevice(Port( backend_domain=self.vm, port_id="1-1.1.1", devclass="bus", @@ -596,10 +596,10 @@ def test_020_deserialize(self): 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(Port(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( - Device(Port( + VirtualDevice(Port( backend_domain=self.vm, port_id="1-1.1.1", devclass="bus", @@ -622,7 +622,7 @@ def test_021_invalid_deserialize(self): 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(Port(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) @@ -631,12 +631,12 @@ def test_022_invalid_deserialize_2(self): 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(Port(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 = Device(Port(self.vm, '1-1.1.1', 'bus')) + expected_device = VirtualDevice(Port(self.vm, '1-1.1.1', 'bus')) expected = DeviceAssignment( expected_device, frontend_domain=self.vm, diff --git a/qubes/tests/devices_block.py b/qubes/tests/devices_block.py index 0ba70907b..7e537889f 100644 --- a/qubes/tests/devices_block.py +++ b/qubes/tests/devices_block.py @@ -26,7 +26,7 @@ import qubes.tests import qubes.ext.block from qubes.device_protocol import DeviceInterface, Port, DeviceInfo, \ - DeviceAssignment, Device + DeviceAssignment modules_disk = ''' diff --git a/qubes/tests/integ/audio.py b/qubes/tests/integ/audio.py index bf13c4d0a..336de9f33 100644 --- a/qubes/tests/integ/audio.py +++ b/qubes/tests/integ/audio.py @@ -272,7 +272,7 @@ async def _check_audio_input_status(vm, status): def attach_mic(self): deva = qubes.device_protocol.DeviceAssignment( - qubes.device_protocol.Device( + 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) @@ -281,7 +281,7 @@ def attach_mic(self): def detach_mic(self): deva = qubes.device_protocol.DeviceAssignment( - qubes.device_protocol.Device( + 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/devices_block.py b/qubes/tests/integ/devices_block.py index c72bb42ce..1642278b5 100644 --- a/qubes/tests/integ/devices_block.py +++ b/qubes/tests/integ/devices_block.py @@ -328,7 +328,7 @@ def setUp(self): def test_000_attach_reattach(self): ass = qubes.device_protocol.DeviceAssignment( - qubes.device_protocol.Device( + qubes.device_protocol.VirtualDevice( qubes.device_protocol.Port( self.backend, self.device_ident, 'block') )) diff --git a/qubes/tests/vm/qubesvm.py b/qubes/tests/vm/qubesvm.py index 8071c2bcd..c3caea257 100644 --- a/qubes/tests/vm/qubesvm.py +++ b/qubes/tests/vm/qubesvm.py @@ -1309,7 +1309,7 @@ def test_600_libvirt_xml_hvm_pcidev(self): # even with meminfo-writer enabled, should have memory==maxmem vm.features['service.meminfo-writer'] = True assignment = qubes.device_protocol.DeviceAssignment( - qubes.device_protocol.Device( + qubes.device_protocol.VirtualDevice( qubes.device_protocol.Port( backend_domain=vm, # this is violation of API, # but for PCI the argument is unused @@ -1398,7 +1398,7 @@ 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( - qubes.device_protocol.Device( + qubes.device_protocol.VirtualDevice( qubes.device_protocol.Port( backend_domain=vm, # this is violation of API, # but for PCI the argument is unused @@ -1488,7 +1488,7 @@ def test_600_libvirt_xml_hvm_cdrom_boot(self): dom0.events_enabled = True self.app.vmm.offline_mode = False dev = qubes.device_protocol.DeviceAssignment( - qubes.device_protocol.Device( + qubes.device_protocol.VirtualDevice( qubes.device_protocol.Port( backend_domain=dom0, port_id='sda', @@ -1599,7 +1599,7 @@ 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( - qubes.device_protocol.Device( + qubes.device_protocol.VirtualDevice( qubes.device_protocol.Port( backend_domain=dom0, port_id='sda', diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index 352fb65d5..b9c3f3fc8 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -309,13 +309,13 @@ def load_extras(self): identity = options.get('identity') del options['identity'] else: - identity = node.get('identity', 'any') + identity = node.get('identity', '*') device_assignment = qubes.device_protocol.DeviceAssignment( - qubes.device_protocol.Device( + qubes.device_protocol.VirtualDevice( qubes.device_protocol.Port( backend_domain=self.app.domains[ node.get('backend-domain')], - port_id=node.get('id'), + port_id=node.get('id', '*'), devclass=devclass, ), device_id=identity, @@ -380,7 +380,7 @@ def __xml__(self): node.set('backend-domain', assignment.backend_domain.name) node.set('id', assignment.port_id) node.set('mode', assignment.mode.value) - identity = assignment.device_id or 'any' + identity = assignment.device_id or '*' node.set('identity', identity) for key, val in assignment.options.items(): option_node = lxml.etree.Element('option') From 727ee09b2bae041e145690f5501935d83dbfb7c2 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Thu, 15 Aug 2024 18:07:13 +0200 Subject: [PATCH 19/35] q-dev: device -> devices --- qubes/device_protocol.py | 88 ++++++++++++++++++++++--------------- qubes/devices.py | 34 +++++++------- qubes/ext/block.py | 34 +++++++------- qubes/ext/utils.py | 23 ++++++---- qubes/tests/devices.py | 8 ++-- qubes/tests/integ/backup.py | 7 ++- qubes/vm/qubesvm.py | 13 ++++-- 7 files changed, 117 insertions(+), 90 deletions(-) diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index b45b10b25..313e7691e 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -175,6 +175,7 @@ def parse_basic_device_properties( f"Unrecognized device identity '{properties['device_id']}' " f"expected '{expected_device.device_id}'" ) + expected._device_id = properties.get('device_id', expected_devid) properties['port'] = expected @@ -317,13 +318,21 @@ def devclass(self) -> str: 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, ): + # TODO! one of them cannot be None self.port: Optional[Port] = port - self._device_id = device_id if device_id else '*' + self._device_id = device_id def clone(self, **kwargs): """ @@ -346,29 +355,33 @@ def port(self, value): @property def device_id(self): - return self._device_id - - @device_id.setter - def device_id(self, value): - self._device_id = value if value else '*' + if self._device_id is not None: + return self._device_id + return '*' @property def backend_domain(self): - if self.port != '*': + if self.port != '*' and self.port.backend_domain is not None: return self.port.backend_domain - return None + return '*' @property def port_id(self): - if self.port != '*': + if self.port != '*' and self.port.port_id is not None: return self.port.port_id - return None + return '*' @property def devclass(self): - if self.port != '*': + if self.port != '*' and self.port.devclass is not None: return self.port.devclass - return None + return '*' + + @property + def description(self): + if self.device_id == '*': + return 'any device' + return self.device_id def __hash__(self): return hash((self.port, self.device_id)) @@ -1004,8 +1017,8 @@ def __init__( mode: Union[str, AssignmentMode] = "manual", ): if isinstance(device, DeviceInfo): - device = VirtualDevice(device.port, device.device_id) - self._device_ident = device + device = VirtualDevice(device.port, device._device_id) + self.virtual_device = device self.__options = options or {} if isinstance(mode, AssignmentMode): self.mode = mode @@ -1017,12 +1030,8 @@ def clone(self, **kwargs): """ Clone object and substitute attributes with explicitly given. """ - kwargs["device"] = kwargs.get( - "device", VirtualDevice( - Port(self.backend_domain, self.port_id, self.devclass), - self.device_id - )) attr = { + "device": self.virtual_device, "options": self.options, "mode": self.mode, "frontend_domain": self.frontend_domain, @@ -1031,13 +1040,13 @@ def clone(self, **kwargs): return self.__class__(**attr) def __repr__(self): - return f"{self._device_ident!r}" + return f"{self.virtual_device!r}" def __str__(self): - return f"{self._device_ident}" + return f"{self.virtual_device}" def __hash__(self): - return hash(self._device_ident) + return hash(self.virtual_device) def __eq__(self, other): if isinstance(other, (VirtualDevice, DeviceAssignment)): @@ -1050,36 +1059,40 @@ def __eq__(self, other): def __lt__(self, other): if isinstance(other, DeviceAssignment): - return self._device_ident < other._device_ident + return self.virtual_device < other.virtual_device if isinstance(other, VirtualDevice): - return self._device_ident < other + return self.virtual_device < other raise TypeError( f"Comparing instances of {type(self)} and '{type(other)}' " "is not supported") @property def backend_domain(self): - return self._device_ident.port.backend_domain + return self.virtual_device.port.backend_domain @property def port_id(self): - return self._device_ident.port.port_id + return self.virtual_device.port.port_id @property def devclass(self): - return self._device_ident.port.devclass + return self.virtual_device.port.devclass @property def device_id(self): - return self._device_ident.device_id + return self.virtual_device.device_id @property - def device(self) -> DeviceInfo: + def devices(self) -> List[DeviceInfo]: """Get DeviceInfo object corresponding to this DeviceAssignment""" - if self.port_id: - dev = self.backend_domain.devices[self.devclass][self.port_id] - # TODO: device identity could not match - return dev + if self.port_id != '*': + # could return UnknownDevice + return [self.backend_domain.devices[self.devclass][self.port_id]] + result = [] + for dev in self.backend_domain.devices[self.devclass]: + if dev.device_id == self.device_id: + result.append(dev) + return result @property def port(self) -> Port: @@ -1109,7 +1122,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 == self.frontend_domain: + return True + return False @property def required(self) -> bool: @@ -1145,7 +1161,7 @@ def serialize(self) -> bytes: """ Serialize an object to be transmitted via Qubes API. """ - properties = self._device_ident.serialize() + properties = self.virtual_device.serialize() properties += b' ' + DeviceSerializer.pack_property( 'mode', self.mode.value) if self.frontend_domain is not None: @@ -1189,6 +1205,8 @@ def _deserialize( expected_device, properties) # we do not need port, we need device del properties['port'] + expected_device._device_id = properties.get( + 'device_id', expected_device.device_id) properties.pop('device_id', None) properties['device'] = expected_device diff --git a/qubes/devices.py b/qubes/devices.py index 54177b2d8..847712f6f 100644 --- a/qubes/devices.py +++ b/qubes/devices.py @@ -204,8 +204,12 @@ async def attach(self, assignment: DeviceAssignment): self._vm,"VM not running, cannot attach device," " do you mean `assign`?") - device = assignment.device - if device in self.get_attached_devices(): + if len(assignment.devices) != 1: + raise ValueError( + f'Cannot attach ambiguous {assignment.devclass} device.') + + device = assignment.devices[0] + if device in [ass.devices[0] for ass in self.get_attached_devices()]: raise DeviceAlreadyAttached( 'device {!s} of class {} already attached to {!s}'.format( device, self._bus, self._vm)) @@ -227,7 +231,7 @@ async def assign(self, assignment: DeviceAssignment): f'Trying to assign {assignment.devclass} device ' f'when {self._bus} device expected.') - device = assignment.device + device = assignment.virtual_device if assignment in self.get_assigned_devices(): raise DeviceAlreadyAssigned( f'{self._bus} device {device!s} ' @@ -264,7 +268,7 @@ async def update_required(self, device: VirtualDevice, required: bool): 'VM must be running to modify device assignment' ) assignments = [a for a in self.get_assigned_devices() - if a.device == device] + if a.virtual_device == device] if not assignments: raise qubes.exc.QubesValueError( f'Device {device} not assigned to {self._vm.name}') @@ -301,35 +305,35 @@ async def detach(self, port: Port): "Can not detach a required device from a non halted qube. " "You need to unassign device first.") - # use the local object - port = assignment.device.port + # use the local object, only one device can match + port = assignment.devices[0].port await self._vm.fire_event_async( 'device-pre-detach:' + self._bus, pre_event=True, port=port) await self._vm.fire_event_async( 'device-detach:' + self._bus, port=port) - async def unassign(self, device_assignment: DeviceAssignment): + async def unassign(self, assignment: DeviceAssignment): """ Unassign device from domain. """ all_ass = [] - for assignment in self.get_assigned_devices(): - all_ass.append(assignment.devclass) - if device_assignment == assignment: + for assign in self.get_assigned_devices(): + all_ass.append(assign.devclass) + if assignment == assign: # load all options - device_assignment = assignment + assignment = assign break else: raise DeviceNotAssigned( - f'{self._bus} device at port {device_assignment}' - f'not assigned to {self._vm!s} | {all_ass} vs {device_assignment.devclass}') + f'{self._bus} device at port {assignment}' + f'not assigned to {self._vm!s} ' + f'| {all_ass} vs {assignment.devclass}') 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]: """ diff --git a/qubes/ext/block.py b/qubes/ext/block.py index af16ae048..b171dc39d 100644 --- a/qubes/ext/block.py +++ b/qubes/ext/block.py @@ -546,27 +546,25 @@ async def on_domain_start(self, vm, _event, **_kwargs): def notify_auto_attached(self, vm, assignment): identity = assignment.device_id - device = assignment.device - if identity not in ('*', device.device_id): - print("Unrecognized identity, skipping attachment of device " - f"from the port {assignment}", file=sys.stderr) - raise qubes.devices.UnrecognizedDevice( - f"Device presented identity {device.device_id} " - f"does not match expected {identity}" - ) + for device in assignment.devices: + if identity not in ('*', device.device_id): + print("Unrecognized identity, skipping attachment of device " + f"from the port {assignment}", file=sys.stderr) + continue - if assignment.mode.value == "ask-to-attach": - if vm.name != confirm_device_attachment(device, {vm: assignment}): - return + if assignment.mode.value == "ask-to-attach": + if vm.name != confirm_device_attachment(device, + {vm: assignment}): + continue - self.pre_attachment_internal( - vm, device, assignment.options, expected_attachment=vm) + self.pre_attachment_internal( + vm, device, assignment.options, expected_attachment=vm) - asyncio.ensure_future(vm.fire_event_async( - 'device-attach:block', - device=device, - options=assignment.options, - )) + asyncio.ensure_future(vm.fire_event_async( + 'device-attach:block', + device=device, + options=assignment.options, + )) async def attach_and_notify(self, vm, assignment): # bypass DeviceCollection logic preventing double attach diff --git a/qubes/ext/utils.py b/qubes/ext/utils.py index 3ff46b96c..a294dbfe8 100644 --- a/qubes/ext/utils.py +++ b/qubes/ext/utils.py @@ -27,6 +27,7 @@ from typing import Type from qubes import device_protocol +from qubes.device_protocol import VirtualDevice def device_list_change( @@ -65,18 +66,22 @@ def device_list_change( if not front_vm.is_running(): continue for assignment in front_vm.devices[devclass].get_assigned_devices(): - if (assignment.backend_domain == vm - and assignment.device_id == assignment.device.device_id - and assignment.port_id in added - and assignment.port_id not in attached - ): - frontends = to_attach.get(assignment.port_id, {}) - frontends[front_vm] = assignment - to_attach[assignment.port_id] = frontends + for device in assignment.devices: + if (device.backend_domain == vm + and assignment.device_id == device.device_id + and device.port_id in added + and device.port_id not in attached + ): + frontends = to_attach.get(device.port_id, {}) + # make it unique + frontends[front_vm] = assignment.clone( + device=VirtualDevice(device.port, device.device_id)) + to_attach[device.port_id] = frontends for port_id, frontends in to_attach.items(): if len(frontends) > 1: - device = tuple(frontends.values())[0].device + # unique + device = tuple(frontends.values())[0].devices[0] target_name = confirm_device_attachment(device, frontends) for front in frontends: if front.name == target_name: diff --git a/qubes/tests/devices.py b/qubes/tests/devices.py index 944ab95d1..cf43f566f 100644 --- a/qubes/tests/devices.py +++ b/qubes/tests/devices.py @@ -50,7 +50,7 @@ def __init__(self, app, name, *args, **kwargs): self.app = app self.name = name self.device = TestDevice( - Port(self, 'testport', 'testclass'), 'testdev') + Port(self, 'testport', 'testclass'), device_id='testdev') self.events_enabled = True self.devices = { 'testclass': qubes.devices.DeviceCollection(self, 'testclass') @@ -147,8 +147,6 @@ def test_011_empty_unassign(self): def test_012_double_attach(self): self.attach() with self.assertRaises(qubes.devices.DeviceAlreadyAttached): - print(self.assignment, file=sys.stderr) # TODO - print(self.assignment.device, file=sys.stderr) # TODO self.loop.run_until_complete( self.collection.attach(self.assignment)) @@ -212,7 +210,7 @@ def test_020_update_required_to_false(self): self.assertEqual( {self.assignment}, set(self.collection.get_assigned_devices())) self.loop.run_until_complete( - self.collection.update_required(self.device.port, False)) + self.collection.update_required(self.device, False)) self.assertEqual( {self.assignment}, set(self.collection.get_assigned_devices())) self.assertEqual( @@ -233,7 +231,7 @@ def test_021_update_required_to_true(self): self.assertEqual({self.assignment}, set(self.collection.get_attached_devices())) self.loop.run_until_complete( - self.collection.update_required(self.device.port, True)) + self.collection.update_required(self.device, True)) self.assertEqual({self.assignment}, set(self.collection.get_assigned_devices())) self.assertEqual({self.assignment}, 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/vm/qubesvm.py b/qubes/vm/qubesvm.py index fa7fd3ec6..5ad582527 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -1184,10 +1184,15 @@ 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 isinstance(device, + qubes.device_protocol.UnknownDevice): + continue + else: + break + else: raise qubes.exc.QubesException( f'{devclass.capitalize()} device {ass} ' f'not available' From e886cb128b0dfbf925fc53ac53dc7715d5d097b0 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Fri, 16 Aug 2024 20:01:34 +0200 Subject: [PATCH 20/35] q-dev: matches --- qubes/device_protocol.py | 11 +++++++++++ qubes/ext/block.py | 3 +-- qubes/ext/utils.py | 4 +--- qubes/tests/devices_block.py | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index 313e7691e..ebd6ad972 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -1211,3 +1211,14 @@ def _deserialize( properties['device'] = expected_device return cls(**properties) + + def matches(self, device: VirtualDevice) -> bool: + if self.backend_domain != '*' and self.backend_domain != device.backend_domain: + return False + if self.port_id != '*' and self.port_id != device.port_id: + return False + if self.devclass != '*' and self.devclass != device.devclass: + return False + if self.device_id != '*' and self.device_id != device.device_id: + return False + return True diff --git a/qubes/ext/block.py b/qubes/ext/block.py index b171dc39d..95d05d68d 100644 --- a/qubes/ext/block.py +++ b/qubes/ext/block.py @@ -545,9 +545,8 @@ async def on_domain_start(self, vm, _event, **_kwargs): self.notify_auto_attached(vm, assignment) def notify_auto_attached(self, vm, assignment): - identity = assignment.device_id for device in assignment.devices: - if identity not in ('*', device.device_id): + if not assignment.matches(device): print("Unrecognized identity, skipping attachment of device " f"from the port {assignment}", file=sys.stderr) continue diff --git a/qubes/ext/utils.py b/qubes/ext/utils.py index a294dbfe8..2841edca1 100644 --- a/qubes/ext/utils.py +++ b/qubes/ext/utils.py @@ -18,7 +18,6 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA. -import importlib import asyncio import subprocess @@ -67,8 +66,7 @@ def device_list_change( continue for assignment in front_vm.devices[devclass].get_assigned_devices(): for device in assignment.devices: - if (device.backend_domain == vm - and assignment.device_id == device.device_id + if (assignment.matches(device) and device.port_id in added and device.port_id not in attached ): diff --git a/qubes/tests/devices_block.py b/qubes/tests/devices_block.py index 7e537889f..e12a56017 100644 --- a/qubes/tests/devices_block.py +++ b/qubes/tests/devices_block.py @@ -731,7 +731,7 @@ def test_062_on_qdb_change_attached(self): '/qubes-block-devices/sda/size': b'1024000', '/qubes-block-devices/sda/mode': b'r', }, domain_xml=domain_xml_template.format("")) - exp_dev = qubes.ext.block.BlockDevice(back_vm, 'sda') + exp_dev = qubes.ext.block.BlockDevice(back_vm, 'sda') self.ext.devices_cache = {'sys-usb': {'sda': None}} From d9ac860042e6b2ae6629f7748198398e34093bf6 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Fri, 16 Aug 2024 23:19:07 +0200 Subject: [PATCH 21/35] q-dev: backend_name --- qubes/api/admin.py | 9 +++++--- qubes/device_protocol.py | 44 ++++++++++++++++++++++++++++------------ qubes/vm/__init__.py | 8 +++++--- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/qubes/api/admin.py b/qubes/api/admin.py index 3f257d7a3..13d7f1e65 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -1314,12 +1314,15 @@ 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) -> DeviceInfo: + 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) # load all info, may raise KeyError, either on domain or port_id - return self.app.domains[ - _dev.backend_domain].devices[devclass][_dev.port_id] + try: + return self.app.domains[ + _dev.backend_domain].devices[devclass][_dev.port_id] + except KeyError: + return _dev # Assign/Unassign action can modify only persistent state of running VM. # For this reason, write=True diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index ebd6ad972..414ad7f4a 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -236,29 +236,35 @@ def __init__(self, backend_domain, port_id, devclass): self.__devclass = devclass def __hash__(self): - return hash((self.backend_domain.name, self.port_id, self.devclass)) + return hash((self.backend_name, self.port_id, self.devclass)) def __eq__(self, other): if isinstance(other, Port): return ( - self.backend_domain == other.backend_domain and - self.port_id == other.port_id and - self.devclass == other.devclass + 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_domain.name, self.devclass, self.port_id) < \ - (other.backend_domain.name, other.devclass, other.port_id) + 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_domain.name}+{self.port_id}" + return f"{self.backend_name}+{self.port_id}" def __str__(self): - return f"{self.backend_domain.name}:{self.port_id}" + return f"{self.backend_name}:{self.port_id}" + + @property + def backend_name(self) -> str: + if self.backend_domain is not None: + return self.backend_domain.name + return "*" @classmethod def from_qarg( @@ -302,7 +308,7 @@ def port_id(self) -> str: return self.__port_id @property - def backend_domain(self) -> QubesVM: + def backend_domain(self) -> Optional[QubesVM]: """ Which domain exposed this port. (immutable)""" return self.__backend_domain @@ -365,6 +371,13 @@ def backend_domain(self): return self.port.backend_domain return '*' + @property + def backend_name(self): + if self.port != '*': + return self.port.backend_name + return '*' + + @property def port_id(self): if self.port != '*' and self.port.port_id is not None: @@ -476,7 +489,8 @@ def _parse( ) -> 'VirtualDevice': if backend is None: backend_name, identity = representation.split(sep, 1) - backend = get_domain(backend_name) + if backend_name != '*': + backend = get_domain(backend_name) else: identity = representation port_id, _, devid = identity.partition(':') @@ -1068,15 +1082,19 @@ def __lt__(self, other): @property def backend_domain(self): - return self.virtual_device.port.backend_domain + return self.virtual_device.backend_domain + + @property + def backend_name(self) -> str: + return self.virtual_device.backend_name @property def port_id(self): - return self.virtual_device.port.port_id + return self.virtual_device.port_id @property def devclass(self): - return self.virtual_device.port.devclass + return self.virtual_device.devclass @property def device_id(self): diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index b9c3f3fc8..87e3fa389 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -310,11 +310,13 @@ def load_extras(self): 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( qubes.device_protocol.VirtualDevice( qubes.device_protocol.Port( - backend_domain=self.app.domains[ - node.get('backend-domain')], + backend_domain=backend, port_id=node.get('id', '*'), devclass=devclass, ), @@ -377,7 +379,7 @@ def __xml__(self): devices.set('class', devclass) for assignment in self.devices[devclass].get_assigned_devices(): node = lxml.etree.Element('device') - node.set('backend-domain', assignment.backend_domain.name) + 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 '*' From f37afb75ab838782d8e21a9a664093adf3362e60 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Sat, 17 Aug 2024 11:04:49 +0200 Subject: [PATCH 22/35] q-dev: device_protocol --- qubes/api/admin.py | 25 +++++++++++++++---------- qubes/device_protocol.py | 25 +++++++++++++------------ qubes/devices.py | 6 ++---- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/qubes/api/admin.py b/qubes/api/admin.py index 13d7f1e65..d1546e0b4 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 Port, VirtualDevice, DeviceInfo +from qubes.device_protocol import (Port, VirtualDevice, UnknownDevice, + DeviceAssignment) class QubesMgmtEventsDispatcher: @@ -1317,10 +1318,15 @@ async def vm_device_assign(self, endpoint, untrusted_payload): 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: - return self.app.domains[ + dev = self.app.domains[ _dev.backend_domain].devices[devclass][_dev.port_id] + if isinstance(dev, UnknownDevice): + return _dev + return dev except KeyError: return _dev @@ -1331,14 +1337,15 @@ def load_device_info(self, devclass) -> VirtualDevice: endpoints=( ep.name for ep in importlib.metadata.entry_points(group='qubes.devices')), - no_payload=True, scope='local', write=True) - async def vm_device_unassign(self, endpoint): + scope='local', write=True) + async def vm_device_unassign(self, endpoint, untrusted_payload): devclass = endpoint dev = self.load_device_info(devclass) + assignment = DeviceAssignment.deserialize( + untrusted_payload, expected_device=dev) self.fire_event_for_permission(device=dev, devclass=devclass) - assignment = qubes.device_protocol.DeviceAssignment(dev) await self.dest.devices[devclass].unassign(assignment) self.app.save() @@ -1349,14 +1356,12 @@ async def vm_device_unassign(self, endpoint): endpoints=( ep.name for ep in importlib.metadata.entry_points(group='qubes.devices')), - scope='local', execute=True) - async def vm_device_attach(self, endpoint, untrusted_payload): + no_payload=True, scope='local', execute=True) + async def vm_device_attach(self, endpoint): devclass = endpoint dev = self.load_device_info(devclass) - assignment = qubes.device_protocol.DeviceAssignment.deserialize( - untrusted_payload, expected_device=dev - ) + assignment = DeviceAssignment(dev) self.fire_event_for_permission( device=dev, devclass=devclass, diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index 414ad7f4a..37785a7a0 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -143,7 +143,7 @@ def parse_basic_device_properties( the expected values. """ expected = expected_device.port - exp_vm_name = expected.backend_domain.name + 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']}" @@ -262,7 +262,7 @@ def __str__(self): @property def backend_name(self) -> str: - if self.backend_domain is not None: + if self.backend_domain not in (None, "*"): return self.backend_domain.name return "*" @@ -305,7 +305,9 @@ def port_id(self) -> str: Unique for given domain and devclass. """ - return self.__port_id + if self.__port_id is not None: + return self.__port_id + return '*' @property def backend_domain(self) -> Optional[QubesVM]: @@ -336,7 +338,7 @@ def __init__( port: Optional[Port] = None, device_id: Optional[str] = None, ): - # TODO! one of them cannot be None + assert port is not None or device_id is not None self.port: Optional[Port] = port self._device_id = device_id @@ -377,7 +379,6 @@ def backend_name(self): return self.port.backend_name return '*' - @property def port_id(self): if self.port != '*' and self.port.port_id is not None: @@ -494,8 +495,8 @@ def _parse( else: identity = representation port_id, _, devid = identity.partition(':') - if devid in ('', '*'): - devid = '*' + if devid == '': + devid = None return cls( Port(backend_domain=backend, port_id=port_id, devclass=devclass), device_id=devid @@ -513,7 +514,7 @@ def serialize(self) -> bytes: ('devclass', self.devclass))) properties += b' ' + DeviceSerializer.pack_property( - 'backend_domain', self.backend_domain.name) + 'backend_domain', self.backend_name) return properties @@ -1141,7 +1142,7 @@ def attached(self) -> bool: Returns False if device is attached to different domain """ for device in self.devices: - if device.attachment == self.frontend_domain: + if device.attachment and device.attachment == self.frontend_domain: return True return False @@ -1231,11 +1232,11 @@ def _deserialize( return cls(**properties) def matches(self, device: VirtualDevice) -> bool: - if self.backend_domain != '*' and self.backend_domain != device.backend_domain: + if self.devclass != device.devclass: return False - if self.port_id != '*' and self.port_id != device.port_id: + if self.backend_domain != device.backend_domain: return False - if self.devclass != '*' and self.devclass != device.devclass: + if self.port_id != '*' and self.port_id != device.port_id: return False if self.device_id != '*' and self.device_id != device.device_id: return False diff --git a/qubes/devices.py b/qubes/devices.py index 847712f6f..498468327 100644 --- a/qubes/devices.py +++ b/qubes/devices.py @@ -319,16 +319,14 @@ async def unassign(self, assignment: DeviceAssignment): """ all_ass = [] for assign in self.get_assigned_devices(): - all_ass.append(assign.devclass) + all_ass.append(assign) if assignment == assign: # load all options assignment = assign break else: raise DeviceNotAssigned( - f'{self._bus} device at port {assignment}' - f'not assigned to {self._vm!s} ' - f'| {all_ass} vs {assignment.devclass}') + f'{self._bus} device {assignment} not assigned to {self._vm!s}') self._set.discard(assignment) From 18b95027c04a680517656714fc1181e5c640fb03 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Mon, 19 Aug 2024 08:51:20 +0200 Subject: [PATCH 23/35] q-dev: cleanup --- qubes/api/admin.py | 15 +++++++-------- qubes/devices.py | 2 -- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/qubes/api/admin.py b/qubes/api/admin.py index d1546e0b4..0d9238204 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -1337,12 +1337,11 @@ def load_device_info(self, devclass) -> VirtualDevice: endpoints=( ep.name for ep in importlib.metadata.entry_points(group='qubes.devices')), - scope='local', write=True) - async def vm_device_unassign(self, endpoint, untrusted_payload): + no_payload=True, scope='local', write=True) + async def vm_device_unassign(self, endpoint): devclass = endpoint dev = self.load_device_info(devclass) - assignment = DeviceAssignment.deserialize( - untrusted_payload, expected_device=dev) + assignment = DeviceAssignment(dev) self.fire_event_for_permission(device=dev, devclass=devclass) @@ -1356,12 +1355,12 @@ async def vm_device_unassign(self, endpoint, untrusted_payload): endpoints=( ep.name for ep in importlib.metadata.entry_points(group='qubes.devices')), - no_payload=True, scope='local', execute=True) - async def vm_device_attach(self, endpoint): + scope='local', execute=True) + async def vm_device_unassign(self, endpoint, untrusted_payload): devclass = endpoint dev = self.load_device_info(devclass) - - assignment = DeviceAssignment(dev) + assignment = DeviceAssignment.deserialize( + untrusted_payload, expected_device=dev) self.fire_event_for_permission( device=dev, devclass=devclass, diff --git a/qubes/devices.py b/qubes/devices.py index 498468327..c30344e40 100644 --- a/qubes/devices.py +++ b/qubes/devices.py @@ -59,7 +59,6 @@ `device-list-change:class` event. """ import itertools -import sys from typing import Iterable import qubes.exc @@ -348,7 +347,6 @@ def get_attached_devices(self) -> Iterable[DeviceAssignment]: for dev, options in attached: for assignment in self._set: if dev == assignment: - print("ok", file=sys.stderr) yield assignment break else: From 2c110ca89a17615921a9c3a07728d2266ff6a5d4 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Mon, 19 Aug 2024 13:37:21 +0200 Subject: [PATCH 24/35] q-dev: fixes --- qubes/api/admin.py | 13 ++++++++----- qubes/device_protocol.py | 2 +- qubes/tests/devices_block.py | 2 -- templates/libvirt/xen.xml | 4 ++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/qubes/api/admin.py b/qubes/api/admin.py index 0d9238204..0efca4b46 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -27,6 +27,7 @@ import string import subprocess import pathlib +import sys import libvirt import lxml.etree @@ -1273,22 +1274,24 @@ async def vm_device_attached(self, endpoint): raise qubes.exc.QubesException("qubesd shutdown in progress") raise if self.arg: - select_backend, select_ident = self.arg.split('+', 1) + select_backend, select_ident = self.arg.split('+', 1) # TODO device_assignments = [dev for dev in device_assignments 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}' f'+{assignment.port_id}:{assignment.device_id}'): assignment.serialize().decode('ascii', errors="ignore") for assignment in device_assignments} - return ''.join('{} {}\n'.format(port_id, dev_info[port_id]) for port_id in sorted(dev_info)) @@ -1356,7 +1359,7 @@ async def vm_device_unassign(self, endpoint): ep.name for ep in importlib.metadata.entry_points(group='qubes.devices')), scope='local', execute=True) - async def vm_device_unassign(self, endpoint, untrusted_payload): + async def vm_device_attach(self, endpoint, untrusted_payload): devclass = endpoint dev = self.load_device_info(devclass) assignment = DeviceAssignment.deserialize( diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index 37785a7a0..b2e88ad12 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -1032,7 +1032,7 @@ def __init__( mode: Union[str, AssignmentMode] = "manual", ): if isinstance(device, DeviceInfo): - device = VirtualDevice(device.port, device._device_id) + device = VirtualDevice(device.port, device.device_id) self.virtual_device = device self.__options = options or {} if isinstance(mode, AssignmentMode): diff --git a/qubes/tests/devices_block.py b/qubes/tests/devices_block.py index e12a56017..97e0702ab 100644 --- a/qubes/tests/devices_block.py +++ b/qubes/tests/devices_block.py @@ -673,8 +673,6 @@ def test_060_on_qdb_change_added(self): self.ext.on_qdb_change(back_vm, None, None) self.assertEqual(self.ext.devices_cache, {'sys-usb': {'sda': None}}) - print(back_vm.fired_events, file=sys.stderr) # TODO - print(exp_dev, file=sys.stderr) # TODO self.assertEqual( back_vm.fired_events[ ('device-added:block', frozenset({('device', exp_dev)}))], 1) diff --git a/templates/libvirt/xen.xml b/templates/libvirt/xen.xml index 95b59a0b6..c4d2f819d 100644 --- a/templates/libvirt/xen.xml +++ b/templates/libvirt/xen.xml @@ -157,7 +157,7 @@ {# start external devices from xvdi #} {% set counter = {'i': 4} %} {% for assignment in vm.devices.block.get_assigned_devices(True) %} - {% set device = assignment.device %} + {% set device = assignment.devices[0] %} {% set options = assignment.options %} {% include 'libvirt/devices/block.xml' %} {% endfor %} @@ -167,7 +167,7 @@ {% endif %} {% for assignment in vm.devices.pci.get_assigned_devices(True) %} - {% set device = assignment.device %} + {% set device = assignment.devices[0] %} {% set options = assignment.options %} {% set power_mgmt = vm.app.domains[0].features.get('suspend-s0ix', False) %} From d3f44ae32be01900166c3dedbce35b835ad36a12 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Tue, 20 Aug 2024 00:24:03 +0200 Subject: [PATCH 25/35] q-dev: deny list --- qubes/api/admin.py | 5 ++--- qubes/device_protocol.py | 12 ++++++++++++ qubes/devices.py | 2 ++ qubes/ext/admin.py | 39 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/qubes/api/admin.py b/qubes/api/admin.py index 0efca4b46..9258ca136 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -1366,9 +1366,8 @@ async def vm_device_attach(self, endpoint, untrusted_payload): 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 ) diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index b2e88ad12..182f84f09 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -698,6 +698,18 @@ def _load_classes(bus: str): return result + def matches(self, other: 'DeviceInterface') -> bool: + pattern = repr(self) + candidate = repr(other) + if len(pattern) != len(candidate): + return False + for p, c in zip(pattern, candidate): + if p == '*': + continue + if p != c: + return False + return True + class DeviceInfo(VirtualDevice): """ Holds all information about a device """ diff --git a/qubes/devices.py b/qubes/devices.py index c30344e40..23cbcb4f7 100644 --- a/qubes/devices.py +++ b/qubes/devices.py @@ -67,6 +67,8 @@ DeviceAssignment, VirtualDevice) +DEVICE_DENY_LIST = "/etc/qubes/device-deny.list" + class DeviceNotAssigned(qubes.exc.QubesException, KeyError): """ Trying to unassign not assigned device. diff --git a/qubes/ext/admin.py b/qubes/ext/admin.py index 4abc04418..40a83b58d 100644 --- a/qubes/ext/admin.py +++ b/qubes/ext/admin.py @@ -16,6 +16,8 @@ # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, see . +import importlib +import sys import qubes.api import qubes.api.internal @@ -24,6 +26,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 +165,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_usb_device_attacg( + self, vm, event, dest, arg, device, mode, options, **kwargs + ): + # ignore auto-attachment + if mode != 'manual': + return + + # load device deny list + deny = {} + try: + with open(DEVICE_DENY_LIST, 'r') as file: + for line in file: + line = line.strip() + + if line: + name, *values = line.split() + + values = ' '.join(values).replace(',', ' ').split() + values = set([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() + From ed705a2acb8e2f989ec948b90d77c3a21716617a Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Tue, 20 Aug 2024 07:39:02 +0200 Subject: [PATCH 26/35] q-dev: assignment.device --- qubes/device_protocol.py | 17 +++++++++++++++++ qubes/devices.py | 6 +++--- qubes/ext/utils.py | 2 +- templates/libvirt/xen.xml | 18 ++++++++++-------- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index 182f84f09..c22f16860 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -1125,6 +1125,23 @@ def devices(self) -> List[DeviceInfo]: result.append(dev) return result + @property + def device(self) -> DeviceInfo: + """ + 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. + """ + devices = self.devices + if len(devices) == 1: + return devices[0] + if len(devices) > 1: + raise ProtocolError("Too many devices matches to assignment") + if len(devices) == 0: + raise ProtocolError("Any devices matches to assignment") + @property def port(self) -> Port: """ diff --git a/qubes/devices.py b/qubes/devices.py index 23cbcb4f7..3f7c26b15 100644 --- a/qubes/devices.py +++ b/qubes/devices.py @@ -209,8 +209,8 @@ async def attach(self, assignment: DeviceAssignment): raise ValueError( f'Cannot attach ambiguous {assignment.devclass} device.') - device = assignment.devices[0] - if device in [ass.devices[0] for ass in self.get_attached_devices()]: + device = assignment.device + 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)) @@ -307,7 +307,7 @@ async def detach(self, port: Port): "You need to unassign device first.") # use the local object, only one device can match - port = assignment.devices[0].port + port = assignment.device.port await self._vm.fire_event_async( 'device-pre-detach:' + self._bus, pre_event=True, port=port) diff --git a/qubes/ext/utils.py b/qubes/ext/utils.py index 2841edca1..4a7ae9bf1 100644 --- a/qubes/ext/utils.py +++ b/qubes/ext/utils.py @@ -79,7 +79,7 @@ def device_list_change( for port_id, frontends in to_attach.items(): if len(frontends) > 1: # unique - device = tuple(frontends.values())[0].devices[0] + device = tuple(frontends.values())[0].device target_name = confirm_device_attachment(device, frontends) for front in frontends: if front.name == target_name: diff --git a/templates/libvirt/xen.xml b/templates/libvirt/xen.xml index c4d2f819d..80acf3397 100644 --- a/templates/libvirt/xen.xml +++ b/templates/libvirt/xen.xml @@ -157,9 +157,10 @@ {# start external devices from xvdi #} {% set counter = {'i': 4} %} {% for assignment in vm.devices.block.get_assigned_devices(True) %} - {% set device = assignment.devices[0] %} - {% set options = assignment.options %} - {% include 'libvirt/devices/block.xml' %} + {% 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.devices[0] %} - {% 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' %} From 53a3f9afa7d3edcada75a486ebbfaa79350584c4 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Tue, 20 Aug 2024 16:27:51 +0200 Subject: [PATCH 27/35] q-dev: error handling --- qubes/ext/utils.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/qubes/ext/utils.py b/qubes/ext/utils.py index 4a7ae9bf1..4c005c0a5 100644 --- a/qubes/ext/utils.py +++ b/qubes/ext/utils.py @@ -20,6 +20,7 @@ # USA. import asyncio import subprocess +import sys import qubes @@ -138,11 +139,15 @@ def confirm_device_attachment(device, frontends) -> str: guivm = 'dom0' # TODO # TODO: guivm rpc? - proc = subprocess.Popen( - ["attach-confirm", guivm, - 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() + try: + proc = subprocess.Popen( + ["attach-confirm", guivm, + 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(exc, file=sys.stderr) + return "" From 5ec0b6820690db11d8633f938db549ecbec18ca7 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Tue, 20 Aug 2024 17:57:33 +0200 Subject: [PATCH 28/35] q-dev: add tests --- qubes/api/admin.py | 4 +- qubes/devices.py | 7 +- qubes/ext/admin.py | 2 +- qubes/tests/api_admin.py | 283 +++++++++++++++++++++++++-------------- qubes/tests/devices.py | 23 ++++ 5 files changed, 216 insertions(+), 103 deletions(-) diff --git a/qubes/api/admin.py b/qubes/api/admin.py index 9258ca136..960acc67d 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -1274,7 +1274,7 @@ async def vm_device_attached(self, endpoint): raise qubes.exc.QubesException("qubesd shutdown in progress") raise if self.arg: - select_backend, select_ident = self.arg.split('+', 1) # TODO + select_backend, select_ident = self.arg.split('+', 1) device_assignments = [dev for dev in device_assignments if (str(dev.backend_domain), dev.port_id) == (select_backend, select_ident)] @@ -1414,7 +1414,7 @@ async def vm_device_set_required(self, endpoint, untrusted_payload): 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=assignment) await self.dest.devices[devclass].update_required(dev, assignment) self.app.save() diff --git a/qubes/devices.py b/qubes/devices.py index 3f7c26b15..3addbebc1 100644 --- a/qubes/devices.py +++ b/qubes/devices.py @@ -238,6 +238,9 @@ async def assign(self, assignment: DeviceAssignment): 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) await self._vm.fire_event_async( @@ -281,8 +284,10 @@ async def update_required(self, device: VirtualDevice, required: bool): if assignment.required == required: return - assignments[0] = assignment.clone( + new_assignment = assignment.clone( mode='required' if required else 'auto-attach') + self._set.discard(assignment) + self._set.add(new_assignment) await self._vm.fire_event_async( 'device-assignment-changed:' + self._bus, device=device) diff --git a/qubes/ext/admin.py b/qubes/ext/admin.py index 40a83b58d..a06a1ef2c 100644 --- a/qubes/ext/admin.py +++ b/qubes/ext/admin.py @@ -168,7 +168,7 @@ def on_tag_add(self, vm, event, tag, **kwargs): @qubes.ext.handler(*(f'admin-permission:admin.vm.device.{ep.name}.Attach' for ep in importlib.metadata.entry_points(group='qubes.devices'))) - def on_usb_device_attacg( + def on_device_attach( self, vm, event, dest, arg, device, mode, options, **kwargs ): # ignore auto-attachment diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index fa4366e59..dcce64303 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( - Port(self.vm, '1234', 'testclass'), product='Some device') + dev = DeviceInfo(Port( + self.vm, '1234', 'testclass'), product='Some device') dev.extra_prop = 'xx' yield dev - dev = qubes.device_protocol.DeviceInfo( - Port(self.vm, '4321', 'testclass'), 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' " - "device_id='0000:0000::?******' vendor='unknown' " - "devclass='peripheral' product='Some_device' port_id='1234' " - "name='unknown' backend_domain='test-vm1' interfaces='?******'\n" - "4321 serial='unknown' manufacturer='unknown' " - "device_id='0000:0000::?******' vendor='unknown' " - "devclass='peripheral' product='Some_other_device' " - "port_id='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' " - "device_id='0000:0000::?******' vendor='unknown' " - "devclass='peripheral' product='Some_other_device' " - "port_id='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,36 +1759,35 @@ 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( - qubes.device_protocol.Port(self.vm, '1234', 'test'), - mode='required') + 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' " - "port_id='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( - qubes.device_protocol.Port(self.vm, '1234', 'test'), + 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', 'test', mode='required') + 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' " - "port_id='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' " - "port_id='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): @@ -1801,35 +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' " - "port_id='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( - qubes.device_protocol.Port(self.vm, '1234', 'test'), - mode='required') + 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( - qubes.device_protocol.Port(self.vm, '4321', 'test'), - mode='required') + 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' " - "port_id='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(Port(self.vm, '1234', 'testclass')) + dev = qubes.device_protocol.DeviceInfo( + Port(self.vm, '1234', 'testclass')) yield (dev, {'attach_opt': 'value'}) - dev = qubes.device_protocol.DeviceInfo(Port(self.vm, '1234', '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): @@ -1838,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' " - "port_id='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): @@ -1852,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', @@ -1873,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', @@ -1895,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', @@ -1914,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) @@ -1929,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'], @@ -1948,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'], @@ -1967,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( @@ -1986,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( @@ -1995,9 +1999,83 @@ 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( - qubes.device_protocol.Port(self.vm, '1234', 'test'), + 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)) @@ -2008,16 +2086,17 @@ 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( - qubes.device_protocol.Port(self.vm, '1234', 'test'), + 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)) @@ -2029,16 +2108,17 @@ 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( - qubes.device_protocol.Port(self.vm, '1234', 'test'), + 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)) @@ -2048,15 +2128,16 @@ 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( - qubes.device_protocol.Port(self.vm, '1234', 'test'), + 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)) @@ -2066,18 +2147,19 @@ 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( - qubes.device_protocol.Port(self.vm, '1234', 'test'), + 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)) @@ -2087,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): @@ -2102,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) @@ -2117,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', - port=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): @@ -2163,8 +2245,8 @@ 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( - qubes.device_protocol.Port(self.vm, '1234', 'test'), + assignment = DeviceAssignment(VirtualDevice(Port( + self.vm, '1234', 'testclass')), mode='required') self.loop.run_until_complete( self.vm2.devices['testclass'].assign(assignment)) @@ -2863,8 +2945,8 @@ def test_642_vm_create_disposable_not_allowed(self, storage_mock): self.assertFalse(self.app.save.called) def test_650_vm_device_set_required_true(self): - assignment = qubes.device_protocol.DeviceAssignment( - qubes.device_protocol.Port(self.vm, '1234', 'test'), + 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)) @@ -2877,10 +2959,11 @@ def test_650_vm_device_set_required_true(self): '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'test-vm1', b'test-vm1+1234:bee', b'True') self.assertIsNone(value) - dev = qubes.device_protocol.DeviceInfo(Port(self.vm, '1234', 'testclass')) + 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) @@ -2889,12 +2972,12 @@ def test_650_vm_device_set_required_true(self): 'admin-permission:admin.vm.device.testclass.Set.required') mock_action.assert_called_once_with( self.vm, f'device-assignment-changed:testclass', - device=self.vm.devices['testclass']['1234']) + 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( - qubes.device_protocol.Port(self.vm, '1234', 'test'), + 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)) @@ -2907,10 +2990,11 @@ def test_651_vm_device_set_required_false(self): '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'test-vm1', b'test-vm1+1234:bee', b'False') self.assertIsNone(value) - dev = qubes.device_protocol.DeviceInfo(Port(self.vm, '1234', 'testclass')) + 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) @@ -2919,12 +3003,12 @@ def test_651_vm_device_set_required_false(self): 'admin-permission:admin.vm.device.testclass.Set.required') mock_action.assert_called_once_with( self.vm, f'device-assignment-changed:testclass', - device=self.vm.devices['testclass']['1234']) + 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( - qubes.device_protocol.Port(self.vm, '1234', 'test'), + 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)) @@ -2932,17 +3016,18 @@ def test_652_vm_device_set_required_true_unchanged(self): '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'test-vm1', b'test-vm1+1234:bee', b'True') self.assertIsNone(value) - dev = qubes.device_protocol.DeviceInfo(Port(self.vm, '1234', 'testclass')) + 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.app.save.assert_called_once_with() def test_653_vm_device_set_required_false_unchanged(self): - assignment = qubes.device_protocol.DeviceAssignment( - qubes.device_protocol.Port(self.vm, '1234', 'test'), + assignment = DeviceAssignment(VirtualDevice(Port( + self.vm, '1234', 'testclass')), mode='auto-attach', options={'opt1': 'value'}) self.loop.run_until_complete( self.vm.devices['testclass'].assign(assignment)) diff --git a/qubes/tests/devices.py b/qubes/tests/devices.py index cf43f566f..2b6b92da9 100644 --- a/qubes/tests/devices.py +++ b/qubes/tests/devices.py @@ -297,6 +297,29 @@ def test_035_unassign(self): 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') + self.assertEventFired(self.emitter, 'device-unassign:testclass') + def test_040_detach_required(self): self.loop.run_until_complete(self.collection.assign(self.assignment)) self.attach() From e47795af86b1789e269d5b295a5c42a2f0c95cc1 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Wed, 21 Aug 2024 00:39:17 +0200 Subject: [PATCH 29/35] q-dev: fix block auto-attach --- qubes/api/admin.py | 2 +- qubes/device_protocol.py | 4 ++- qubes/ext/block.py | 53 ++++++++++++++++++++---------------- qubes/ext/utils.py | 21 ++++++++------ qubes/tests/devices_block.py | 3 +- 5 files changed, 48 insertions(+), 35 deletions(-) diff --git a/qubes/api/admin.py b/qubes/api/admin.py index 960acc67d..5de6f7fc8 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -1220,7 +1220,7 @@ async def vm_device_available(self, endpoint): devices = self.fire_event_for_filter(devices, devclass=devclass) dev_info = {f'{dev.port_id}:{dev.device_id}': dev.serialize().decode() for dev in devices} - return ''.join('{} {}\n'.format(port_id, dev_info[port_id]) + 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 diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index c22f16860..0831528ce 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -893,7 +893,7 @@ def serialize(self) -> bytes: if getattr(self, key) != getattr(default, key))) if self.attachment: - properties = DeviceSerializer.pack_property( + properties += b' ' + DeviceSerializer.pack_property( 'attachment', self.attachment.name) properties += b' ' + DeviceSerializer.pack_property( @@ -1120,6 +1120,8 @@ def devices(self) -> List[DeviceInfo]: # 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) diff --git a/qubes/ext/block.py b/qubes/ext/block.py index 95d05d68d..4de9355f1 100644 --- a/qubes/ext/block.py +++ b/qubes/ext/block.py @@ -541,34 +541,41 @@ 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) - - def notify_auto_attached(self, vm, assignment): - for device in assignment.devices: - if not assignment.matches(device): - print("Unrecognized identity, skipping attachment of device " - f"from the port {assignment}", file=sys.stderr) + 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 - - if assignment.mode.value == "ask-to-attach": - if vm.name != confirm_device_attachment(device, - {vm: assignment}): + # TODO: notify? + for device in assignment.devices: + if isinstance(device, qubes.device_protocol.UnknownDevice): continue - - self.pre_attachment_internal( - vm, device, assignment.options, expected_attachment=vm) - - asyncio.ensure_future(vm.fire_event_async( - 'device-attach:block', - device=device, - options=assignment.options, - )) + 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=qubes.device_protocol.VirtualDevice( + device.port, device.device_id)) + for assignment in to_attach.values(): + await 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, assignment) + 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=str(device), options=assignment.options) @qubes.ext.handler('domain-shutdown') async def on_domain_shutdown(self, vm, event, **_kwargs): diff --git a/qubes/ext/utils.py b/qubes/ext/utils.py index 4c005c0a5..e0782fe17 100644 --- a/qubes/ext/utils.py +++ b/qubes/ext/utils.py @@ -43,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}', port=dev.port)) - 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) + 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) @@ -73,8 +73,12 @@ def device_list_change( ): frontends = to_attach.get(device.port_id, {}) # make it unique - frontends[front_vm] = assignment.clone( + 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(): @@ -137,7 +141,6 @@ def compare_device_cache(vm, devices_cache, current_devices): def confirm_device_attachment(device, frontends) -> str: guivm = 'dom0' # TODO - # TODO: guivm rpc? try: proc = subprocess.Popen( diff --git a/qubes/tests/devices_block.py b/qubes/tests/devices_block.py index 97e0702ab..f2a662372 100644 --- a/qubes/tests/devices_block.py +++ b/qubes/tests/devices_block.py @@ -668,7 +668,7 @@ def test_060_on_qdb_change_added(self): '/qubes-block-devices/sda/size': b'1024000', '/qubes-block-devices/sda/mode': b'r', }, domain_xml=domain_xml_template.format("")) - exp_dev = qubes.ext.block.BlockDevice(back_vm, 'sda') + exp_dev = qubes.ext.block.BlockDevice(back_vm, 'sda') self.ext.on_qdb_change(back_vm, None, None) @@ -713,6 +713,7 @@ def test_061_on_qdb_change_auto_attached(self): # In the case of block devices it is the same, # but notify_auto_attached is synchronous + # TODO! self.ext.attach_and_notify = self.ext.notify_auto_attached with mock.patch('asyncio.ensure_future'): self.ext.on_qdb_change(back_vm, None, None) From 4dbfa2833b28a1863be25f030fa9ed8f2a30b662 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Mon, 26 Aug 2024 12:09:32 +0200 Subject: [PATCH 30/35] q-dev: add block devices tests --- qubes/ext/block.py | 10 +- qubes/ext/utils.py | 3 +- qubes/tests/devices_block.py | 465 +++++++++++++++++++++++++---------- 3 files changed, 346 insertions(+), 132 deletions(-) diff --git a/qubes/ext/block.py b/qubes/ext/block.py index 4de9355f1..6424a862e 100644 --- a/qubes/ext/block.py +++ b/qubes/ext/block.py @@ -552,6 +552,8 @@ async def on_domain_start(self, vm, _event, **_kwargs): 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 " @@ -560,11 +562,9 @@ async def on_domain_start(self, vm, _event, **_kwargs): # chose first assignment (the most specific) and ignore rest if device not in to_attach: # make it unique - to_attach[device] = assignment.clone( - device=qubes.device_protocol.VirtualDevice( - device.port, device.device_id)) + to_attach[device] = assignment.clone(device=device) for assignment in to_attach.values(): - await self.attach_and_notify(vm, assignment) + asyncio.ensure_future(self.attach_and_notify(vm, assignment)) async def attach_and_notify(self, vm, assignment): # bypass DeviceCollection logic preventing double attach @@ -575,7 +575,7 @@ async def attach_and_notify(self, vm, assignment): self.on_device_pre_attached_block( vm, 'device-pre-attach:block', device, assignment.options) await vm.fire_event_async( - 'device-attach:block', device=str(device), options=assignment.options) + 'device-attach:block', device=device, options=assignment.options) @qubes.ext.handler('domain-shutdown') async def on_domain_shutdown(self, vm, event, **_kwargs): diff --git a/qubes/ext/utils.py b/qubes/ext/utils.py index e0782fe17..05d43f619 100644 --- a/qubes/ext/utils.py +++ b/qubes/ext/utils.py @@ -65,7 +65,8 @@ def device_list_change( for front_vm in vm.app.domains: if not front_vm.is_running(): continue - for assignment in front_vm.devices[devclass].get_assigned_devices(): + 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 diff --git a/qubes/tests/devices_block.py b/qubes/tests/devices_block.py index f2a662372..298da302c 100644 --- a/qubes/tests/devices_block.py +++ b/qubes/tests/devices_block.py @@ -18,15 +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 sys +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, Port, DeviceInfo, \ - DeviceAssignment + 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 = ''' @@ -136,6 +146,11 @@ def __init__(self, backend_vm, devclass): def get_assigned_devices(self): return self._assigned + 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.port_id == port_id: @@ -177,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): @@ -444,12 +469,7 @@ def test_033_list_attached_cdrom(self): 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, {}) @@ -464,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, @@ -485,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, @@ -507,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): @@ -521,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): @@ -535,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): @@ -548,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): @@ -562,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, {}) @@ -583,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'}) @@ -604,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'}) @@ -625,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' @@ -648,12 +618,7 @@ def test_050_detach(self): 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') @@ -662,12 +627,8 @@ def test_051_detach_not_attached(self): 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("")) + 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) @@ -677,15 +638,12 @@ def test_060_on_qdb_change_added(self): 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 = qubes.ext.block.BlockDevice(back_vm, 'sda') - front = TestVM({}, domain_xml=domain_xml_template.format(""), + @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("")) @@ -706,30 +664,131 @@ def test_061_on_qdb_change_auto_attached(self): dom0.devices['block'] = TestDeviceCollection( backend_vm=dom0, devclass='block') - front.devices['block']._assigned.append( - DeviceAssignment(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() + + 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')) - # In the case of block devices it is the same, - # but notify_auto_attached is synchronous - # TODO! - self.ext.attach_and_notify = self.ext.notify_auto_attached + 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_062_on_qdb_change_attached(self): + 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_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("")) + 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}} @@ -771,14 +830,9 @@ 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("")) + 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') @@ -836,17 +890,12 @@ def test_063_on_qdb_change_changed(self): 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 = qubes.ext.block.BlockDevice(back_vm, 'sda') + exp_dev = qubes.ext.block.BlockDevice(back_vm, 'sda') disk = ''' @@ -858,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}) @@ -891,5 +940,169 @@ def test_064_on_qdb_change_removed_attached(self): '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() From 3a60033c2eed77fa9b6e7f4f31aa005fd21a7165 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Mon, 26 Aug 2024 20:11:00 +0200 Subject: [PATCH 31/35] q-dev: update device_protocol.py --- qubes/device_protocol.py | 159 ++++++++++++++++++++++++++------------- 1 file changed, 106 insertions(+), 53 deletions(-) diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index 0831528ce..8863b649d 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -52,6 +52,9 @@ def qbool(value): class DeviceSerializer: + """ + Group of method for serialization of device properties. + """ ALLOWED_CHARS_KEY = set( string.digits + string.ascii_letters + r"!#$%&()*+,-./:;<>?@[\]^_{|}~") @@ -175,7 +178,7 @@ def parse_basic_device_properties( f"Unrecognized device identity '{properties['device_id']}' " f"expected '{expected_device.device_id}'" ) - expected._device_id = properties.get('device_id', expected_devid) + properties['device_id'] = properties.get('device_id', expected_devid) properties['port'] = expected @@ -203,8 +206,8 @@ def sanitize_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 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 @@ -227,7 +230,7 @@ class Port: Attributes: backend_domain (QubesVM): The domain which exposes devices, e.g.`sys-usb`. - port_id (str): A unique identifier for the port within the backend domain. + 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): @@ -262,6 +265,7 @@ def __str__(self): @property def backend_name(self) -> str: + # pylint: disable=missing-function-docstring if self.backend_domain not in (None, "*"): return self.backend_domain.name return "*" @@ -270,6 +274,9 @@ def backend_name(self) -> str: 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: @@ -280,6 +287,9 @@ def from_qarg( def from_str( cls, representation: str, devclass, domains, blind=False ) -> 'Port': + """ + Parse string : to retrieve Port. + """ if blind: get_domain = domains.get_blind else: @@ -294,6 +304,9 @@ def _parse( 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) @@ -342,7 +355,7 @@ def __init__( self.port: Optional[Port] = port self._device_id = device_id - def clone(self, **kwargs): + def clone(self, **kwargs) -> 'VirtualDevice': """ Clone object and substitute attributes with explicitly given. """ @@ -351,48 +364,67 @@ def clone(self, **kwargs): "device_id": self.device_id, } attr.update(kwargs) - return self.__class__(**attr) + return VirtualDevice(**attr) @property - def port(self): + def port(self) -> Union[Port, str]: + # pylint: disable=missing-function-docstring return self._port @port.setter - def port(self, value): + 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): + def device_id(self) -> str: + # pylint: disable=missing-function-docstring if self._device_id is not None: return self._device_id return '*' @property - def backend_domain(self): + 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): + 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): + 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): + 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): + def description(self) -> str: + """ + Return human-readable description of the device identity. + """ if self.device_id == '*': return 'any device' return self.device_id @@ -429,17 +461,16 @@ def __lt__(self, other): if self.port != '*' and other.port == '*': return False reprs = {self: [self.port], other: [other.port]} - for obj in reprs: + for obj, obj_repr in reprs.items(): if obj.device_id != '*': - reprs[obj].append(obj.device_id) + obj_repr.append(obj.device_id) return reprs[self] < reprs[other] - elif isinstance(other, Port): + if isinstance(other, Port): _other = VirtualDevice(other, '*') return self < _other - else: - raise TypeError( - f"Comparing instances of {type(self)} and '{type(other)}' " - "is not supported") + 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}" @@ -456,6 +487,9 @@ def from_qarg( blind=False, backend=None, ) -> 'VirtualDevice': + """ + Parse qrexec argument +: to get device info + """ if backend is None: if blind: get_domain = domains.get_blind @@ -470,6 +504,9 @@ 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 @@ -488,6 +525,9 @@ def _parse( 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 != '*': @@ -699,14 +739,23 @@ 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 p, c in zip(pattern, candidate): - if p == '*': + for patt, cand in zip(pattern, candidate): + if patt == '*': continue - if p != c: + if patt != cand: return False return True @@ -907,7 +956,8 @@ def serialize(self) -> bytes: 'parent_devclass', self.parent_device.devclass) for key, value in self.data.items(): - properties += b' ' + DeviceSerializer.pack_property("_" + key, value) + properties += b' ' + DeviceSerializer.pack_property( + "_" + key, value) return properties @@ -930,6 +980,7 @@ def deserialize( device = cls._deserialize(rest, device) # pylint: disable=broad-exception-caught except Exception as exc: + print(str(exc), file=sys.stderr) device = UnknownDevice.from_device(device) return device @@ -1004,12 +1055,19 @@ def device_id(self, value): class UnknownDevice(DeviceInfo): # pylint: disable=too-few-public-methods """Unknown device - for example, exposed by domain not running currently""" + @staticmethod - def from_device(device) -> 'UnknownDevice': + def from_device(device: VirtualDevice) -> 'UnknownDevice': + """ + Return `UnknownDevice` based on any virtual device. + """ return UnknownDevice(device.port, device_id=device.device_id) class AssignmentMode(Enum): + """ + Device assignment modes + """ MANUAL = "manual" ASK = "ask-to-attach" AUTO = "auto-attach" @@ -1017,23 +1075,8 @@ class AssignmentMode(Enum): class DeviceAssignment: - """ 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. + """ + Maps a device to a frontend_domain. """ def __init__( @@ -1094,28 +1137,33 @@ def __lt__(self, other): "is not supported") @property - def backend_domain(self): + 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): + def port_id(self) -> str: + # pylint: disable=missing-function-docstring return self.virtual_device.port_id @property - def devclass(self): + def devclass(self) -> str: + # pylint: disable=missing-function-docstring return self.virtual_device.devclass @property - def device_id(self): + def device_id(self) -> str: + # pylint: disable=missing-function-docstring return self.virtual_device.device_id @property def devices(self) -> List[DeviceInfo]: - """Get DeviceInfo object corresponding to this DeviceAssignment""" + """Get DeviceInfo objects corresponding to this DeviceAssignment""" if self.port_id != '*': # could return UnknownDevice return [self.backend_domain.devices[self.devclass][self.port_id]] @@ -1219,7 +1267,8 @@ def serialize(self) -> bytes: 'frontend_domain', self.frontend_domain.name) for key, value in self.options.items(): - properties += b' ' + DeviceSerializer.pack_property("_" + key, value) + properties += b' ' + DeviceSerializer.pack_property( + "_" + key, value) return properties @@ -1253,22 +1302,26 @@ def _deserialize( DeviceSerializer.parse_basic_device_properties( expected_device, properties) + + expected_device = expected_device.clone( + device_id=properties['device_id']) # we do not need port, we need device del properties['port'] - expected_device._device_id = properties.get( - 'device_id', expected_device.device_id) 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 != '*' and self.port_id != device.port_id: + if self.port_id not in ('*', device.port_id): return False - if self.device_id != '*' and self.device_id != device.device_id: + if self.device_id not in ('*', device.device_id): return False return True From 6b6ca59ea8d487a62f57c47be215919158ce893f Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Tue, 27 Aug 2024 11:40:29 +0200 Subject: [PATCH 32/35] q-dev: fix tests and make linter happy --- qubes/api/admin.py | 5 ++--- qubes/device_protocol.py | 3 +-- qubes/ext/admin.py | 7 +++---- qubes/ext/pci.py | 8 +++++++- qubes/ext/utils.py | 13 ++++++------- qubes/tests/devices_pci.py | 1 + qubes/tests/vm/init.py | 11 +++++++++-- qubes/tests/vm/qubesvm.py | 14 ++++++++++++++ qubes/vm/qubesvm.py | 7 +++---- 9 files changed, 46 insertions(+), 23 deletions(-) diff --git a/qubes/api/admin.py b/qubes/api/admin.py index 5de6f7fc8..31778c166 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -27,7 +27,6 @@ import string import subprocess import pathlib -import sys import libvirt import lxml.etree @@ -46,8 +45,8 @@ import qubes.vm import qubes.vm.adminvm import qubes.vm.qubesvm -from qubes.device_protocol import (Port, VirtualDevice, UnknownDevice, - DeviceAssignment) +from qubes.device_protocol import ( + VirtualDevice, UnknownDevice, DeviceAssignment) class QubesMgmtEventsDispatcher: diff --git a/qubes/device_protocol.py b/qubes/device_protocol.py index 8863b649d..f206afdcc 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -1189,8 +1189,7 @@ def device(self) -> DeviceInfo: return devices[0] if len(devices) > 1: raise ProtocolError("Too many devices matches to assignment") - if len(devices) == 0: - raise ProtocolError("Any devices matches to assignment") + raise ProtocolError("Any devices matches to assignment") @property def port(self) -> Port: diff --git a/qubes/ext/admin.py b/qubes/ext/admin.py index a06a1ef2c..6fe1a2108 100644 --- a/qubes/ext/admin.py +++ b/qubes/ext/admin.py @@ -17,7 +17,6 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, see . import importlib -import sys import qubes.api import qubes.api.internal @@ -171,6 +170,7 @@ def on_tag_add(self, vm, event, tag, **kwargs): def on_device_attach( self, vm, event, dest, arg, device, mode, options, **kwargs ): + # pylint: disable=unused-argument # ignore auto-attachment if mode != 'manual': return @@ -178,7 +178,7 @@ def on_device_attach( # load device deny list deny = {} try: - with open(DEVICE_DENY_LIST, 'r') as file: + with open(DEVICE_DENY_LIST, 'r', encoding="utf-8") as file: for line in file: line = line.strip() @@ -186,7 +186,7 @@ def on_device_attach( name, *values = line.split() values = ' '.join(values).replace(',', ' ').split() - values = set([v for v in values if len(v) > 0]) + values = {v for v in values if len(v) > 0} deny[name] = deny.get(name, set()).union(set(values)) except IOError: @@ -198,4 +198,3 @@ def on_device_attach( for devint in device.interfaces: if pattern.matches(devint): raise qubes.exc.PermissionDenied() - diff --git a/qubes/ext/pci.py b/qubes/ext/pci.py index 0fef28838..fae3090d9 100644 --- a/qubes/ext/pci.py +++ b/qubes/ext/pci.py @@ -223,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 @@ -287,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 = \ diff --git a/qubes/ext/utils.py b/qubes/ext/utils.py index 05d43f619..4bf6ec2b7 100644 --- a/qubes/ext/utils.py +++ b/qubes/ext/utils.py @@ -141,17 +141,16 @@ def compare_device_cache(vm, devices_cache, current_devices): def confirm_device_attachment(device, frontends) -> str: - guivm = 'dom0' # TODO - try: + # pylint: disable=consider-using-with proc = subprocess.Popen( - ["attach-confirm", guivm, - device.backend_domain.name, device.port_id, - device.description, + ["attach-confirm", device.backend_domain.name, + device.port_id, device.description, *[f.name for f in frontends.keys()]], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) (target_name, _) = proc.communicate() return target_name.decode() except Exception as exc: - print(exc, file=sys.stderr) + print("attach-confirm", exc, file=sys.stderr) return "" diff --git a/qubes/tests/devices_pci.py b/qubes/tests/devices_pci.py index 6f68cb89c..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) 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 c3caea257..16ee344f1 100644 --- a/qubes/tests/vm/qubesvm.py +++ b/qubes/tests/vm/qubesvm.py @@ -1891,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/qubesvm.py b/qubes/vm/qubesvm.py index 5ad582527..c9cba58c6 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -1187,10 +1187,9 @@ async def start(self, start_guid=True, notify_function=None, if not ass.required: continue for device in ass.devices: - if isinstance(device, - qubes.device_protocol.UnknownDevice): - continue - else: + if not isinstance( + device, qubes.device_protocol.UnknownDevice + ): break else: raise qubes.exc.QubesException( From 94836fb88983f0c2a9d9e3e7ca2b20fdaf5a695c Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Tue, 27 Aug 2024 23:07:48 +0200 Subject: [PATCH 33/35] q-dev: update pci tests and cleanup --- qubes/ext/block.py | 1 - qubes/ext/pci.py | 2 -- qubes/tests/integ/devices_pci.py | 21 +++++++++------------ 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/qubes/ext/block.py b/qubes/ext/block.py index 6424a862e..243fc5dc5 100644 --- a/qubes/ext/block.py +++ b/qubes/ext/block.py @@ -548,7 +548,6 @@ async def on_domain_start(self, vm, _event, **_kwargs): if assignment.required: # already attached continue - # TODO: notify? for device in assignment.devices: if isinstance(device, qubes.device_protocol.UnknownDevice): continue diff --git a/qubes/ext/pci.py b/qubes/ext/pci.py index fae3090d9..eaaffe2a8 100644 --- a/qubes/ext/pci.py +++ b/qubes/ext/pci.py @@ -393,7 +393,6 @@ def on_device_pre_attached_pci(self, vm, event, device, options): return try: - # TODO? device = _cache_get(device.backend_domain, device.port_id) self.bind_pci_to_pciback(vm.app, device) vm.libvirt_domain.attachDevice( @@ -446,7 +445,6 @@ def on_device_pre_detached_pci(self, vm, event, port): def on_domain_pre_start(self, vm, _event, **_kwargs): # Bind pci devices to pciback driver for assignment in vm.devices['pci'].get_assigned_devices(): - # TODO? device = _cache_get(assignment.backend_domain, assignment.port_id) self.bind_pci_to_pciback(vm.app, device) diff --git a/qubes/tests/integ/devices_pci.py b/qubes/tests/integ/devices_pci.py index 03f75af78..459123d5d 100644 --- a/qubes/tests/integ/devices_pci.py +++ b/qubes/tests/integ/devices_pci.py @@ -39,10 +39,7 @@ def setUp(self): pcidev = os.environ['QUBES_TEST_PCIDEV'] self.dev = self.app.domains[0].devices['pci'][pcidev] self.assignment = DeviceAssignment( - self.dev, mode='auto-attach' - ) - self.required_assignment = DeviceAssignment( - self.dev, mode='required', + self.dev, mode='required' ) if isinstance(self.dev, qubes.device_protocol.UnknownDevice): self.skipTest('Specified device {} does not exists'.format(pcidev)) @@ -91,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) @@ -99,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')) @@ -119,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'] @@ -129,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) @@ -151,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) @@ -160,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()) From bb02ee26713bf12a12dcfce0286c2e582eb62a51 Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Wed, 28 Aug 2024 15:31:31 +0200 Subject: [PATCH 34/35] q-dev: update qubes.rng and fix tests --- qubes/tests/vm/qubesvm.py | 4 ++-- relaxng/qubes.rng | 23 ++++++++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/qubes/tests/vm/qubesvm.py b/qubes/tests/vm/qubesvm.py index 16ee344f1..744b7e5d5 100644 --- a/qubes/tests/vm/qubesvm.py +++ b/qubes/tests/vm/qubesvm.py @@ -1495,7 +1495,7 @@ def test_600_libvirt_xml_hvm_cdrom_boot(self): devclass="block", ) ), - {'devtype': 'cdrom', 'read-only': 'yes'}, + options={'devtype': 'cdrom', 'read-only': 'yes'}, mode='required') self.loop.run_until_complete(vm.devices['block'].assign(dev)) libvirt_xml = vm.create_config_file() @@ -1606,7 +1606,7 @@ def test_600_libvirt_xml_hvm_cdrom_dom0_kernel_boot(self): devclass="block", ) ), - {'devtype': 'cdrom', 'read-only': 'yes'}, + options={'devtype': 'cdrom', 'read-only': 'yes'}, mode='required') self.loop.run_until_complete(vm.devices['block'].assign(dev)) libvirt_xml = vm.create_config_file() 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_-]+ + + + From 65f14db518dc5ed62ab13c04622bde83fa4373af Mon Sep 17 00:00:00 2001 From: Piotr Bartman-Szwarc Date: Wed, 28 Aug 2024 16:35:48 +0200 Subject: [PATCH 35/35] q-dev: Set.required -> Set.assignment --- Makefile | 10 +-- .../90-admin-default.policy.header | 4 +- qubes/api/admin.py | 21 +++--- qubes/devices.py | 16 ++--- qubes/tests/api_admin.py | 72 ++++++++++++------- qubes/tests/devices.py | 58 +++++++++++---- rpm_spec/core-dom0.spec.in | 4 +- 7 files changed, 119 insertions(+), 66 deletions(-) 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/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 31778c166..1318ec0ce 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -46,7 +46,7 @@ import qubes.vm.adminvm import qubes.vm.qubesvm from qubes.device_protocol import ( - VirtualDevice, UnknownDevice, DeviceAssignment) + VirtualDevice, UnknownDevice, DeviceAssignment, AssignmentMode) class QubesMgmtEventsDispatcher: @@ -1391,7 +1391,7 @@ async def vm_device_detach(self, endpoint): # 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) @@ -1405,17 +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() dev = VirtualDevice.from_qarg(self.arg, devclass, self.app.domains) - self.fire_event_for_permission(device=dev, mode=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/devices.py b/qubes/devices.py index 3addbebc1..61890e4ef 100644 --- a/qubes/devices.py +++ b/qubes/devices.py @@ -64,7 +64,8 @@ import qubes.exc import qubes.utils from qubes.device_protocol import (Port, DeviceInfo, UnknownDevice, - DeviceAssignment, VirtualDevice) + DeviceAssignment, VirtualDevice, + AssignmentMode) DEVICE_DENY_LIST = "/etc/qubes/device-deny.list" @@ -257,14 +258,14 @@ def load_assignment(self, device_assignment: DeviceAssignment): assert device_assignment.attach_automatically self._set.add(device_assignment) - async def update_required(self, device: VirtualDevice, required: bool): + async def update_assignment( + self, device: VirtualDevice, mode: AssignmentMode + ): """ Update `required` flag of an already attached device. :param VirtualDevice 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 AssignmentMode mode: new assignment mode """ if self._vm.is_halted(): raise qubes.exc.QubesVMNotStartedError( @@ -281,11 +282,10 @@ async def update_required(self, device: VirtualDevice, 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 - new_assignment = assignment.clone( - mode='required' if required else 'auto-attach') + new_assignment = assignment.clone(mode=mode) self._set.discard(assignment) self._set.add(new_assignment) await self._vm.fire_event_async( diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index dcce64303..13b22f3a4 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -2944,7 +2944,7 @@ 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): + 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'}) @@ -2958,24 +2958,25 @@ 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:bee', b'True') + b'admin.vm.device.testclass.Set.assignment', + b'test-vm1', b'test-vm1+1234:bee', b'required') self.assertIsNone(value) dev = DeviceInfo(Port( self.vm, '1234', 'testclass'), device_id='bee') - required = self.vm.devices['testclass'].get_assigned_devices( - required_only=True) + 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=assignment.virtual_device) self.app.save.assert_called_once_with() - def test_651_vm_device_set_required_false(self): + 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'}) @@ -2989,8 +2990,8 @@ 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:bee', 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 = DeviceInfo(Port(self.vm, '1234', 'testclass'), @@ -2998,74 +2999,91 @@ def test_651_vm_device_set_required_false(self): 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=assignment.virtual_device) self.app.save.assert_called_once_with() - def test_652_vm_device_set_required_true_unchanged(self): + 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:bee', b'True') + b'admin.vm.device.testclass.Set.assignment', + b'test-vm1', b'test-vm1+1234:bee', b'auto-attach') + self.assertIsNone(value) 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): + def test_653_vm_device_set_mode_unchanged(self): assignment = DeviceAssignment(VirtualDevice(Port( - self.vm, '1234', 'testclass')), - mode='auto-attach', options={'opt1': 'value'}) + 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(Port(self.vm, '1234', 'testclass')) + 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') + 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') + 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/devices.py b/qubes/tests/devices.py index 2b6b92da9..72d2d2fd3 100644 --- a/qubes/tests/devices.py +++ b/qubes/tests/devices.py @@ -23,7 +23,8 @@ import qubes.devices from qubes.device_protocol import (Port, DeviceInfo, DeviceAssignment, - DeviceInterface, UnknownDevice, VirtualDevice) + DeviceInterface, UnknownDevice, + VirtualDevice, AssignmentMode) import qubes.tests @@ -200,7 +201,7 @@ def test_018_list_available(self): 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() @@ -210,13 +211,35 @@ def test_020_update_required_to_false(self): self.assertEqual( {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.assignment}, set(self.collection.get_assigned_devices())) self.assertEqual( {self.assignment}, set(self.collection.get_attached_devices())) - def test_021_update_required_to_true(self): + 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())) @@ -231,13 +254,15 @@ def test_021_update_required_to_true(self): self.assertEqual({self.assignment}, set(self.collection.get_attached_devices())) self.loop.run_until_complete( - self.collection.update_required(self.device, True)) + self.collection.update_assignment( + self.device, AssignmentMode.REQUIRED)) self.assertEqual({self.assignment}, - set(self.collection.get_assigned_devices())) - self.assertEqual({self.assignment}, - set(self.collection.get_attached_devices())) + 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.assignment}, @@ -245,18 +270,25 @@ def test_022_update_required_reject_not_running(self): 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 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