diff --git a/qubes/app.py b/qubes/app.py index 72b11e75..47b2d371 100644 --- a/qubes/app.py +++ b/qubes/app.py @@ -35,6 +35,7 @@ import time import traceback import uuid +import itertools import lxml.etree import jinja2 @@ -857,6 +858,8 @@ class Qubes(qubes.PropertyHolder): return element + def __str__(self): + return type(self).__name__ def save(self, lock=True): '''Save all data to qubes.xml @@ -1186,14 +1189,17 @@ class Qubes(qubes.PropertyHolder): @qubes.events.handler('domain-pre-delete') def on_domain_pre_deleted(self, event, vm): # pylint: disable=unused-argument - if isinstance(vm, qubes.vm.templatevm.TemplateVM): - appvms = self.domains.get_vms_based_on(vm) - if appvms: - raise qubes.exc.QubesException( - 'Cannot remove template that has dependent AppVMs. ' - 'Affected are: {}'.format(', '.join( - appvm.name for appvm in sorted(appvms)))) - + for obj in itertools.chain(self.domains, (self,)): + for prop in obj.property_list(): + try: + if isinstance(prop, qubes.vm.VMProperty) and \ + getattr(obj, prop.__name__) == vm: + self.log.error( + 'Cannot remove %s, used by %s.%s', + vm, obj, prop.__name__) + raise qubes.exc.QubesVMInUseError(vm) + except AttributeError: + pass @qubes.events.handler('domain-delete') def on_domain_deleted(self, event, vm): diff --git a/qubes/exc.py b/qubes/exc.py index 8b3fa68c..2dee60a4 100644 --- a/qubes/exc.py +++ b/qubes/exc.py @@ -42,6 +42,11 @@ class QubesVMError(QubesException): super(QubesVMError, self).__init__(msg) self.vm = vm +class QubesVMInUseError(QubesVMError): + '''VM is in use, cannot remove.''' + def __init__(self, vm, msg=None): + super(QubesVMInUseError, self).__init__(vm, + msg or 'Domain is in use: {!r}'.format(vm.name)) class QubesVMNotStartedError(QubesVMError): '''Domain is not started. diff --git a/qubes/ext/block.py b/qubes/ext/block.py index 28c4128f..38d5ae28 100644 --- a/qubes/ext/block.py +++ b/qubes/ext/block.py @@ -58,6 +58,8 @@ class BlockDevice(qubes.devices.DeviceInfo): string.ascii_letters + string.digits + '()+,-.:=_/ '} untrusted_desc = self.backend_domain.untrusted_qdb.read( '/qubes-block-devices/{}/desc'.format(self.ident)) + if not untrusted_desc: + return '' desc = ''.join((chr(c) if c in safe_set else '_') for c in untrusted_desc) self._description = desc @@ -136,14 +138,13 @@ class BlockDeviceExtension(qubes.ext.Extension): def on_device_list_block(self, vm, event): # pylint: disable=unused-argument,no-self-use - safe_set = string.ascii_letters + string.digits if not vm.is_running(): return untrusted_qubes_devices = vm.untrusted_qdb.list('/qubes-block-devices/') untrusted_idents = set(untrusted_path.split('/', 3)[2] for untrusted_path in untrusted_qubes_devices) for untrusted_ident in untrusted_idents: - if not all(c in safe_set for c in untrusted_ident): + if not name_re.match(untrusted_ident): msg = ("%s vm's device path name contains unsafe characters. " "Skipping it.") vm.log.warning(msg % vm.name) @@ -161,7 +162,9 @@ class BlockDeviceExtension(qubes.ext.Extension): if not vm.is_running(): return if not vm.app.vmm.offline_mode: - yield self.device_get(vm, ident) + device_info = self.device_get(vm, ident) + if device_info: + yield device_info @qubes.ext.handler('device-list-attached:block') def on_device_list_attached(self, vm, event, **kwargs): @@ -202,6 +205,8 @@ class BlockDeviceExtension(qubes.ext.Extension): else: options['read-only'] = 'no' options['frontend-dev'] = frontend_dev + if disk.get('device') != 'disk': + options['devtype'] = disk.get('device') if dev_path.startswith('/dev/'): ident = dev_path[len('/dev/'):] diff --git a/qubes/storage/kernels.py b/qubes/storage/kernels.py index b570d38c..aadacd83 100644 --- a/qubes/storage/kernels.py +++ b/qubes/storage/kernels.py @@ -83,8 +83,8 @@ class LinuxModules(Volume): def is_dirty(self): return False - def clone(self, source): - if isinstance(source, LinuxModules): + def import_volume(self, src_volume): + if isinstance(src_volume, LinuxModules): # do nothing return self raise StoragePoolException('clone of LinuxModules volume from ' diff --git a/qubes/tests/app.py b/qubes/tests/app.py index 5ef51f8d..d6a7eda6 100644 --- a/qubes/tests/app.py +++ b/qubes/tests/app.py @@ -271,13 +271,13 @@ class TC_30_VMCollection(qubes.tests.QubesTestCase): # pass -class TC_90_Qubes(qubes.tests.QubesTestCase): +class TC_89_QubesEmpty(qubes.tests.QubesTestCase): def tearDown(self): try: os.unlink('/tmp/qubestest.xml') except: pass - super(TC_90_Qubes, self).tearDown() + super().tearDown() @qubes.tests.skipUnlessDom0 def test_000_init_empty(self): @@ -288,25 +288,105 @@ class TC_90_Qubes(qubes.tests.QubesTestCase): pass qubes.Qubes.create_empty_store('/tmp/qubestest.xml').close() - def test_100_clockvm(self): - app = qubes.Qubes('/tmp/qubestest.xml', load=False, offline_mode=True) - app.load_initial_values() - template = app.add_new_vm('TemplateVM', name='test-template', +class TC_90_Qubes(qubes.tests.QubesTestCase): + def tearDown(self): + try: + os.unlink('/tmp/qubestest.xml') + except: + pass + super().tearDown() + + def setUp(self): + super(TC_90_Qubes, self).setUp() + self.app = qubes.Qubes('/tmp/qubestest.xml', load=False, + offline_mode=True) + self.addCleanup(self.cleanup_qubes) + self.app.load_initial_values() + self.template = self.app.add_new_vm('TemplateVM', name='test-template', label='green') - appvm = app.add_new_vm('AppVM', name='test-vm', template=template, + + def cleanup_qubes(self): + self.app.close() + del self.app + try: + del self.template + except AttributeError: + pass + + def test_100_clockvm(self): + appvm = self.app.add_new_vm('AppVM', name='test-vm', template=self.template, label='red') - self.assertIsNone(app.clockvm) + self.assertIsNone(self.app.clockvm) self.assertNotIn('service.clocksync', appvm.features) - self.assertNotIn('service.clocksync', template.features) - app.clockvm = appvm + self.assertNotIn('service.clocksync', self.template.features) + self.app.clockvm = appvm self.assertIn('service.clocksync', appvm.features) self.assertTrue(appvm.features['service.clocksync']) - app.clockvm = template + self.app.clockvm = self.template self.assertNotIn('service.clocksync', appvm.features) - self.assertIn('service.clocksync', template.features) - self.assertTrue(template.features['service.clocksync']) - app.close() + self.assertIn('service.clocksync', self.template.features) + self.assertTrue(self.template.features['service.clocksync']) + + def test_200_remove_template(self): + appvm = self.app.add_new_vm('AppVM', name='test-vm', + template=self.template, + label='red') + with mock.patch.object(self.app, 'vmm'): + with self.assertRaises(qubes.exc.QubesException): + del self.app.domains[self.template] + + def test_201_remove_netvm(self): + netvm = self.app.add_new_vm('AppVM', name='test-netvm', + template=self.template, provides_network=True, + label='red') + appvm = self.app.add_new_vm('AppVM', name='test-vm', + template=self.template, + label='red') + appvm.netvm = netvm + with mock.patch.object(self.app, 'vmm'): + with self.assertRaises(qubes.exc.QubesVMInUseError): + del self.app.domains[netvm] + + def test_202_remove_default_netvm(self): + netvm = self.app.add_new_vm('AppVM', name='test-netvm', + template=self.template, provides_network=True, + label='red') + self.app.default_netvm = netvm + with mock.patch.object(self.app, 'vmm'): + with self.assertRaises(qubes.exc.QubesVMInUseError): + del self.app.domains[netvm] + + def test_203_remove_default_dispvm(self): + appvm = self.app.add_new_vm('AppVM', name='test-appvm', + template=self.template, + label='red') + self.app.default_dispvm = appvm + with mock.patch.object(self.app, 'vmm'): + with self.assertRaises(qubes.exc.QubesVMInUseError): + del self.app.domains[appvm] + + def test_204_remove_appvm_dispvm(self): + dispvm = self.app.add_new_vm('AppVM', name='test-appvm', + template=self.template, + label='red') + appvm = self.app.add_new_vm('AppVM', name='test-appvm2', + template=self.template, default_dispvm=dispvm, + label='red') + with mock.patch.object(self.app, 'vmm'): + with self.assertRaises(qubes.exc.QubesVMInUseError): + del self.app.domains[dispvm] + + def test_205_remove_appvm_dispvm(self): + appvm = self.app.add_new_vm('AppVM', name='test-appvm', + template=self.template, template_for_dispvms=True, + label='red') + dispvm = self.app.add_new_vm('DispVM', name='test-dispvm', + template=appvm, + label='red') + with mock.patch.object(self.app, 'vmm'): + with self.assertRaises(qubes.exc.QubesVMInUseError): + del self.app.domains[appvm] @qubes.tests.skipUnlessGit def test_900_example_xml_in_doc(self): diff --git a/qubes/tests/devices_block.py b/qubes/tests/devices_block.py index a2e385ae..8adbf806 100644 --- a/qubes/tests/devices_block.py +++ b/qubes/tests/devices_block.py @@ -328,6 +328,29 @@ class TC_00_Block(qubes.tests.QubesTestCase): self.assertEqual(options['frontend-dev'], 'xvdi') self.assertEqual(options['read-only'], 'no') + def test_033_list_attached_cdrom(self): + disk = ''' + + + + + + + + ''' + vm = TestVM({}, domain_xml=domain_xml_template.format(disk)) + vm.app.domains['test-vm'] = vm + vm.app.domains['sys-usb'] = TestVM({}, name='sys-usb') + devices = sorted(list(self.ext.on_device_list_attached(vm, ''))) + self.assertEqual(len(devices), 1) + dev = devices[0][0] + options = devices[0][1] + self.assertEqual(dev.backend_domain, vm.app.domains['sys-usb']) + self.assertEqual(dev.ident, 'sr0') + self.assertEqual(options['frontend-dev'], 'xvdi') + self.assertEqual(options['read-only'], 'yes') + self.assertEqual(options['devtype'], 'cdrom') + def test_040_attach(self): back_vm = TestVM(name='sys-usb', qdb={ '/qubes-block-devices/sda': b'', diff --git a/qubes/tests/vm/adminvm.py b/qubes/tests/vm/adminvm.py index 0560e130..6b70ac48 100644 --- a/qubes/tests/vm/adminvm.py +++ b/qubes/tests/vm/adminvm.py @@ -38,7 +38,7 @@ class TC_00_AdminVM(qubes.tests.QubesTestCase): qubes.vm.adminvm.AdminVM, 'start_qdb_watch') as mock_qdb: self.vm = qubes.vm.adminvm.AdminVM(self.app, xml=None) - mock_qdb.assert_called_once_with('dom0') + mock_qdb.assert_called_once_with() self.addCleanup(self.cleanup_adminvm) except: # pylint: disable=bare-except if self.id().endswith('.test_000_init'): diff --git a/qubes/tests/vm/appvm.py b/qubes/tests/vm/appvm.py index 73bfc207..5dfc9c5d 100644 --- a/qubes/tests/vm/appvm.py +++ b/qubes/tests/vm/appvm.py @@ -130,6 +130,11 @@ class TC_90_AppVM(qubes.tests.vm.qubesvm.QubesVMTestsMixin, with self.assertRaises(qubes.exc.QubesVMNotHaltedError): vm.template = self.template + def test_004_template_reset(self): + vm = self.get_vm() + with self.assertRaises(qubes.exc.QubesValueError): + vm.template = qubes.property.DEFAULT + def test_500_property_migrate_template_for_dispvms(self): xml_template = ''' diff --git a/qubes/tests/vm/dispvm.py b/qubes/tests/vm/dispvm.py index f8b3174d..4236d258 100644 --- a/qubes/tests/vm/dispvm.py +++ b/qubes/tests/vm/dispvm.py @@ -103,6 +103,24 @@ class TC_00_DispVM(qubes.tests.QubesTestCase): dispvm = self.loop.run_until_complete( qubes.vm.dispvm.DispVM.from_appvm(self.appvm)) + def test_002_template_change(self): + self.appvm.template_for_dispvms = True + orig_getitem = self.app.domains.__getitem__ + with mock.patch.object(self.app, 'domains', wraps=self.app.domains) \ + as mock_domains: + mock_domains.configure_mock(**{ + 'get_new_unused_dispid': mock.Mock(return_value=42), + '__getitem__.side_effect': orig_getitem + }) + dispvm = self.app.add_new_vm(qubes.vm.dispvm.DispVM, + name='test-dispvm', template=self.appvm) + + with self.assertRaises(qubes.exc.QubesValueError): + dispvm.template = self.appvm + with self.assertRaises(qubes.exc.QubesValueError): + dispvm.template = qubes.property.DEFAULT + + def test_010_create_direct(self): self.appvm.template_for_dispvms = True orig_getitem = self.app.domains.__getitem__ diff --git a/qubes/tests/vm/qubesvm.py b/qubes/tests/vm/qubesvm.py index ccd62b4b..55646b1d 100644 --- a/qubes/tests/vm/qubesvm.py +++ b/qubes/tests/vm/qubesvm.py @@ -71,15 +71,15 @@ class TC_00_setters(qubes.tests.QubesTestCase): def test_000_setter_qid(self): self.assertEqual( - qubes.vm.qubesvm._setter_qid(self.vm, self.prop, 5), 5) + qubes.vm._setter_qid(self.vm, self.prop, 5), 5) def test_001_setter_qid_lt_0(self): with self.assertRaises(ValueError): - qubes.vm.qubesvm._setter_qid(self.vm, self.prop, -1) + qubes.vm._setter_qid(self.vm, self.prop, -1) def test_002_setter_qid_gt_max(self): with self.assertRaises(ValueError): - qubes.vm.qubesvm._setter_qid(self.vm, + qubes.vm._setter_qid(self.vm, self.prop, qubes.config.max_qid + 5) @unittest.skip('test not implemented') diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index fe2e1288..2c534773 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -27,6 +27,7 @@ import asyncio import re import string +import uuid import lxml.etree @@ -64,6 +65,26 @@ def validate_name(holder, prop, value): raise qubes.exc.QubesValueError( 'VM name cannot be \'none\' nor \'default\'') +def setter_label(self, prop, value): + ''' Helper for setting the domain label ''' + # pylint: disable=unused-argument + if isinstance(value, qubes.Label): + return value + if isinstance(value, str) and value.startswith('label-'): + return self.app.labels[int(value.split('-', 1)[1])] + + return self.app.get_label(value) + + +def _setter_qid(self, prop, value): + ''' Helper for setting the domain qid ''' + # pylint: disable=unused-argument + value = int(value) + if not 0 <= value <= qubes.config.max_qid: + raise ValueError( + '{} value must be between 0 and qubes.config.max_qid'.format( + prop.__name__)) + return value class Features(dict): '''Manager of the features. @@ -262,15 +283,35 @@ class BaseVM(qubes.PropertyHolder): ''' # pylint: disable=no-member + uuid = qubes.property('uuid', type=uuid.UUID, write_once=True, + clone=False, + doc='UUID from libvirt.') + + name = qubes.property('name', type=str, write_once=True, + clone=False, + doc='User-specified name of the domain.') + + qid = qubes.property('qid', type=int, write_once=True, + setter=_setter_qid, + clone=False, + doc='''Internal, persistent identificator of particular domain. Note + this is different from Xen domid.''') + + label = qubes.property('label', + setter=setter_label, + doc='''Colourful label assigned to VM. This is where the colour of the + padlock is set.''') + def __init__(self, app, xml, features=None, devices=None, tags=None, **kwargs): # pylint: disable=redefined-outer-name + self._qdb_watch_paths = set() + self._qdb_connection_watch = None + # self.app must be set before super().__init__, because some property # setters need working .app attribute #: mother :py:class:`qubes.Qubes` object - self._qdb_watch_paths = set() - self._qdb_connection_watch = None self.app = app super(BaseVM, self).__init__(xml, **kwargs) @@ -445,14 +486,20 @@ class BaseVM(qubes.PropertyHolder): self._qdb_connection_watch.close() self._qdb_connection_watch = None - def start_qdb_watch(self, name, loop=None): + def start_qdb_watch(self, loop=None): '''Start watching QubesDB Calling this method in appropriate time is responsibility of child class. ''' + # cleanup old watch connection first, if any + if self._qdb_connection_watch is not None: + asyncio.get_event_loop().remove_reader( + self._qdb_connection_watch.watch_fd()) + self._qdb_connection_watch.close() + import qubesdb # pylint: disable=import-error - self._qdb_connection_watch = qubesdb.QubesDB(name) + self._qdb_connection_watch = qubesdb.QubesDB(self.name) if loop is None: loop = asyncio.get_event_loop() loop.add_reader(self._qdb_connection_watch.watch_fd(), @@ -460,6 +507,10 @@ class BaseVM(qubes.PropertyHolder): for path in self._qdb_watch_paths: self._qdb_connection_watch.watch(path) + @qubes.stateless_property + def klass(self): + '''Domain class name''' + return type(self).__name__ class VMProperty(qubes.property): '''Property that is referring to a VM @@ -531,14 +582,3 @@ class VMProperty(qubes.property): return untrusted_vmname validate_name(None, self, untrusted_vmname) return untrusted_vmname - - -def setter_label(self, prop, value): - ''' Helper for setting the domain label ''' - # pylint: disable=unused-argument - if isinstance(value, qubes.Label): - return value - if isinstance(value, str) and value.startswith('label-'): - return self.app.labels[int(value.split('-', 1)[1])] - - return self.app.get_label(value) diff --git a/qubes/vm/adminvm.py b/qubes/vm/adminvm.py index 868656fb..40566c9d 100644 --- a/qubes/vm/adminvm.py +++ b/qubes/vm/adminvm.py @@ -24,10 +24,12 @@ ''' This module contains the AdminVM implementation ''' import libvirt + import qubes import qubes.exc import qubes.vm + class AdminVM(qubes.vm.BaseVM): '''Dom0''' @@ -36,12 +38,6 @@ class AdminVM(qubes.vm.BaseVM): name = qubes.property('name', default='dom0', setter=qubes.property.forbidden) - label = qubes.property('label', - setter=qubes.vm.setter_label, - saver=(lambda self, prop, value: 'label-{}'.format(value.index)), - doc='''Colourful label assigned to VM. This is where the colour of the - padlock is set.''') - qid = qubes.property('qid', default=0, setter=qubes.property.forbidden) @@ -62,7 +58,7 @@ class AdminVM(qubes.vm.BaseVM): self._libvirt_domain = None if not self.app.vmm.offline_mode: - self.start_qdb_watch('dom0') + self.start_qdb_watch() def __str__(self): return self.name diff --git a/qubes/vm/appvm.py b/qubes/vm/appvm.py index b853265b..8ab808d3 100644 --- a/qubes/vm/appvm.py +++ b/qubes/vm/appvm.py @@ -104,6 +104,12 @@ class AppVM(qubes.vm.qubesvm.QubesVM): ''' # pylint: disable=unused-argument assert self.template + @qubes.events.handler('property-pre-del:template') + def on_property_pre_del_template(self, event, name, oldvalue=None): + '''Forbid deleting template of running VM + ''' # pylint: disable=unused-argument,no-self-use + raise qubes.exc.QubesValueError('Cannot unset template') + @qubes.events.handler('property-pre-set:template') def on_property_pre_set_template(self, event, name, newvalue, oldvalue=None): diff --git a/qubes/vm/dispvm.py b/qubes/vm/dispvm.py index c70f4449..fc160b6a 100644 --- a/qubes/vm/dispvm.py +++ b/qubes/vm/dispvm.py @@ -121,8 +121,9 @@ class DispVM(qubes.vm.qubesvm.QubesVM): ''' # pylint: disable=unused-argument assert self.template - @qubes.events.handler('property-pre-set:template') - def on_property_pre_set_template(self, event, name, newvalue, + @qubes.events.handler('property-pre-set:template', + 'property-pre-del:template') + def on_property_pre_set_template(self, event, name, newvalue=None, oldvalue=None): ''' Disposable VM cannot have template changed ''' # pylint: disable=unused-argument @@ -137,7 +138,7 @@ class DispVM(qubes.vm.qubesvm.QubesVM): ''' with (yield from self.startup_lock): yield from self.storage.stop() - if self.auto_cleanup: + if self.auto_cleanup and self in self.app.domains: yield from self.remove_from_disk() del self.app.domains[self] self.app.save() @@ -164,7 +165,7 @@ class DispVM(qubes.vm.qubesvm.QubesVM): if not appvm.template_for_dispvms: raise qubes.exc.QubesException( 'Refusing to create DispVM out of this AppVM, because ' - 'template_for_appvms=False') + 'template_for_dispvms=False') app = appvm.app dispvm = app.add_new_vm( cls, @@ -197,10 +198,19 @@ class DispVM(qubes.vm.qubesvm.QubesVM): def start(self, **kwargs): # pylint: disable=arguments-differ - # sanity check, if template_for_dispvm got changed in the meantime - if not self.template.template_for_dispvms: - raise qubes.exc.QubesException( - 'template for DispVM ({}) needs to have ' - 'template_for_dispvms=True'.format(self.template.name)) + try: + # sanity check, if template_for_dispvm got changed in the meantime + if not self.template.template_for_dispvms: + raise qubes.exc.QubesException( + 'template for DispVM ({}) needs to have ' + 'template_for_dispvms=True'.format(self.template.name)) - yield from super(DispVM, self).start(**kwargs) + yield from super(DispVM, self).start(**kwargs) + except: + # cleanup also on failed startup; there is potential race with + # self.on_domain_shutdown_coro, so check if wasn't already removed + if self.auto_cleanup and self in self.app.domains: + yield from self.remove_from_disk() + del self.app.domains[self] + self.app.save() + raise diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index 1eaf97e0..c2c660e0 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -59,17 +59,6 @@ MEM_OVERHEAD_BASE = (3 + 1) * 1024 * 1024 MEM_OVERHEAD_PER_VCPU = 3 * 1024 * 1024 / 2 -def _setter_qid(self, prop, value): - ''' Helper for setting the domain qid ''' - # pylint: disable=unused-argument - value = int(value) - if not 0 <= value <= qubes.config.max_qid: - raise ValueError( - '{} value must be between 0 and qubes.config.max_qid'.format( - prop.__name__)) - return value - - def _setter_kernel(self, prop, value): ''' Helper for setting the domain kernel and running sanity checks on it. ''' # pylint: disable=unused-argument @@ -369,30 +358,6 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): # properties loaded from XML # - label = qubes.property('label', - setter=qubes.vm.setter_label, - saver=(lambda self, prop, value: 'label-{}'.format(value.index)), - doc='''Colourful label assigned to VM. This is where the colour of the - padlock is set.''') - -# provides_network = qubes.property('provides_network', -# type=bool, setter=qubes.property.bool, -# doc='`True` if it is NetVM or ProxyVM, false otherwise.') - - qid = qubes.property('qid', type=int, write_once=True, - setter=_setter_qid, - clone=False, - doc='''Internal, persistent identificator of particular domain. Note - this is different from Xen domid.''') - - name = qubes.property('name', type=str, write_once=True, - clone=False, - doc='User-specified name of the domain.') - - uuid = qubes.property('uuid', type=uuid.UUID, write_once=True, - clone=False, - doc='UUID from libvirt.') - virt_mode = qubes.property('virt_mode', type=str, setter=_setter_virt_mode, default='hvm', @@ -747,7 +712,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): self.storage = qubes.storage.Storage(self) if not self.app.vmm.offline_mode and self.is_running(): - self.start_qdb_watch(self.name) + self.start_qdb_watch() @qubes.events.handler('property-set:label') def on_property_set_label(self, event, name, newvalue, oldvalue=None): @@ -839,17 +804,24 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): pre_event=True, start_guid=start_guid, mem_required=mem_required) - yield from self.storage.verify() + try: + yield from self.storage.verify() - if self.netvm is not None: - # pylint: disable = no-member - if self.netvm.qid != 0: - if not self.netvm.is_running(): - yield from self.netvm.start(start_guid=start_guid, - notify_function=notify_function) + if self.netvm is not None: + # pylint: disable = no-member + if self.netvm.qid != 0: + if not self.netvm.is_running(): + yield from self.netvm.start(start_guid=start_guid, + notify_function=notify_function) - qmemman_client = yield from asyncio.get_event_loop().\ - run_in_executor(None, self.request_memory, mem_required) + qmemman_client = yield from asyncio.get_event_loop().\ + run_in_executor(None, self.request_memory, mem_required) + + except Exception as exc: + # let anyone receiving domain-pre-start know that startup failed + yield from self.fire_event_async('domain-start-failed', + reason=str(exc)) + raise try: yield from self.storage.start() @@ -857,6 +829,13 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): self.libvirt_domain.createWithFlags( libvirt.VIR_DOMAIN_START_PAUSED) + + except Exception as exc: + # let anyone receiving domain-pre-start know that startup failed + yield from self.fire_event_async('domain-start-failed', + reason=str(exc)) + raise + finally: if qmemman_client: qmemman_client.close() @@ -868,34 +847,27 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): self.log.info('Setting Qubes DB info for the VM') yield from self.start_qubesdb() self.create_qdb_entries() + self.start_qdb_watch() self.log.warning('Activating the {} VM'.format(self.name)) self.libvirt_domain.resume() - # close() is not really needed, because the descriptor is - # close-on-exec anyway, the reason to postpone close() is that - # possibly xl is not done constructing the domain after its main - # process exits so we close() when we know the domain is up the - # successful unpause is some indicator of it - if qmemman_client: - qmemman_client.close() - qmemman_client = None - yield from self.start_qrexec_daemon() yield from self.fire_event_async('domain-start', start_guid=start_guid) - except: # pylint: disable=bare-except + except Exception as exc: # pylint: disable=bare-except if self.is_running() or self.is_paused(): # This avoids losing the exception if an exception is # raised in self.force_shutdown(), because the vm is not # running or paused yield from self.kill() # pylint: disable=not-an-iterable + + # let anyone receiving domain-pre-start know that startup failed + yield from self.fire_event_async('domain-start-failed', + reason=str(exc)) raise - finally: - if qmemman_client: - qmemman_client.close() return self @@ -905,7 +877,11 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): Do not allow domain to be started again until this finishes. ''' with (yield from self.startup_lock): - yield from self.storage.stop() + try: + yield from self.storage.stop() + except qubes.storage.StoragePoolException: + self.log.exception('Failed to stop storage for domain %s', + self.name) @qubes.events.handler('domain-shutdown') def on_domain_shutdown(self, _event, **_kwargs): @@ -1767,8 +1743,6 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): self.fire_event('domain-qdb-create') - self.start_qdb_watch(self.name) - # TODO async; update this in constructor def _update_libvirt_domain(self): '''Re-initialise :py:attr:`libvirt_domain`.'''