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.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 \
|
||||||
|
@ -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
|
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
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.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
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
|
||||||
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
|
||||||
|
@ -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')),
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user