diff --git a/qubes/api/admin.py b/qubes/api/admin.py index fff39a710..1a4fa001f 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -1343,7 +1343,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=devclass) + qubes.device_protocol.Port(dev.backend_domain, dev.ident, devclass)) await self.dest.devices[devclass].unassign(assignment) self.app.save() @@ -1397,7 +1397,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=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 ac365f63a..f74b8b2d4 100644 --- a/qubes/device_protocol.py +++ b/qubes/device_protocol.py @@ -243,8 +243,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****") @@ -349,7 +349,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( @@ -538,8 +538,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]: @@ -784,15 +786,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): @@ -800,28 +816,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: @@ -857,11 +859,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: @@ -869,11 +871,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]: @@ -938,9 +943,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 ebc54eee1..918c025c6 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -1786,7 +1786,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)) @@ -1799,7 +1799,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)) @@ -1837,11 +1837,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', @@ -2024,7 +2026,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)) @@ -2044,7 +2047,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)) @@ -2065,7 +2069,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)) @@ -2083,7 +2088,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)) @@ -2104,7 +2110,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)) @@ -2191,7 +2198,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)) @@ -2910,7 +2918,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)) @@ -2940,7 +2949,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)) @@ -2970,7 +2980,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)) @@ -2988,7 +2999,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 aaec7c754..f5e61433c 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 fcb4fbd47..0be5a79ae 100644 --- a/qubes/tests/vm/qubesvm.py +++ b/qubes/tests/vm/qubesvm.py @@ -1310,11 +1310,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, ) @@ -1397,10 +1399,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) @@ -1483,7 +1487,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", + ), options={'devtype': 'cdrom', 'read-only': 'yes'}, attach_automatically=True, required=True) self.loop.run_until_complete(vm.devices['block'].assign(dev)) @@ -1588,7 +1596,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", + ), options={'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