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:
parent
43fd1d76e8
commit
28737d16ce
2
Makefile
2
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 \
|
||||
|
@ -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()
|
@ -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()
|
@ -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
|
||||
|
5
qubes-rpc/qubes.NotifyTools
Normal file → Executable file
5
qubes-rpc/qubes.NotifyTools
Normal file → Executable 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
5
qubes-rpc/qubes.NotifyUpdates
Normal file → Executable 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
164
qubes/api/misc.py
Normal 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()
|
@ -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',
|
||||
):
|
||||
|
410
qubes/tests/api_misc.py
Normal file
410
qubes/tests/api_misc.py
Normal 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, [])
|
||||
|
@ -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
|
||||
|
@ -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')),
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user