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.
This commit is contained in:
Marek Marczykowski-Górecki 2017-06-26 01:52:13 +02:00
parent e5a9c46e3d
commit d00e4538bf
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
5 changed files with 64 additions and 25 deletions

View File

@ -969,7 +969,7 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
assignment = qubes.devices.DeviceAssignment( assignment = qubes.devices.DeviceAssignment(
dev.backend_domain, dev.ident, dev.backend_domain, dev.ident,
options=options, persistent=persistent) options=options, persistent=persistent)
self.dest.devices[devclass].attach(assignment) yield from self.dest.devices[devclass].attach(assignment)
self.app.save() self.app.save()
@qubes.api.method('admin.vm.device.{endpoint}.Detach', endpoints=(ep.name @qubes.api.method('admin.vm.device.{endpoint}.Detach', endpoints=(ep.name
@ -991,7 +991,7 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
assignment = qubes.devices.DeviceAssignment( assignment = qubes.devices.DeviceAssignment(
dev.backend_domain, dev.ident) dev.backend_domain, dev.ident)
self.dest.devices[devclass].detach(assignment) yield from self.dest.devices[devclass].detach(assignment)
self.app.save() self.app.save()
@qubes.api.method('admin.vm.firewall.Get', no_payload=True) @qubes.api.method('admin.vm.firewall.Get', no_payload=True)

View File

@ -36,14 +36,21 @@ Such extension should provide:
including bus-specific properties) including bus-specific properties)
- handle `device-attach:bus` and `device-detach:bus` events for - handle `device-attach:bus` and `device-detach:bus` events for
performing the attach/detach action; events are fired even when domain isn't 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 - handle `device-list:bus` event - list devices exposed by particular
domain; it should return list of appropriate DeviceInfo objects domain; it should return list of appropriate DeviceInfo objects
- handle `device-get:bus` event - get one device object exposed by this - handle `device-get:bus` event - get one device object exposed by this
domain of given identifier domain of given identifier
- handle `device-list-attached:class` event - list currently attached - handle `device-list-attached:class` event - list currently attached
devices to this domain 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 import qubes.utils
@ -111,24 +118,32 @@ class DeviceCollection(object):
Fired when device is attached to a VM. 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 :param device: :py:class:`DeviceInfo` object to be attached
.. event:: device-pre-attach:<class> (device) .. event:: device-pre-attach:<class> (device)
Fired before device is attached to a VM 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 :param device: :py:class:`DeviceInfo` object to be attached
.. event:: device-detach:<class> (device) .. event:: device-detach:<class> (device)
Fired when device is detached from a VM. 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 :param device: :py:class:`DeviceInfo` object to be attached
.. event:: device-pre-detach:<class> (device) .. event:: device-pre-detach:<class> (device)
Fired before device is detached from a VM 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 device: :py:class:`DeviceInfo` object to be attached
.. event:: device-list:<class> .. event:: device-list:<class>
@ -160,6 +175,7 @@ class DeviceCollection(object):
self.devclass = qubes.utils.get_entry_point_one( self.devclass = qubes.utils.get_entry_point_one(
'qubes.devices', self._bus) 'qubes.devices', self._bus)
@asyncio.coroutine
def attach(self, device_assignment: DeviceAssignment): def attach(self, device_assignment: DeviceAssignment):
'''Attach (add) device to domain. '''Attach (add) device to domain.
@ -180,13 +196,26 @@ class DeviceCollection(object):
raise DeviceAlreadyAttached( raise DeviceAlreadyAttached(
'device {!s} of class {} already attached to {!s}'.format( 'device {!s} of class {} already attached to {!s}'.format(
device, self._bus, self._vm)) 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) device=device, options=device_assignment.options)
if device_assignment.persistent: if device_assignment.persistent:
self._set.add(device_assignment) 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) 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): def detach(self, device_assignment: DeviceAssignment):
'''Detach (remove) device from domain. '''Detach (remove) device from domain.
@ -208,13 +237,14 @@ class DeviceCollection(object):
device_assignment.ident, self._bus, self._vm)) device_assignment.ident, self._bus, self._vm))
device = device_assignment.device 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) pre_event=True, device=device)
if device in self._set: if device in self._set:
device_assignment.persistent = True device_assignment.persistent = True
self._set.discard(device_assignment) 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): def attached(self):
'''List devices which are (or may be) attached to this vm ''' '''List devices which are (or may be) attached to this vm '''

View File

