diff --git a/qubes/config.py b/qubes/config.py index 889ed850..6c51e96e 100644 --- a/qubes/config.py +++ b/qubes/config.py @@ -44,6 +44,8 @@ system_path = { # qubes_icon_dir is obsolete # use QIcon.fromTheme() where applicable 'qubes_icon_dir': '/usr/share/icons/hicolor/128x128/devices', + + 'dom0_services_dir': '/var/run/qubes-service', } defaults = { diff --git a/qubes/ext/services.py b/qubes/ext/services.py index 52557d89..080fc59c 100644 --- a/qubes/ext/services.py +++ b/qubes/ext/services.py @@ -18,18 +18,46 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, see . -'''Extension responsible for qvm-service framework''' +"""Extension responsible for qvm-service framework""" +import os import qubes.ext +import qubes.config + class ServicesExtension(qubes.ext.Extension): - '''This extension export features with 'service.' prefix to QubesDB in + """This extension export features with 'service.' prefix to QubesDB in /qubes-service/ tree. - ''' + """ + + @staticmethod + def add_dom0_service(vm, service): + try: + os.makedirs( + qubes.config.system_path['dom0_services_dir'], exist_ok=True) + service = '{}/{}'.format( + qubes.config.system_path['dom0_services_dir'], service) + if not os.path.exists(service): + os.mknod(service) + except PermissionError: + vm.log.warning("Cannot write to {}".format( + qubes.config.system_path['dom0_services_dir'])) + + @staticmethod + def remove_dom0_service(vm, service): + try: + service = '{}/{}'.format( + qubes.config.system_path['dom0_services_dir'], service) + if os.path.exists(service): + os.remove(service) + except PermissionError: + vm.log.warning("Cannot write to {}".format( + qubes.config.system_path['dom0_services_dir'])) + # pylint: disable=no-self-use @qubes.ext.handler('domain-qdb-create') def on_domain_qdb_create(self, vm, event): - '''Actually export features''' + """Actually export features""" # pylint: disable=unused-argument for feature, value in vm.features.items(): if not feature.startswith('service.'): @@ -37,15 +65,15 @@ class ServicesExtension(qubes.ext.Extension): service = feature[len('service.'):] # forcefully convert to '0' or '1' vm.untrusted_qdb.write('/qubes-service/{}'.format(service), - str(int(bool(value)))) + str(int(bool(value)))) # always set meminfo-writer according to maxmem vm.untrusted_qdb.write('/qubes-service/meminfo-writer', - '1' if vm.maxmem > 0 else '0') + '1' if vm.maxmem > 0 else '0') @qubes.ext.handler('domain-feature-set:*') def on_domain_feature_set(self, vm, event, feature, value, oldvalue=None): - '''Update /qubes-service/ QubesDB tree in runtime''' + """Update /qubes-service/ QubesDB tree in runtime""" # pylint: disable=unused-argument # TODO: remove this compatibility hack in Qubes 4.1 @@ -68,11 +96,17 @@ class ServicesExtension(qubes.ext.Extension): service = feature[len('service.'):] # forcefully convert to '0' or '1' vm.untrusted_qdb.write('/qubes-service/{}'.format(service), - str(int(bool(value)))) + str(int(bool(value)))) + + if vm.name == "dom0": + if str(int(bool(value))) == "1": + self.add_dom0_service(vm, service) + else: + self.remove_dom0_service(vm, service) @qubes.ext.handler('domain-feature-delete:*') def on_domain_feature_delete(self, vm, event, feature): - '''Update /qubes-service/ QubesDB tree in runtime''' + """Update /qubes-service/ QubesDB tree in runtime""" # pylint: disable=unused-argument if not vm.is_running(): return @@ -84,9 +118,12 @@ class ServicesExtension(qubes.ext.Extension): return vm.untrusted_qdb.rm('/qubes-service/{}'.format(service)) + if vm.name == "dom0": + self.remove_dom0_service(vm, service) + @qubes.ext.handler('domain-load') def on_domain_load(self, vm, event): - '''Migrate meminfo-writer service into maxmem''' + """Migrate meminfo-writer service into maxmem""" # pylint: disable=no-self-use,unused-argument if 'service.meminfo-writer' in vm.features: # if was set to false, force maxmem=0 @@ -95,9 +132,19 @@ class ServicesExtension(qubes.ext.Extension): vm.maxmem = 0 del vm.features['service.meminfo-writer'] + if vm.name == "dom0": + for feature, value in vm.features.items(): + if not feature.startswith('service.'): + continue + service = feature[len('service.'):] + if str(int(bool(value))) == "1": + self.add_dom0_service(vm, service) + else: + self.remove_dom0_service(vm, service) + @qubes.ext.handler('features-request') def supported_services(self, vm, event, untrusted_features): - '''Handle advertisement of supported services''' + """Handle advertisement of supported services""" # pylint: disable=no-self-use,unused-argument if getattr(vm, 'template', None): diff --git a/qubes/tests/ext.py b/qubes/tests/ext.py index 582adece..8767dfef 100644 --- a/qubes/tests/ext.py +++ b/qubes/tests/ext.py @@ -18,13 +18,13 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, see . -from unittest import mock - +import os import qubes.ext.core_features import qubes.ext.services import qubes.ext.windows import qubes.tests +from unittest import mock class TC_00_CoreFeatures(qubes.tests.QubesTestCase): def setUp(self): @@ -235,19 +235,27 @@ class TC_20_Services(qubes.tests.QubesTestCase): def setUp(self): super().setUp() self.ext = qubes.ext.services.ServicesExtension() - self.vm = mock.MagicMock() self.features = {} - self.vm.configure_mock(**{ - 'template': None, - 'maxmem': 1024, - 'is_running.return_value': True, + specs = { 'features.get.side_effect': self.features.get, 'features.items.side_effect': self.features.items, 'features.__iter__.side_effect': self.features.__iter__, 'features.__contains__.side_effect': self.features.__contains__, 'features.__setitem__.side_effect': self.features.__setitem__, 'features.__delitem__.side_effect': self.features.__delitem__, - }) + } + vmspecs = {**specs, **{ + 'template': None, + 'maxmem': 1024, + 'is_running.return_value': True, + }} + dom0specs = {**specs, **{ + 'name': "dom0", + }} + self.vm = mock.MagicMock() + self.vm.configure_mock(**vmspecs) + self.dom0 = mock.MagicMock() + self.dom0.configure_mock(**dom0specs) def test_000_write_to_qdb(self): self.features['service.test1'] = '1' @@ -322,3 +330,55 @@ class TC_20_Services(qubes.tests.QubesTestCase): self.assertEqual(self.features, { 'supported-service.test2': True, }) + + def test_013_feature_set_dom0(self): + self.test_base_dir = '/tmp/qubes-test-dir' + self.base_dir_patch = mock.patch.dict( + qubes.config.system_path, {'dom0_services_dir': self.test_base_dir}) + self.base_dir_patch.start() + self.addCleanup(self.base_dir_patch.stop) + service = 'guivm-gui-agent' + service_path = self.test_base_dir + '/' + service + + self.ext.on_domain_feature_set( + self.dom0, + 'feature-set:service.service.guivm-gui-agent', + 'service.guivm-gui-agent', '1') + self.assertEqual(os.path.exists(service_path), True) + + def test_014_feature_delete_dom0(self): + self.test_base_dir = '/tmp/qubes-test-dir' + self.base_dir_patch = mock.patch.dict( + qubes.config.system_path, {'dom0_services_dir': self.test_base_dir}) + self.base_dir_patch.start() + self.addCleanup(self.base_dir_patch.stop) + service = 'guivm-gui-agent' + service_path = self.test_base_dir + '/' + service + + self.ext.on_domain_feature_set( + self.dom0, + 'feature-set:service.service.guivm-gui-agent', + 'service.guivm-gui-agent', '1') + + self.ext.on_domain_feature_delete( + self.dom0, + 'feature-delete:service.service.guivm-gui-agent', + 'service.guivm-gui-agent') + + self.assertEqual(os.path.exists(service_path), False) + + def test_014_feature_set_empty_value_dom0(self): + self.test_base_dir = '/tmp/qubes-test-dir' + self.base_dir_patch = mock.patch.dict( + qubes.config.system_path, {'dom0_services_dir': self.test_base_dir}) + self.base_dir_patch.start() + self.addCleanup(self.base_dir_patch.stop) + service = 'guivm-gui-agent' + service_path = self.test_base_dir + '/' + service + + self.ext.on_domain_feature_set( + self.dom0, + 'feature-set:service.service.guivm-gui-agent', + 'service.guivm-gui-agent', '') + + self.assertEqual(os.path.exists(service_path), False)