From d90bd1f9b45d1b668048fb0a45cc1e71dfae33da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Mon, 25 Nov 2024 03:29:27 +0100 Subject: [PATCH 1/2] Make "multimedia" devices more detailed Distinguish between audio, video and display adapters. This is especially relevant when setting up sys-audio or sys-gui-gpu, otherwise it's hard to find which "multimedia" device is the right one. --- qubesadmin/device_protocol.py | 5 +++-- qubesadmin/tests/tools/qvm_device.py | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/qubesadmin/device_protocol.py b/qubesadmin/device_protocol.py index 049da454..c2a609b2 100644 --- a/qubesadmin/device_protocol.py +++ b/qubesadmin/device_protocol.py @@ -646,13 +646,14 @@ class DeviceCategory(Enum): Microphone = ("m******",) # Multimedia = Audio, Video, Displays etc. Multimedia = ( - "u01****", - "u0e****", "u06****", "u10****", "p03****", "p04****", ) + Audio = ("p0403**", "u01****") + Display = ("p0300**", "p0380**") + Video = ("p0400**", "u0e****") Wireless = ("ue0****", "p0d****") Bluetooth = ("ue00101", "p0d11**") Storage = ("b******", "u08****", "p01****") diff --git a/qubesadmin/tests/tools/qvm_device.py b/qubesadmin/tests/tools/qvm_device.py index f243a6d0..b6c88be6 100644 --- a/qubesadmin/tests/tools/qvm_device.py +++ b/qubesadmin/tests/tools/qvm_device.py @@ -80,7 +80,7 @@ def test_000_list_all(self): ['testclass', 'list'], app=self.app) self.assertEqual( [x.rstrip() for x in buf.getvalue().splitlines()], - ['test-vm1:dev1 Multimedia: itl test-device', + ['test-vm1:dev1 Audio: itl test-device', 'test-vm2:dev2 ?******: ? ` test-device'] ) @@ -155,7 +155,7 @@ def test_002_list_attach(self): ['testclass', 'list', 'test-vm3'], app=self.app) self.assertEqual( buf.getvalue(), - 'test-vm1:dev1 Multimedia: itl test-device ' + 'test-vm1:dev1 Audio: itl test-device ' 'test-vm3 (attached)\n' ) @@ -573,7 +573,7 @@ def test_060_device_info(self): qubesadmin.tools.qvm_device.main( ['testclass', 'info', 'test-vm1:dev1'], app=self.app) - self.assertIn('Multimedia: itl test-device\n' + self.assertIn('Audio: itl test-device\n' 'device ID: dead:beef:babe:u012345', buf.getvalue()) self.assertAllCalled() From 34d5e8de7d18235ac65e40a5850cc4fbee4c9559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Mon, 16 Dec 2024 10:55:19 +0100 Subject: [PATCH 2/2] Sync device_protocol with core-admin Update mostly type hints. --- qubesadmin/device_protocol.py | 57 +++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/qubesadmin/device_protocol.py b/qubesadmin/device_protocol.py index c2a609b2..43bf0f53 100644 --- a/qubesadmin/device_protocol.py +++ b/qubesadmin/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 @@ -29,6 +29,8 @@ The same in `qubes-core-admin` and `qubes-core-admin-client`, should be moved to one place. """ + + import string import sys from enum import Enum @@ -102,8 +104,8 @@ def unpack_properties( "ascii", errors="strict" ).strip() - properties = {} - options = {} + properties: Dict[str, str] = {} + options: Dict[str, str] = {} if not ut_decoded: return properties, options @@ -241,7 +243,7 @@ def deserialize_str(value: str) -> str: def sanitize_str( untrusted_value: str, allowed_chars: set, - replace_char: str = None, + replace_char: Optional[str] = None, error_message: str = "", ) -> str: """ @@ -276,7 +278,10 @@ class Port: """ def __init__( - self, backend_domain: Optional[QubesVM], port_id: str, devclass: str + self, + backend_domain: Optional[QubesVM], + port_id: Optional[str], + devclass: Optional[str], ): self.__backend_domain = backend_domain self.__port_id = port_id @@ -390,6 +395,7 @@ def has_devclass(self): class AnyPort(Port): """Represents any port in virtual devices ("*")""" + def __init__(self, devclass: str): super().__init__(None, "*", devclass) @@ -415,14 +421,14 @@ def __init__( device_id: Optional[str] = None, ): assert not isinstance(port, AnyPort) or device_id is not None - self.port: Optional[Port] = port + self.port: Optional[Port] = port # type: ignore self._device_id = device_id def clone(self, **kwargs) -> "VirtualDevice": """ Clone object and substitute attributes with explicitly given. """ - attr = { + attr: Dict[str, Any] = { "port": self.port, "device_id": self.device_id, } @@ -449,7 +455,7 @@ def port(self, value: Union[Port, str, None]): @property def device_id(self) -> str: # pylint: disable=missing-function-docstring - if self.is_device_id_set: + if self._device_id is not None and self.is_device_id_set: return self._device_id return "*" @@ -487,7 +493,7 @@ def description(self) -> str: """ Return human-readable description of the device identity. """ - if self.device_id == "*": + if not self.device_id or self.device_id == "*": return "any device" return self.device_id @@ -601,12 +607,11 @@ def _parse( backend = get_domain(backend_name) else: identity = representation + port_id, _, devid = identity.partition(":") - if devid == "": - devid = None return cls( Port(backend_domain=backend, port_id=port_id, devclass=devclass), - device_id=devid, + device_id=devid or None, ) def serialize(self) -> bytes: @@ -870,7 +875,7 @@ def __init__( name: Optional[str] = None, serial: Optional[str] = None, interfaces: Optional[List[DeviceInterface]] = None, - parent: Optional[Port] = None, + parent: Optional["DeviceInfo"] = None, attachment: Optional[QubesVM] = None, device_id: Optional[str] = None, **kwargs, @@ -1023,6 +1028,8 @@ def subdevices(self) -> List[VirtualDevice]: If the device has subdevices (e.g., partitions of a USB stick), the subdevices id should be here. """ + if not self.backend_domain: + return [] return [ dev for devclass in self.backend_domain.devices.keys() @@ -1124,7 +1131,7 @@ def _deserialize( if "attachment" not in properties or not properties["attachment"]: properties["attachment"] = None - else: + elif expected_device.backend_domain: app = expected_device.backend_domain.app properties["attachment"] = app.domains.get_blind( properties["attachment"] @@ -1281,7 +1288,7 @@ def __lt__(self, other): ) @property - def backend_domain(self) -> QubesVM: + def backend_domain(self) -> Optional[QubesVM]: # pylint: disable=missing-function-docstring return self.virtual_device.backend_domain @@ -1308,14 +1315,15 @@ def device_id(self) -> str: @property def devices(self) -> List[DeviceInfo]: """Get DeviceInfo objects corresponding to this DeviceAssignment""" + result: List[DeviceInfo] = [] + if not self.backend_domain: + return result if self.port_id != "*": dev = self.backend_domain.devices[self.devclass][self.port_id] - if ( - isinstance(dev, UnknownDevice) - or dev.device_id == self.device_id + if isinstance(dev, UnknownDevice) or ( + dev and self.device_id in (dev.device_id, "*") ): return [dev] - result = [] if self.device_id == "0000:0000::?******": return result for dev in self.backend_domain.devices[self.devclass]: @@ -1355,8 +1363,13 @@ def frontend_domain(self) -> Optional[QubesVM]: def frontend_domain(self, frontend_domain: Optional[Union[str, QubesVM]]): """Which domain the device is attached/assigned to.""" if isinstance(frontend_domain, str): - frontend_domain = self.backend_domain.app.domains[frontend_domain] - self.__frontend_domain = frontend_domain + if not self.backend_domain: + raise ProtocolError("Cannot determine backend domain") + self.__frontend_domain: Optional[QubesVM] = ( + self.backend_domain.app.domains[frontend_domain] + ) + else: + self.__frontend_domain = frontend_domain @property def attached(self) -> bool: