From d00e4538bf5d5e9be4d611ea99879c1ef00f8671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Mon, 26 Jun 2017 01:52:13 +0200 Subject: [PATCH] devices: make attach/detach related events async This will allow starting processes and calling RPC services in those events. This if required for usb devices, which are attached using RPC services. Intentionally keep device listing events synchronous only - to discourage putting long-running actions there. This change also require some not-async attach method version for loading devices from qubes.xml - have `load_persistent` for this. --- qubes/api/admin.py | 4 ++-- qubes/devices.py | 40 +++++++++++++++++++++++++++++++++++----- qubes/tests/api_admin.py | 15 ++++++++++----- qubes/tests/devices.py | 28 ++++++++++++++++------------ qubes/vm/__init__.py | 2 +- 5 files changed, 64 insertions(+), 25 deletions(-) diff --git a/qubes/api/admin.py b/qubes/api/admin.py index 5a72c1e2..8a9fb05c 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -969,7 +969,7 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI): assignment = qubes.devices.DeviceAssignment( dev.backend_domain, dev.ident, options=options, persistent=persistent) - self.dest.devices[devclass].attach(assignment) + yield from self.dest.devices[devclass].attach(assignment) self.app.save() @qubes.api.method('admin.vm.device.{endpoint}.Detach', endpoints=(ep.name @@ -991,7 +991,7 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI): assignment = qubes.devices.DeviceAssignment( dev.backend_domain, dev.ident) - self.dest.devices[devclass].detach(assignment) + yield from self.dest.devices[devclass].detach(assignment) self.app.save() @qubes.api.method('admin.vm.firewall.Get', no_payload=True) diff --git a/qubes/devices.py b/qubes/devices.py index 2e48ea82..320dec28 100644 --- a/qubes/devices.py +++ b/qubes/devices.py @@ -36,14 +36,21 @@ Such extension should provide: including bus-specific properties) - handle `device-attach:bus` and `device-detach:bus` events for performing the attach/detach action; events are fired even when domain isn't - running and extension should be prepared for this + running and extension should be prepared for this; handlers for those events + can be coroutines - handle `device-list:bus` event - list devices exposed by particular domain; it should return list of appropriate DeviceInfo objects - handle `device-get:bus` event - get one device object exposed by this domain of given identifier - handle `device-list-attached:class` event - list currently attached devices to this domain + +Note that device-listing event handlers can not be asynchronous. This for +example means you can not call qrexec service there. This is intentional to +keep device listing operation cheap. You need to design the extension to take +this into account (for example by using QubesDB). ''' +import asyncio import qubes.utils @@ -111,24 +118,32 @@ class DeviceCollection(object): Fired when device is attached to a VM. + Handler for this event can be asynchronous (a coroutine). + :param device: :py:class:`DeviceInfo` object to be attached .. event:: device-pre-attach: (device) Fired before device is attached to a VM + Handler for this event can be asynchronous (a coroutine). + :param device: :py:class:`DeviceInfo` object to be attached .. event:: device-detach: (device) Fired when device is detached from a VM. + Handler for this event can be asynchronous (a coroutine). + :param device: :py:class:`DeviceInfo` object to be attached .. event:: device-pre-detach: (device) 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 .. event:: device-list: @@ -160,6 +175,7 @@ class DeviceCollection(object): self.devclass = qubes.utils.get_entry_point_one( 'qubes.devices', self._bus) + @asyncio.coroutine def attach(self, device_assignment: DeviceAssignment): '''Attach (add) device to domain. @@ -180,13 +196,26 @@ class DeviceCollection(object): raise DeviceAlreadyAttached( 'device {!s} of class {} already attached to {!s}'.format( device, self._bus, self._vm)) - self._vm.fire_event('device-pre-attach:'+self._bus, pre_event=True, + yield from self._vm.fire_event_async('device-pre-attach:' + self._bus, + pre_event=True, device=device, options=device_assignment.options) if device_assignment.persistent: self._set.add(device_assignment) - self._vm.fire_event('device-attach:' + self._bus, + yield from self._vm.fire_event_async('device-attach:' + self._bus, device=device, options=device_assignment.options) + def load_persistent(self, device_assignment: DeviceAssignment): + '''Load DeviceAssignment retrieved from qubes.xml + + This can be used only for loading qubes.xml, when VM events are not + enabled yet. + ''' + assert not self._vm.events_enabled + assert device_assignment.persistent + device_assignment.bus = self._bus + self._set.add(device_assignment) + + @asyncio.coroutine def detach(self, device_assignment: DeviceAssignment): '''Detach (remove) device from domain. @@ -208,13 +237,14 @@ class DeviceCollection(object): device_assignment.ident, self._bus, self._vm)) device = device_assignment.device - self._vm.fire_event('device-pre-detach:' + self._bus, + yield from self._vm.fire_event_async('device-pre-detach:' + self._bus, pre_event=True, device=device) if device in self._set: device_assignment.persistent = True self._set.discard(device_assignment) - self._vm.fire_event('device-detach:' + self._bus, device=device) + yield from self._vm.fire_event_async('device-detach:' + self._bus, + device=device) def attached(self): '''List devices which are (or may be) attached to this vm ''' diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index 1de24d3b..45b2cffb 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -1319,7 +1319,8 @@ class TC_00_VMs(AdminAPITestCase): def test_470_vm_device_list_persistent(self): assignment = qubes.devices.DeviceAssignment(self.vm, '1234', persistent=True) - self.vm.devices['testclass'].attach(assignment) + self.loop.run_until_complete( + self.vm.devices['testclass'].attach(assignment)) value = self.call_mgmt_func(b'admin.vm.device.testclass.List', b'test-vm1') self.assertEqual(value, @@ -1329,10 +1330,12 @@ class TC_00_VMs(AdminAPITestCase): def test_471_vm_device_list_persistent_options(self): assignment = qubes.devices.DeviceAssignment(self.vm, '1234', persistent=True, options={'opt1': 'value'}) - self.vm.devices['testclass'].attach(assignment) + self.loop.run_until_complete( + self.vm.devices['testclass'].attach(assignment)) assignment = qubes.devices.DeviceAssignment(self.vm, '4321', persistent=True) - self.vm.devices['testclass'].attach(assignment) + self.loop.run_until_complete( + self.vm.devices['testclass'].attach(assignment)) value = self.call_mgmt_func(b'admin.vm.device.testclass.List', b'test-vm1') self.assertEqual(value, @@ -1360,7 +1363,8 @@ class TC_00_VMs(AdminAPITestCase): self.device_list_attached_testclass) assignment = qubes.devices.DeviceAssignment(self.vm, '4321', persistent=True) - self.vm.devices['testclass'].attach(assignment) + self.loop.run_until_complete( + self.vm.devices['testclass'].attach(assignment)) value = self.call_mgmt_func(b'admin.vm.device.testclass.List', b'test-vm1') self.assertEqual(value, @@ -1373,7 +1377,8 @@ class TC_00_VMs(AdminAPITestCase): self.device_list_attached_testclass) assignment = qubes.devices.DeviceAssignment(self.vm, '4321', persistent=True) - self.vm.devices['testclass'].attach(assignment) + self.loop.run_until_complete( + self.vm.devices['testclass'].attach(assignment)) value = self.call_mgmt_func(b'admin.vm.device.testclass.List', b'test-vm1', b'test-vm1+1234') self.assertEqual(value, diff --git a/qubes/tests/devices.py b/qubes/tests/devices.py index ff48800a..3433eba2 100644 --- a/qubes/tests/devices.py +++ b/qubes/tests/devices.py @@ -91,15 +91,15 @@ class TC_00_DeviceCollection(qubes.tests.QubesTestCase): self.assertFalse(self.collection._set) def test_001_attach(self): - self.collection.attach(self.assignment) + self.loop.run_until_complete(self.collection.attach(self.assignment)) self.assertEventFired(self.emitter, 'device-pre-attach:testclass') self.assertEventFired(self.emitter, 'device-attach:testclass') self.assertEventNotFired(self.emitter, 'device-pre-detach:testclass') self.assertEventNotFired(self.emitter, 'device-detach:testclass') def test_002_detach(self): - self.collection.attach(self.assignment) - self.collection.detach(self.assignment) + self.loop.run_until_complete(self.collection.attach(self.assignment)) + self.loop.run_until_complete(self.collection.detach(self.assignment)) self.assertEventFired(self.emitter, 'device-pre-attach:testclass') self.assertEventFired(self.emitter, 'device-attach:testclass') self.assertEventFired(self.emitter, 'device-pre-detach:testclass') @@ -107,24 +107,27 @@ class TC_00_DeviceCollection(qubes.tests.QubesTestCase): def test_010_empty_detach(self): with self.assertRaises(LookupError): - self.collection.detach(self.assignment) + self.loop.run_until_complete( + self.collection.detach(self.assignment)) def test_011_double_attach(self): - self.collection.attach(self.assignment) + self.loop.run_until_complete(self.collection.attach(self.assignment)) with self.assertRaises(qubes.devices.DeviceAlreadyAttached): - self.collection.attach(self.assignment) + self.loop.run_until_complete( + self.collection.attach(self.assignment)) def test_012_double_detach(self): - self.collection.attach(self.assignment) - self.collection.detach(self.assignment) + self.loop.run_until_complete(self.collection.attach(self.assignment)) + self.loop.run_until_complete(self.collection.detach(self.assignment)) with self.assertRaises(qubes.devices.DeviceNotAttached): - self.collection.detach(self.assignment) + self.loop.run_until_complete( + self.collection.detach(self.assignment)) def test_013_list_attached_persistent(self): self.assertEqual(set([]), set(self.collection.persistent())) - self.collection.attach(self.assignment) + self.loop.run_until_complete(self.collection.attach(self.assignment)) self.assertEventFired(self.emitter, 'device-list-attached:testclass') self.assertEqual({self.device}, set(self.collection.persistent())) self.assertEqual({self.device}, @@ -135,7 +138,7 @@ class TC_00_DeviceCollection(qubes.tests.QubesTestCase): def test_014_list_attached_non_persistent(self): self.assignment.persistent = False self.emitter.running = True - self.collection.attach(self.assignment) + self.loop.run_until_complete(self.collection.attach(self.assignment)) # device-attach event not implemented, so manipulate object manually self.device.frontend_domain = self.emitter self.assertEqual({self.device}, @@ -167,6 +170,7 @@ class TC_01_DeviceManager(qubes.tests.QubesTestCase): backend_domain=device.backend_domain, ident=device.ident, persistent=True) - self.manager['testclass'].attach(assignment) + self.loop.run_until_complete( + self.manager['testclass'].attach(assignment)) self.assertEventFired(self.emitter, 'device-attach:testclass') diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index a78fdf4e..039aece6 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -316,7 +316,7 @@ class BaseVM(qubes.PropertyHolder): options, persistent=True ) - self.devices[devclass].attach(device_assignment) + self.devices[devclass].load_persistent(device_assignment) # tags for node in self.xml.xpath('./tags/tag'):