Handling VM dependencies in GUI tools

Added the following reactions ot VM dependencies in GUI tools:
- Qube Manager will inform the user why they cannot delete a VM (which
properties of which VMS [or global] are using the VM)
- Settings window will try to intelligently rename VMs (change
properties to the new name, if possible, and inform the user what went
wrong if not)
- Settings window will inform the user why they cannot delete a VM
Also, renaming VM from Settings launched from Qube Manager will
refresh the VM list through a horrible hack, to be replaced by a neater
Admin API solution in some near future.

depends on ca848ca7bd

fixes QubesOS/qubes-issues#3992
fixes QubesOS/qubes-issues#3993
fixes QubesOS/qubes-issues#3892
fixes QubesOS/qubes-issues#3499
This commit is contained in:
Christopher Laprise 2018-07-20 00:03:37 +02:00 committed by Marta Marczykowska-Górecka
parent d46bf2aa8e
commit b2fdbb950a
No known key found for this signature in database
GPG Key ID: 9A752C30B26FD04B
3 changed files with 108 additions and 19 deletions

View File

@ -50,6 +50,7 @@ from . import global_settings
from . import restore from . import restore
from . import backup from . import backup
from . import log_dialog from . import log_dialog
from . import utils as manager_utils
class SearchBox(QtGui.QLineEdit): class SearchBox(QtGui.QLineEdit):
@ -413,7 +414,11 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
def check_updates(self): def check_updates(self):
for vm in self.qubes_app.domains: for vm in self.qubes_app.domains:
if vm.klass in {'TemplateVM', 'StandaloneVM'}: if vm.klass in {'TemplateVM', 'StandaloneVM'}:
self.vms_in_table[vm.qid].update() try:
self.vms_in_table[vm.qid].update()
except exc.QubesException:
# the VM might have vanished in the meantime
pass
def on_domain_added(self, _, domain): def on_domain_added(self, _, domain):
#needs to clear cache #needs to clear cache
@ -686,26 +691,20 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
vm = self.get_selected_vm() vm = self.get_selected_vm()
dependencies = utils.vm_usage(self.qubes_app, vm) dependencies = utils.vm_dependencies(self.qubes_app, vm)
if dependencies: if dependencies:
list_text = "<br>" list_text = "<br>" + \
for (holder, prop) in dependencies: manager_utils.format_dependencies_list(dependencies) + \
if holder is None: "<br>"
list_text += "- Global property <b>{}</b> <br>".format(prop)
else:
list_text += "- <b>{}</b> for qube <b>{}</b> <br>".format(
prop, holder.name)
list_text += "<br>"
info_dialog = QtGui.QMessageBox(self) info_dialog = QtGui.QMessageBox(self)
info_dialog.setWindowTitle(self.tr("Warning!")) info_dialog.setWindowTitle(self.tr("Warning!"))
info_dialog.setText( info_dialog.setText(
self.tr("This qube cannot be removed. It is used as:" self.tr("This qube cannot be removed. It is used as:"
" <br>" + list_text + "<small>If you want to " " <br> {} <small>If you want to remove this qube, "
" remove this qube, you should remove or change " "you should remove or change settings of each qube "
" settings of each qube or setting that uses" "or setting that uses it.</small>".format(list_text)))
" it.</small>"))
info_dialog.setModal(False) info_dialog.setModal(False)
info_dialog.show() info_dialog.show()
self.qt_app.processEvents() self.qt_app.processEvents()
@ -1001,8 +1000,24 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
settings_window = settings.VMSettingsWindow( settings_window = settings.VMSettingsWindow(
vm, self.qt_app, "basic") vm, self.qt_app, "basic")
settings_window.exec_() settings_window.exec_()
self.vms_in_table[vm.qid].update()
vm_deleted = False
try:
# the VM might not exist after running Settings - it might
# have been cloned or removed
self.vms_in_table[vm.qid].update()
except exc.QubesException:
# TODO: this will be replaced by proper signal handling once
# settings are migrated to AdminAPI
vm_deleted = True
if vm_deleted:
for row in self.vms_in_table:
try:
self.vms_in_table[row].update()
except exc.QubesException:
pass
# noinspection PyArgumentList # noinspection PyArgumentList
@QtCore.pyqtSlot(name='on_action_appmenus_triggered') @QtCore.pyqtSlot(name='on_action_appmenus_triggered')

View File

