From 1deb3221c740c9b4e5530bc43e90fe180473667d Mon Sep 17 00:00:00 2001 From: Wojtek Porczyk Date: Thu, 8 Jan 2015 19:35:59 +0100 Subject: [PATCH] qubes: fix netvm semantics WRT default values Automatic acquiring default*_netvm, default_template and {clock,update}vm is no more. This will be moved to firstboot. Advanced users (those, who elect not to autoconfig their initial VMs) will have to deal with that. Acknowledgement: This commit is a result of core3 review by Marek. --- qubes/__init__.py | 223 ++++++++++++++++++++++++++++++-------------- qubes/vm/qubesvm.py | 8 +- 2 files changed, 157 insertions(+), 74 deletions(-) diff --git a/qubes/__init__.py b/qubes/__init__.py index e4ce8e55..91344203 100644 --- a/qubes/__init__.py +++ b/qubes/__init__.py @@ -389,7 +389,8 @@ class VMCollection(object): :raises ValueError: when there is already VM which has equal ``qid`` ''' - # XXX this violates duck typing, should we do it? + # this violates duck typing, but is needed + # for VMProperty to function correctly if not isinstance(value, qubes.vm.BaseVM): raise TypeError('{} holds only BaseVM instances'.format(self.__class__.__name__)) @@ -526,7 +527,10 @@ class property(object): #: If property has no default value, this will unset it. DEFAULT = object() - def __init__(self, name, setter=None, saver=None, type=None, default=None, + # internal use only + _NO_DEFAULT = object() + + def __init__(self, name, setter=None, saver=None, type=None, default=_NO_DEFAULT, load_stage=2, order=0, save_via_ref=False, doc=None): self.__name__ = name self._setter = setter @@ -556,7 +560,7 @@ class property(object): except AttributeError: # sys.stderr.write(' __get__ except\n') - if self._default is None: + if self._default is self._NO_DEFAULT: raise AttributeError('property {!r} not set'.format(self.__name__)) elif isinstance(self._default, collections.Callable): return self._default(instance) @@ -753,7 +757,44 @@ class PropertyHolder(qubes.events.Emitter): :param value: value ''' - setattr(self, prop._attr_name, value) + setattr(self, self.get_property_def(prop)._attr_name, value) + + + def property_is_default(self, prop): + '''Check whether property is in it's default value. + + Properties when unset may return some default value, so + ``hasattr(vm, prop.__name__)`` is wrong in some circumstances. This + method allows for checking if the value returned is in fact it's + default value. + + :param qubes.property prop: property object of particular interest + :rtype: bool + ''' + + return hasattr(self, self.get_property_def(prop)._attr_name) + + + def get_property_def(self, prop): + '''Return property definition object. + + If prop is already :py:class:`qubes.property` instance, return the same + object. + + :param prop: property object or name + :type prop: qubes.property or str + :rtype: qubes.property + ''' + + if isinstance(prop, qubes.property): + return prop + + for p in self.get_props_list(): + if p.__name__ == prop: + return p + + raise AttributeError('No property {!r} found in {!r}'.format( + prop, self.__class__)) def load_properties(self, load_stage=None): @@ -841,6 +882,30 @@ class PropertyHolder(qubes.events.Emitter): self.fire_event('cloned-properties', src, proplist) + def require_property(self, prop, allow_none=False, hard=False): + '''Complain badly when property is not set. + + :param prop: property name or object + :type prop: qubes.property or str + :param bool allow_none: if :py:obj:`True`, don't complain if :py:obj:`None` is found + :param bool hard: if :py:obj:`True`, raise :py:class:`AssertionError`; if :py:obj:`False`, log warning instead + ''' + + if isinstance(qubes.property, prop): + prop = prop.__name__ + + try: + value = getattr(self, prop) + if value is None and not allow_none: + raise AttributeError() + except AttributeError: + msg = 'Required property {!r} not set on {!r}'.format(prop, self) + if hard: + raise AssertionError(msg) + else: + self.log(msg) + + import qubes.vm @@ -852,19 +917,34 @@ class VMProperty(property): and all supported by :py:class:`property` with the exception of ``type`` and ``setter`` ''' - def __init__(self, name, vmclass=qubes.vm.BaseVM, **kwargs): + def __init__(self, name, vmclass=qubes.vm.BaseVM, allow_none=False, **kwargs): if 'type' in kwargs: raise TypeError("'type' keyword parameter is unsupported in {}".format( self.__class__.__name__)) if 'setter' in kwargs: raise TypeError("'setter' keyword parameter is unsupported in {}".format( self.__class__.__name__)) + if not issubclass(vmclass, qubes.vm.BaseVM): + raise TypeError("'vmclass' should specify a subclass of qubes.vm.BaseVM") + super(VMProperty, self).__init__(name, **kwargs) self.vmclass = vmclass - + self.allow_none = allow_none def __set__(self, instance, value): + if value is None: + if self.allow_none: + super(VMProperty, self).__set__(self, instance, vm) + return + else: + raise ValueError( + 'Property {!r} does not allow setting to {!r}'.format( + self.__name__, value)) + + # XXX this may throw LookupError; that's good until introduction + # of QubesNoSuchVMException or whatever vm = instance.app.domains[value] + if not isinstance(vm, self.vmclass): raise TypeError('wrong VM class: domains[{!r}] if of type {!s} and not {!s}'.format( value, vm.__class__.__name__, self.vmclass.__name__)) @@ -875,7 +955,6 @@ class VMProperty(property): import qubes.vm.qubesvm import qubes.vm.templatevm - class Qubes(PropertyHolder): '''Main Qubes application @@ -922,10 +1001,13 @@ class Qubes(PropertyHolder): Methods and attributes: ''' - default_netvm = VMProperty('default_netvm', load_stage=3, - doc='Default NetVM for new AppVMs') - default_fw_netvm = VMProperty('default_fw_netvm', load_stage=3, - doc='Default NetVM for new ProxyVMs') + default_netvm = VMProperty('default_netvm', load_stage=3, default=None, + doc='''Default NetVM for AppVMs. Initial state is :py:obj:`None`, which + means that AppVMs are not connected to the Internet.''') + default_fw_netvm = VMProperty('default_fw_netvm', load_stage=3, default=None, + doc='''Default NetVM for ProxyVMs. Initial state is :py:obj:`None`, which + means that ProxyVMs (including FirewallVM) are not connected to the + Internet.''') default_template = VMProperty('default_template', load_stage=3, vmclass=qubes.vm.templatevm.TemplateVM, doc='Default template for new AppVMs') @@ -1006,40 +1088,22 @@ class Qubes(PropertyHolder): # stage 5: misc fixups - # if we have no default netvm, make first one the default - if not hasattr(self, 'default_netvm'): - for vm in self.domains: - if hasattr(vm, 'provides_network') and hasattr(vm, 'netvm'): - self.default_netvm = vm - break - - if not hasattr(self, 'default_fw_netvm'): - for vm in self.domains: - if hasattr(vm, 'provides_network') and not hasattr(vm, 'netvm'): - self.default_netvm = vm - break - - # first found template vm is the default - if not hasattr(self, 'default_template'): - for vm in self.domains: - if isinstance(vm, qubes.vm.templatevm.TemplateVM): - self.default_template = vm - break - - # if there was no clockvm entry in qubes.xml, try to determine default: - # root of default NetVM chain - if not hasattr(self, 'clockvm') and hasattr(self, 'default_netvm'): - clockvm = self.default_netvm - # Find root of netvm chain - while clockvm.netvm is not None: - clockvm = clockvm.netvm - - self.clockvm = clockvm + self.require_property('default_fw_netvm', allow_none=True) + self.require_property('default_netvm', allow_none=True) + self.require_property('default_template') + self.require_property('clockvm') + self.require_property('updatevm') # Disable ntpd in ClockVM - to not conflict with ntpdate (both are # using 123/udp port) if hasattr(self, 'clockvm'): - self.clockvm.services['ntpd'] = False + if 'ntpd' in self.clockvm.services: + if self.clockvm.services['ntpd']: + self.log.warning("VM set as clockvm ({!r}) has enabled " + "'ntpd' service! Expect failure when syncing time in " + "dom0.".format(self.clockvm)) + else: + self.clockvm.services['ntpd'] = False def _init(self): @@ -1102,7 +1166,6 @@ class Qubes(PropertyHolder): return labels - def add_new_vm(self, vm): '''Add new Virtual Machine to colletion @@ -1110,35 +1173,16 @@ class Qubes(PropertyHolder): self.domains.add(vm) - @qubes.events.handler('domain-added') - def on_domain_addedd(self, event, vm): - # make first created NetVM the default one - if not hasattr(self, 'default_fw_netvm') \ - and vm.provides_network \ - and not hasattr(vm, 'netvm'): - self.default_fw_netvm = vm - if not hasattr(self, 'default_netvm') \ - and vm.provides_network \ - and hasattr(vm, 'netvm'): - self.default_netvm = vm - - # make first created TemplateVM the default one - if not hasattr(self, 'default_template') \ - and not hasattr(vm, 'template'): - self.default_template = vm - - # make first created ProxyVM the UpdateVM - if not hasattr(self, 'default_netvm') \ - and vm.provides_network \ - and hasattr(vm, 'netvm'): - self.updatevm = vm - - # by default ClockVM is the first NetVM - if not hasattr(self, 'clockvm') \ - and vm.provides_network \ - and hasattr(vm, 'netvm'): - self.default_clockvm = vm + @qubes.events.handler('domain-pre-deleted') + def on_domain_pre_deleted(self, event, vm): + if isinstance(vm, qubes.vm.templatevm.TemplateVM): + appvms = self.get_vms_based_on(vm) + if appvms: + raise QubesException( + 'Cannot remove template that has dependent AppVMs. ' + 'Affected are: {}'.format(', '.join( + vm.name for name in sorted(appvms)))) @qubes.events.handler('domain-deleted') @@ -1157,5 +1201,44 @@ class Qubes(PropertyHolder): return super(QubesVmCollection, self).pop(qid) + @qubes.events.handler('property-pre-set:clockvm') + def on_property_pre_set_clockvm(self, event, name, newvalue, oldvalue=None): + if 'ntpd' in newvalue.services: + if newvalue.services['ntpd']: + raise QubesException( + 'Cannot set {!r} as {!r} property since it has ntpd enabled.'.format( + newvalue, name)) + else: + newvalue.services['ntpd'] = False + + + @qubes.events.handler('property-pre-set:default_netvm') + def on_property_pre_set_default_netvm(self, event, name, newvalue, oldvalue=None): + if newvalue is not None and oldvalue is not None \ + and oldvalue.is_running() and not newvalue.is_running() \ + and self.domains.get_vms_connected_to(oldvalue): + raise QubesException( + 'Cannot change default_netvm to domain that is not running ({!r}).'.format( + newvalue)) + + + @qubes.events.handler('property-set:default_fw_netvm') + def on_property_set_default_netvm(self, event, name, newvalue, oldvalue=None): + for vm in self.domains: + if not vm.provides_network and vm.property_is_default('netvm'): + # fire property-del:netvm as it is responsible for resetting + # netvm to it's default value + vm.fire_event('property-del:netvm', 'netvm', newvalue, oldvalue) + + + @qubes.events.handler('property-set:default_netvm') + def on_property_set_default_netvm(self, event, name, newvalue, oldvalue=None): + for vm in self.domains: + if vm.provides_network and vm.property_is_default('netvm'): + # fire property-del:netvm as it is responsible for resetting + # netvm to it's default value + vm.fire_event('property-del:netvm', 'netvm', newvalue, oldvalue) + + # load plugins import qubes._pluginloader diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index 9c9bc24d..be2d9765 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -111,12 +111,12 @@ class QubesVM(qubes.vm.BaseVM): 'of the padlock.') # XXX swallowed uses_default_netvm - netvm = qubes.property('netvm', load_stage=4, + netvm = qubes.VMProperty('netvm', load_stage=4, allow_none=True, default=(lambda self: self.app.default_fw_netvm if self.provides_network else self.app.default_netvm), doc='VM that provides network connection to this domain. ' - 'When :py:obj:`False`, machine is disconnected. ' - 'When :py:obj:`None` (or absent), domain uses default NetVM.') + 'When :py:obj:`None`, machine is disconnected. ' + 'When absent, domain uses default NetVM.') provides_network = qubes.property('provides_network', type=bool, doc=':py:obj:`True` if it is NetVM or ProxyVM, false otherwise') @@ -465,7 +465,7 @@ class QubesVM(qubes.vm.BaseVM): # we are changing to default netvm new_netvm = self.netvm if new_netvm == old_netvm: return - self.on_property_set_netvm(self, event, name, new_netvm, old_netvm) + self.fire_event('property-set:netvm', 'netvm', new_netvm, old_netvm) @qubes.events.handler('property-set:netvm')