@ -1319,7 +1319,8 @@ class TC_00_VMs(AdminAPITestCase):
def test_470_vm_device_list_persistent(self): def test_470_vm_device_list_persistent(self):
assignment = qubes.devices.DeviceAssignment(self.vm, '1234', assignment = qubes.devices.DeviceAssignment(self.vm, '1234',
persistent=True) 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', value = self.call_mgmt_func(b'admin.vm.device.testclass.List',
b'test-vm1') b'test-vm1')
self.assertEqual(value, self.assertEqual(value,
@ -1329,10 +1330,12 @@ class TC_00_VMs(AdminAPITestCase):
def test_471_vm_device_list_persistent_options(self): def test_471_vm_device_list_persistent_options(self):
assignment = qubes.devices.DeviceAssignment(self.vm, '1234', assignment = qubes.devices.DeviceAssignment(self.vm, '1234',
persistent=True, options={'opt1': 'value'}) 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', assignment = qubes.devices.DeviceAssignment(self.vm, '4321',
persistent=True) 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', value = self.call_mgmt_func(b'admin.vm.device.testclass.List',
b'test-vm1') b'test-vm1')
self.assertEqual(value, self.assertEqual(value,
@ -1360,7 +1363,8 @@ class TC_00_VMs(AdminAPITestCase):
self.device_list_attached_testclass) self.device_list_attached_testclass)
assignment = qubes.devices.DeviceAssignment(self.vm, '4321', assignment = qubes.devices.DeviceAssignment(self.vm, '4321',
persistent=True) 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', value = self.call_mgmt_func(b'admin.vm.device.testclass.List',
b'test-vm1') b'test-vm1')
self.assertEqual(value, self.assertEqual(value,
@ -1373,7 +1377,8 @@ class TC_00_VMs(AdminAPITestCase):
self.device_list_attached_testclass) self.device_list_attached_testclass)
assignment = qubes.devices.DeviceAssignment(self.vm, '4321', assignment = qubes.devices.DeviceAssignment(self.vm, '4321',
persistent=True) 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', value = self.call_mgmt_func(b'admin.vm.device.testclass.List',
b'test-vm1', b'test-vm1+1234') b'test-vm1', b'test-vm1+1234')
self.assertEqual(value, self.assertEqual(value,

View File

@ -91,15 +91,15 @@ class TC_00_DeviceCollection(qubes.tests.QubesTestCase):
self.assertFalse(self.collection._set) self.assertFalse(self.collection._set)
def test_001_attach(self): 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-pre-attach:testclass')
self.assertEventFired(self.emitter, 'device-attach:testclass') self.assertEventFired(self.emitter, 'device-attach:testclass')
self.assertEventNotFired(self.emitter, 'device-pre-detach:testclass') self.assertEventNotFired(self.emitter, 'device-pre-detach:testclass')
self.assertEventNotFired(self.emitter, 'device-detach:testclass') self.assertEventNotFired(self.emitter, 'device-detach:testclass')
def test_002_detach(self): def test_002_detach(self):
self.collection.attach(self.assignment) self.loop.run_until_complete(self.collection.attach(self.assignment))
self.collection.detach(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-pre-attach:testclass')
self.assertEventFired(self.emitter, 'device-attach:testclass') self.assertEventFired(self.emitter, 'device-attach:testclass')
self.assertEventFired(self.emitter, 'device-pre-detach: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): def test_010_empty_detach(self):
with self.assertRaises(LookupError): 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): 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): 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): def test_012_double_detach(self):
self.collection.attach(self.assignment) self.loop.run_until_complete(self.collection.attach(self.assignment))
self.collection.detach(self.assignment) self.loop.run_until_complete(self.collection.detach(self.assignment))
with self.assertRaises(qubes.devices.DeviceNotAttached): 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): def test_013_list_attached_persistent(self):
self.assertEqual(set([]), set(self.collection.persistent())) 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.assertEventFired(self.emitter, 'device-list-attached:testclass')
self.assertEqual({self.device}, set(self.collection.persistent())) self.assertEqual({self.device}, set(self.collection.persistent()))
self.assertEqual({self.device}, self.assertEqual({self.device},
@ -135,7 +138,7 @@ class TC_00_DeviceCollection(qubes.tests.QubesTestCase):
def test_014_list_attached_non_persistent(self): def test_014_list_attached_non_persistent(self):
self.assignment.persistent = False self.assignment.persistent = False
self.emitter.running = True 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 # device-attach event not implemented, so manipulate object manually
self.device.frontend_domain = self.emitter self.device.frontend_domain = self.emitter
self.assertEqual({self.device}, self.assertEqual({self.device},
@ -167,6 +170,7 @@ class TC_01_DeviceManager(qubes.tests.QubesTestCase):
backend_domain=device.backend_domain, backend_domain=device.backend_domain,
ident=device.ident, ident=device.ident,
persistent=True) persistent=True)
self.manager['testclass'].attach(assignment) self.loop.run_until_complete(
self.manager['testclass'].attach(assignment))
self.assertEventFired(self.emitter, 'device-attach:testclass') self.assertEventFired(self.emitter, 'device-attach:testclass')

View File

@ -316,7 +316,7 @@ class BaseVM(qubes.PropertyHolder):
options, options,
persistent=True persistent=True
) )
self.devices[devclass].attach(device_assignment) self.devices[devclass].load_persistent(device_assignment)
# tags # tags
for node in self.xml.xpath('./tags/tag'): for node in self.xml.xpath('./tags/tag'):