From 93686eae0629f2efb045d1e6c4a1f523ff0a493d Mon Sep 17 00:00:00 2001 From: Wojtek Porczyk Date: Thu, 3 Mar 2016 13:03:27 +0100 Subject: [PATCH] qubes/vm: change services to features --- doc/example.xml | 8 +-- qubes/__init__.py | 22 ++++--- qubes/tests/int/basic.py | 4 +- qubes/tests/vm/init.py | 18 +++--- qubes/tools/qvm_create.py | 2 +- qubes/vm/__init__.py | 120 +++++++++++++++++++++++++++++++------- qubes/vm/qubesvm.py | 17 +++--- relaxng/qubes.rng | 25 ++++---- 8 files changed, 144 insertions(+), 72 deletions(-) diff --git a/doc/example.xml b/doc/example.xml index 0d1b5756..c55b952b 100644 --- a/doc/example.xml +++ b/doc/example.xml @@ -16,10 +16,10 @@ - - meminfo-writer - qubes-firewall - + + + 1 + 01:23.45 diff --git a/qubes/__init__.py b/qubes/__init__.py index aeb0bd52..79afe630 100644 --- a/qubes/__init__.py +++ b/qubes/__init__.py @@ -1262,13 +1262,12 @@ class Qubes(PropertyHolder): # Disable ntpd in ClockVM - to not conflict with ntpdate (both are # using 123/udp port) if hasattr(self, 'clockvm') and self.clockvm is not None: - 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)) + if self.clockvm.features.get('services/ntpd', False): + 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 + self.clockvm.features['services/ntpd'] = '' for vm in self.domains: vm.events_enabled = True @@ -1461,13 +1460,12 @@ class Qubes(PropertyHolder): # pylint: disable=unused-argument,no-self-use if newvalue is None: return - if 'ntpd' in newvalue.services: - if newvalue.services['ntpd']: - raise qubes.exc.QubesVMError(newvalue, - 'Cannot set {!r} as {!r} since it has ntpd enabled.'.format( - newvalue.name, name)) + if newvalue.features.get('services/ntpd', False): + raise qubes.exc.QubesVMError(newvalue, + 'Cannot set {!r} as {!r} since it has ntpd enabled.'.format( + newvalue.name, name)) else: - newvalue.services['ntpd'] = False + newvalue.features['services/ntpd'] = '' @qubes.events.handler( diff --git a/qubes/tests/int/basic.py b/qubes/tests/int/basic.py index 1d3af6df..04cea94f 100644 --- a/qubes/tests/int/basic.py +++ b/qubes/tests/int/basic.py @@ -178,7 +178,7 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase): self.assertEquals(testvm1.include_in_backups, testvm2.include_in_backups) 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(), testvm2.get_firewall_conf()) @@ -227,7 +227,7 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase): self.assertEquals(testvm1.include_in_backups, testvm3.include_in_backups) 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(), testvm3.get_firewall_conf()) diff --git a/qubes/tests/vm/init.py b/qubes/tests/vm/init.py index 865d6471..22138c95 100644 --- a/qubes/tests/vm/init.py +++ b/qubes/tests/vm/init.py @@ -114,11 +114,11 @@ class TC_10_BaseVM(qubes.tests.QubesTestCase): tagvalue - - testservice - enabledservice - disabledservice - + + + + aqq + 00:11.22 @@ -145,10 +145,10 @@ class TC_10_BaseVM(qubes.tests.QubesTestCase): self.assertEqual(vm.testlabel, 'label-1') self.assertEqual(vm.defaultprop, 'defaultvalue') self.assertEqual(vm.tags, {'testtag': 'tagvalue'}) - self.assertEqual(vm.services, { - 'testservice': True, - 'enabledservice': True, - 'disabledservice': False, + self.assertEqual(vm.features, { + 'testfeature_none': None, + 'testfeature_empty': '', + 'testfeature_aqq': 'aqq', }) self.assertItemsEqual(vm.devices.keys(), ('pci',)) diff --git a/qubes/tools/qvm_create.py b/qubes/tools/qvm_create.py index 5cd017f3..71c16b28 100644 --- a/qubes/tools/qvm_create.py +++ b/qubes/tools/qvm_create.py @@ -23,7 +23,7 @@ # TODO list available classes # TODO list labels (maybe in qvm-prefs) -# TODO services, devices, tags +# TODO features, devices, tags from __future__ import print_function diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index a75261ed..0b096dc9 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -48,6 +48,85 @@ import qubes.plugins 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): '''Metaclass for :py:class:`.BaseVM`''' 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) - class DeviceCollection(object): '''Bag for devices. @@ -146,7 +224,7 @@ class BaseVM(qubes.PropertyHolder): __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): # pylint: disable=redefined-outer-name @@ -157,8 +235,8 @@ class BaseVM(qubes.PropertyHolder): super(BaseVM, self).__init__(xml, **kwargs) - #: dictionary of services that are run on this domain - self.services = services or {} + #: dictionary of features of this qube + self.features = Features(self, features) #: :py:class:`DeviceManager` object keeping devices that are attached to #: this domain @@ -168,10 +246,9 @@ class BaseVM(qubes.PropertyHolder): self.tags = tags or {} if self.xml is not None: - # services - for node in xml.xpath('./services/service'): - self.services[node.text] = bool( - ast.literal_eval(node.get('enabled', 'True').capitalize())) + # features + for node in xml.xpath('./features/service'): + self.features[node.get('name')] = node.text # devices (pci, usb, ...) for parent in xml.xpath('./devices'): @@ -214,14 +291,12 @@ class BaseVM(qubes.PropertyHolder): element.append(self.xml_properties()) - services = lxml.etree.Element('services') - for service in self.services: - node = lxml.etree.Element('service') - node.text = service - if not self.services[service]: - node.set('enabled', 'false') - services.append(node) - element.append(services) + features = lxml.etree.Element('features') + for feature in self.features: + node = lxml.etree.Element('service', name=feature) + node.text = self.features[feature] if feature else None + features.append(node) + element.append(features) for devclass in self.devices: devices = lxml.etree.Element('devices') @@ -325,9 +400,8 @@ class BaseVM(qubes.PropertyHolder): args['vcpus'] = str(self.vcpus) 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'] if self.netvm is not None: @@ -481,10 +555,12 @@ class BaseVM(qubes.PropertyHolder): # Automatically enable/disable 'yum-proxy-setup' service based on # allowYumProxy if conf['allowYumProxy']: - self.services['yum-proxy-setup'] = True + self.features['yum-proxy-setup'] = '1' else: - if self.services.has_key('yum-proxy-setup'): - self.services.pop('yum-proxy-setup') + try: + del self.features['yum-proxy-setup'] + except KeyError: + pass if expiring_rules_present: subprocess.call(["sudo", "systemctl", "start", diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index b942c6b9..6cbbd521 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -449,17 +449,17 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): if len(self.devices['pci']) > 0: # 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) \ - and 'meminfo-writer' not in self.services: + and 'meminfo-writer' not in self.features: # Always set if meminfo-writer should be active or not - self.services['meminfo-writer'] = True + self.features['meminfo-writer'] = '1' if xml is None: # new qube, disable updates check if requested for new qubes # TODO: when features (#1637) are done, migrate to plugin 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 self.storage = None @@ -658,7 +658,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): try: if preparing_dvm: - self.services['qubes-dvm'] = True + self.features['dvm'] = True self.log.info('Setting Qubes DB info for the VM') self.start_qubesdb() @@ -1707,10 +1707,9 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): if tzname: self.qdb.write('/timezone', tzname) - for srv in self.services.keys(): - # convert True/False to "1"/"0" - self.qdb.write('/qubes-service/{0}'.format(srv), - str(int(self.services[srv]))) + for feature, value in self.features.items(): + self.qdb.write('/features/{0}'.format(feature), + str(value) if value else '') self.qdb.write('/devices/block', '') self.qdb.write('/devices/usb', '') diff --git a/relaxng/qubes.rng b/relaxng/qubes.rng index 95fafc51..c40b4534 100644 --- a/relaxng/qubes.rng +++ b/relaxng/qubes.rng @@ -153,27 +153,26 @@ the parser will complain about missing combine= attribute on the second . - + - Container for services. + Container for features. - + - One service that is either enabled or disabled. + One feature of this domain. - - - - Whether service is enabled or disabled. - Default is ``true``. - + + + Name of the feature. + - - - + + [a-z0-9_-]+ + + [a-z0-9_-]+