From 4a426e6d8596d3b2196865445018456670d7d06d Mon Sep 17 00:00:00 2001 From: Piotr Bartman <prbartman@invisiblethingslab.com> Date: Thu, 1 Feb 2024 09:38:26 +0100 Subject: [PATCH] q-dev: implementation of DeviceInfo.attachment --- qubes/api/admin.py | 10 ++---- qubes/devices.py | 87 +++++++++++++++++++++++++++++----------------- qubes/ext/block.py | 4 +-- 3 files changed, 61 insertions(+), 40 deletions(-) diff --git a/qubes/api/admin.py b/qubes/api/admin.py index d6b4842bf..c796dbd4a 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -1300,7 +1300,6 @@ async def vm_device_assign(self, endpoint, untrusted_payload): 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): - # TODO DeviceAssignment.deserialize() ? Device devclass = endpoint # qrexec already verified that no strange characters are in self.arg @@ -1359,7 +1358,6 @@ async def vm_device_attach(self, endpoint, untrusted_payload): for ep in importlib.metadata.entry_points(group='qubes.devices')), no_payload=True, scope='local', execute=True) async def vm_device_detach(self, endpoint): - # TODO DeviceAssignment.deserialize() ? Device devclass = endpoint # qrexec already verified that no strange characters are in self.arg @@ -1374,15 +1372,13 @@ async def vm_device_detach(self, endpoint): assignment = qubes.devices.DeviceAssignment( dev.backend_domain, dev.ident) await self.dest.devices[devclass].detach(assignment) - self.app.save() # TODO: not needed - # Attach/Detach action can both modify persistent state (with - # required=True or required=False) and volatile state of running VM - # (with required=None). For this reason, write=True + execute=True + # Assign/Unassign action can modify only persistent state of running VM. + # For this reason, write=True @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, execute=True) + scope='local', write=True) async def vm_device_set_assignment(self, endpoint, untrusted_payload): """ Update assignment of already attached device. diff --git a/qubes/devices.py b/qubes/devices.py index f0d865811..18aa46ded 100644 --- a/qubes/devices.py +++ b/qubes/devices.py @@ -318,7 +318,6 @@ def _load_classes(bus: str): class DeviceInfo(Device): """ Holds all information about a device """ - # pylint: disable=too-few-public-methods def __init__( self, backend_domain: 'qubes.vm.BaseVM', @@ -331,6 +330,7 @@ def __init__( serial: Optional[str] = None, interfaces: Optional[List[DeviceInterface]] = None, parent: Optional[Device] = None, + attachment: Optional['qubes.vm.BaseVM'] = None, **kwargs ): super().__init__(backend_domain, ident, devclass) @@ -342,6 +342,7 @@ def __init__( self._serial = serial self._interfaces = interfaces self._parent = parent + self._attachment = attachment self.data = kwargs @@ -473,17 +474,17 @@ def subdevices(self) -> List['DeviceInfo']: if dev.parent_device.ident == self.ident] @property - def attachments(self) -> List['DeviceAssignment']: + def attachment(self) -> Optional['qubes.vm.BaseVM']: """ - Device attachments + VM to which device is attached (frontend domain). """ - return [] # TODO + return self._attachment def serialize(self) -> bytes: """ Serialize object to be transmitted via Qubes API. """ - # 'backend_domain', 'interfaces', 'data', 'parent_device' + # 'backend_domain', 'attachment', 'interfaces', 'data', 'parent_device' # are not string, so they need special treatment default_attrs = { 'ident', 'devclass', 'vendor', 'product', 'manufacturer', 'name', @@ -494,9 +495,14 @@ def serialize(self) -> bytes: (key, getattr(self, key)) for key in default_attrs) ) - back_name = serialize_str(self.backend_domain.name) - backend_domain_prop = (b"backend_domain=" + back_name.encode('ascii')) - properties += b' ' + backend_domain_prop + qname = serialize_str(self.backend_domain.name) + backend_prop = (b"backend_domain=" + qname.encode('ascii')) + properties += b' ' + backend_prop + + if self.attachment: + qname = serialize_str(self.attachment.name) + attachment_prop = (b"attachment=" + qname.encode('ascii')) + properties += b' ' + attachment_prop interfaces = serialize_str( ''.join(repr(ifc) for ifc in self.interfaces)) @@ -573,6 +579,13 @@ def _deserialize( f"when expected devices from {expected_backend_domain.name}.") properties['backend_domain'] = expected_backend_domain + if 'attachment' not in properties or not properties['attachment']: + properties['attachment'] = None + else: + app = expected_backend_domain.app + properties['attachment'] = app.domains.get_blind( + properties['attachment']) + if expected_devclass and properties['devclass'] != expected_devclass: raise UnexpectedDeviceProperty( f"Got {properties['devclass']} device " @@ -597,10 +610,6 @@ def _deserialize( return cls(**properties) - @property - def frontend_domain(self): - return self.data.get("frontend_domain", None) - @property def full_identity(self) -> str: """ @@ -645,7 +654,6 @@ def sanitize_str( """ if replace_char is None: if any(x not in allowed_chars for x in untrusted_value): - print(untrusted_value, file=sys.stderr) # TODO raise qubes.api.ProtocolError(error_message) return untrusted_value result = "" @@ -678,8 +686,9 @@ class DeviceAssignment(Device): 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 domain is halted - or device manually detached. + 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. """ @@ -725,10 +734,11 @@ def frontend_domain( @property def attached(self) -> bool: """ - Is the device already attached to the fronted domain? + Is the device attached to the fronted domain? + + Returns False if device is attached to different domain """ - return (self.frontend_domain is not None - and self.frontend_domain.is_running()) + return self.device.attachment == self.frontend_domain @property def required(self) -> bool: @@ -738,6 +748,10 @@ def required(self) -> bool: """ return self.__required + @required.setter + def required(self, required: bool): + self.__required = required + @property def attach_automatically(self) -> bool: """ @@ -746,6 +760,10 @@ def attach_automatically(self) -> bool: """ return self.__attach_automatically + @attach_automatically.setter + def attach_automatically(self, attach_automatically: bool): + self.__attach_automatically = attach_automatically + @property def options(self) -> Dict[str, Any]: """ Device options (same as in the legacy API). """ @@ -773,10 +791,7 @@ def serialize(self) -> bytes: properties += b' ' + backend_domain_prop if self.frontend_domain is not None: - if isinstance(self.frontend_domain, str): - front_name = serialize_str(self.frontend_domain) # TODO - else: - front_name = serialize_str(self.frontend_domain.name) + front_name = serialize_str(self.frontend_domain.name) frontend_domain_prop = ( b"frontend_domain=" + front_name.encode('ascii')) properties += b' ' + frontend_domain_prop @@ -925,6 +940,23 @@ class DeviceCollection: :param device: :py:class:`DeviceInfo` object to be attached + .. event:: device-assign:<class> (device, options) + + Fired when device is assigned to a VM. + + Handler for this event may be asynchronous. + + :param device: :py:class:`DeviceInfo` object to be assigned + :param options: :py:class:`dict` of assignment options + + .. event:: device-unassign:<class> (device) + + Fired when device is unassigned from a VM. + + Handler for this event can be asynchronous (a coroutine). + + :param device: :py:class:`DeviceInfo` object to be unassigned + .. event:: device-list:<class> Fired to get list of devices exposed by a VM. Handlers of this @@ -1008,10 +1040,6 @@ async def assign(self, assignment: DeviceAssignment): "Only pci and usb devices can be assigned " "to be automatically attached.") - await self._vm.fire_event_async( - 'device-pre-assign:' + self._bus, - pre_event=True, device=device, options=assignment.options) - self._set.add(assignment) await self._vm.fire_event_async( @@ -1112,12 +1140,9 @@ async def unassign(self, device_assignment: DeviceAssignment): "Can not remove an required assignment from " "a non halted qube.") - device = device_assignment.device - await self._vm.fire_event_async( - 'device-pre-unassign:' + self._bus, pre_event=True, device=device) - self._set.discard(device_assignment) + device = device_assignment.device await self._vm.fire_event_async( 'device-unassign:' + self._bus, device=device) @@ -1145,7 +1170,7 @@ def get_attached_devices(self) -> Iterable[DeviceAssignment]: backend_domain=dev.backend_domain, ident=dev.ident, options=options, - frontend_domain=dev.frontend_domain, + frontend_domain=self._vm, devclass=dev.devclass, attach_automatically=False, required=False, diff --git a/qubes/ext/block.py b/qubes/ext/block.py index 34a86f3db..d56809cb0 100644 --- a/qubes/ext/block.py +++ b/qubes/ext/block.py @@ -169,7 +169,7 @@ def on_domain_init_load(self, vm, event): # avoid building a cache on domain-init, as it isn't fully set yet, # and definitely isn't running yet current_devices = { - dev.ident: dev.frontend_domain + dev.ident: dev.attachment for dev in self.on_device_list_block(vm, None) } self.devices_cache[vm.name] = current_devices @@ -181,7 +181,7 @@ def on_qdb_change(self, vm, event, path): """A change in QubesDB means a change in device list.""" # pylint: disable=unused-argument vm.fire_event('device-list-change:block') - current_devices = dict((dev.ident, dev.frontend_domain) + current_devices = dict((dev.ident, dev.attachment) for dev in self.on_device_list_block(vm, None)) # send events about devices detached/attached outside by themselves