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 + + + Template + + + + :/templatevm.png:/templatevm.png + + + + + Network + + + + :/netvm.png:/netvm.png + + @@ -361,6 +379,8 @@ Template + +