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.
This commit is contained in:
Marek Marczykowski-Górecki 2017-06-03 04:52:30 +02:00
parent 43fd1d76e8
commit 28737d16ce
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
12 changed files with 609 additions and 209 deletions

View File

@ -170,8 +170,6 @@ endif
cp qubes-rpc/qubes.GetRandomizedTime $(DESTDIR)/etc/qubes-rpc/ cp qubes-rpc/qubes.GetRandomizedTime $(DESTDIR)/etc/qubes-rpc/
cp qubes-rpc/qubes.NotifyTools $(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.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/ install qubes-rpc/qubesd-query-fast $(DESTDIR)/usr/libexec/qubes/
for method in $(ADMIN_API_METHODS_SIMPLE); do \ for method in $(ADMIN_API_METHODS_SIMPLE); do \
ln -s ../../usr/libexec/qubes/qubesd-query-fast \ ln -s ../../usr/libexec/qubes/qubesd-query-fast \

View File

@ -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()

View File

@ -1,92 +0,0 @@
#!/usr/bin/python
#
# The Qubes OS Project, http://www.qubes-os.org
#
# Copyright (C) 2012 Marek Marczykowski <marmarek@invisiblethingslab.com>
#
# 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()

View File

@ -1,13 +1,4 @@
#!/usr/bin/env python2 #!/bin/sh
import os exec /usr/bin/qubesd-query -c /var/run/qubesd.misc.sock -e --fail \
import qubes "$QREXEC_REMOTE_DOMAIN" qubes.FeaturesRequest dom0 "" >/dev/null 2>&1
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()

5
qubes-rpc/qubes.NotifyTools Normal file → Executable file
View File

@ -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

5
qubes-rpc/qubes.NotifyUpdates Normal file → Executable file
View File

@ -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

164
qubes/api/misc.py Normal file
View File

@ -0,0 +1,164 @@
# -*- encoding: utf8 -*-
#
# The Qubes OS Project, http://www.qubes-os.org
#
# Copyright (C) 2017 Marek Marczykowski-Górecki
# <marmarek@invisiblethingslab.com>
#
# 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 <http://www.gnu.org/licenses/>.
''' 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()

View File

@ -969,6 +969,7 @@ def load_tests(loader, tests, pattern): # pylint: disable=unused-argument
'qubes.tests.app', 'qubes.tests.app',
'qubes.tests.tarwriter', 'qubes.tests.tarwriter',
'qubes.tests.api_admin', 'qubes.tests.api_admin',
'qubes.tests.api_misc',
'qubespolicy.tests', 'qubespolicy.tests',
'qubes.tests.tools.qubesd', 'qubes.tests.tools.qubesd',
): ):

410
qubes/tests/api_misc.py Normal file
View File

@ -0,0 +1,410 @@
# -*- encoding: utf8 -*-
#
# The Qubes OS Project, http://www.qubes-os.org
#
# Copyright (C) 2017 Marek Marczykowski-Górecki
# <marmarek@invisiblethingslab.com>
#
# 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 <http://www.gnu.org/licenses/>.
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, [])

View File

@ -15,11 +15,13 @@ import qubes
import qubes.api import qubes.api
import qubes.api.admin import qubes.api.admin
import qubes.api.internal import qubes.api.internal
import qubes.api.misc
import qubes.utils import qubes.utils
import qubes.vm.qubesvm import qubes.vm.qubesvm
QUBESD_SOCK = '/var/run/qubesd.sock' QUBESD_SOCK = '/var/run/qubesd.sock'
QUBESD_INTERNAL_SOCK = '/var/run/qubesd.internal.sock' QUBESD_INTERNAL_SOCK = '/var/run/qubesd.internal.sock'
QUBESD_MISC_SOCK = '/var/run/qubesd.misc.sock'
class QubesDaemonProtocol(asyncio.Protocol): class QubesDaemonProtocol(asyncio.Protocol):
buffer_size = 65536 buffer_size = 65536
@ -170,10 +172,10 @@ class QubesDaemonProtocol(asyncio.Protocol):
self.transport.write(str(exc).encode('utf-8') + b'\0') 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)) print('caught {}, exiting'.format(signame))
server.close() for server in servers:
server_internal.close() server.close()
loop.stop() loop.stop()
parser = qubes.tools.QubesArgumentParser(description='Qubes OS daemon') 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)) app=args.app, debug=args.debug), QUBESD_INTERNAL_SOCK))
shutil.chown(QUBESD_INTERNAL_SOCK, group='qubes') 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) os.umask(old_umask)
del old_umask del old_umask
for signame in ('SIGINT', 'SIGTERM'): for signame in ('SIGINT', 'SIGTERM'):
loop.add_signal_handler(getattr(signal, signame), 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() qubes.utils.systemd_notify()
# make sure children will not inherit this # make sure children will not inherit this

View File

@ -509,6 +509,10 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
default=(lambda self: self.app.default_dispvm), default=(lambda self: self.app.default_dispvm),
doc='Default VM to be used as Disposable VM for service calls.') 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', updateable = qubes.property('updateable',
default=(lambda self: not hasattr(self, 'template')), default=(lambda self: not hasattr(self, 'template')),

View File

@ -241,8 +241,9 @@ fi
%dir %{python3_sitelib}/qubes/api/__pycache__ %dir %{python3_sitelib}/qubes/api/__pycache__
%{python3_sitelib}/qubes/api/__pycache__/* %{python3_sitelib}/qubes/api/__pycache__/*
%{python3_sitelib}/qubes/api/__init__.py %{python3_sitelib}/qubes/api/__init__.py
%{python3_sitelib}/qubes/api/internal.py
%{python3_sitelib}/qubes/api/admin.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
%dir %{python3_sitelib}/qubes/vm/__pycache__ %dir %{python3_sitelib}/qubes/vm/__pycache__
@ -298,6 +299,7 @@ fi
%{python3_sitelib}/qubes/tests/extra.py %{python3_sitelib}/qubes/tests/extra.py
%{python3_sitelib}/qubes/tests/api_admin.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/app.py
%{python3_sitelib}/qubes/tests/devices.py %{python3_sitelib}/qubes/tests/devices.py
%{python3_sitelib}/qubes/tests/devices_block.py %{python3_sitelib}/qubes/tests/devices_block.py
@ -380,8 +382,6 @@ fi
/usr/lib/qubes/cleanup-dispvms /usr/lib/qubes/cleanup-dispvms
/usr/lib/qubes/fix-dir-perms.sh /usr/lib/qubes/fix-dir-perms.sh
/usr/lib/qubes/startup-misc.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 /usr/libexec/qubes/qubesd-query-fast
%{_unitdir}/qubes-core.service %{_unitdir}/qubes-core.service
%{_unitdir}/qubes-netvm.service %{_unitdir}/qubes-netvm.service