diff --git a/qubesmanager/qube_manager.py b/qubesmanager/qube_manager.py
index 8b4a1fb..659e543 100644
--- a/qubesmanager/qube_manager.py
+++ b/qubesmanager/qube_manager.py
@@ -709,6 +709,8 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QMainWindow):
self.frame_width = 0
self.frame_height = 0
+ self.init_template_menu()
+ self.init_network_menu()
self.__init_context_menu()
self.tools_context_menu = QMenu(self)
@@ -740,6 +742,8 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QMainWindow):
self.proxy.setFilterKeyColumn(2)
self.proxy.setFilterCaseSensitivity(Qt.CaseInsensitive)
self.proxy.layoutChanged.connect(self.save_sorting)
+ self.proxy.layoutChanged.connect(self.update_template_menu)
+ self.proxy.layoutChanged.connect(self.update_network_menu)
self.show_running.stateChanged.connect(self.invalidate)
self.show_halted.stateChanged.connect(self.invalidate)
@@ -762,7 +766,6 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QMainWindow):
column = self.qubes_model.columns_indices[col_no]
action = self.menu_view.addAction(column)
action.setData(column)
- action.setCheckable(True)
action.toggled.connect(partial(self.showhide_column, col_no))
self.menu_view.addSeparator()
@@ -822,9 +825,77 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QMainWindow):
self.check_updates()
+ def change_template(self, template):
+ selected_vms = self.get_selected_vms()
+ reply = QMessageBox.question(
+ self, self.tr("Template Change Confirmation"),
+ self.tr("Do you want to change '{0}'
"
+ "to Template '{1}'?").format(
+ ', '.join(vm.name for vm in selected_vms), template),
+ QMessageBox.Yes | QMessageBox.Cancel)
+
+ if reply == QMessageBox.Yes:
+ errors = []
+ for info in selected_vms:
+ try:
+ info.vm.template = template
+ except exc.QubesValueError as ex:
+ errors.append((info.name, str(ex)))
+
+ for error in errors:
+ QMessageBox.warning(self, self.tr("{0} template change failed!")
+ .format(error[0]), error[1])
+
+
+ def change_network(self, netvm_name):
+ selected_vms = self.get_selected_vms()
+ reply = QMessageBox.question(
+ self, self.tr("Network Change Confirmation"),
+ self.tr("Do you want to change '{0}'
"
+ "to Network '{1}'?").format(
+ ', '.join(vm.name for vm in selected_vms), netvm_name),
+ QMessageBox.Yes | QMessageBox.Cancel)
+
+ if reply != QMessageBox.Yes:
+ return
+
+ if netvm_name not in [None, 'default']:
+ check_power = any(info.state['power'] == 'Running' for info
+ in self.get_selected_vms())
+ netvm = self.qubes_cache.get_vm(name=netvm_name)
+ if check_power and netvm.state['power'] != 'Running':
+ reply = QMessageBox.question(
+ self, self.tr("Qube Start Confirmation"),
+ self.tr("
Can not change netvm to a halted Qube.
"
+ "Do you want to start the Qube '{0}'?").format(
+ netvm_name),
+ QMessageBox.Yes | QMessageBox.Cancel)
+
+ if reply == QMessageBox.Yes:
+ self.start_vm(netvm.vm, True)
+ else:
+ return
+
+ errors = []
+ for info in self.get_selected_vms():
+ try:
+ if netvm_name == 'default':
+ delattr(info.vm, 'netvm')
+ else:
+ info.vm.netvm = netvm_name
+ except exc.QubesValueError as ex:
+ errors.append((info.name, str(ex)))
+
+ for error in errors:
+ QMessageBox.warning(self, self.tr("{0} network change failed!")
+ .format(error[0]), error[1])
+
+
def __init_context_menu(self):
self.context_menu = QMenu(self)
self.context_menu.addAction(self.action_settings)
+ self.context_menu.addAction(self.template_menu.menuAction())
+ self.context_menu.addAction(self.network_menu.menuAction())
self.context_menu.addAction(self.action_editfwrules)
self.context_menu.addAction(self.action_appmenus)
self.context_menu.addAction(self.action_set_keyboard_layout)
@@ -885,6 +956,33 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QMainWindow):
progress.setValue(row_no)
+ def init_template_menu(self):
+ self.template_menu.clear()
+ for vm in self.qubes_app.domains:
+ if vm.klass == 'TemplateVM':
+ action = self.template_menu.addAction(vm.name)
+ action.setData(vm.name)
+ action.triggered.connect(partial(self.change_template, vm.name))
+
+ def _get_default_netvm(self):
+ for vm in self.qubes_app.domains:
+ if vm.klass == 'AppVM':
+ return vm.property_get_default('netvm')
+
+ def init_network_menu(self):
+ default = self._get_default_netvm()
+ self.network_menu.clear()
+ action = self.network_menu.addAction("None")
+ action.triggered.connect(partial(self.change_network, None))
+ action = self.network_menu.addAction("default ({0})".format(default))
+ action.triggered.connect(partial(self.change_network, 'default'))
+
+ for vm in self.qubes_app.domains:
+ if vm.qid != 0 and vm.provides_network:
+ action = self.network_menu.addAction(vm.name)
+ action.setData(vm.name)
+ action.triggered.connect(partial(self.change_network, vm.name))
+
def setup_application(self):
self.qt_app.setApplicationName(self.tr("Qube Manager"))
self.qt_app.setWindowIcon(QIcon.fromTheme("qubes-manager"))
@@ -942,12 +1040,16 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QMainWindow):
domain = self.qubes_app.domains[vm]
self.qubes_cache.add_vm(domain)
self.proxy.invalidate()
+ if domain.klass == 'TemplateVM':
+ self.init_template_menu()
except (exc.QubesException, KeyError):
pass
def on_domain_removed(self, _submitter, _event, **kwargs):
self.qubes_cache.remove_vm(name=kwargs['vm'])
self.proxy.invalidate()
+ self.init_template_menu()
+ self.init_network_menu()
def on_domain_status_changed(self, vm, event, **_kwargs):
try:
@@ -977,6 +1079,8 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QMainWindow):
return
try:
+ if event.endswith(':provides_network'):
+ self.init_network_menu()
self.qubes_cache.get_vm(qid=vm.qid).update(event=event)
self.proxy.invalidate()
except exc.QubesDaemonAccessError:
@@ -1061,6 +1165,8 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QMainWindow):
def table_selection_changed(self):
# Since selection could have multiple domains
# enable all first and then filter them
+ self.template_menu.setEnabled(True)
+ self.network_menu.setEnabled(True)
for action in self.toolbar.actions() + self.context_menu.actions():
action.setEnabled(True)
@@ -1071,17 +1177,20 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QMainWindow):
['Running', 'Transient', 'Halting', 'Dying']:
self.action_resumevm.setEnabled(False)
self.action_removevm.setEnabled(False)
+ self.template_menu.setEnabled(False)
elif vm.state['power'] == 'Paused':
self.action_removevm.setEnabled(False)
self.action_pausevm.setEnabled(False)
self.action_set_keyboard_layout.setEnabled(False)
self.action_restartvm.setEnabled(False)
self.action_open_console.setEnabled(False)
+ self.template_menu.setEnabled(False)
elif vm.state['power'] == 'Suspend':
self.action_set_keyboard_layout.setEnabled(False)
self.action_removevm.setEnabled(False)
self.action_pausevm.setEnabled(False)
self.action_open_console.setEnabled(False)
+ self.template_menu.setEnabled(False)
elif vm.state['power'] == 'Halted':
self.action_set_keyboard_layout.setEnabled(False)
self.action_pausevm.setEnabled(False)
@@ -1104,9 +1213,15 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QMainWindow):
self.action_editfwrules.setEnabled(False)
self.action_set_keyboard_layout.setEnabled(False)
self.action_run_command_in_vm.setEnabled(False)
+ self.template_menu.setEnabled(False)
+ self.network_menu.setEnabled(False)
elif vm.klass == 'DispVM':
self.action_appmenus.setEnabled(False)
self.action_restartvm.setEnabled(False)
+ self.template_menu.setEnabled(False)
+ elif vm.klass == 'TemplateVM':
+ self.template_menu.setEnabled(False)
+ self.network_menu.setEnabled(False)
if vm.vm.features.get('internal', False):
self.action_appmenus.setEnabled(False)
@@ -1114,6 +1229,47 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QMainWindow):
if not vm.updateable and vm.klass != 'AdminVM':
self.action_updatevm.setEnabled(False)
+ self.update_template_menu()
+ self.update_network_menu()
+
+ def update_template_menu(self):
+ if not self.template_menu.isEnabled():
+ return
+
+ for entry in self.template_menu.actions():
+ entry.setIcon(QIcon())
+
+ vms = self.get_selected_vms()
+ for vm in vms:
+ for entry in self.template_menu.actions():
+ if entry.data() == vm.template:
+ if len(vms) == 1:
+ entry.setIcon(QIcon(":/on.png"))
+ else:
+ entry.setIcon(QIcon(":/transient.png"))
+
+ def update_network_menu(self):
+ if not self.network_menu.isEnabled():
+ return
+
+ for entry in self.network_menu.actions():
+ entry.setIcon(QIcon())
+
+ if len(self.get_selected_vms()) == 1:
+ icon = QIcon(":/on.png")
+ else:
+ icon = QIcon(":/transient.png")
+
+ for vm in self.get_selected_vms():
+ if vm.netvm == "n/a":
+ self.network_menu.actions()[0].setIcon(QIcon(icon))
+ elif vm.vm.property_is_default("netvm"):
+ self.network_menu.actions()[1].setIcon(QIcon(icon))
+ else:
+ for entry in self.network_menu.actions():
+ if entry.data() == vm.netvm:
+ entry.setIcon(icon)
+
# noinspection PyArgumentList
@pyqtSlot(name='on_action_createvm_triggered')
def action_createvm_triggered(self):
@@ -1202,7 +1358,7 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QMainWindow):
self.start_vm(vm)
- def start_vm(self, vm):
+ def start_vm(self, vm, wait=False):
if manager_utils.is_running(vm, False):
return
@@ -1211,6 +1367,10 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QMainWindow):
thread.finished.connect(self.clear_threads)
thread.start()
+ if wait:
+ with common_threads.busy_cursor():
+ thread.wait()
+
# noinspection PyArgumentList
@pyqtSlot(name='on_action_startvm_tools_install_triggered')
# TODO: replace with boot from device
diff --git a/qubesmanager/settings.py b/qubesmanager/settings.py
index c55a266..ce77905 100644
--- a/qubesmanager/settings.py
+++ b/qubesmanager/settings.py
@@ -432,6 +432,7 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtWidgets.QDialog):
self.netVM.setCurrentIndex(-1)
self.netVM.currentIndexChanged.connect(self.check_warn_dispvmnetvm)
+ self.netVM.currentIndexChanged.connect(self.check_warn_templatenetvm)
try:
self.include_in_backups.setChecked(self.vm.include_in_backups)
@@ -625,6 +626,16 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtWidgets.QDialog):
self.init_mem.value() * 10 < self.max_mem_size.value():
self.warn_too_much_mem_label.setVisible(True)
+ def check_warn_templatenetvm(self):
+ if self.vm.klass == 'TemplateVM':
+ QtWidgets.QMessageBox.warning(
+ self,
+ self.tr("Warning!"),
+ self.tr("Connecting a TemplateVM directly to a network is higly"
+ " discouraged!
You are breaking a basic par"
+ "t of Qubes security and there is probably no real need"
+ " to do so. Continue at your own risk."))
+
def check_warn_dispvmnetvm(self):
if not hasattr(self.vm, 'default_dispvm'):
self.warn_netvm_dispvm.setVisible(False)
diff --git a/ui/qubemanager.ui b/ui/qubemanager.ui
index bbed67c..26cd341 100644
--- a/ui/qubemanager.ui
+++ b/ui/qubemanager.ui
@@ -350,6 +350,24 @@ Template
&Qube
+
+
@@ -361,6 +379,8 @@ Template
+
+