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.
This commit is contained in:
Wojtek Porczyk 2015-01-08 19:35:59 +01:00
parent c2a35c02b4
commit 1deb3221c7
2 changed files with 157 additions and 74 deletions

View File

@ -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

View File

@ -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')