@ -34,6 +34,7 @@ import traceback
import sys import sys
from qubesadmin.tools import QubesArgumentParser from qubesadmin.tools import QubesArgumentParser
from qubesadmin import devices from qubesadmin import devices
from qubesadmin import utils as admin_utils
import qubesadmin.exc import qubesadmin.exc
from . import utils from . import utils
@ -481,10 +482,35 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtGui.QDialog):
return False return False
return True return True
def _rename_vm(self, t_monitor, name): def _rename_vm(self, t_monitor, name, dependencies):
try: try:
self.vm.app.clone_vm(self.vm, name) new_vm = self.vm.app.clone_vm(self.vm, name)
del self.vm.app.domains[self.vm.name]
failed_props = []
for (holder, prop) in dependencies:
try:
if holder is None:
setattr(self.vm.app, prop, new_vm)
else:
setattr(holder, prop, new_vm)
except qubesadmin.exc.QubesException as qex:
failed_props += (holder, prop)
if not failed_props:
del self.vm.app.domains[self.vm.name]
else:
list_text = utils.format_dependencies_list(failed_props)
QtGui.QMessageBox.warning(
self,
self.tr("Warning: rename partially unsuccessful"),
self.tr("Some properties could not be changed to the new "
"name. The system has now both {} and {} qubes. "
"To resolve this, please check and change the "
"following properties and remove the qube {} "
"manually.<br> ".format(
self.vm.name, name, self.vm.name) + list_text))
except qubesadmin.exc.QubesException as qex: except qubesadmin.exc.QubesException as qex:
t_monitor.set_error_msg(str(qex)) t_monitor.set_error_msg(str(qex))
@ -494,13 +520,31 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtGui.QDialog):
t_monitor.set_finished() t_monitor.set_finished()
def rename_vm(self): def rename_vm(self):
dependencies = admin_utils.vm_dependencies(self.vm.app, self.vm)
running_dependencies = [vm.name for (vm, prop) in dependencies
if vm and prop == 'template'
and vm.is_running()]
if running_dependencies:
QtGui.QMessageBox.warning(
self,
self.tr("Qube cannot be renamed!"),
self.tr(
"The following qubes using this qube as a template are "
"running: <br> {}. <br> In order to rename this qube, you "
"must first shut them down.".format(
", ".join(running_dependencies))))
return
new_vm_name, ok = QtGui.QInputDialog.getText( new_vm_name, ok = QtGui.QInputDialog.getText(
self, self,
self.tr('Rename qube'), self.tr('Rename qube'),
self.tr('New name: (WARNING: all other changes will be discarded)')) self.tr('New name: (WARNING: all other changes will be discarded)'))
if ok: if ok:
if self._run_in_thread(self._rename_vm, new_vm_name): if self._run_in_thread(self._rename_vm, new_vm_name, dependencies):
self.done(0) self.done(0)
def _remove_vm(self, t_monitor): def _remove_vm(self, t_monitor):
@ -516,6 +560,21 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtGui.QDialog):
def remove_vm(self): def remove_vm(self):
dependencies = admin_utils.vm_dependencies(self.vm.app, self.vm)
if dependencies:
list_text = utils.format_dependencies_list(dependencies)
QtGui.QMessageBox.warning(
self,
self.tr("Qube cannot be removed!"),
self.tr("This qube cannot be removed. It is used as:"
" <br> {} <small>If you want to remove this qube, "
"you should remove or change settings of each qube "
"or setting that uses it.</small>".format(list_text)))
return
answer, ok = QtGui.QInputDialog.getText( answer, ok = QtGui.QInputDialog.getText(
self, self,
self.tr('Delete qube'), self.tr('Delete qube'),

View File

@ -172,3 +172,18 @@ def get_path_from_vm(vm, service_name):
assert '\0' not in untrusted_path assert '\0' not in untrusted_path
return untrusted_path.strip() return untrusted_path.strip()
raise ValueError('Unexpected characters in path.') raise ValueError('Unexpected characters in path.')
def format_dependencies_list(dependencies):
"""Given a list of tuples representing properties, formats them in
a readable list."""
list_text = ""
for (holder, prop) in dependencies:
if holder is None:
list_text += "- Global property <b>{}</b> <br>".format(prop)
else:
list_text += "- <b>{}</b> for qube <b>{}</b> <br>".format(
prop, holder.name)
return list_text