Browse Source

Merge remote-tracking branch 'origin/pr/273'

* origin/pr/273: (24 commits)
  Fix coherence in network menu when adding/removing domains
  Fix too long line
  Add warning if trying to change template VM
  Disable network menu for templates
  Display default netvm
  Fix possible 'None' default error
  Added default option for network change
  Wrap warnings message in self.tr()
  Added wait argument to start_vm
  Better dialog creation
  Added try/except for starting netvm
  Added error message to dialogs
  Added proper error handling and Check netvm_name is not None
  Changed checkboxes to icons
  Added change network confirmation
  Added Template Change Confirmation
  Moved change_* funcs after __init__()
  Added QMessageBox if netvm is halted and user wants to start it
  Added network_menu updates
  Added try/except for change_network
  ...
Marek Marczykowski-Górecki 3 years ago
parent
commit
d253b50545
3 changed files with 193 additions and 2 deletions
  1. 162 2
      qubesmanager/qube_manager.py
  2. 11 0
      qubesmanager/settings.py
  3. 20 0
      ui/qubemanager.ui

+ 162 - 2
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}'<br>"
+                "to Template <b>'{1}'</b>?").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}'<br>"
+                "to Network <b>'{1}'</b>?").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("<br>Can not change netvm to a halted Qube.<br>"
+                        "Do you want to start the Qube <b>'{0}'</b>?").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

+ 11 - 0
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! <br> <small>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.</small>"))
+
     def check_warn_dispvmnetvm(self):
         if not hasattr(self.vm, 'default_dispvm'):
             self.warn_netvm_dispvm.setVisible(False)

+ 20 - 0
ui/qubemanager.ui

@@ -350,6 +350,24 @@ Template</string>
     <property name="title">
      <string>&amp;Qube</string>
     </property>
+    <widget class="QMenu" name="template_menu">
+     <property name="title">
+      <string>Template</string>
+     </property>
+     <property name="icon">
+      <iconset resource="../resources.qrc">
+       <normaloff>:/templatevm.png</normaloff>:/templatevm.png</iconset>
+     </property>
+    </widget>
+    <widget class="QMenu" name="network_menu">
+     <property name="title">
+      <string>Network</string>
+     </property>
+     <property name="icon">
+      <iconset>
+       <normaloff>:/netvm.png</normaloff>:/netvm.png</iconset>
+     </property>
+    </widget>
     <addaction name="action_createvm"/>
     <addaction name="action_removevm"/>
     <addaction name="action_clonevm"/>
@@ -361,6 +379,8 @@ Template</string>
     <addaction name="action_killvm"/>
     <addaction name="separator"/>
     <addaction name="action_settings"/>
+    <addaction name="template_menu"/>
+    <addaction name="network_menu"/>
     <addaction name="action_editfwrules"/>
     <addaction name="action_appmenus"/>
     <addaction name="action_updatevm"/>