From 28737d16ce795028b6b81cfefc546234ce2d76fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Sat, 3 Jun 2017 04:52:30 +0200 Subject: [PATCH] Migrate qubes.NotifyTools, qubes.FeaturesRequest, qubes.NotifyUpdates Make them call into qubesd. Create separate socket for "misc" calls - VM accessible, but not part of Admin API. --- Makefile | 2 - qubes-rpc/qubes-notify-tools | 94 -------- qubes-rpc/qubes-notify-updates | 92 ------- qubes-rpc/qubes.FeaturesRequest | 15 +- qubes-rpc/qubes.NotifyTools | 5 +- qubes-rpc/qubes.NotifyUpdates | 5 +- qubes/api/misc.py | 164 +++++++++++++ qubes/tests/__init__.py | 1 + qubes/tests/api_misc.py | 410 ++++++++++++++++++++++++++++++++ qubes/tools/qubesd.py | 20 +- qubes/vm/qubesvm.py | 4 + rpm_spec/core-dom0.spec | 6 +- 12 files changed, 609 insertions(+), 209 deletions(-) delete mode 100755 qubes-rpc/qubes-notify-tools delete mode 100755 qubes-rpc/qubes-notify-updates mode change 100644 => 100755 qubes-rpc/qubes.NotifyTools mode change 100644 => 100755 qubes-rpc/qubes.NotifyUpdates create mode 100644 qubes/api/misc.py create mode 100644 qubes/tests/api_misc.py diff --git a/Makefile b/Makefile index 2beb2a21..64492ec4 100644 --- a/Makefile +++ b/Makefile @@ -170,8 +170,6 @@ endif cp qubes-rpc/qubes.GetRandomizedTime $(DESTDIR)/etc/qubes-rpc/ cp qubes-rpc/qubes.NotifyTools $(DESTDIR)/etc/qubes-rpc/ cp qubes-rpc/qubes.NotifyUpdates $(DESTDIR)/etc/qubes-rpc/ - cp qubes-rpc/qubes-notify-updates $(DESTDIR)/usr/libexec/qubes/ - cp qubes-rpc/qubes-notify-tools $(DESTDIR)/usr/libexec/qubes/ install qubes-rpc/qubesd-query-fast $(DESTDIR)/usr/libexec/qubes/ for method in $(ADMIN_API_METHODS_SIMPLE); do \ ln -s ../../usr/libexec/qubes/qubesd-query-fast \ diff --git a/qubes-rpc/qubes-notify-tools b/qubes-rpc/qubes-notify-tools deleted file mode 100755 index 273df90d..00000000 --- a/qubes-rpc/qubes-notify-tools +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/python2 - -import os -import re -import sys -import subprocess -from qubes.qubes import QubesVmCollection,QubesException,QubesHVm - -def main(): - - source = os.getenv("QREXEC_REMOTE_DOMAIN") - - if source is None: - print >> sys.stderr, 'This script must be called as qrexec service!' - exit(1) - - prev_qrexec_installed = False - source_vm = None - qvm_collection = QubesVmCollection() - qvm_collection.lock_db_for_writing() - try: - qvm_collection.load() - - source_vm = qvm_collection.get_vm_by_name(source) - if source_vm is None: - raise QubesException('Domain ' + source + ' does not exists (?!)') - - if not isinstance(source_vm, QubesHVm): - raise QubesException('Service qubes.ToolsNotify is designed only for HVM domains') - - # for now used only to check for the tools presence - untrusted_version = source_vm.qdb.read('/qubes-tools/version') - # reserved for future use - untrusted_os = source_vm.qdb.read('/qubes-tools/os') - # qrexec agent presence (0 or 1) - untrusted_qrexec = source_vm.qdb.read('/qubes-tools/qrexec') - # gui agent presence (0 or 1) - untrusted_gui = source_vm.qdb.read('/qubes-tools/gui') - # default user for qvm-run etc - untrusted_user = source_vm.qdb.read('/qubes-tools/default-user') - - if untrusted_version is None: - # tools didn't advertised its features; it's strange that this - # service is called, but ignore it - return - - # any suspicious string will raise exception here - version = int(untrusted_version) - - # untrusted_os - ignore for now - - if untrusted_qrexec is None: - qrexec = 0 - else: - qrexec = int(untrusted_qrexec) - - if untrusted_gui is None: - gui = 0 - else: - gui = int(untrusted_gui) - - if untrusted_user is not None and re.match(r'^[a-zA-Z0-9-]{1,255}$', untrusted_user): - assert '@' not in untrusted_user - assert '/' not in untrusted_user - - user = untrusted_user - else: - user = None - - prev_qrexec_installed = source_vm.qrexec_installed - # Let the tools to be able to enable *or disable* each particular component - source_vm.qrexec_installed = qrexec > 0 - source_vm.guiagent_installed = gui > 0 - - if user is not None: - source_vm.default_user = user - - qvm_collection.save() - - except Exception as e: - print >> sys.stderr, e.message - exit(1) - finally: - qvm_collection.unlock_db() - - if not prev_qrexec_installed and source_vm.qrexec_installed: - retcode = subprocess.call(['qvm-sync-appmenus', '--force-rpc']) - if retcode == 0 and hasattr(source_vm, 'appmenus_recreate'): - # TODO: call the same for child VMs? This isn't done for Linux VMs, - # so probably should be ignored for Windows also - source_vm.appmenus_recreate() - - -main() diff --git a/qubes-rpc/qubes-notify-updates b/qubes-rpc/qubes-notify-updates deleted file mode 100755 index 9ece7c42..00000000 --- a/qubes-rpc/qubes-notify-updates +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/python -# -# The Qubes OS Project, http://www.qubes-os.org -# -# Copyright (C) 2012 Marek Marczykowski -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# -import os -import os.path -import sys -import subprocess -import shutil -import grp -from datetime import datetime -from qubes.qubes import QubesVmCollection -from qubes.qubes import vm_files - -def main(): - - qvm_collection = QubesVmCollection() - qvm_collection.lock_db_for_reading() - qvm_collection.load() - qvm_collection.unlock_db() - - source = os.getenv("QREXEC_REMOTE_DOMAIN") - - if source is None: - print >> sys.stderr, 'This script must be called as qrexec service!' - exit(1) - - source_vm = qvm_collection.get_vm_by_name(source) - if source_vm is None: - print >> sys.stderr, 'Domain ' + source + ' does not exist (?!)' - exit(1) - - os.umask(0002) - qubes_gid = grp.getgrnam('qubes').gr_gid - - untrusted_update_count = sys.stdin.readline(128).strip() - if not untrusted_update_count.isdigit(): - print >> sys.stderr, 'Domain ' + source + ' sent invalid number of updates: %s' % untrusted_update_count - exit(1) - # now sanitized - update_count = untrusted_update_count - if source_vm.updateable: - # Just trust information from VM itself - update_f = open(source_vm.dir_path + '/' + vm_files["updates_stat_file"], "w") - update_f.write(update_count) - update_f.close() - try: - os.chown(source_vm.dir_path + '/' + vm_files["updates_stat_file"], -1, qubes_gid) - except OSError: - pass - elif source_vm.template is not None: - # Hint about updates availability in template - # If template is running - it will notify about updates itself - if source_vm.template.is_running(): - return - # Ignore no-updates info - if int(update_count) > 0: - stat_file = source_vm.template.dir_path + '/' + vm_files["updates_stat_file"] - # If VM is started before last updates.stat - it means that updates - # already was installed (but VM still hasn't been restarted), or other - # VM has already notified about updates availability - if os.path.exists(stat_file) and \ - source_vm.get_start_time() < datetime.fromtimestamp(os.path.getmtime(stat_file)): - return - update_f = open(stat_file, "w") - update_f.write(update_count) - update_f.close() - try: - os.chown(stat_file, -1, qubes_gid) - except OSError: - pass - else: - print >> sys.stderr, 'Ignoring notification of no updates' - -main() diff --git a/qubes-rpc/qubes.FeaturesRequest b/qubes-rpc/qubes.FeaturesRequest index b0ec5c19..a3f561c5 100755 --- a/qubes-rpc/qubes.FeaturesRequest +++ b/qubes-rpc/qubes.FeaturesRequest @@ -1,13 +1,4 @@ -#!/usr/bin/env python2 +#!/bin/sh -import os -import qubes - -PREFIX = '/features-request/' - -app = qubes.Qubes() -vm = app.domains[os.environ['QREXEC_REMOTE_DOMAIN']] -vm.fire_event('features-request', - untrusted_features={key[len(PREFIX):]: vm.qdb.read(key) - for key in vm.qdb.list(PREFIX)}) -app.save() +exec /usr/bin/qubesd-query -c /var/run/qubesd.misc.sock -e --fail \ + "$QREXEC_REMOTE_DOMAIN" qubes.FeaturesRequest dom0 "" >/dev/null 2>&1 diff --git a/qubes-rpc/qubes.NotifyTools b/qubes-rpc/qubes.NotifyTools old mode 100644 new mode 100755 index 14ccc083..71bc422d --- a/qubes-rpc/qubes.NotifyTools +++ b/qubes-rpc/qubes.NotifyTools @@ -1 +1,4 @@ -/usr/libexec/qubes/qubes-notify-tools +#!/bin/sh + +exec /usr/bin/qubesd-query -c /var/run/qubesd.misc.sock -e --fail \ + "$QREXEC_REMOTE_DOMAIN" qubes.NotifyTools dom0 "" >/dev/null 2>&1 diff --git a/qubes-rpc/qubes.NotifyUpdates b/qubes-rpc/qubes.NotifyUpdates old mode 100644 new mode 100755 index 7a60d883..f51e4fc2 --- a/qubes-rpc/qubes.NotifyUpdates +++ b/qubes-rpc/qubes.NotifyUpdates @@ -1 +1,4 @@ -/usr/libexec/qubes/qubes-notify-updates +#!/bin/sh + +exec /usr/bin/qubesd-query -c /var/run/qubesd.misc.sock --fail \ + "$QREXEC_REMOTE_DOMAIN" qubes.NotifyUpdates dom0 "" >/dev/null 2>&1 diff --git a/qubes/api/misc.py b/qubes/api/misc.py new file mode 100644 index 00000000..8de735fb --- /dev/null +++ b/qubes/api/misc.py @@ -0,0 +1,164 @@ +# -*- encoding: utf8 -*- +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2017 Marek Marczykowski-Górecki +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, see . + +''' Interface for methods not being part of Admin API, but still handled by +qubesd. ''' + +import asyncio +import string + +import qubes.api +import qubes.api.admin +import qubes.vm.dispvm + + +class QubesMiscAPI(qubes.api.AbstractQubesAPI): + @qubes.api.method('qubes.FeaturesRequest', no_payload=True) + @asyncio.coroutine + def qubes_features_request(self): + ''' qubes.FeaturesRequest handler + + VM (mostly templates) can request some features from dom0 for itself. + Then dom0 (qubesd extension) may respect this request or ignore it. + + Technically, VM first write requested features into QubesDB in + `/features-request/` subtree, then call this method. The method will + dispatch 'features-request' event, which may be handled by + appropriate extensions. Requests not explicitly handled by some + extension are ignored. + ''' + assert self.dest.name == 'dom0' + assert not self.arg + + prefix = '/features-request/' + + untrusted_features = {key[len(prefix):]: + self.src.qdb.read(key).decode('ascii', errors='strict') + for key in self.src.qdb.list(prefix)} + + safe_set = string.ascii_letters + string.digits + for untrusted_key in untrusted_features: + untrusted_value = untrusted_features[untrusted_key] + assert all((c in safe_set) for c in untrusted_value) + + self.src.fire_event('features-request', + untrusted_features=untrusted_features) + self.app.save() + + @qubes.api.method('qubes.NotifyTools', no_payload=True) + @asyncio.coroutine + def qubes_notify_tools(self): + ''' + Legacy version of qubes.FeaturesRequest, used by Qubes Windows Tools + ''' + assert self.dest.name == 'dom0' + assert not self.arg + + if getattr(self.src, 'template', None): + self.src.log.warning( + 'Ignoring qubes.NotifyTools for template-based VM') + return + + # for now used only to check for the tools presence + untrusted_version = self.src.qdb.read('/qubes-tools/version') + + # reserved for future use + #untrusted_os = self.src.qdb.read('/qubes-tools/os') + + # qrexec agent presence (0 or 1) + untrusted_qrexec = self.src.qdb.read('/qubes-tools/qrexec') + + # gui agent presence (0 or 1) + untrusted_gui = self.src.qdb.read('/qubes-tools/gui') + + # default user for qvm-run etc + # starting with Qubes 4.x ignored + #untrusted_user = self.src.qdb.read('/qubes-tools/default-user') + + if untrusted_version is None: + # tools didn't advertised its features; it's strange that this + # service is called, but ignore it + return + + # any suspicious string will raise exception here + int(untrusted_version) + del untrusted_version + + # untrusted_os - ignore for now + + if untrusted_qrexec is None: + qrexec = False + else: + qrexec = bool(int(untrusted_qrexec)) + del untrusted_qrexec + + if untrusted_gui is None: + gui = False + else: + gui = bool(int(untrusted_gui)) + del untrusted_gui + + # ignore default_user + + prev_qrexec = self.src.features.get('qrexec', False) + # Let the tools to be able to enable *or disable* + # each particular component + self.src.features['qrexec'] = qrexec + self.src.features['gui'] = gui + self.app.save() + + if not prev_qrexec and qrexec: + # if this is the first time qrexec was advertised, now can finish + # template setup + self.src.fire_event('template-postinstall') + + @qubes.api.method('qubes.NotifyUpdates') + @asyncio.coroutine + def qubes_notify_updates(self, untrusted_payload): + ''' + Receive VM notification about updates availability + + Payload contains a single integer - either 0 (no updates) or some + positive value (some updates). + ''' + + untrusted_update_count = untrusted_payload.strip() + assert untrusted_update_count.isdigit() + # now sanitized + update_count = int(untrusted_update_count) + del untrusted_update_count + + if self.src.updateable: + # Just trust information from VM itself + self.src.updates_available = bool(update_count) + self.app.save() + elif getattr(self.src, 'template', None) is not None: + # Hint about updates availability in template + # If template is running - it will notify about updates itself + if self.src.template.is_running(): + return + # Ignore no-updates info + if update_count > 0: + # If VM is outdated, updates were probably already installed + # in the template - ignore info + if self.src.storage.outdated_volumes: + return + self.src.template.updates_available = bool(update_count) + self.app.save() diff --git a/qubes/tests/__init__.py b/qubes/tests/__init__.py index 419b044a..84628c2d 100644 --- a/qubes/tests/__init__.py +++ b/qubes/tests/__init__.py @@ -969,6 +969,7 @@ def load_tests(loader, tests, pattern): # pylint: disable=unused-argument 'qubes.tests.app', 'qubes.tests.tarwriter', 'qubes.tests.api_admin', + 'qubes.tests.api_misc', 'qubespolicy.tests', 'qubes.tests.tools.qubesd', ): diff --git a/qubes/tests/api_misc.py b/qubes/tests/api_misc.py new file mode 100644 index 00000000..46c71f03 --- /dev/null +++ b/qubes/tests/api_misc.py @@ -0,0 +1,410 @@ +# -*- encoding: utf8 -*- +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2017 Marek Marczykowski-Górecki +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, see . +import asyncio +from unittest import mock +import qubes.tests +import qubes.api.misc + + +class TC_00_API_Misc(qubes.tests.QubesTestCase): + def setUp(self): + super(TC_00_API_Misc, self).setUp() + self.src = mock.NonCallableMagicMock() + self.app = mock.NonCallableMock() + self.dest = mock.NonCallableMock() + self.dest.name = 'dom0' + self.app.configure_mock(domains={ + 'dom0': self.dest, + 'test-vm': self.src, + }) + + def configure_qdb(self, entries): + self.src.configure_mock(**{ + 'qdb.read.side_effect': (lambda path: entries.get(path, None)), + 'qdb.list.side_effect': (lambda path: sorted(entries.keys())), + }) + + def call_mgmt_func(self, method, arg=b'', payload=b''): + mgmt_obj = qubes.api.misc.QubesMiscAPI(self.app, + b'test-vm', method, b'dom0', arg) + + loop = asyncio.get_event_loop() + response = loop.run_until_complete( + mgmt_obj.execute(untrusted_payload=payload)) + return response + + def test_000_features_request(self): + qdb_entries = { + '/features-request/feature1': b'1', + '/features-request/feature2': b'', + '/features-request/feature3': b'other', + } + self.configure_qdb(qdb_entries) + response = self.call_mgmt_func(b'qubes.FeaturesRequest') + self.assertIsNone(response) + self.assertEqual(self.app.mock_calls, [ + mock.call.save() + ]) + self.assertEqual(self.src.mock_calls, [ + mock.call.qdb.list('/features-request/'), + mock.call.qdb.read('/features-request/feature1'), + mock.call.qdb.read('/features-request/feature2'), + mock.call.qdb.read('/features-request/feature3'), + mock.call.fire_event('features-request', untrusted_features={ + 'feature1': '1', 'feature2': '', 'feature3': 'other'}) + ]) + + def test_001_features_request_empty(self): + self.configure_qdb({}) + response = self.call_mgmt_func(b'qubes.FeaturesRequest') + self.assertIsNone(response) + self.assertEqual(self.app.mock_calls, [ + mock.call.save() + ]) + self.assertEqual(self.src.mock_calls, [ + mock.call.qdb.list('/features-request/'), + mock.call.fire_event('features-request', untrusted_features={}) + ]) + + def test_002_features_request_invalid1(self): + qdb_entries = { + '/features-request/feature1': b'test spaces', + } + self.configure_qdb(qdb_entries) + with self.assertRaises(AssertionError): + self.call_mgmt_func(b'qubes.FeaturesRequest') + self.assertEqual(self.app.mock_calls, []) + self.assertEqual(self.src.mock_calls, [ + mock.call.qdb.list('/features-request/'), + mock.call.qdb.read('/features-request/feature1'), + ]) + + def test_003_features_request_invalid2(self): + qdb_entries = { + '/features-request/feature1': b'\xfe\x01', + } + self.configure_qdb(qdb_entries) + with self.assertRaises(UnicodeDecodeError): + self.call_mgmt_func(b'qubes.FeaturesRequest') + self.assertEqual(self.app.mock_calls, []) + self.assertEqual(self.src.mock_calls, [ + mock.call.qdb.list('/features-request/'), + mock.call.qdb.read('/features-request/feature1'), + ]) + + def test_010_notify_tools(self): + qdb_entries = { + '/qubes-tools/version': b'1', + '/qubes-tools/qrexec': b'1', + '/qubes-tools/gui': b'1', + '/qubes-tools/os': b'Linux', + '/qubes-tools/default-user': b'user', + } + self.configure_qdb(qdb_entries) + del self.src.template + self.src.configure_mock(**{'features.get.return_value': False}) + response = self.call_mgmt_func(b'qubes.NotifyTools') + self.assertIsNone(response) + self.assertEqual(self.app.mock_calls, [ + mock.call.save() + ]) + self.assertEqual(self.src.mock_calls, [ + mock.call.qdb.read('/qubes-tools/version'), + mock.call.qdb.read('/qubes-tools/qrexec'), + mock.call.qdb.read('/qubes-tools/gui'), + mock.call.features.get('qrexec', False), + mock.call.features.__setitem__('qrexec', True), + mock.call.features.__setitem__('gui', True), + mock.call.fire_event('template-postinstall') + ]) + + def test_011_notify_tools_uninstall(self): + qdb_entries = { + '/qubes-tools/version': b'1', + '/qubes-tools/qrexec': b'0', + '/qubes-tools/gui': b'0', + '/qubes-tools/os': b'Linux', + '/qubes-tools/default-user': b'user', + } + self.configure_qdb(qdb_entries) + del self.src.template + self.src.configure_mock(**{'features.get.return_value': True}) + response = self.call_mgmt_func(b'qubes.NotifyTools') + self.assertIsNone(response) + self.assertEqual(self.app.mock_calls, [ + mock.call.save() + ]) + self.assertEqual(self.src.mock_calls, [ + mock.call.qdb.read('/qubes-tools/version'), + mock.call.qdb.read('/qubes-tools/qrexec'), + mock.call.qdb.read('/qubes-tools/gui'), + mock.call.features.get('qrexec', False), + mock.call.features.__setitem__('qrexec', False), + mock.call.features.__setitem__('gui', False), + ]) + + def test_012_notify_tools_uninstall2(self): + qdb_entries = { + '/qubes-tools/version': b'1', + '/qubes-tools/os': b'Linux', + '/qubes-tools/default-user': b'user', + } + self.configure_qdb(qdb_entries) + del self.src.template + self.src.configure_mock(**{'features.get.return_value': True}) + response = self.call_mgmt_func(b'qubes.NotifyTools') + self.assertIsNone(response) + self.assertEqual(self.app.mock_calls, [ + mock.call.save() + ]) + self.assertEqual(self.src.mock_calls, [ + mock.call.qdb.read('/qubes-tools/version'), + mock.call.qdb.read('/qubes-tools/qrexec'), + mock.call.qdb.read('/qubes-tools/gui'), + mock.call.features.get('qrexec', False), + mock.call.features.__setitem__('qrexec', False), + mock.call.features.__setitem__('gui', False), + ]) + + def test_013_notify_tools_no_version(self): + qdb_entries = { + '/qubes-tools/qrexec': b'0', + '/qubes-tools/gui': b'0', + '/qubes-tools/os': b'Linux', + '/qubes-tools/default-user': b'user', + } + self.configure_qdb(qdb_entries) + del self.src.template + self.src.configure_mock(**{'features.get.return_value': True}) + response = self.call_mgmt_func(b'qubes.NotifyTools') + self.assertIsNone(response) + self.assertEqual(self.app.mock_calls, []) + self.assertEqual(self.src.mock_calls, [ + mock.call.qdb.read('/qubes-tools/version'), + mock.call.qdb.read('/qubes-tools/qrexec'), + mock.call.qdb.read('/qubes-tools/gui'), + ]) + + def test_014_notify_tools_invalid_version(self): + qdb_entries = { + '/qubes-tools/version': b'this is invalid', + '/qubes-tools/qrexec': b'0', + '/qubes-tools/gui': b'0', + '/qubes-tools/os': b'Linux', + '/qubes-tools/default-user': b'user', + } + self.configure_qdb(qdb_entries) + del self.src.template + self.src.configure_mock(**{'features.get.return_value': True}) + with self.assertRaises(ValueError): + self.call_mgmt_func(b'qubes.NotifyTools') + self.assertEqual(self.app.mock_calls, []) + self.assertEqual(self.src.mock_calls, [ + mock.call.qdb.read('/qubes-tools/version'), + mock.call.qdb.read('/qubes-tools/qrexec'), + mock.call.qdb.read('/qubes-tools/gui'), + ]) + + + def test_015_notify_tools_invalid_value_qrexec(self): + qdb_entries = { + '/qubes-tools/version': b'1', + '/qubes-tools/qrexec': b'invalid', + '/qubes-tools/gui': b'0', + '/qubes-tools/os': b'Linux', + '/qubes-tools/default-user': b'user', + } + self.configure_qdb(qdb_entries) + del self.src.template + self.src.configure_mock(**{'features.get.return_value': True}) + with self.assertRaises(ValueError): + self.call_mgmt_func(b'qubes.NotifyTools') + self.assertEqual(self.app.mock_calls, []) + self.assertEqual(self.src.mock_calls, [ + mock.call.qdb.read('/qubes-tools/version'), + mock.call.qdb.read('/qubes-tools/qrexec'), + mock.call.qdb.read('/qubes-tools/gui'), + ]) + + def test_016_notify_tools_invalid_value_gui(self): + qdb_entries = { + '/qubes-tools/version': b'1', + '/qubes-tools/qrexec': b'1', + '/qubes-tools/gui': b'invalid', + '/qubes-tools/os': b'Linux', + '/qubes-tools/default-user': b'user', + } + self.configure_qdb(qdb_entries) + del self.src.template + self.src.configure_mock(**{'features.get.return_value': True}) + with self.assertRaises(ValueError): + self.call_mgmt_func(b'qubes.NotifyTools') + self.assertEqual(self.app.mock_calls, []) + self.assertEqual(self.src.mock_calls, [ + mock.call.qdb.read('/qubes-tools/version'), + mock.call.qdb.read('/qubes-tools/qrexec'), + mock.call.qdb.read('/qubes-tools/gui'), + ]) + + def test_017_notify_tools_template_based(self): + qdb_entries = { + '/qubes-tools/version': b'1', + '/qubes-tools/qrexec': b'1', + '/qubes-tools/gui': b'invalid', + '/qubes-tools/os': b'Linux', + '/qubes-tools/default-user': b'user', + } + self.configure_qdb(qdb_entries) + self.src.configure_mock(**{'features.get.return_value': True}) + response = self.call_mgmt_func(b'qubes.NotifyTools') + self.assertIsNone(response) + self.assertEqual(self.app.mock_calls, []) + self.assertEqual(self.src.mock_calls, [ + mock.call.template.__bool__(), + mock.call.log.warning( + 'Ignoring qubes.NotifyTools for template-based VM') + ]) + + def test_018_notify_tools_already_installed(self): + qdb_entries = { + '/qubes-tools/version': b'1', + '/qubes-tools/qrexec': b'1', + '/qubes-tools/gui': b'1', + '/qubes-tools/os': b'Linux', + '/qubes-tools/default-user': b'user', + } + self.configure_qdb(qdb_entries) + del self.src.template + self.src.configure_mock(**{'features.get.return_value': True}) + response = self.call_mgmt_func(b'qubes.NotifyTools') + self.assertIsNone(response) + self.assertEqual(self.app.mock_calls, [ + mock.call.save() + ]) + self.assertEqual(self.src.mock_calls, [ + mock.call.qdb.read('/qubes-tools/version'), + mock.call.qdb.read('/qubes-tools/qrexec'), + mock.call.qdb.read('/qubes-tools/gui'), + mock.call.features.get('qrexec', False), + mock.call.features.__setitem__('qrexec', True), + mock.call.features.__setitem__('gui', True), + ]) + + def test_020_notify_updates_standalone(self): + del self.src.template + response = self.call_mgmt_func(b'qubes.NotifyUpdates', payload=b'1\n') + self.assertIsNone(response) + self.assertEqual(self.src.mock_calls, [ + mock.call.updateable.__bool__(), + ]) + self.assertEqual(self.src.updates_available, True) + self.assertEqual(self.app.mock_calls, [ + mock.call.save() + ]) + + def test_021_notify_updates_standalone2(self): + del self.src.template + response = self.call_mgmt_func(b'qubes.NotifyUpdates', payload=b'0\n') + self.assertIsNone(response) + self.assertEqual(self.src.mock_calls, [ + mock.call.updateable.__bool__(), + ]) + self.assertEqual(self.src.updates_available, False) + self.assertEqual(self.app.mock_calls, [ + mock.call.save() + ]) + + def test_022_notify_updates_invalid(self): + del self.src.template + with self.assertRaises(AssertionError): + self.call_mgmt_func(b'qubes.NotifyUpdates', payload=b'') + self.assertEqual(self.src.mock_calls, []) + # not set property returns Mock() + self.assertIsInstance(self.src.updates_available, mock.Mock) + self.assertEqual(self.app.mock_calls, []) + + def test_023_notify_updates_invalid2(self): + del self.src.template + with self.assertRaises(AssertionError): + self.call_mgmt_func(b'qubes.NotifyUpdates', payload=b'no updates') + self.assertEqual(self.src.mock_calls, []) + # not set property returns Mock() + self.assertIsInstance(self.src.updates_available, mock.Mock) + self.assertEqual(self.app.mock_calls, []) + + def test_024_notify_updates_template_based_no_updates(self): + '''No updates on template-based VM, should not reset state''' + self.src.updateable = False + self.src.template.is_running.return_value = False + response = self.call_mgmt_func(b'qubes.NotifyUpdates', payload=b'0\n') + self.assertIsNone(response) + self.assertEqual(self.src.mock_calls, [ + mock.call.template.is_running(), + ]) + # not set property returns Mock() + self.assertIsInstance(self.src.template.updates_available, mock.Mock) + self.assertIsInstance(self.src.updates_available, mock.Mock) + self.assertEqual(self.app.mock_calls, []) + + def test_025_notify_updates_template_based(self): + '''Some updates on template-based VM, should save flag''' + self.src.updateable = False + self.src.template.is_running.return_value = False + self.src.storage.outdated_volumes = [] + response = self.call_mgmt_func(b'qubes.NotifyUpdates', payload=b'1\n') + self.assertIsNone(response) + self.assertEqual(self.src.mock_calls, [ + mock.call.template.is_running(), + ]) + # not set property returns Mock() + self.assertIsInstance(self.src.updates_available, mock.Mock) + self.assertEqual(self.src.template.updates_available, True) + self.assertEqual(self.app.mock_calls, [ + mock.call.save() + ]) + + def test_026_notify_updates_template_based_outdated(self): + self.src.updateable = False + self.src.template.is_running.return_value = False + self.src.storage.outdated_volumes = ['root'] + response = self.call_mgmt_func(b'qubes.NotifyUpdates', payload=b'1\n') + self.assertIsNone(response) + self.assertEqual(self.src.mock_calls, [ + mock.call.template.is_running(), + ]) + # not set property returns Mock() + self.assertIsInstance(self.src.updates_available, mock.Mock) + self.assertIsInstance(self.src.template.updates_available, mock.Mock) + self.assertEqual(self.app.mock_calls, []) + + def test_027_notify_updates_template_based_template_running(self): + self.src.updateable = False + self.src.template.is_running.return_value = True + self.src.storage.outdated_volumes = [] + response = self.call_mgmt_func(b'qubes.NotifyUpdates', payload=b'1\n') + self.assertIsNone(response) + self.assertEqual(self.src.mock_calls, [ + mock.call.template.is_running(), + ]) + # not set property returns Mock() + self.assertIsInstance(self.src.template.updates_available, mock.Mock) + self.assertIsInstance(self.src.updates_available, mock.Mock) + self.assertEqual(self.app.mock_calls, []) + diff --git a/qubes/tools/qubesd.py b/qubes/tools/qubesd.py index 88adb149..43fe69c3 100644 --- a/qubes/tools/qubesd.py +++ b/qubes/tools/qubesd.py @@ -15,11 +15,13 @@ import qubes import qubes.api import qubes.api.admin import qubes.api.internal +import qubes.api.misc import qubes.utils import qubes.vm.qubesvm QUBESD_SOCK = '/var/run/qubesd.sock' QUBESD_INTERNAL_SOCK = '/var/run/qubesd.internal.sock' +QUBESD_MISC_SOCK = '/var/run/qubesd.misc.sock' class QubesDaemonProtocol(asyncio.Protocol): buffer_size = 65536 @@ -170,10 +172,10 @@ class QubesDaemonProtocol(asyncio.Protocol): self.transport.write(str(exc).encode('utf-8') + b'\0') -def sighandler(loop, signame, server, server_internal): +def sighandler(loop, signame, *servers): print('caught {}, exiting'.format(signame)) - server.close() - server_internal.close() + for server in servers: + server.close() loop.stop() parser = qubes.tools.QubesArgumentParser(description='Qubes OS daemon') @@ -213,12 +215,22 @@ def main(args=None): app=args.app, debug=args.debug), QUBESD_INTERNAL_SOCK)) shutil.chown(QUBESD_INTERNAL_SOCK, group='qubes') + try: + os.unlink(QUBESD_MISC_SOCK) + except FileNotFoundError: + pass + server_misc = loop.run_until_complete(loop.create_unix_server( + functools.partial(QubesDaemonProtocol, + qubes.api.misc.QubesMiscAPI, + app=args.app, debug=args.debug), QUBESD_MISC_SOCK)) + shutil.chown(QUBESD_MISC_SOCK, group='qubes') + os.umask(old_umask) del old_umask for signame in ('SIGINT', 'SIGTERM'): loop.add_signal_handler(getattr(signal, signame), - sighandler, loop, signame, server, server_internal) + sighandler, loop, signame, server, server_internal, server_misc) qubes.utils.systemd_notify() # make sure children will not inherit this diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index 0b0812cd..eb3b1058 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -509,6 +509,10 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): default=(lambda self: self.app.default_dispvm), doc='Default VM to be used as Disposable VM for service calls.') + updates_available = qubes.property('updates_available', + type=bool, + default=False, + doc='If updates are pending to be installed') updateable = qubes.property('updateable', default=(lambda self: not hasattr(self, 'template')), diff --git a/rpm_spec/core-dom0.spec b/rpm_spec/core-dom0.spec index 006a1296..17a1ed0c 100644 --- a/rpm_spec/core-dom0.spec +++ b/rpm_spec/core-dom0.spec @@ -241,8 +241,9 @@ fi %dir %{python3_sitelib}/qubes/api/__pycache__ %{python3_sitelib}/qubes/api/__pycache__/* %{python3_sitelib}/qubes/api/__init__.py -%{python3_sitelib}/qubes/api/internal.py %{python3_sitelib}/qubes/api/admin.py +%{python3_sitelib}/qubes/api/internal.py +%{python3_sitelib}/qubes/api/misc.py %dir %{python3_sitelib}/qubes/vm %dir %{python3_sitelib}/qubes/vm/__pycache__ @@ -298,6 +299,7 @@ fi %{python3_sitelib}/qubes/tests/extra.py %{python3_sitelib}/qubes/tests/api_admin.py +%{python3_sitelib}/qubes/tests/api_misc.py %{python3_sitelib}/qubes/tests/app.py %{python3_sitelib}/qubes/tests/devices.py %{python3_sitelib}/qubes/tests/devices_block.py @@ -380,8 +382,6 @@ fi /usr/lib/qubes/cleanup-dispvms /usr/lib/qubes/fix-dir-perms.sh /usr/lib/qubes/startup-misc.sh -/usr/libexec/qubes/qubes-notify-tools -/usr/libexec/qubes/qubes-notify-updates /usr/libexec/qubes/qubesd-query-fast %{_unitdir}/qubes-core.service %{_unitdir}/qubes-netvm.service