qubes/vm: change services to features

This commit is contained in:
Wojtek Porczyk 2016-03-03 13:03:27 +01:00
parent bf78e662f6
commit 93686eae06
8 changed files with 144 additions and 72 deletions

View File

@ -16,10 +16,10 @@
<property name="label" ref="label-1" /> <property name="label" ref="label-1" />
</properties> </properties>
<services> <features>
<service enabled="false">meminfo-writer</service> <feature name="meminfo-writer"></feature>
<service>qubes-firewall</service> <feature name="qubes-firewall">1</feature>
</services> </features>
<devices class="pci"> <devices class="pci">
<device>01:23.45</device> <device>01:23.45</device>

View File

@ -1262,13 +1262,12 @@ class Qubes(PropertyHolder):
# Disable ntpd in ClockVM - to not conflict with ntpdate (both are # Disable ntpd in ClockVM - to not conflict with ntpdate (both are
# using 123/udp port) # using 123/udp port)
if hasattr(self, 'clockvm') and self.clockvm is not None: if hasattr(self, 'clockvm') and self.clockvm is not None:
if 'ntpd' in self.clockvm.services: if self.clockvm.features.get('services/ntpd', False):
if self.clockvm.services['ntpd']: self.log.warning("VM set as clockvm ({!r}) has enabled 'ntpd' "
self.log.warning("VM set as clockvm ({!r}) has enabled " "service! Expect failure when syncing time in dom0.".format(
"'ntpd' service! Expect failure when syncing time in " self.clockvm))
"dom0.".format(self.clockvm))
else: else:
self.clockvm.services['ntpd'] = False self.clockvm.features['services/ntpd'] = ''
for vm in self.domains: for vm in self.domains:
vm.events_enabled = True vm.events_enabled = True
@ -1461,13 +1460,12 @@ class Qubes(PropertyHolder):
# pylint: disable=unused-argument,no-self-use # pylint: disable=unused-argument,no-self-use
if newvalue is None: if newvalue is None:
return return
if 'ntpd' in newvalue.services: if newvalue.features.get('services/ntpd', False):
if newvalue.services['ntpd']:
raise qubes.exc.QubesVMError(newvalue, raise qubes.exc.QubesVMError(newvalue,
'Cannot set {!r} as {!r} since it has ntpd enabled.'.format( 'Cannot set {!r} as {!r} since it has ntpd enabled.'.format(
newvalue.name, name)) newvalue.name, name))
else: else:
newvalue.services['ntpd'] = False newvalue.features['services/ntpd'] = ''
@qubes.events.handler( @qubes.events.handler(

View File

@ -178,7 +178,7 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
self.assertEquals(testvm1.include_in_backups, self.assertEquals(testvm1.include_in_backups,
testvm2.include_in_backups) testvm2.include_in_backups)
self.assertEquals(testvm1.default_user, testvm2.default_user) self.assertEquals(testvm1.default_user, testvm2.default_user)
self.assertEquals(testvm1.services, testvm2.services) self.assertEquals(testvm1.features, testvm2.features)
self.assertEquals(testvm1.get_firewall_conf(), self.assertEquals(testvm1.get_firewall_conf(),
testvm2.get_firewall_conf()) testvm2.get_firewall_conf())
@ -227,7 +227,7 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
self.assertEquals(testvm1.include_in_backups, self.assertEquals(testvm1.include_in_backups,
testvm3.include_in_backups) testvm3.include_in_backups)
self.assertEquals(testvm1.default_user, testvm3.default_user) self.assertEquals(testvm1.default_user, testvm3.default_user)
self.assertEquals(testvm1.services, testvm3.services) self.assertEquals(testvm1.features, testvm3.features)
self.assertEquals(testvm1.get_firewall_conf(), self.assertEquals(testvm1.get_firewall_conf(),
testvm3.get_firewall_conf()) testvm3.get_firewall_conf())

View File

@ -114,11 +114,11 @@ class TC_10_BaseVM(qubes.tests.QubesTestCase):
<tag name="testtag">tagvalue</tag> <tag name="testtag">tagvalue</tag>
</tags> </tags>
<services> <features>
<service>testservice</service> <feature name="testfeature_none"/>
<service enabled="True">enabledservice</service> <feature name="testfeature_empty"></feature>
<service enabled="False">disabledservice</service> <feature name="testfeature_aqq">aqq</feature>
</services> </features>
<devices class="pci"> <devices class="pci">
<device>00:11.22</device> <device>00:11.22</device>
@ -145,10 +145,10 @@ class TC_10_BaseVM(qubes.tests.QubesTestCase):
self.assertEqual(vm.testlabel, 'label-1') self.assertEqual(vm.testlabel, 'label-1')
self.assertEqual(vm.defaultprop, 'defaultvalue') self.assertEqual(vm.defaultprop, 'defaultvalue')
self.assertEqual(vm.tags, {'testtag': 'tagvalue'}) self.assertEqual(vm.tags, {'testtag': 'tagvalue'})
self.assertEqual(vm.services, { self.assertEqual(vm.features, {
'testservice': True, 'testfeature_none': None,
'enabledservice': True, 'testfeature_empty': '',
'disabledservice': False, 'testfeature_aqq': 'aqq',
}) })
self.assertItemsEqual(vm.devices.keys(), ('pci',)) self.assertItemsEqual(vm.devices.keys(), ('pci',))

View File

@ -23,7 +23,7 @@
# TODO list available classes # TODO list available classes
# TODO list labels (maybe in qvm-prefs) # TODO list labels (maybe in qvm-prefs)
# TODO services, devices, tags # TODO features, devices, tags
from __future__ import print_function from __future__ import print_function

View File

@ -48,6 +48,85 @@ import qubes.plugins
import qubes.tools.qvm_ls import qubes.tools.qvm_ls
class Features(dict):
'''Manager of the features.
This class inherits from dict, but has most of the methods that manipulate
the item disarmed (they raise NotImplementedError). The ones that are left
fire appropriate events on the qube that owns an instance of this class.
'''
#
# Those are the methods that affect contents. Either disarm them or make
# them report appropriate events. Good approach is to rewrite them carefully
# using official documentation, but use only our (overloaded) methods.
#
def __init__(self, vm, other=None, **kwargs):
super(Features, self).__init__()
self.vm = vm
self.update(other, **kwargs)
def __delitem__(self, key):
super(Features, self).__delitem__(key)
self.vm.fire_event('domain-feature-delete', key)
def __setitem__(self, key, value):
self.vm.fire_event('domain-feature-set', key, value)
super(Features, self).__setitem__(key, value)
def clear(self):
for key in self:
del self[key]
def pop(self):
'''Not implemented
:raises: NotImplementedError
'''
raise NotImplementedError()
def popitem(self):
'''Not implemented
:raises: NotImplementedError
'''
raise NotImplementedError()
def setdefault(self):
'''Not implemented
:raises: NotImplementedError
'''
raise NotImplementedError()
def update(self, other=None, **kwargs):
if other is not None:
if hasattr(other, 'keys'):
for key in other:
self[key] = other[key]
else:
for key, value in other:
self[key] = value
for key in kwargs:
self[key] = kwargs[key]
#
# end of overriding
#
_NO_DEFAULT = object()
def check_with_template(self, feature, default=_NO_DEFAULT):
if feature in self:
return self[feature]
if hasattr(self.vm, 'template') and self.vm.template is not None \
and feature in self.vm.template.features:
return self.vm.template.features[feature]
if default is self._NO_DEFAULT:
raise KeyError(feature)
return default
class BaseVMMeta(qubes.plugins.Plugin, qubes.events.EmitterMeta): class BaseVMMeta(qubes.plugins.Plugin, qubes.events.EmitterMeta):
'''Metaclass for :py:class:`.BaseVM`''' '''Metaclass for :py:class:`.BaseVM`'''
def __init__(cls, name, bases, dict_): def __init__(cls, name, bases, dict_):
@ -55,7 +134,6 @@ class BaseVMMeta(qubes.plugins.Plugin, qubes.events.EmitterMeta):
qubes.tools.qvm_ls.process_class(cls) qubes.tools.qvm_ls.process_class(cls)
class DeviceCollection(object): class DeviceCollection(object):
'''Bag for devices. '''Bag for devices.
@ -146,7 +224,7 @@ class BaseVM(qubes.PropertyHolder):
__metaclass__ = BaseVMMeta __metaclass__ = BaseVMMeta
def __init__(self, app, xml, services=None, devices=None, tags=None, def __init__(self, app, xml, features=None, devices=None, tags=None,
**kwargs): **kwargs):
# pylint: disable=redefined-outer-name # pylint: disable=redefined-outer-name
@ -157,8 +235,8 @@ class BaseVM(qubes.PropertyHolder):
super(BaseVM, self).__init__(xml, **kwargs) super(BaseVM, self).__init__(xml, **kwargs)
#: dictionary of services that are run on this domain #: dictionary of features of this qube
self.services = services or {} self.features = Features(self, features)
#: :py:class:`DeviceManager` object keeping devices that are attached to #: :py:class:`DeviceManager` object keeping devices that are attached to
#: this domain #: this domain
@ -168,10 +246,9 @@ class BaseVM(qubes.PropertyHolder):
self.tags = tags or {} self.tags = tags or {}
if self.xml is not None: if self.xml is not None:
# services # features
for node in xml.xpath('./services/service'): for node in xml.xpath('./features/service'):
self.services[node.text] = bool( self.features[node.get('name')] = node.text
ast.literal_eval(node.get('enabled', 'True').capitalize()))
# devices (pci, usb, ...) # devices (pci, usb, ...)
for parent in xml.xpath('./devices'): for parent in xml.xpath('./devices'):
@ -214,14 +291,12 @@ class BaseVM(qubes.PropertyHolder):
element.append(self.xml_properties()) element.append(self.xml_properties())
services = lxml.etree.Element('services') features = lxml.etree.Element('features')
for service in self.services: for feature in self.features:
node = lxml.etree.Element('service') node = lxml.etree.Element('service', name=feature)
node.text = service node.text = self.features[feature] if feature else None
if not self.services[service]: features.append(node)
node.set('enabled', 'false') element.append(features)
services.append(node)
element.append(services)
for devclass in self.devices: for devclass in self.devices:
devices = lxml.etree.Element('devices') devices = lxml.etree.Element('devices')
@ -325,9 +400,8 @@ class BaseVM(qubes.PropertyHolder):
args['vcpus'] = str(self.vcpus) args['vcpus'] = str(self.vcpus)
args['mem'] = str(min(self.memory, self.maxmem)) args['mem'] = str(min(self.memory, self.maxmem))
if 'meminfo-writer' in self.services \
and not self.services['meminfo-writer']:
# If dynamic memory management disabled, set maxmem=mem # If dynamic memory management disabled, set maxmem=mem
if not self.features.get('meminfo-writer', True):
args['maxmem'] = args['mem'] args['maxmem'] = args['mem']
if self.netvm is not None: if self.netvm is not None:
@ -481,10 +555,12 @@ class BaseVM(qubes.PropertyHolder):
# Automatically enable/disable 'yum-proxy-setup' service based on # Automatically enable/disable 'yum-proxy-setup' service based on
# allowYumProxy # allowYumProxy
if conf['allowYumProxy']: if conf['allowYumProxy']:
self.services['yum-proxy-setup'] = True self.features['yum-proxy-setup'] = '1'
else: else:
if self.services.has_key('yum-proxy-setup'): try:
self.services.pop('yum-proxy-setup') del self.features['yum-proxy-setup']
except KeyError:
pass
if expiring_rules_present: if expiring_rules_present:
subprocess.call(["sudo", "systemctl", "start", subprocess.call(["sudo", "systemctl", "start",

View File

@ -449,17 +449,17 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
if len(self.devices['pci']) > 0: if len(self.devices['pci']) > 0:
# Force meminfo-writer disabled when VM have PCI devices # Force meminfo-writer disabled when VM have PCI devices
self.services['meminfo-writer'] = False self.features['meminfo-writer'] = None
elif not isinstance(self, qubes.vm.adminvm.AdminVM) \ elif not isinstance(self, qubes.vm.adminvm.AdminVM) \
and 'meminfo-writer' not in self.services: and 'meminfo-writer' not in self.features:
# Always set if meminfo-writer should be active or not # Always set if meminfo-writer should be active or not
self.services['meminfo-writer'] = True self.features['meminfo-writer'] = '1'
if xml is None: if xml is None:
# new qube, disable updates check if requested for new qubes # new qube, disable updates check if requested for new qubes
# TODO: when features (#1637) are done, migrate to plugin # TODO: when features (#1637) are done, migrate to plugin
if not self.app.check_updates_vm: if not self.app.check_updates_vm:
self.services['qubes-update-check'] = False self.features['check-updates'] = None
# will be initialized after loading all the properties # will be initialized after loading all the properties
self.storage = None self.storage = None
@ -658,7 +658,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
try: try:
if preparing_dvm: if preparing_dvm:
self.services['qubes-dvm'] = True self.features['dvm'] = True
self.log.info('Setting Qubes DB info for the VM') self.log.info('Setting Qubes DB info for the VM')
self.start_qubesdb() self.start_qubesdb()
@ -1707,10 +1707,9 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
if tzname: if tzname:
self.qdb.write('/timezone', tzname) self.qdb.write('/timezone', tzname)
for srv in self.services.keys(): for feature, value in self.features.items():
# convert True/False to "1"/"0" self.qdb.write('/features/{0}'.format(feature),
self.qdb.write('/qubes-service/{0}'.format(srv), str(value) if value else '')
str(int(self.services[srv])))
self.qdb.write('/devices/block', '') self.qdb.write('/devices/block', '')
self.qdb.write('/devices/usb', '') self.qdb.write('/devices/usb', '')

View File

@ -153,27 +153,26 @@ the parser will complain about missing combine= attribute on the second <start>.
<ref name="properties" /> <ref name="properties" />
<optional> <optional>
<element name="services"> <element name="features">
<doc:description> <doc:description>
Container for services. Container for features.
</doc:description> </doc:description>
<oneOrMore> <oneOrMore>
<element name="service"> <element name="feature">
<doc:description> <doc:description>
One service that is either enabled or disabled. One feature of this domain.
</doc:description> </doc:description>
<optional> <attribute name="name">
<attribute name="enabled">
<doc:description> <doc:description>
Whether service is enabled or disabled. Name of the feature.
Default is ``true``.
</doc:description> </doc:description>
<data type="boolean" /> <data type="string">
<param name="pattern">[a-z0-9_-]+</param>
</data>
</attribute> </attribute>
</optional>
<data type="string"> <data type="string">
<param name="pattern">[a-z0-9_-]+</param> <param name="pattern">[a-z0-9_-]+</param>