Global Settings now can function with partial permissions

Unreadable properties/features will be disabled, the tool will start
even if it can access nothing or almost nothing, and errors on settings
features/properties will now be communicated to the user.
This commit is contained in:
Marta Marczykowska-Górecka 2020-08-04 21:53:03 +02:00
parent 1f933b775a
commit 7cbc7d9db1
No known key found for this signature in database
GPG Key ID: 9A752C30B26FD04B
3 changed files with 191 additions and 75 deletions

View File

@ -25,6 +25,7 @@ import subprocess
from PyQt5 import QtWidgets, QtCore, QtGui # pylint: disable=import-error from PyQt5 import QtWidgets, QtCore, QtGui # pylint: disable=import-error
from qubesadmin.utils import parse_size from qubesadmin.utils import parse_size
from qubesadmin import exc
from . import ui_globalsettingsdlg # pylint: disable=no-name-in-module from . import ui_globalsettingsdlg # pylint: disable=no-name-in-module
from . import utils from . import utils
@ -83,6 +84,8 @@ class GlobalSettingsWindow(ui_globalsettingsdlg.Ui_GlobalSettings,
self.__init_updates__() self.__init_updates__()
self.__init_gui_defaults() self.__init_gui_defaults()
self.errors = []
def setup_application(self): def setup_application(self):
self.app.setApplicationName(self.tr("Qubes Global Settings")) self.app.setApplicationName(self.tr("Qubes Global Settings"))
self.app.setWindowIcon(QtGui.QIcon.fromTheme("qubes-manager")) self.app.setWindowIcon(QtGui.QIcon.fromTheme("qubes-manager"))
@ -112,7 +115,7 @@ class GlobalSettingsWindow(ui_globalsettingsdlg.Ui_GlobalSettings,
utils.initialize_widget_with_vms( utils.initialize_widget_with_vms(
widget=self.default_netvm_combo, widget=self.default_netvm_combo,
qubes_app=self.qubes_app, qubes_app=self.qubes_app,
filter_function=(lambda vm: vm.provides_network), filter_function=(lambda vm: getattr(vm, 'provides_network', False)),
allow_none=True, allow_none=True,
holder=self.qubes_app, holder=self.qubes_app,
property_name="default_netvm" property_name="default_netvm"
@ -142,39 +145,67 @@ class GlobalSettingsWindow(ui_globalsettingsdlg.Ui_GlobalSettings,
def __apply_system_defaults__(self): def __apply_system_defaults__(self):
# updatevm # updatevm
if utils.did_widget_selection_change(self.update_vm_combo): if utils.did_widget_selection_change(self.update_vm_combo):
self.qubes_app.updatevm = self.update_vm_combo.currentData() try:
self.qubes_app.updatevm = self.update_vm_combo.currentData()
except exc.QubesException as ex:
self.errors.append(
"Failed to set UpdateVM due to {}".format(str(ex)))
# clockvm # clockvm
if utils.did_widget_selection_change(self.clock_vm_combo): if utils.did_widget_selection_change(self.clock_vm_combo):
self.qubes_app.clockvm = self.clock_vm_combo.currentData() try:
self.qubes_app.clockvm = self.clock_vm_combo.currentData()
except exc.QubesException as ex:
self.errors.append(
"Failed to set ClockVM due to {}".format(str(ex)))
# default netvm # default netvm
if utils.did_widget_selection_change(self.default_netvm_combo): if utils.did_widget_selection_change(self.default_netvm_combo):
self.qubes_app.default_netvm = \ try:
self.default_netvm_combo.currentData() self.qubes_app.default_netvm = \
self.default_netvm_combo.currentData()
except exc.QubesException as ex:
self.errors.append(
"Failed to set Default NetVM due to {}".format(str(ex)))
# default template # default template
if utils.did_widget_selection_change(self.default_template_combo): if utils.did_widget_selection_change(self.default_template_combo):
self.qubes_app.default_template = \ try:
self.default_template_combo.currentData() self.qubes_app.default_template = \
self.default_template_combo.currentData()
except exc.QubesException as ex:
self.errors.append(
"Failed to set Default Template due to {}".format(str(ex)))
# default_dispvm # default_dispvm
if utils.did_widget_selection_change(self.default_dispvm_combo): if utils.did_widget_selection_change(self.default_dispvm_combo):
self.qubes_app.default_dispvm = \ try:
self.default_dispvm_combo.currentData() self.qubes_app.default_dispvm = \
self.default_dispvm_combo.currentData()
except exc.QubesException as ex:
self.errors.append(
"Failed to set Default DispVM due to {}".format(str(ex)))
def __init_kernel_defaults__(self): def __init_kernel_defaults__(self):
utils.initialize_widget_with_kernels( try:
widget=self.default_kernel_combo, utils.initialize_widget_with_kernels(
qubes_app=self.qubes_app, widget=self.default_kernel_combo,
allow_none=True, qubes_app=self.qubes_app,
holder=self.qubes_app, allow_none=True,
property_name='default_kernel') holder=self.qubes_app,
property_name='default_kernel')
except exc.QubesPropertyAccessError:
self.default_kernel_combo.clear()
self.default_kernel_combo.setEnabled(False)
def __apply_kernel_defaults__(self): def __apply_kernel_defaults__(self):
if utils.did_widget_selection_change(self.default_kernel_combo): if utils.did_widget_selection_change(self.default_kernel_combo):
self.qubes_app.default_kernel = \ try:
self.default_kernel_combo.currentData() self.qubes_app.default_kernel = \
self.default_kernel_combo.currentData()
except exc.QubesException as ex:
self.errors.append(
"Failed to set Default Kernel due to {}".format(str(ex)))
def __init_gui_defaults(self): def __init_gui_defaults(self):
utils.initialize_widget( utils.initialize_widget(
@ -210,8 +241,8 @@ class GlobalSettingsWindow(ui_globalsettingsdlg.Ui_GlobalSettings,
('tinted icon with modified white', 'tint+whitehack'), ('tinted icon with modified white', 'tint+whitehack'),
('tinted icon with 50% saturation', 'tint+saturation50') ('tinted icon with 50% saturation', 'tint+saturation50')
], ],
selected_value=self.vm.features.get('gui-default-trayicon-mode', selected_value=utils.get_feature(
None)) self.vm, 'gui-default-trayicon-mode', None))
utils.initialize_widget( utils.initialize_widget(
widget=self.securecopy, widget=self.securecopy,
@ -220,8 +251,8 @@ class GlobalSettingsWindow(ui_globalsettingsdlg.Ui_GlobalSettings,
('Ctrl+Shift+C', 'Ctrl-Shift-c'), ('Ctrl+Shift+C', 'Ctrl-Shift-c'),
('Ctrl+Win+C', 'Ctrl-Mod4-c'), ('Ctrl+Win+C', 'Ctrl-Mod4-c'),
], ],
selected_value=self.vm.features.get( selected_value=utils.get_feature(
'gui-default-secure-copy-sequence', None)) self.vm, 'gui-default-secure-copy-sequence', None))
utils.initialize_widget( utils.initialize_widget(
widget=self.securepaste, widget=self.securepaste,
@ -231,15 +262,25 @@ class GlobalSettingsWindow(ui_globalsettingsdlg.Ui_GlobalSettings,
('Ctrl+Win+V', 'Ctrl-Mod4-v'), ('Ctrl+Win+V', 'Ctrl-Mod4-v'),
('Ctrl+Insert', 'Ctrl-Ins'), ('Ctrl+Insert', 'Ctrl-Ins'),
], ],
selected_value=self.vm.features.get( selected_value=utils.get_feature(
'gui-default-secure-paste-sequence', None)) self.vm, 'gui-default-secure-paste-sequence', None))
def __apply_feature_change(self, widget, feature): def __apply_feature_change(self, widget, feature):
if utils.did_widget_selection_change(widget): if utils.did_widget_selection_change(widget):
if widget.currentData() is None: if widget.currentData() is None:
del self.vm.features[feature] try:
del self.vm.features[feature]
except exc.QubesDaemonCommunicationError:
self.errors.append(
"Failed to set {} due to insufficient "
"permissions".format(feature))
else: else:
self.vm.features[feature] = widget.currentData() try:
self.vm.features[feature] = widget.currentData()
except exc.QubesDaemonCommunicationError as ex:
self.errors.append(
"Failed to set {} due to insufficient "
"permissions".format(feature))
def __apply_gui_defaults(self): def __apply_gui_defaults(self):
self.__apply_feature_change(widget=self.allow_fullscreen, self.__apply_feature_change(widget=self.allow_fullscreen,
@ -255,25 +296,35 @@ class GlobalSettingsWindow(ui_globalsettingsdlg.Ui_GlobalSettings,
def __init_mem_defaults__(self): def __init_mem_defaults__(self):
# qmemman settings # qmemman settings
self.qmemman_config = ConfigParser() try:
self.vm_min_mem_val = '200MiB' # str(qmemman_algo.MIN_PREFMEM) self.qmemman_config = ConfigParser()
self.dom0_mem_boost_val = '350MiB' # str(qmemman_algo.DOM0_MEM_BOOST) self.vm_min_mem_val = '200MiB' # str(qmemman_algo.MIN_PREFMEM)
self.dom0_mem_boost_val = '350MiB' # str(qmemman_algo.DOM0_MEM_BOOST)
self.qmemman_config.read(qmemman_config_path) self.qmemman_config.read(qmemman_config_path)
if self.qmemman_config.has_section('global'): if self.qmemman_config.has_section('global'):
self.vm_min_mem_val = \ self.vm_min_mem_val = \
self.qmemman_config.get('global', 'vm-min-mem') self.qmemman_config.get('global', 'vm-min-mem')
self.dom0_mem_boost_val = \ self.dom0_mem_boost_val = \
self.qmemman_config.get('global', 'dom0-mem-boost') self.qmemman_config.get('global', 'dom0-mem-boost')
self.vm_min_mem_val = parse_size(self.vm_min_mem_val) self.vm_min_mem_val = parse_size(self.vm_min_mem_val)
self.dom0_mem_boost_val = parse_size(self.dom0_mem_boost_val) self.dom0_mem_boost_val = parse_size(self.dom0_mem_boost_val)
self.min_vm_mem.setValue(int(self.vm_min_mem_val / 1024 / 1024)) self.min_vm_mem.setValue(
self.dom0_mem_boost.setValue(int(self.dom0_mem_boost_val / 1024 / 1024)) int(self.vm_min_mem_val / 1024 / 1024))
self.dom0_mem_boost.setValue(
int(self.dom0_mem_boost_val / 1024 / 1024))
except exc.QubesException:
self.min_vm_mem.setEnabled(False)
self.dom0_mem_boost.setEnabled(False)
def __apply_mem_defaults__(self): def __apply_mem_defaults__(self):
if not self.min_vm_mem.isEnabled() or \
not self.dom0_mem_boost.isEnabled():
return
# qmemman settings # qmemman settings
current_min_vm_mem = self.min_vm_mem.value() current_min_vm_mem = self.min_vm_mem.value()
current_dom0_mem_boost = self.dom0_mem_boost.value() current_dom0_mem_boost = self.dom0_mem_boost.value()
@ -295,9 +346,14 @@ class GlobalSettingsWindow(ui_globalsettingsdlg.Ui_GlobalSettings,
'global', 'cache-margin-factor', str(1.3)) 'global', 'cache-margin-factor', str(1.3))
# removed qmemman_algo.CACHE_FACTOR # removed qmemman_algo.CACHE_FACTOR
qmemman_config_file = open(qmemman_config_path, 'a') try:
self.qmemman_config.write(qmemman_config_file) qmemman_config_file = open(qmemman_config_path, 'a')
qmemman_config_file.close() self.qmemman_config.write(qmemman_config_file)
qmemman_config_file.close()
except Exception as ex:
self.errors.append(
"Failed to set memory settings due to {}".format(
str(ex)))
else: else:
# If there already is a 'global' section, we don't use # If there already is a 'global' section, we don't use
@ -312,7 +368,14 @@ class GlobalSettingsWindow(ui_globalsettingsdlg.Ui_GlobalSettings,
config_lines = [] config_lines = []
qmemman_config_file = open(qmemman_config_path, 'r') try:
qmemman_config_file = open(qmemman_config_path, 'r')
except Exception as ex:
self.errors.append(
"Failed to set memory settings due to {}".format(
str(ex)))
return
for line in qmemman_config_file: for line in qmemman_config_file:
if line.strip().startswith('vm-min-mem'): if line.strip().startswith('vm-min-mem'):
config_lines.append(lines_to_add['vm-min-mem']) config_lines.append(lines_to_add['vm-min-mem'])
@ -328,28 +391,44 @@ class GlobalSettingsWindow(ui_globalsettingsdlg.Ui_GlobalSettings,
for line in lines_to_add: for line in lines_to_add:
config_lines.append(line) config_lines.append(line)
qmemman_config_file = open(qmemman_config_path, 'w') try:
qmemman_config_file.writelines(config_lines) qmemman_config_file = open(qmemman_config_path, 'w')
qmemman_config_file.close() qmemman_config_file.writelines(config_lines)
qmemman_config_file.close()
except Exception as ex:
self.errors.append(
"Failed to set memory settings due to {}".format(
str(ex)))
return
def __init_updates__(self): def __init_updates__(self):
self.updates_dom0_val = bool( self.updates_dom0_val = bool(
self.qubes_app.domains['dom0'].features.get( utils.get_feature(self.qubes_app.domains['dom0'],
'service.qubes-update-check', True)) 'service.qubes-update-check',
True))
self.updates_dom0.setChecked(self.updates_dom0_val) self.updates_dom0.setChecked(self.updates_dom0_val)
self.updates_vm.setChecked(self.qubes_app.check_updates_vm) try:
self.updates_vm.setChecked(self.qubes_app.check_updates_vm)
except exc.QubesPropertyAccessError:
self.updates_vm.isEnabled(False)
self.enable_updates_all.clicked.connect(self.__enable_updates_all) self.enable_updates_all.clicked.connect(self.__enable_updates_all)
self.disable_updates_all.clicked.connect(self.__disable_updates_all) self.disable_updates_all.clicked.connect(self.__disable_updates_all)
self.repos = repos = dict() self.repos = repos = dict()
for i in _run_qrexec_repo('qubes.repos.List').split('\n'): try:
lst = i.split('\0') for i in _run_qrexec_repo('qubes.repos.List').split('\n'):
# Keyed by repo name lst = i.split('\0')
dct = repos[lst[0]] = dict() # Keyed by repo name
dct['prettyname'] = lst[1] dct = repos[lst[0]] = dict()
dct['enabled'] = lst[2] == 'enabled' dct['prettyname'] = lst[1]
dct['enabled'] = lst[2] == 'enabled'
except Exception as ex:
self.dom0_updates_repo.setEnabled(False)
self.itl_tmpl_updates_repo.setEnabled(False)
self.comm_tmpl_updates_repo.setEnabled(False)
if repos['qubes-dom0-unstable']['enabled']: if repos['qubes-dom0-unstable']['enabled']:
self.dom0_updates_repo.setCurrentIndex(3) self.dom0_updates_repo.setCurrentIndex(3)
@ -401,18 +480,38 @@ class GlobalSettingsWindow(ui_globalsettingsdlg.Ui_GlobalSettings,
self.__set_updates_all(False) self.__set_updates_all(False)
def __set_updates_all(self, state): def __set_updates_all(self, state):
errors = []
for vm in self.qubes_app.domains: for vm in self.qubes_app.domains:
if vm.klass != "AdminVM": if vm.klass != "AdminVM":
vm.features['service.qubes-update-check'] = state try:
vm.features['service.qubes-update-check'] = state
except exc.QubesDaemonCommunicationError:
errors.append(vm.name)
if errors:
QtWidgets.QMessageBox.warning(
self, "Error!",
"Failed to set state for some qubes: {}".format(
", ".join(errors)))
def __apply_updates__(self): def __apply_updates__(self):
if self.updates_dom0.isChecked() != self.updates_dom0_val: if self.updates_dom0.isEnabled() and \
self.qubes_app.domains['dom0'].features[ self.updates_dom0.isChecked() != self.updates_dom0_val:
'service.qubes-update-check'] = \ try:
self.updates_dom0.isChecked() self.qubes_app.domains['dom0'].features[
'service.qubes-update-check'] = \
self.updates_dom0.isChecked()
except exc.QubesDaemonCommunicationError:
self.errors.append("Failed to change dom0 update value due "
"to insufficient permissions.")
if self.qubes_app.check_updates_vm != self.updates_vm.isChecked(): if self.updates_vm.isEnabled() and \
self.qubes_app.check_updates_vm = self.updates_vm.isChecked() self.qubes_app.check_updates_vm != self.updates_vm.isChecked():
try:
self.qubes_app.check_updates_vm = self.updates_vm.isChecked()
except exc.QubesPropertyAccessError:
self.errors.append("Failed to set qube update checking due "
"to insufficient permissions.")
def _manage_repos(self, repolist, action): def _manage_repos(self, repolist, action):
for name in repolist: for name in repolist:
@ -470,17 +569,21 @@ class GlobalSettingsWindow(ui_globalsettingsdlg.Ui_GlobalSettings,
self._manage_repos(disable, 'Disable') self._manage_repos(disable, 'Disable')
def __apply_repos__(self): def __apply_repos__(self):
self._handle_dom0_updates_combobox( if self.dom0_updates_repo.isEnabled():
self.dom0_updates_repo.currentIndex()) self._handle_dom0_updates_combobox(
self._handle_itl_tmpl_updates_combobox( self.dom0_updates_repo.currentIndex())
self.itl_tmpl_updates_repo.currentIndex()) if self.itl_tmpl_updates_repo.isEnabled():
self._handle_comm_tmpl_updates_combobox( self._handle_itl_tmpl_updates_combobox(
self.comm_tmpl_updates_repo.currentIndex()) self.itl_tmpl_updates_repo.currentIndex())
if self.comm_tmpl_updates_repo.isEnabled():
self._handle_comm_tmpl_updates_combobox(
self.comm_tmpl_updates_repo.currentIndex())
def reject(self): def reject(self):
self.done(0) self.done(0)
def save_and_apply(self): def save_and_apply(self):
self.errors = []
self.__apply_system_defaults__() self.__apply_system_defaults__()
self.__apply_kernel_defaults__() self.__apply_kernel_defaults__()
@ -489,6 +592,11 @@ class GlobalSettingsWindow(ui_globalsettingsdlg.Ui_GlobalSettings,
self.__apply_repos__() self.__apply_repos__()
self.__apply_gui_defaults() self.__apply_gui_defaults()
if self.errors:
err_msg = "Failed to apply some settings:\n" + "\n".join(
self.errors)
QtWidgets.QMessageBox.warning(self, "Error", err_msg)
def main(): def main():
utils.run_synchronous(GlobalSettingsWindow) utils.run_synchronous(GlobalSettingsWindow)

View File

@ -105,8 +105,11 @@ class GlobalSettingsTest(unittest.TestCase):
# correct defaultDispVM # correct defaultDispVM
selected_default_dispvm = self.dialog.default_dispvm_combo.currentText() selected_default_dispvm = self.dialog.default_dispvm_combo.currentText()
correct_default_dispvm = \ current_default_dispvm = getattr(self.qapp, 'default_dispvm', None)
str(getattr(self.qapp, 'default_dispvm', "(none)")) if current_default_dispvm is None:
correct_default_dispvm = "(none)"
else:
correct_default_dispvm = str(current_default_dispvm)
self.assertTrue( self.assertTrue(
selected_default_dispvm.startswith(correct_default_dispvm), selected_default_dispvm.startswith(correct_default_dispvm),
"Incorrect defaultDispVM loaded") "Incorrect defaultDispVM loaded")
@ -118,11 +121,8 @@ class GlobalSettingsTest(unittest.TestCase):
def test_02_dom0_updates_load(self): def test_02_dom0_updates_load(self):
# check dom0 updates # check dom0 updates
try: dom0_updates = self.qapp.domains[
dom0_updates = self.qapp.domains[ 'dom0'].features.get('service.qubes-update-check', True)
'dom0'].features['service.qubes-update-check']
except KeyError:
dom0_updates = True
self.assertEqual(bool(dom0_updates), self.assertEqual(bool(dom0_updates),
self.dialog.updates_dom0.isChecked(), self.dialog.updates_dom0.isChecked(),

View File

@ -31,6 +31,7 @@ from contextlib import suppress
import sys import sys
import qasync import qasync
from qubesadmin import events from qubesadmin import events
from qubesadmin import exc
from PyQt5 import QtWidgets, QtCore, QtGui # pylint: disable=import-error from PyQt5 import QtWidgets, QtCore, QtGui # pylint: disable=import-error
@ -91,11 +92,18 @@ class SizeSpinBox(QtWidgets.QSpinBox):
return int(float(value) * multiplier) return int(float(value) * multiplier)
def get_feature(vm, feature_name, default_value):
try:
return vm.features.get(feature_name, default_value)
except exc.QubesDaemonCommunicationError:
return default_value
def get_boolean_feature(vm, feature_name): def get_boolean_feature(vm, feature_name):
"""heper function to get a feature converted to a Bool if it does exist. """heper function to get a feature converted to a Bool if it does exist.
Necessary because of the true/false in features being coded as 1/empty Necessary because of the true/false in features being coded as 1/empty
string.""" string."""
result = vm.features.get(feature_name, None) result = get_feature(vm, feature_name, None)
if result is not None: if result is not None:
result = bool(result) result = bool(result)
return result return result