From 6f2f57caead89f99c25a95d1d2b6039bb13956aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Pierret=20=28fepitre=29?= Date: Sun, 16 Feb 2020 16:07:44 +0100 Subject: [PATCH 1/8] services: make PEP8 happier --- qubes/ext/services.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/qubes/ext/services.py b/qubes/ext/services.py index 52557d89..76c313d0 100644 --- a/qubes/ext/services.py +++ b/qubes/ext/services.py @@ -18,18 +18,20 @@ # 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 qubes.ext + 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. - ''' + """ + # 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 +39,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 +70,11 @@ 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)))) @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 @@ -86,7 +88,7 @@ class ServicesExtension(qubes.ext.Extension): @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 @@ -97,7 +99,7 @@ class ServicesExtension(qubes.ext.Extension): @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): From d0a8b49cc96f4b1d82f6812543a0935170ff5c3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Pierret=20=28fepitre=29?= Date: Sun, 16 Feb 2020 18:53:42 +0100 Subject: [PATCH 2/8] services: handle /var/run/qubes/'SERVICE NAME' for dom0 --- qubes/ext/services.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/qubes/ext/services.py b/qubes/ext/services.py index 76c313d0..17d8b28b 100644 --- a/qubes/ext/services.py +++ b/qubes/ext/services.py @@ -20,6 +20,7 @@ """Extension responsible for qvm-service framework""" +import os import qubes.ext @@ -72,6 +73,10 @@ class ServicesExtension(qubes.ext.Extension): vm.untrusted_qdb.write('/qubes-service/{}'.format(service), str(int(bool(value)))) + if vm.name == "dom0" and str(int(bool(value))) == "1" and not \ + os.path.exists('/var/run/qubes-service/{}'.format(service)): + os.mknod('/var/run/qubes-service/{}'.format(service)) + @qubes.ext.handler('domain-feature-delete:*') def on_domain_feature_delete(self, vm, event, feature): """Update /qubes-service/ QubesDB tree in runtime""" @@ -86,6 +91,10 @@ class ServicesExtension(qubes.ext.Extension): return vm.untrusted_qdb.rm('/qubes-service/{}'.format(service)) + if vm.name == "dom0" and os.path.exists( + '/var/run/qubes-service/{}'.format(service)): + os.remove('/var/run/qubes-service/{}'.format(service)) + @qubes.ext.handler('domain-load') def on_domain_load(self, vm, event): """Migrate meminfo-writer service into maxmem""" @@ -97,6 +106,16 @@ class ServicesExtension(qubes.ext.Extension): vm.maxmem = 0 del vm.features['service.meminfo-writer'] + if vm.name == "dom0": + os.makedirs('/var/run/qubes-service/', exist_ok=True) + for feature, value in vm.features.items(): + if not feature.startswith('service.'): + continue + service = feature[len('service.'):] + if str(int(bool(value))) == "1" and not os.path.exists( + '/var/run/qubes-service/{}'.format(service)): + os.mknod('/var/run/qubes-service/{}'.format(service)) + @qubes.ext.handler('features-request') def supported_services(self, vm, event, untrusted_features): """Handle advertisement of supported services""" From a7e7166f7a84820df045f57cbb3fdce0b161ffbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Pierret=20=28fepitre=29?= Date: Tue, 18 Feb 2020 11:18:01 +0100 Subject: [PATCH 3/8] services: handle dom0 write permission errors --- qubes/ext/services.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/qubes/ext/services.py b/qubes/ext/services.py index 17d8b28b..dc43e378 100644 --- a/qubes/ext/services.py +++ b/qubes/ext/services.py @@ -29,6 +29,23 @@ class ServicesExtension(qubes.ext.Extension): /qubes-service/ tree. """ + @staticmethod + def add_dom0_services(vm, service): + try: + os.makedirs('/var/run/qubes-service/', exist_ok=True) + if not os.path.exists('/var/run/qubes-service/{}'.format(service)): + os.mknod('/var/run/qubes-service/{}'.format(service)) + except PermissionError: + vm.log.warning("Cannot write to /var/run/qubes-service") + + @staticmethod + def remove_dom0_services(vm, service): + try: + if os.path.exists('/var/run/qubes-service/{}'.format(service)): + os.remove('/var/run/qubes-service/{}'.format(service)) + except PermissionError: + vm.log.warning("Cannot write to /var/run/qubes-service") + # pylint: disable=no-self-use @qubes.ext.handler('domain-qdb-create') def on_domain_qdb_create(self, vm, event): @@ -73,9 +90,8 @@ class ServicesExtension(qubes.ext.Extension): vm.untrusted_qdb.write('/qubes-service/{}'.format(service), str(int(bool(value)))) - if vm.name == "dom0" and str(int(bool(value))) == "1" and not \ - os.path.exists('/var/run/qubes-service/{}'.format(service)): - os.mknod('/var/run/qubes-service/{}'.format(service)) + if vm.name == "dom0" and str(int(bool(value))) == "1": + self.add_dom0_services(vm, service) @qubes.ext.handler('domain-feature-delete:*') def on_domain_feature_delete(self, vm, event, feature): @@ -91,9 +107,8 @@ class ServicesExtension(qubes.ext.Extension): return vm.untrusted_qdb.rm('/qubes-service/{}'.format(service)) - if vm.name == "dom0" and os.path.exists( - '/var/run/qubes-service/{}'.format(service)): - os.remove('/var/run/qubes-service/{}'.format(service)) + if vm.name == "dom0": + self.remove_dom0_services(vm, service) @qubes.ext.handler('domain-load') def on_domain_load(self, vm, event): @@ -107,7 +122,6 @@ class ServicesExtension(qubes.ext.Extension): del vm.features['service.meminfo-writer'] if vm.name == "dom0": - os.makedirs('/var/run/qubes-service/', exist_ok=True) for feature, value in vm.features.items(): if not feature.startswith('service.'): continue From 0b8e5400a3d76f30bbe16717415714dadd99d3d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Pierret=20=28fepitre=29?= Date: Tue, 18 Feb 2020 11:44:57 +0100 Subject: [PATCH 4/8] config: specify dom0 services path --- qubes/config.py | 2 ++ qubes/ext/services.py | 19 ++++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/qubes/config.py b/qubes/config.py index 889ed850..6b56b90b 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/lib/qubes-services', } defaults = { diff --git a/qubes/ext/services.py b/qubes/ext/services.py index dc43e378..392bd0db 100644 --- a/qubes/ext/services.py +++ b/qubes/ext/services.py @@ -22,6 +22,9 @@ import os import qubes.ext +import qubes.config + +dom0_services_dir = qubes.config.system_path['dom0_services_dir'] class ServicesExtension(qubes.ext.Extension): @@ -32,19 +35,21 @@ class ServicesExtension(qubes.ext.Extension): @staticmethod def add_dom0_services(vm, service): try: - os.makedirs('/var/run/qubes-service/', exist_ok=True) - if not os.path.exists('/var/run/qubes-service/{}'.format(service)): - os.mknod('/var/run/qubes-service/{}'.format(service)) + os.makedirs(dom0_services_dir, exist_ok=True) + service = '{}/{}'.format(dom0_services_dir, service) + if not os.path.exists(service): + os.mknod(service) except PermissionError: - vm.log.warning("Cannot write to /var/run/qubes-service") + vm.log.warning("Cannot write to {}".format(dom0_services_dir)) @staticmethod def remove_dom0_services(vm, service): try: - if os.path.exists('/var/run/qubes-service/{}'.format(service)): - os.remove('/var/run/qubes-service/{}'.format(service)) + service = '{}/{}'.format(dom0_services_dir, service) + if os.path.exists(service): + os.remove(service) except PermissionError: - vm.log.warning("Cannot write to /var/run/qubes-service") + vm.log.warning("Cannot write to {}".format(dom0_services_dir)) # pylint: disable=no-self-use @qubes.ext.handler('domain-qdb-create') From 9a6ff177ce86818630d963ab1782b941b5bac3f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Pierret=20=28fepitre=29?= Date: Thu, 20 Feb 2020 14:53:44 +0100 Subject: [PATCH 5/8] tests: add/remove services in dom0 --- qubes/ext/services.py | 17 ++++++++----- qubes/tests/ext.py | 59 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/qubes/ext/services.py b/qubes/ext/services.py index 392bd0db..e07c7c43 100644 --- a/qubes/ext/services.py +++ b/qubes/ext/services.py @@ -24,8 +24,6 @@ import os import qubes.ext import qubes.config -dom0_services_dir = qubes.config.system_path['dom0_services_dir'] - class ServicesExtension(qubes.ext.Extension): """This extension export features with 'service.' prefix to QubesDB in @@ -35,21 +33,26 @@ class ServicesExtension(qubes.ext.Extension): @staticmethod def add_dom0_services(vm, service): try: - os.makedirs(dom0_services_dir, exist_ok=True) - service = '{}/{}'.format(dom0_services_dir, service) + 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(dom0_services_dir)) + vm.log.warning("Cannot write to {}".format( + qubes.config.system_path['dom0_services_dir'])) @staticmethod def remove_dom0_services(vm, service): try: - service = '{}/{}'.format(dom0_services_dir, service) + 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(dom0_services_dir)) + 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') diff --git a/qubes/tests/ext.py b/qubes/tests/ext.py index 582adece..308670ee 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,38 @@ 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() + 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() + 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) \ No newline at end of file From 31c098d1bd69a6ccde9049a4fefb6e6a816104cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Pierret=20=28fepitre=29?= Date: Thu, 27 Feb 2020 10:08:53 +0100 Subject: [PATCH 6/8] config: fix mistake in path for services --- qubes/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qubes/config.py b/qubes/config.py index 6b56b90b..6c51e96e 100644 --- a/qubes/config.py +++ b/qubes/config.py @@ -45,7 +45,7 @@ system_path = { # use QIcon.fromTheme() where applicable 'qubes_icon_dir': '/usr/share/icons/hicolor/128x128/devices', - 'dom0_services_dir': '/var/lib/qubes-services', + 'dom0_services_dir': '/var/run/qubes-service', } defaults = { From bdc3c6588bf0870e659729515bf1f8b6efb4e4da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Pierret=20=28fepitre=29?= Date: Sun, 8 Mar 2020 09:51:30 +0100 Subject: [PATCH 7/8] services: fixes from Marek's comments --- qubes/ext/services.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/qubes/ext/services.py b/qubes/ext/services.py index e07c7c43..857e155b 100644 --- a/qubes/ext/services.py +++ b/qubes/ext/services.py @@ -31,7 +31,7 @@ class ServicesExtension(qubes.ext.Extension): """ @staticmethod - def add_dom0_services(vm, service): + def add_dom0_service(vm, service): try: os.makedirs( qubes.config.system_path['dom0_services_dir'], exist_ok=True) @@ -44,7 +44,7 @@ class ServicesExtension(qubes.ext.Extension): qubes.config.system_path['dom0_services_dir'])) @staticmethod - def remove_dom0_services(vm, service): + def remove_dom0_service(vm, service): try: service = '{}/{}'.format( qubes.config.system_path['dom0_services_dir'], service) @@ -99,7 +99,7 @@ class ServicesExtension(qubes.ext.Extension): str(int(bool(value)))) if vm.name == "dom0" and str(int(bool(value))) == "1": - self.add_dom0_services(vm, service) + self.add_dom0_service(vm, service) @qubes.ext.handler('domain-feature-delete:*') def on_domain_feature_delete(self, vm, event, feature): @@ -116,7 +116,7 @@ class ServicesExtension(qubes.ext.Extension): vm.untrusted_qdb.rm('/qubes-service/{}'.format(service)) if vm.name == "dom0": - self.remove_dom0_services(vm, service) + self.remove_dom0_service(vm, service) @qubes.ext.handler('domain-load') def on_domain_load(self, vm, event): @@ -134,9 +134,8 @@ class ServicesExtension(qubes.ext.Extension): if not feature.startswith('service.'): continue service = feature[len('service.'):] - if str(int(bool(value))) == "1" and not os.path.exists( - '/var/run/qubes-service/{}'.format(service)): - os.mknod('/var/run/qubes-service/{}'.format(service)) + if str(int(bool(value))) == "1": + self.add_dom0_service(vm, service) @qubes.ext.handler('features-request') def supported_services(self, vm, event, untrusted_features): From a61bb9a0cbaf6682d836ef6c7201f17f67eee083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Pierret=20=28fepitre=29?= Date: Sun, 8 Mar 2020 10:01:26 +0100 Subject: [PATCH 8/8] Ensure empty service value delete /var/run/qubes-service/ file - Add tests and stop patch path from Marek's comment --- qubes/ext/services.py | 9 +++++++-- qubes/tests/ext.py | 21 +++++++++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/qubes/ext/services.py b/qubes/ext/services.py index 857e155b..080fc59c 100644 --- a/qubes/ext/services.py +++ b/qubes/ext/services.py @@ -98,8 +98,11 @@ class ServicesExtension(qubes.ext.Extension): vm.untrusted_qdb.write('/qubes-service/{}'.format(service), str(int(bool(value)))) - if vm.name == "dom0" and str(int(bool(value))) == "1": - self.add_dom0_service(vm, service) + 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): @@ -136,6 +139,8 @@ class ServicesExtension(qubes.ext.Extension): 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): diff --git a/qubes/tests/ext.py b/qubes/tests/ext.py index 308670ee..8767dfef 100644 --- a/qubes/tests/ext.py +++ b/qubes/tests/ext.py @@ -336,6 +336,7 @@ class TC_20_Services(qubes.tests.QubesTestCase): 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 @@ -343,7 +344,6 @@ class TC_20_Services(qubes.tests.QubesTestCase): 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): @@ -351,6 +351,7 @@ class TC_20_Services(qubes.tests.QubesTestCase): 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 @@ -364,4 +365,20 @@ class TC_20_Services(qubes.tests.QubesTestCase): 'feature-delete:service.service.guivm-gui-agent', 'service.guivm-gui-agent') - self.assertEqual(os.path.exists(service_path), False) \ No newline at end of file + 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)