瀏覽代碼

Merge branch 'master' into virt-mode-select-fixes

Marta Marczykowska-Górecka 6 年之前
父節點
當前提交
48c3bfa798
共有 48 個文件被更改,包括 4296 次插入1895 次删除
  1. 10 4
      .travis.yml
  2. 211 0
      ci/pylintrc
  3. 11 0
      ci/requirements.txt
  4. 9 0
      qubes-backup-restore.desktop
  5. 9 0
      qubes-backup.desktop
  6. 0 1
      qubes-global-settings.desktop
  7. 9 0
      qubes-qube-manager.desktop
  8. 7 9
      qubesmanager/about.py
  9. 14 21
      qubesmanager/appmenu_select.py
  10. 237 304
      qubesmanager/backup.py
  11. 83 62
      qubesmanager/backup_utils.py
  12. 3 3
      qubesmanager/block.py
  13. 43 22
      qubesmanager/bootfromdevice.py
  14. 3 3
      qubesmanager/clipboard.py
  15. 24 24
      qubesmanager/create_new_vm.py
  16. 259 236
      qubesmanager/firewall.py
  17. 75 62
      qubesmanager/global_settings.py
  18. 11 10
      qubesmanager/informationnotes.py
  19. 20 22
      qubesmanager/log_dialog.py
  20. 21 21
      qubesmanager/multiselectwidget.py
  21. 1188 0
      qubesmanager/qube_manager.py
  22. 7 8
      qubesmanager/releasenotes.py
  23. 159 214
      qubesmanager/restore.py
  24. 325 265
      qubesmanager/settings.py
  25. 184 390
      qubesmanager/table_widgets.py
  26. 4 6
      qubesmanager/thread_monitor.py
  27. 3 4
      qubesmanager/utils.py
  28. 11 1
      rpm_spec/qmgr.spec
  29. 4 1
      setup.py
  30. 5 0
      test-packages/qubes/backup/__init__.py
  31. 1 0
      test-packages/qubes/storage/__init__.py
  32. 2 0
      test-packages/qubes/storage/file.py
  33. 4 0
      test-packages/qubesadmin/__init__.py
  34. 1 0
      test-packages/qubesadmin/backup/__init__.py
  35. 19 0
      test-packages/qubesadmin/backup/restore/__init__.py
  36. 4 0
      test-packages/qubesadmin/devices.py
  37. 5 0
      test-packages/qubesadmin/events.py
  38. 6 0
      test-packages/qubesadmin/exc.py
  39. 4 0
      test-packages/qubesadmin/firewall.py
  40. 9 0
      test-packages/qubesadmin/tools/__init__.py
  41. 4 0
      test-packages/qubesadmin/tools/qvm_start.py
  42. 10 0
      test-packages/qubesadmin/utils.py
  43. 145 107
      ui/backupdlg.ui
  44. 45 42
      ui/newfwruledlg.ui
  45. 841 0
      ui/qubemanager.ui
  46. 9 3
      ui/restoredlg.ui
  47. 237 49
      ui/settingsdlg.ui
  48. 1 1
      version

+ 10 - 4
.travis.yml

@@ -1,7 +1,13 @@
 sudo: required
 dist: trusty
-language: generic
-install: git clone https://github.com/QubesOS/qubes-builder ~/qubes-builder
-script: ~/qubes-builder/scripts/travis-build
+language: python
+python:
+  - '3.5'
+install:
+ - pip install --quiet -r ci/requirements.txt
+ - git clone https://github.com/QubesOS/qubes-builder ~/qubes-builder
+script:
+ - PYTHONPATH=test-packages pylint --rcfile=ci/pylintrc qubesmanager
+ - ~/qubes-builder/scripts/travis-build
 env:
- - DIST_DOM0=fc23 USE_QUBES_REPO_VERSION=3.2 USE_QUBES_REPO_TESTING=1
+ - DIST_DOM0=fc25 USE_QUBES_REPO_VERSION=4.0 USE_QUBES_REPO_TESTING=1

+ 211 - 0
ci/pylintrc

@@ -0,0 +1,211 @@
+[MASTER]
+persistent=no
+ignore=tests,
+    ui_about.py,
+    ui_backupdlg.py,
+    ui_bootfromdevice.py,
+    ui_globalsettingsdlg.py,
+    ui_informationnotes.py,
+    ui_logdlg.py,
+    ui_multiselectwidget.py,
+    ui_newappvmdlg.py,
+    ui_newfwruledlg.py,
+    ui_releasenotes.py,
+    ui_restoredlg.py,
+    ui_settingsdlg.py,
+    resources_rc.py
+
+[MESSAGES CONTROL]
+# abstract-class-little-used: see http://www.logilab.org/ticket/111138
+# deprecated-method:
+#   enable again after disabling py-3.4.3 asyncio.ensure_future compat hack
+disable=
+  abstract-class-little-used,
+  bad-continuation,
+  cyclic-import,
+  deprecated-method,
+  duplicate-code,
+  file-ignored,
+  fixme,
+  inconsistent-return-statements,
+  locally-disabled,
+  locally-enabled,
+  logging-format-interpolation,
+  missing-docstring,
+  star-args,
+  useless-super-delegation,
+  wrong-import-order
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html
+output-format=colorized
+
+#files-output=no
+reports=yes
+
+[TYPECHECK]
+#ignored-classes=
+
+ignore-mixin-members=yes
+generated-members=
+  iter_entry_points,
+  Element,ElementTree,QName,SubElement,fromstring,parse,tostring,
+
+[BASIC]
+
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=
+
+# Regular expression which should only match correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression which should only match correct module level names
+const-rgx=(([A-Za-z_][A-Za-z0-9_]*)|(__.*__))$
+
+# Regular expression which should only match correct class names
+class-rgx=([A-Z_][a-zA-Z0-9]+|TC_\d\d_[a-zA-Z0-9_]+)$
+
+# Regular expression which should only match correct function names
+function-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct method names
+method-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct instance attribute names
+attr-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct argument names
+argument-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct variable names
+variable-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct list comprehension /
+# generator expression variable names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=e,i,j,k,m,p,v,ex,Run,_,log,vm,ok,ip
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# Regular expression which should only match functions or classes name which do
+# not require a docstring
+no-docstring-rgx=__.*__
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,FIX,XXX,TODO
+
+
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=80
+
+# Maximum number of lines in a module
+max-module-lines=3000
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string='    '
+
+
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching the beginning of the name of dummy variables
+# (i.e. not used).
+dummy-variables-rgx=_|dummy
+
+
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,TERMIOS,Bastion,rexec
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=35
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*
+
+# Maximum number of locals for function / method body
+# Let's have max-args + 5
+max-locals=40
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of branch for function / method body
+# 4x the default value
+max-branches=48
+
+# Maximum number of statements in function / method body
+# Double default
+max-statements=100
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=15
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=100
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=Exception,EnvironmentError
+
+# vim: ft=conf

+ 11 - 0
ci/requirements.txt

@@ -0,0 +1,11 @@
+# WARNING: those requirements are used only for travis-ci.org
+# they SHOULD NOT be used under normal conditions; use system package manager
+coverage
+codecov
+docutils
+jinja2
+lxml
+pylint
+sphinx
+pydbus
+PyYAML

+ 9 - 0
qubes-backup-restore.desktop

@@ -0,0 +1,9 @@
+[Desktop Entry]
+Type=Application
+Exec=qubes-backup-restore
+Icon=qubes-manager
+Terminal=false
+Name=Restore Backup
+GenericName=Restore Backup
+StartupNotify=false
+Categories=System;

+ 9 - 0
qubes-backup.desktop

@@ -0,0 +1,9 @@
+[Desktop Entry]
+Type=Application
+Exec=qubes-backup
+Icon=qubes-manager
+Terminal=false
+Name=Backup Qubes
+GenericName=Backup Qubes
+StartupNotify=false
+Categories=System;

+ 0 - 1
qubes-global-settings.desktop

@@ -1,7 +1,6 @@
 [Desktop Entry]
 Type=Application
 Exec=qubes-global-settings
-Path=/user/bin
 Icon=qubes-manager
 Terminal=false
 Name=Qubes Global Settings

+ 9 - 0
qubes-qube-manager.desktop

@@ -0,0 +1,9 @@
+[Desktop Entry]
+Type=Application
+Exec=qubes-qube-manager
+Icon=qubes-manager
+Terminal=false
+Name=Qube Manager
+GenericName=Qube Manager
+StartupNotify=false
+Categories=System;

+ 7 - 9
qubesmanager/about.py

@@ -1,4 +1,4 @@
-#!/usr/bin/python2
+#!/usr/bin/python3
 # coding=utf-8
 #
 # The Qubes OS Project, http://www.qubes-os.org
@@ -16,21 +16,19 @@
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
 #
 #
-from PyQt4.QtCore import SIGNAL, SLOT
-from PyQt4.QtGui import QDialog, QIcon
+from PyQt4.QtCore import SIGNAL, SLOT  # pylint: disable=import-error
+from PyQt4.QtGui import QDialog, QIcon  # pylint: disable=import-error
 from qubesmanager.releasenotes import ReleaseNotesDialog
 from qubesmanager.informationnotes import InformationNotesDialog
 
-from .ui_about import *
+from . import ui_about  # pylint: disable=no-name-in-module
 
 
-
-class AboutDialog(Ui_AboutDialog, QDialog):
+class AboutDialog(ui_about.Ui_AboutDialog, QDialog):
     def __init__(self):
         super(AboutDialog, self).__init__()
 

+ 14 - 21
qubesmanager/appmenu_select.py

@@ -14,28 +14,19 @@
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
 #
 #
 
-import os
 import subprocess
-import sys
-import time
 
-from operator import itemgetter
-
-from PyQt4.QtCore import *
-from PyQt4.QtGui import *
-from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent
-
-import qubesmanager.resources_rc
+import PyQt4.QtGui  # pylint: disable=import-error
 
 # TODO description in tooltip
 # TODO icon
-class AppListWidgetItem(QListWidgetItem):
+# pylint: disable=too-few-public-methods
+class AppListWidgetItem(PyQt4.QtGui.QListWidgetItem):
     def __init__(self, name, ident, parent=None):
         super(AppListWidgetItem, self).__init__(name, parent)
 #       self.setToolTip(command)
@@ -43,12 +34,12 @@ class AppListWidgetItem(QListWidgetItem):
 
     @classmethod
     def from_line(cls, line):
-        ident, icon_name, name = line.strip().split(maxsplit=2)
+        ident, _icon_name, name = line.strip().split(maxsplit=2)
         return cls(name=name, ident=ident)
 
 
 class AppmenuSelectManager:
-    def __init__(self, vm, apps_multiselect, parent=None):
+    def __init__(self, vm, apps_multiselect):
         self.vm = vm
         self.app_list = apps_multiselect # this is a multiselect wiget
         self.whitelisted = None
@@ -60,7 +51,9 @@ class AppmenuSelectManager:
             ).decode().strip().split('\n') if line]
 
         # Check if appmenu entry is really installed
-#       whitelisted = [a for a in whitelisted if os.path.exists('%s/apps/%s-%s' % (self.vm.dir_path, self.vm.name, a))]
+        # whitelisted = [a for a in whitelisted
+        #  if os.path.exists('%s/apps/%s-%s' %
+        # (self.vm.dir_path, self.vm.name, a))]
 
         self.app_list.clear()
 
@@ -69,11 +62,11 @@ class AppmenuSelectManager:
                     '--get-available', '--i-understand-format-is-unstable',
                     self.vm.name]).decode().splitlines()]
 
-        for a in available_appmenus:
-            if a.ident in self.whitelisted:
-                self.app_list.selected_list.addItem(a)
+        for app in available_appmenus:
+            if app.ident in self.whitelisted:
+                self.app_list.selected_list.addItem(app)
             else:
-                self.app_list.available_list.addItem(a)
+                self.app_list.available_list.addItem(app)
 
         self.app_list.available_list.sortItems()
         self.app_list.selected_list.sortItems()

+ 237 - 304
qubesmanager/backup.py

@@ -1,4 +1,4 @@
-#!/usr/bin/python2
+#!/usr/bin/python3
 #
 # The Qubes OS Project, http://www.qubes-os.org
 #
@@ -15,385 +15,324 @@
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
 #
 #
 
-import sys
-import os
-import signal
-import shutil
-from PyQt4.QtCore import *
-from PyQt4.QtGui import *
+import traceback
 
-from qubes.qubes import QubesVmCollection
-from qubes.qubes import QubesException
-from qubes.qubes import QubesDaemonPidfile
-from qubes.qubes import QubesHost
-from qubes import backup
-from qubes import qubesutils
+import signal
 
-import qubesmanager.resources_rc
+from qubesadmin import Qubes, exc
+from qubesadmin import utils as admin_utils
+from qubes.storage.file import get_disk_usage
 
-from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent
+from PyQt4 import QtCore  # pylint: disable=import-error
+from PyQt4 import QtGui  # pylint: disable=import-error
+from . import ui_backupdlg  # pylint: disable=no-name-in-module
+from . import multiselectwidget
 
+from . import backup_utils
+from . import utils
+import grp
+import pwd
+import sys
+import os
+from . import thread_monitor
+import threading
 import time
-from .thread_monitor import *
-from operator import itemgetter
-
-from datetime import datetime
-from string import replace
 
-from .ui_backupdlg import *
-from .multiselectwidget import *
 
-from .backup_utils import *
-import main
-import grp,pwd
+class BackupVMsWindow(ui_backupdlg.Ui_Backup, multiselectwidget.QtGui.QWizard):
 
-
-class BackupVMsWindow(Ui_Backup, QWizard):
-
-    __pyqtSignals__ = ("backup_progress(int)",)
-
-    def __init__(self, app, qvm_collection, blk_manager, shutdown_vm_func, parent=None):
+    def __init__(self, qt_app, qubes_app, parent=None):
         super(BackupVMsWindow, self).__init__(parent)
 
-        self.app = app
-        self.qvm_collection = qvm_collection
-        self.blk_manager = blk_manager
-        self.shutdown_vm_func = shutdown_vm_func
+        self.qt_app = qt_app
+        self.qubes_app = qubes_app
+        self.backup_settings = QtCore.QSettings()
 
-        self.func_output = []
         self.selected_vms = []
-        self.tmpdir_to_remove = None
         self.canceled = False
-
-        self.vm = self.qvm_collection[0]
-        self.files_to_backup = None
-
-        assert self.vm != None
+        self.thread_monitor = None
 
         self.setupUi(self)
 
         self.progress_status.text = self.tr("Backup in progress...")
-        self.show_running_vms_warning(False)
         self.dir_line_edit.setReadOnly(False)
 
-        self.select_vms_widget = MultiSelectWidget(self)
+        self.select_vms_widget = multiselectwidget.MultiSelectWidget(self)
         self.verticalLayout.insertWidget(1, self.select_vms_widget)
 
-        self.connect(self, SIGNAL("currentIdChanged(int)"), self.current_page_changed)
-        self.connect(self.select_vms_widget, SIGNAL("selected_changed()"), self.check_running)
-        self.connect(self.select_vms_widget, SIGNAL("items_removed(PyQt_PyObject)"), self.vms_removed)
-        self.connect(self.select_vms_widget, SIGNAL("items_added(PyQt_PyObject)"), self.vms_added)
-        self.refresh_button.clicked.connect(self.check_running)
-        self.shutdown_running_vms_button.clicked.connect(self.shutdown_all_running_selected)
-        self.connect(self, SIGNAL("backup_progress(int)"), self.progress_bar.setValue)
-        self.dir_line_edit.connect(self.dir_line_edit, SIGNAL("textChanged(QString)"), self.backup_location_changed)
+        self.connect(self, QtCore.SIGNAL("currentIdChanged(int)"),
+                     self.current_page_changed)
+        self.connect(self.select_vms_widget,
+                     QtCore.SIGNAL("items_removed(PyQt_PyObject)"),
+                     self.vms_removed)
+        self.connect(self.select_vms_widget,
+                     QtCore.SIGNAL("items_added(PyQt_PyObject)"),
+                     self.vms_added)
+        self.dir_line_edit.connect(self.dir_line_edit,
+                                   QtCore.SIGNAL("textChanged(QString)"),
+                                   self.backup_location_changed)
 
         self.select_vms_page.isComplete = self.has_selected_vms
         self.select_dir_page.isComplete = self.has_selected_dir_and_pass
-        #FIXME
-        #this causes to run isComplete() twice, I don't know why
+        # FIXME
+        # this causes to run isComplete() twice, I don't know why
         self.select_vms_page.connect(
                 self.select_vms_widget,
-                SIGNAL("selected_changed()"),
-                SIGNAL("completeChanged()"))
+                QtCore.SIGNAL("selected_changed()"),
+                QtCore.SIGNAL("completeChanged()"))
         self.passphrase_line_edit.connect(
                 self.passphrase_line_edit,
-                SIGNAL("textChanged(QString)"),
+                QtCore.SIGNAL("textChanged(QString)"),
                 self.backup_location_changed)
         self.passphrase_line_edit_verify.connect(
                 self.passphrase_line_edit_verify,
-                SIGNAL("textChanged(QString)"),
+                QtCore.SIGNAL("textChanged(QString)"),
                 self.backup_location_changed)
 
         self.total_size = 0
-        self.__fill_vms_list__()
 
-        fill_appvms_list(self)
-        self.load_settings()
+        self.target_vm_list, self.target_vm_idx = utils.prepare_vm_choice(
+            self.appvm_combobox,
+            self.qubes_app,
+            None,
+            self.qubes_app.domains['dom0'],
+            (lambda vm: vm.klass != 'TemplateVM' and vm.is_running()),
+            allow_internal=False,
+            allow_default=False,
+            allow_none=False
+        )
+
+        selected = self.load_settings()
+        self.__fill_vms_list__(selected)
 
     def load_settings(self):
-        dest_vm_name = main.manager_window.manager_settings.value(
-            'backup/vmname', defaultValue="")
-        dest_vm_idx = self.appvm_combobox.findText(dest_vm_name.toString())
-        if dest_vm_idx > -1:
-            self.appvm_combobox.setCurrentIndex(dest_vm_idx)
-
-        if main.manager_window.manager_settings.contains('backup/path'):
-            dest_path = main.manager_window.manager_settings.value(
-                'backup/path', defaultValue=None)
-            self.dir_line_edit.setText(dest_path.toString())
-
-        if main.manager_window.manager_settings.contains('backup/encrypt'):
-            encrypt = main.manager_window.manager_settings.value(
-                'backup/encrypt', defaultValue=None)
-            self.encryption_checkbox.setChecked(encrypt.toBool())
-
-    def save_settings(self):
-        main.manager_window.manager_settings.setValue(
-            'backup/vmname', self.appvm_combobox.currentText())
-        main.manager_window.manager_settings.setValue(
-            'backup/path', self.dir_line_edit.text())
-        main.manager_window.manager_settings.setValue(
-            'backup/encrypt', self.encryption_checkbox.isChecked())
-
-    def show_running_vms_warning(self, show):
-        self.running_vms_warning.setVisible(show)
-        self.shutdown_running_vms_button.setVisible(show)
-        self.refresh_button.setVisible(show)
-
-    class VmListItem(QListWidgetItem):
+        """
+        Helper function that tries to load existing backup profile
+        (default path: /etc/qubes/backup/qubes-manager-backup.conf )
+        and then apply its contents to the Backup window.
+        :return: list of vms to include in backup, if it exists in the profile,
+        or None if it does not
+        """
+        try:
+            profile_data = backup_utils.load_backup_profile()
+        except FileNotFoundError:
+            return
+        except exc.QubesException:
+            QtGui.QMessageBox.information(
+                None, self.tr("Error loading backup profile"),
+                self.tr("Unable to load saved backup profile."))
+            return
+        if not profile_data:
+            return
+
+        if 'destination_vm' in profile_data:
+            dest_vm_name = profile_data['destination_vm']
+            dest_vm_idx = self.appvm_combobox.findText(dest_vm_name)
+            if dest_vm_idx > -1:
+                self.appvm_combobox.setCurrentIndex(dest_vm_idx)
+
+        if 'destination_path' in profile_data:
+            dest_path = profile_data['destination_path']
+            self.dir_line_edit.setText(dest_path)
+
+        if 'passphrase_text' in profile_data:
+            self.passphrase_line_edit.setText(profile_data['passphrase_text'])
+            self.passphrase_line_edit_verify.setText(
+                profile_data['passphrase_text'])
+
+        if 'compression' in profile_data:
+            self.compress_checkbox.setChecked(profile_data['compression'])
+
+        if 'include' in profile_data:
+            return profile_data['include']
+
+        return None
+
+    def save_settings(self, use_temp):
+        """
+        Helper function that saves backup profile to either
+        /etc/qubes/backup/qubes-manager-backup.conf or
+        /etc/qubes/backup/qubes-manager-backup-tmp.conf
+        :param use_temp: whether to use temporary profile (True) or the default
+         backup profile (False)
+        """
+        settings = {'destination_vm': self.appvm_combobox.currentText(),
+                    'destination_path': self.dir_line_edit.text(),
+                    'include': [vm.name for vm in self.selected_vms],
+                    'passphrase_text': self.passphrase_line_edit.text(),
+                    'compression': self.compress_checkbox.isChecked()}
+
+        backup_utils.write_backup_profile(settings, use_temp)
+
+    class VmListItem(QtGui.QListWidgetItem):
+        # pylint: disable=too-few-public-methods
         def __init__(self, vm):
             self.vm = vm
             if vm.qid == 0:
                 local_user = grp.getgrnam('qubes').gr_mem[0]
                 home_dir = pwd.getpwnam(local_user).pw_dir
-                self.size = qubesutils.get_disk_usage(home_dir)
+                self.size = get_disk_usage(home_dir)
             else:
-                self.size = self.get_vm_size(vm)
-            super(BackupVMsWindow.VmListItem, self).__init__(vm.name+ " (" + qubesutils.size_to_human(self.size) + ")")
-
-        def get_vm_size(self, vm):
-            size = 0
-            if vm.private_img is not None:
-                size += qubesutils.get_disk_usage (vm.private_img)
-
-            if vm.updateable:
-                size += qubesutils.get_disk_usage(vm.root_img)
+                self.size = vm.get_disk_utilization()
+            super(BackupVMsWindow.VmListItem, self).__init__(
+                vm.name + " (" + admin_utils.size_to_human(self.size) + ")")
 
-            return size
-
-
-    def __fill_vms_list__(self):
-        for vm in self.qvm_collection.values():
-            if vm.internal:
+    def __fill_vms_list__(self, selected=None):
+        for vm in self.qubes_app.domains:
+            if vm.features.get('internal', False):
                 continue
 
             item = BackupVMsWindow.VmListItem(vm)
-            if vm.include_in_backups == True:
+            if (selected is None and
+                    getattr(vm, 'include_in_backups', True)) \
+                    or (selected and vm.name in selected):
                 self.select_vms_widget.selected_list.addItem(item)
                 self.total_size += item.size
             else:
                 self.select_vms_widget.available_list.addItem(item)
         self.select_vms_widget.available_list.sortItems()
         self.select_vms_widget.selected_list.sortItems()
-        self.check_running()
-        self.total_size_label.setText(qubesutils.size_to_human(self.total_size))
+
+        self.unrecognized_config_label.setVisible(
+            selected is not None and
+            len(selected) != len(self.select_vms_widget.selected_list))
+        self.total_size_label.setText(
+            admin_utils.size_to_human(self.total_size))
 
     def vms_added(self, items):
         for i in items:
             self.total_size += i.size
-        self.total_size_label.setText(qubesutils.size_to_human(self.total_size))
+        self.total_size_label.setText(
+            admin_utils.size_to_human(self.total_size))
 
     def vms_removed(self, items):
         for i in items:
             self.total_size -= i.size
-        self.total_size_label.setText(qubesutils.size_to_human(self.total_size))
-
-    def check_running(self):
-        some_selected_vms_running = False
-        for i in range(self.select_vms_widget.selected_list.count()):
-            item = self.select_vms_widget.selected_list.item(i)
-            if item.vm.is_running() and item.vm.qid != 0:
-                item.setForeground(QBrush(QColor(255, 0, 0)))
-                some_selected_vms_running = True
-            else:
-                item.setForeground(QBrush(QColor(0, 0, 0)))
-
-        self.show_running_vms_warning(some_selected_vms_running)
+        self.total_size_label.setText(
+            admin_utils.size_to_human(self.total_size))
 
-        for i in range(self.select_vms_widget.available_list.count()):
-            item =  self.select_vms_widget.available_list.item(i)
-            if item.vm.is_running() and item.vm.qid != 0:
-                item.setForeground(QBrush(QColor(255, 0, 0)))
-            else:
-                item.setForeground(QBrush(QColor(0, 0, 0)))
-
-        return some_selected_vms_running
-
-    def shutdown_all_running_selected(self):
-        (names, vms) = self.get_running_vms()
-        if len(vms) == 0:
-            return
-
-        for vm in vms:
-            self.blk_manager.check_if_serves_as_backend(vm)
-
-        reply = QMessageBox.question(None, self.tr("VM Shutdown Confirmation"),
-             self.tr(
-                 "Are you sure you want to power down the following VMs: "
-                 "<b>{0}</b>?<br/>"
-                 "<small>This will shutdown all the running applications "
-                 "within them.</small>").format(', '.join(names)),
-             QMessageBox.Yes | QMessageBox.Cancel)
-
-        self.app.processEvents()
-
-        if reply == QMessageBox.Yes:
-
-            wait_time = 60.0
-            for vm in vms:
-                self.shutdown_vm_func(vm, wait_time*1000)
-
-            progress = QProgressDialog ("Shutting down VMs <b>{0}</b>...".format(', '.join(names)), "", 0, 0)
-            progress.setModal(True)
-            progress.show()
-
-            wait_for = wait_time
-            while self.check_running() and wait_for > 0:
-                self.app.processEvents()
-                time.sleep (0.5)
-                wait_for -= 0.5
-
-            progress.hide()
-
-    def get_running_vms(self):
-        names = []
-        vms = []
-        for i in range(self.select_vms_widget.selected_list.count()):
-            item = self.select_vms_widget.selected_list.item(i)
-            if item.vm.is_running() and item.vm.qid != 0:
-                names.append(item.vm.name)
-                vms.append(item.vm)
-        return (names, vms)
-
-    @pyqtSlot(name='on_select_path_button_clicked')
+    @QtCore.pyqtSlot(name='on_select_path_button_clicked')
     def select_path_button_clicked(self):
-        select_path_button_clicked(self)
+        backup_utils.select_path_button_clicked(self)
 
     def validateCurrentPage(self):
+        # pylint: disable=invalid-name
         if self.currentPage() is self.select_vms_page:
-            if self.check_running():
-                QMessageBox.information(None,
-                    self.tr("Wait!"),
-                    self.tr("Some selected VMs are running. "
-                        "Running VMs can not be backuped. "
-                        "Please shut them down or remove them from the list."))
-                return False
 
             self.selected_vms = []
             for i in range(self.select_vms_widget.selected_list.count()):
-                self.selected_vms.append(self.select_vms_widget.selected_list.item(i).vm)
+                self.selected_vms.append(
+                    self.select_vms_widget.selected_list.item(i).vm)
 
         elif self.currentPage() is self.select_dir_page:
             backup_location = str(self.dir_line_edit.text())
             if not backup_location:
-                QMessageBox.information(None, self.tr("Wait!"),
+                QtGui.QMessageBox.information(
+                    None, self.tr("Wait!"),
                     self.tr("Enter backup target location first."))
                 return False
-            if self.appvm_combobox.currentIndex() == 0 and \
-                   not os.path.isdir(backup_location):
-                QMessageBox.information(None, self.tr("Wait!"),
+            if self.appvm_combobox.currentIndex() == 0 \
+                    and not os.path.isdir(backup_location):
+                QtGui.QMessageBox.information(
+                    None, self.tr("Wait!"),
                     self.tr("Selected directory do not exists or "
                             "not a directory (%s).") % backup_location)
                 return False
-            if not len(self.passphrase_line_edit.text()):
-                QMessageBox.information(None, self.tr("Wait!"),
-                    self.tr("Enter passphrase for backup encryption/verification first."))
+            if not self.passphrase_line_edit.text():
+                QtGui.QMessageBox.information(
+                    None, self.tr("Wait!"),
+                    self.tr("Enter passphrase for backup "
+                            "encryption/verification first."))
                 return False
-            if self.passphrase_line_edit.text() != self.passphrase_line_edit_verify.text():
-                QMessageBox.information(None,
-                    self.tr("Wait!"),
+            if self.passphrase_line_edit.text() !=\
+                    self.passphrase_line_edit_verify.text():
+                QtGui.QMessageBox.information(
+                    None, self.tr("Wait!"),
                     self.tr("Enter the same passphrase in both fields."))
                 return False
 
         return True
 
-    def gather_output(self, s):
-        self.func_output.append(s)
-
-    def update_progress_bar(self, value):
-        self.emit(SIGNAL("backup_progress(int)"), value)
-
-    def __do_backup__(self, thread_monitor):
+    def __do_backup__(self, t_monitor):
         msg = []
 
         try:
-            backup.backup_do(self.dir_line_edit.text(),
-                    self.files_to_backup,
-                    self.passphrase_line_edit.text(),
-                    progress_callback=self.update_progress_bar,
-                    encrypted=self.encryption_checkbox.isChecked(),
-                    appvm=self.target_appvm)
-            #simulate_long_lasting_proces(10, self.update_progress_bar)
-        except backup.BackupCanceledError as ex:
-            msg.append(str(ex))
-            self.canceled = True
-            if ex.tmpdir:
-                self.tmpdir_to_remove = ex.tmpdir
-        except Exception as ex:
-            print("Exception:", ex)
+            vm = self.qubes_app.domains[
+                self.appvm_combobox.currentText()]
+            if not vm.is_running():
+                vm.start()
+            self.qubes_app.qubesd_call(
+                'dom0', 'admin.backup.Execute',
+                backup_utils.get_profile_name(True))
+        except Exception as ex:  # pylint: disable=broad-except
             msg.append(str(ex))
 
-        if len(msg) > 0 :
-            thread_monitor.set_error_msg('\n'.join(msg))
+        if msg:
+            t_monitor.set_error_msg('\n'.join(msg))
 
-        thread_monitor.set_finished()
+        t_monitor.set_finished()
 
+    @staticmethod
+    def cleanup_temporary_files():
+        try:
+            os.remove(backup_utils.get_profile_path(use_temp=True))
+        except FileNotFoundError:
+            pass
 
-    def current_page_changed(self, id):
+    def current_page_changed(self, page_id): # pylint: disable=unused-argument
         old_sigchld_handler = signal.signal(signal.SIGCHLD, signal.SIG_DFL)
         if self.currentPage() is self.confirm_page:
 
-            self.target_appvm = None
-            if self.appvm_combobox.currentIndex() != 0:   #An existing appvm chosen
-                self.target_appvm = self.qvm_collection.get_vm_by_name(
-                        self.appvm_combobox.currentText())
-
-            del self.func_output[:]
-            try:
-                self.files_to_backup = backup.backup_prepare(
-                        self.selected_vms,
-                        print_callback = self.gather_output,
-                        hide_vm_names=self.encryption_checkbox.isChecked())
-            except Exception as ex:
-                print("Exception:", ex)
-                QMessageBox.critical(None,
-                    self.tr("Error while preparing backup."),
-                    self.tr("ERROR: {0}").format(ex))
+            self.save_settings(use_temp=True)
+            backup_summary = self.qubes_app.qubesd_call(
+                'dom0', 'admin.backup.Info',
+                backup_utils.get_profile_name(True))
 
             self.textEdit.setReadOnly(True)
             self.textEdit.setFontFamily("Monospace")
-            self.textEdit.setText("\n".join(self.func_output))
-            self.save_settings()
+            self.textEdit.setText(backup_summary.decode())
 
         elif self.currentPage() is self.commit_page:
+
+            if self.save_profile_checkbox.isChecked():
+                self.save_settings(use_temp=False)
+
             self.button(self.FinishButton).setDisabled(True)
             self.showFileDialog.setEnabled(
                 self.appvm_combobox.currentIndex() != 0)
             self.showFileDialog.setChecked(self.showFileDialog.isEnabled()
                                            and str(self.dir_line_edit.text())
                                            .count("media/") > 0)
-            self.thread_monitor = ThreadMonitor()
-            thread = threading.Thread (target= self.__do_backup__ , args=(self.thread_monitor,))
+            self.thread_monitor = thread_monitor.ThreadMonitor()
+            thread = threading.Thread(
+                target=self.__do_backup__,
+                args=(self.thread_monitor,))
             thread.daemon = True
             thread.start()
 
-            counter = 0
             while not self.thread_monitor.is_finished():
-                self.app.processEvents()
-                time.sleep (0.1)
+                self.qt_app.processEvents()
+                time.sleep(0.1)
 
             if not self.thread_monitor.success:
                 if self.canceled:
-                    self.progress_status.setText(self.tr("Backup aborted."))
-                    if self.tmpdir_to_remove:
-                        if QMessageBox.warning(None, self.tr("Backup aborted"),
-                                self.tr("Do you want to remove temporary files from "
-                                        "%s?") % self.tmpdir_to_remove,
-                                QMessageBox.Yes, QMessageBox.No) == QMessageBox.Yes:
-                            shutil.rmtree(self.tmpdir_to_remove)
+                    self.progress_status.setText(
+                        self.tr(
+                            "Backup aborted. "
+                            "Temporary file may be left at backup location."))
                 else:
                     self.progress_status.setText(self.tr("Backup error."))
-                    QMessageBox.warning(self, self.tr("Backup error!"),
+                    QtGui.QMessageBox.warning(
+                        self, self.tr("Backup error!"),
                         self.tr("ERROR: {}").format(
-                        self.thread_monitor.error_msg))
+                            self.thread_monitor.error_msg))
             else:
+                self.progress_bar.setMaximum(100)
                 self.progress_bar.setValue(100)
                 self.progress_status.setText(self.tr("Backup finished."))
             if self.showFileDialog.isChecked():
@@ -402,84 +341,78 @@ class BackupVMsWindow(Ui_Backup, QWizard):
                     orig_text + self.tr(
                         " Please unmount your backup volume and cancel "
                         "the file selection dialog."))
-                if self.target_appvm:
-                    self.target_appvm.run("QUBESRPC %s dom0" % "qubes"
-                                                               ".SelectDirectory")
+                backup_utils.select_path_button_clicked(self, False, True)
             self.button(self.CancelButton).setEnabled(False)
             self.button(self.FinishButton).setEnabled(True)
             self.showFileDialog.setEnabled(False)
+            self.cleanup_temporary_files()
         signal.signal(signal.SIGCHLD, old_sigchld_handler)
 
     def reject(self):
-        #cancell clicked while the backup is in progress.
-        #calling kill on tar.
         if self.currentPage() is self.commit_page:
-            if backup.backup_cancel():
-                self.button(self.CancelButton).setDisabled(True)
+            self.canceled = True
+            self.qubes_app.qubesd_call(
+                'dom0', 'admin.backup.Cancel',
+                backup_utils.get_profile_name(True))
+            self.progress_bar.setMaximum(100)
+            self.progress_bar.setValue(0)
+            self.button(self.CancelButton).setDisabled(True)
+            self.cleanup_temporary_files()
         else:
+            self.cleanup_temporary_files()
             self.done(0)
 
     def has_selected_vms(self):
         return self.select_vms_widget.selected_list.count() > 0
 
     def has_selected_dir_and_pass(self):
-        if not len(self.passphrase_line_edit.text()):
+        if not self.passphrase_line_edit.text():
             return False
-        if self.passphrase_line_edit.text() != self.passphrase_line_edit_verify.text():
+        if self.passphrase_line_edit.text() != \
+                self.passphrase_line_edit_verify.text():
             return False
         return len(self.dir_line_edit.text()) > 0
 
-    def backup_location_changed(self, new_dir = None):
-        self.select_dir_page.emit(SIGNAL("completeChanged()"))
+    def backup_location_changed(self, new_dir=None):
+        # pylint: disable=unused-argument
+        self.select_dir_page.emit(QtCore.SIGNAL("completeChanged()"))
 
 
 # Bases on the original code by:
 # Copyright (c) 2002-2007 Pascal Varet <p.varet@gmail.com>
 
-def handle_exception(exc_type, exc_value, exc_traceback ):
-    import sys
-    import os.path
-    import traceback
+def handle_exception(exc_type, exc_value, exc_traceback):
+    filename, line, dummy, dummy = traceback.extract_tb(exc_traceback).pop()
+    filename = os.path.basename(filename)
+    error = "%s: %s" % (exc_type.__name__, exc_value)
 
-    filename, line, dummy, dummy = traceback.extract_tb( exc_traceback ).pop()
-    filename = os.path.basename( filename )
-    error    = "%s: %s" % ( exc_type.__name__, exc_value )
+    QtGui.QMessageBox.critical(
+        None,
+        "Houston, we have a problem...",
+        "Whoops. A critical error has occured. This is most likely a bug "
+        "in Qubes Global Settings application.<br><br><b><i>%s</i></b>" %
+        error + "at <b>line %d</b> of file <b>%s</b>.<br/><br/>"
+        % (line, filename))
 
-    QMessageBox.critical(None, "Houston, we have a problem...",
-                         "Whoops. A critical error has occured. This is most likely a bug "
-                         "in Qubes Restore VMs application.<br><br>"
-                         "<b><i>%s</i></b>" % error +
-                         "at <b>line %d</b> of file <b>%s</b>.<br/><br/>"
-                         % ( line, filename ))
 
+def main():
 
-def app_main():
-
-    global qubes_host
-    qubes_host = QubesHost()
-
-    global app
-    app = QApplication(sys.argv)
-    app.setOrganizationName("The Qubes Project")
-    app.setOrganizationDomain("http://qubes-os.org")
-    app.setApplicationName("Qubes Backup VMs")
+    qt_app = QtGui.QApplication(sys.argv)
+    qt_app.setOrganizationName("The Qubes Project")
+    qt_app.setOrganizationDomain("http://qubes-os.org")
+    qt_app.setApplicationName("Qubes Backup VMs")
 
     sys.excepthook = handle_exception
 
-    qvm_collection = QubesVmCollection()
-    qvm_collection.lock_db_for_reading()
-    qvm_collection.load()
-    qvm_collection.unlock_db()
+    app = Qubes()
 
-    global backup_window
-    backup_window = BackupVMsWindow()
+    backup_window = BackupVMsWindow(qt_app, app)
 
     backup_window.show()
 
-    app.exec_()
-    app.exit()
-
+    qt_app.exec_()
+    qt_app.exit()
 
 
 if __name__ == "__main__":
-    app_main()
+    main()

+ 83 - 62
qubesmanager/backup_utils.py

@@ -14,96 +14,117 @@
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
 #
 #
 import re
-import sys
-import os
-from PyQt4.QtCore import *
-from PyQt4.QtGui import *
+from PyQt4 import QtGui  # pylint: disable=import-error
+from PyQt4 import QtCore  # pylint: disable=import-error
 
 import subprocess
-import time
-
-from .thread_monitor import *
+from . import utils
+import yaml
 
 path_re = re.compile(r"[a-zA-Z0-9/:.,_+=() -]*")
 path_max_len = 512
 
+
 def fill_appvms_list(dialog):
+    """
+    Helper function, designed to fill the destination vm combobox in both backup
+    and restore GUI tools.
+    :param dialog: QtGui.QWizard with a combobox called appvm_combobox
+    """
     dialog.appvm_combobox.clear()
     dialog.appvm_combobox.addItem("dom0")
 
-    dialog.appvm_combobox.setCurrentIndex(0) #current selected is null ""
+    dialog.appvm_combobox.setCurrentIndex(0)  # current selected is null ""
 
-    for vm in dialog.qvm_collection.values():
-        if vm.is_appvm() and vm.internal:
-            continue
-        if vm.is_template() and vm.installed_by_rpm:
+    for vm in dialog.qubes_app.domains:
+        if vm.features.get('internal', False) or vm.klass == 'TemplateVM':
             continue
 
         if vm.is_running() and vm.qid != 0:
             dialog.appvm_combobox.addItem(vm.name)
 
+
 def enable_dir_line_edit(dialog, boolean):
     dialog.dir_line_edit.setEnabled(boolean)
     dialog.select_path_button.setEnabled(boolean)
 
-def get_path_for_vm(vm, service_name):
-    if not vm:
-        return None
-    proc = vm.run("QUBESRPC %s dom0" % service_name, passio_popen=True)
-    proc.stdin.close()
-    untrusted_path = proc.stdout.readline(path_max_len)
-    if len(untrusted_path) == 0:
-        return None
-    if path_re.match(untrusted_path):
-        assert '../' not in untrusted_path
-        assert '\0' not in untrusted_path
-        return untrusted_path.strip()
-    else:
-        return None
-
-def select_path_button_clicked(dialog, select_file = False):
+
+def select_path_button_clicked(dialog, select_file=False, read_only=False):
+    """
+    Helper function that displays a file/directory selection wizard. Used by
+    backup and restore GUI tools.
+    :param dialog: QtGui.QWizard with a dir_line_edit text box that wants to
+    receive a file/directory path and appvm_combobox with VM to use
+    :param select_file: True: select file dialog; False: select directory
+    dialog
+    :param read_only: should the dir_line_edit be changed after selecting a file
+    or directory
+    :return:
+    """
     backup_location = str(dialog.dir_line_edit.text())
-    file_dialog = QFileDialog()
+    file_dialog = QtGui.QFileDialog()
     file_dialog.setReadOnly(True)
 
-    if select_file:
-        file_dialog_function = file_dialog.getOpenFileName
-    else:
-        file_dialog_function = file_dialog.getExistingDirectory
-
-    new_appvm = None
     new_path = None
-    if dialog.appvm_combobox.currentIndex() != 0:   #An existing appvm chosen
-        new_appvm = str(dialog.appvm_combobox.currentText())
-        vm = dialog.qvm_collection.get_vm_by_name(new_appvm)
-        if vm:
-            new_path = get_path_for_vm(vm, "qubes.SelectFile" if select_file
-                    else "qubes.SelectDirectory")
-    else:
-        new_path = file_dialog_function(dialog,
-            dialog.tr("Select backup location."),
-            backup_location if backup_location else '/')
-
-    if new_path != None:
-        if os.path.basename(new_path) == 'qubes.xml':
-            backup_location = os.path.dirname(new_path)
+
+    new_appvm = str(dialog.appvm_combobox.currentText())
+    vm = dialog.qubes_app.domains[new_appvm]
+    try:
+        new_path = utils.get_path_from_vm(
+            vm,
+            "qubes.SelectFile" if select_file
+            else "qubes.SelectDirectory")
+    except subprocess.CalledProcessError:
+        if not read_only:
+            QtGui.QMessageBox.warning(
+                None,
+                dialog.tr("Nothing selected!"),
+                dialog.tr("No file or directory selected."))
         else:
-            backup_location = new_path
-        dialog.dir_line_edit.setText(backup_location)
+            return
+
+    if new_path and not read_only:
+        dialog.dir_line_edit.setText(new_path)
+
+    if new_path and backup_location and not read_only:
+        dialog.select_dir_page.emit(QtCore.SIGNAL("completeChanged()"))
+
+
+def get_profile_name(use_temp):
+    backup_profile_name = 'qubes-manager-backup'
+    temp_backup_profile_name = 'qubes-manager-backup-tmp'
+
+    return temp_backup_profile_name if use_temp else backup_profile_name
+
+
+def get_profile_path(use_temp):
+    path = '/etc/qubes/backup/' + get_profile_name(use_temp) + '.conf'
+    return path
+
+
+def load_backup_profile(use_temp=False):
+
+    path = get_profile_path(use_temp)
+
+    with open(path) as profile_file:
+        profile_data = yaml.safe_load(profile_file)
+    return profile_data
+
+
+def write_backup_profile(args, use_temp=False):
+
+    acceptable_fields = ['include', 'passphrase_text', 'compression',
+                         'destination_vm', 'destination_path']
 
-    if (new_path or new_appvm) and len(backup_location) > 0:
-        dialog.select_dir_page.emit(SIGNAL("completeChanged()"))
+    profile_data = {key: value for key, value in args.items()
+                    if key in acceptable_fields}
 
-def simulate_long_lasting_proces(period, progress_callback):
-    for i in range(period):
-        progress_callback((i*100)/period)
-        time.sleep(1)
+    path = get_profile_path(use_temp)
 
-    progress_callback(100)
-    return 0
+    with open(path, 'w') as profile_file:
+        yaml.safe_dump(profile_data, profile_file)

+ 3 - 3
qubesmanager/block.py

@@ -1,5 +1,6 @@
 #!/usr/bin/python2
 # -*- coding: utf8 -*-
+# pylint: skip-file
 #
 # The Qubes OS Project, http://www.qubes-os.org
 #
@@ -15,9 +16,8 @@
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
 
 import threading
 import time

+ 43 - 22
qubesmanager/bootfromdevice.py

@@ -12,20 +12,21 @@
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
 #
 #
 
+import sys
 import subprocess
 from . import utils
-from .firewall import *
-from .ui_bootfromdevice import *
-import qubesadmin.tools.qvm_start as qvm_start
+from . import ui_bootfromdevice  # pylint: disable=no-name-in-module
+from PyQt4 import QtGui, QtCore  # pylint: disable=import-error
+from qubesadmin import tools
+from qubesadmin.tools import qvm_start
 
 
-class VMBootFromDeviceWindow(Ui_BootDialog, QDialog):
+class VMBootFromDeviceWindow(ui_bootfromdevice.Ui_BootDialog, QtGui.QDialog):
     def __init__(self, vm, qapp, parent=None):
         super(VMBootFromDeviceWindow, self).__init__(parent)
 
@@ -33,14 +34,19 @@ class VMBootFromDeviceWindow(Ui_BootDialog, QDialog):
         self.qapp = qapp
 
         self.setupUi(self)
-        self.setWindowTitle(self.tr("Boot {vm} from device").format(vm=self.vm.name))
+        self.setWindowTitle(
+            self.tr("Boot {vm} from device").format(vm=self.vm.name))
 
-        self.connect(self.buttonBox, SIGNAL("accepted()"), self.save_and_apply)
-        self.connect(self.buttonBox, SIGNAL("rejected()"), self.reject)
+        self.connect(
+            self.buttonBox,
+            QtCore.SIGNAL("accepted()"),
+            self.save_and_apply)
+        self.connect(self.buttonBox, QtCore.SIGNAL("rejected()"), self.reject)
 
         # populate buttons and such
         self.__init_buttons__()
-
+        # warn user if the VM is currently running
+        self.__warn_if_running__()
 
     def reject(self):
         self.done(0)
@@ -49,15 +55,30 @@ class VMBootFromDeviceWindow(Ui_BootDialog, QDialog):
         if self.blockDeviceRadioButton.isChecked():
             cdrom_location = self.blockDeviceComboBox.currentText()
         elif self.fileRadioButton.isChecked():
-            cdrom_location = str(self.vm_list[self.fileVM.currentIndex()]) + ":" + self.pathText.text()
+            cdrom_location = str(
+                self.vm_list[self.fileVM.currentIndex()]) + \
+                             ":" + self.pathText.text()
         else:
-            QMessageBox.warning(None,
-                                self.tr(
-                                    "ERROR!"),
-                                self.tr("No file or block device selected; please select one."))
+            QtGui.QMessageBox.warning(
+                None,
+                self.tr("ERROR!"),
+                self.tr("No file or block device selected; please select one."))
             return
+
+        # warn user if the VM is currently running
+        self.__warn_if_running__()
+
         qvm_start.main(['--cdrom', cdrom_location, self.vm.name])
 
+    def __warn_if_running__(self):
+        if self.vm.is_running():
+            QtGui.QMessageBox.warning(
+                None,
+                self.tr("Warning!"),
+                self.tr("Qube must be turned off before booting it from"
+                        "device. Please turn off the qube.")
+            )
+
     def __init_buttons__(self):
         self.fileVM.setEnabled(False)
         self.selectFileButton.setEnabled(False)
@@ -86,7 +107,8 @@ class VMBootFromDeviceWindow(Ui_BootDialog, QDialog):
         )
 
     def radio_button_clicked(self):
-        self.blockDeviceComboBox.setEnabled(self.blockDeviceRadioButton.isChecked())
+        self.blockDeviceComboBox.setEnabled(
+            self.blockDeviceRadioButton.isChecked())
         self.fileVM.setEnabled(self.fileRadioButton.isChecked())
         self.selectFileButton.setEnabled(self.fileRadioButton.isChecked())
         self.pathText.setEnabled(self.fileRadioButton.isChecked())
@@ -103,18 +125,17 @@ class VMBootFromDeviceWindow(Ui_BootDialog, QDialog):
             self.pathText.setText(new_path)
 
 
-parser = qubesadmin.tools.QubesArgumentParser(vmname_nargs=1)
+parser = tools.QubesArgumentParser(vmname_nargs=1)
 
-def main(args=None):
-    global bootfromdevice_window
 
+def main(args=None):
     args = parser.parse_args(args)
     vm = args.domains.pop()
 
-    qapp = QApplication(sys.argv)
+    qapp = QtGui.QApplication(sys.argv)
     qapp.setOrganizationName('Invisible Things Lab')
     qapp.setOrganizationDomain("https://www.qubes-os.org/")
-    qapp.setApplicationName("Qubes VM Settings")
+    qapp.setApplicationName("Boot Qube From Device")
 
 #    if not utils.is_debug(): #FIXME
 #        sys.excepthook = handle_exception

+ 3 - 3
qubesmanager/clipboard.py

@@ -1,4 +1,5 @@
 #!/usr/bin/python2
+# pylint: skip-file
 #
 # The Qubes OS Project, http://www.qubes-os.org
 #
@@ -16,9 +17,8 @@
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
 #
 #
 

+ 24 - 24
qubesmanager/create_new_vm.py

@@ -16,34 +16,30 @@
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
 #
 #
 
-import os
 import sys
 import threading
 import time
 import subprocess
 
-from PyQt4.QtCore import *
-from PyQt4.QtGui import *
+from PyQt4 import QtCore, QtGui  # pylint: disable=import-error
 
 import qubesadmin
 import qubesadmin.tools
-
-import qubesmanager.resources_rc
+import qubesadmin.exc
 
 from . import utils
 
-from .ui_newappvmdlg import Ui_NewVMDlg
+from .ui_newappvmdlg import Ui_NewVMDlg  # pylint: disable=import-error
 from .thread_monitor import ThreadMonitor
 
 
-class NewVmDlg(QDialog, Ui_NewVMDlg):
-    def __init__(self, qtapp, app, parent = None):
+class NewVmDlg(QtGui.QDialog, Ui_NewVMDlg):
+    def __init__(self, qtapp, app, parent=None):
         super(NewVmDlg, self).__init__(parent)
         self.setupUi(self)
 
@@ -51,7 +47,8 @@ class NewVmDlg(QDialog, Ui_NewVMDlg):
         self.app = app
 
         # Theoretically we should be locking for writing here and unlock
-        # only after the VM creation finished. But the code would be more messy...
+        # only after the VM creation finished. But the code would be
+        # more messy...
         # Instead we lock for writing in the actual worker thread
         self.label_list, self.label_idx = utils.prepare_label_choice(
             self.label,
@@ -73,13 +70,13 @@ class NewVmDlg(QDialog, Ui_NewVMDlg):
             (lambda vm: vm.provides_network),
             allow_internal=False, allow_default=True, allow_none=True)
 
-        self.name.setValidator(QRegExpValidator(
-            QRegExp("[a-zA-Z0-9-]*", Qt.CaseInsensitive), None))
+        self.name.setValidator(QtGui.QRegExpValidator(
+            QtCore.QRegExp("[a-zA-Z0-9-]*", QtCore.Qt.CaseInsensitive), None))
         self.name.selectAll()
         self.name.setFocus()
 
-        if len(self.template_list) == 0:
-            QMessageBox.warning(None,
+        if not self.template_list:
+            QtGui.QMessageBox.warning(None,
                 self.tr('No template available!'),
                 self.tr('Cannot create a qube when no template exists.'))
 
@@ -99,7 +96,8 @@ class NewVmDlg(QDialog, Ui_NewVMDlg):
         self.done(0)
 
     def accept(self):
-        vmclass = ('AppVM' if self.vm_type.currentIndex() == 0 else 'StandaloneVM')
+        vmclass = ('AppVM' if self.vm_type.currentIndex() == 0
+                   else 'StandaloneVM')
 
         name = str(self.name.text())
         try:
@@ -107,7 +105,7 @@ class NewVmDlg(QDialog, Ui_NewVMDlg):
         except LookupError:
             pass
         else:
-            QMessageBox.warning(None,
+            QtGui.QMessageBox.warning(None,
                 self.tr('Incorrect qube name!'),
                 self.tr('A qube with the name <b>{}</b> already exists in the '
                         'system!').format(name))
@@ -132,7 +130,7 @@ class NewVmDlg(QDialog, Ui_NewVMDlg):
         thread.daemon = True
         thread.start()
 
-        progress = QProgressDialog(
+        progress = QtGui.QProgressDialog(
             self.tr("Creating new qube <b>{}</b>...").format(name), "", 0, 0)
         progress.setCancelButton(None)
         progress.setModal(True)
@@ -140,12 +138,12 @@ class NewVmDlg(QDialog, Ui_NewVMDlg):
 
         while not thread_monitor.is_finished():
             self.qtapp.processEvents()
-            time.sleep (0.1)
+            time.sleep(0.1)
 
         progress.hide()
 
         if not thread_monitor.success:
-            QMessageBox.warning(None,
+            QtGui.QMessageBox.warning(None,
                 self.tr("Error creating the qube!"),
                 self.tr("ERROR: {}").format(thread_monitor.error_msg))
 
@@ -177,8 +175,10 @@ class NewVmDlg(QDialog, Ui_NewVMDlg):
                 for k, v in properties.items():
                     setattr(vm, k, v)
 
-        except Exception as ex:
-            thread_monitor.set_error_msg(str(ex))
+        except qubesadmin.exc.QubesException as qex:
+            thread_monitor.set_error_msg(str(qex))
+        except Exception as ex:  # pylint: disable=broad-except
+            thread_monitor.set_error_msg(repr(ex))
 
         thread_monitor.set_finished()
 
@@ -218,7 +218,7 @@ parser = qubesadmin.tools.QubesArgumentParser()
 def main(args=None):
     args = parser.parse_args(args)
 
-    qtapp = QApplication(sys.argv)
+    qtapp = QtGui.QApplication(sys.argv)
     qtapp.setOrganizationName('Invisible Things Lab')
     qtapp.setOrganizationDomain('https://www.qubes-os.org/')
     qtapp.setApplicationName('Create qube')

+ 259 - 236
qubesmanager/firewall.py

@@ -13,91 +13,90 @@
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
 #
 #
 
 import datetime
-import ipaddress
-import os
 import re
-import sys
-import xml.etree.ElementTree
-
-from PyQt4.QtCore import *
-from PyQt4.QtGui import *
 
+from PyQt4 import QtCore, QtGui  # pylint: disable=import-error
 import qubesadmin.firewall
 
-from . import ui_newfwruledlg
+from . import ui_newfwruledlg  # pylint: disable=no-name-in-module
 
 
 class FirewallModifiedOutsideError(ValueError):
     pass
 
-class QIPAddressValidator(QValidator):
-    def __init__(self, parent = None):
-        super (QIPAddressValidator, self).__init__(parent)
+class QIPAddressValidator(QtGui.QValidator):
+    # pylint: disable=too-few-public-methods
+    def __init__(self, parent=None):
+        super(QIPAddressValidator, self).__init__(parent)
 
-    def validate(self, input, pos):
-        hostname = str(input)
+    def validate(self, input_string, pos):
+        # pylint: disable=too-many-return-statements,no-self-use
+        hostname = str(input_string)
 
-        if len(hostname) > 255 or len(hostname) == 0:
-            return (QValidator.Intermediate, input, pos)
+        if len(hostname) > 255 or not hostname:
+            return (QtGui.QValidator.Intermediate, input_string, pos)
 
         if hostname == "*":
-            return (QValidator.Acceptable, input, pos)
+            return (QtGui.QValidator.Acceptable, input_string, pos)
 
         unmask = hostname.split("/", 1)
         if len(unmask) == 2:
             hostname = unmask[0]
             mask = unmask[1]
             if mask.isdigit() or mask == "":
-                if re.match("^([0-9]{1,3}\.){3}[0-9]{1,3}$", hostname) is None:
-                    return (QValidator.Invalid, input, pos)
+                if re.match(r"^([0-9]{1,3}\.){3}[0-9]{1,3}$", hostname) is None:
+                    return (QtGui.QValidator.Invalid, input_string, pos)
                 if mask != "":
                     mask = int(unmask[1])
                     if mask < 0 or mask > 32:
-                        return (QValidator.Invalid, input, pos)
+                        return (QtGui.QValidator.Invalid, input_string, pos)
             else:
-                return (QValidator.Invalid, input, pos)
+                return (QtGui.QValidator.Invalid, input_string, pos)
 
         if hostname[-1:] == ".":
             hostname = hostname[:-1]
 
         if hostname[-1:] == "-":
-            return (QValidator.Intermediate, input, pos)
+            return (QtGui.QValidator.Intermediate, input_string, pos)
 
-        allowed = re.compile("(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
+        allowed = re.compile(r"(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
         if all(allowed.match(x) for x in hostname.split(".")):
-            return (QValidator.Acceptable, input, pos)
+            return (QtGui.QValidator.Acceptable, input_string, pos)
 
-        return (QValidator.Invalid, input, pos)
+        return (QtGui.QValidator.Invalid, input_string, pos)
 
-class NewFwRuleDlg (QDialog, ui_newfwruledlg.Ui_NewFwRuleDlg):
-    def __init__(self, parent = None):
-        super (NewFwRuleDlg, self).__init__(parent)
+class NewFwRuleDlg(QtGui.QDialog, ui_newfwruledlg.Ui_NewFwRuleDlg):
+    def __init__(self, parent=None):
+        super(NewFwRuleDlg, self).__init__(parent)
         self.setupUi(self)
 
-        self.set_ok_enabled(False)
+        self.set_ok_state(False)
         self.addressComboBox.setValidator(QIPAddressValidator())
-        self.addressComboBox.editTextChanged.connect(self.address_editing_finished)
-        self.serviceComboBox.setValidator(QRegExpValidator(QRegExp("[a-z][a-z0-9-]+|[0-9]+(-[0-9]+)?", Qt.CaseInsensitive), None))
+        self.addressComboBox.editTextChanged.connect(
+            self.address_editing_finished)
+        self.serviceComboBox.setValidator(QtGui.QRegExpValidator(
+            QtCore.QRegExp("[a-z][a-z0-9-]+|[0-9]+(-[0-9]+)?",
+                           QtCore.Qt.CaseInsensitive), None))
         self.serviceComboBox.setEnabled(False)
-        self.serviceComboBox.setInsertPolicy(QComboBox.InsertAtBottom)
+        self.serviceComboBox.setInsertPolicy(QtGui.QComboBox.InsertAtBottom)
         self.populate_combos()
-        self.serviceComboBox.setInsertPolicy(QComboBox.InsertAtTop)
+        self.serviceComboBox.setInsertPolicy(QtGui.QComboBox.InsertAtTop)
 
     def accept(self):
         if self.tcp_radio.isChecked() or self.udp_radio.isChecked():
-            if len(self.serviceComboBox.currentText()) == 0:
-                msg = QMessageBox()
+            if not self.serviceComboBox.currentText():
+                msg = QtGui.QMessageBox()
                 msg.warning(self, self.tr("Firewall rule"),
-                    self.tr("You need to fill service name/port for TCP/UDP rule"))
+                    self.tr("You need to fill service "
+                            "name/port for TCP/UDP rule"))
                 return
-        QDialog.accept(self)
+        QtGui.QDialog.accept(self)
 
     def populate_combos(self):
         example_addresses = [
@@ -108,8 +107,8 @@ class NewFwRuleDlg (QDialog, ui_newfwruledlg.Ui_NewFwRuleDlg):
         displayed_services = [
                 '',
                 'http', 'https', 'ftp', 'ftps', 'smtp',
-                'smtps', 'pop3', 'pop3s', 'imap', 'imaps', 'odmr', 
-                'nntp', 'nntps', 'ssh', 'telnet', 'telnets', 'ntp', 
+                'smtps', 'pop3', 'pop3s', 'imap', 'imaps', 'odmr',
+                'nntp', 'nntps', 'ssh', 'telnet', 'telnets', 'ntp',
                 'snmp', 'ldap', 'ldaps', 'irc', 'ircs', 'xmpp-client',
                 'syslog', 'printer', 'nfs', 'x11',
                 '1024-1234'
@@ -120,12 +119,12 @@ class NewFwRuleDlg (QDialog, ui_newfwruledlg.Ui_NewFwRuleDlg):
             self.serviceComboBox.addItem(service)
 
     def address_editing_finished(self):
-        self.set_ok_enabled(True)
+        self.set_ok_state(True)
 
-    def set_ok_enabled(self, on):
-        ok_button = self.buttonBox.button(QDialogButtonBox.Ok)
+    def set_ok_state(self, ok_state):
+        ok_button = self.buttonBox.button(QtGui.QDialogButtonBox.Ok)
         if ok_button is not None:
-            ok_button.setEnabled(on)
+            ok_button.setEnabled(ok_state)
 
     def on_tcp_radio_toggled(self, checked):
         if checked:
@@ -139,55 +138,43 @@ class NewFwRuleDlg (QDialog, ui_newfwruledlg.Ui_NewFwRuleDlg):
         if checked:
             self.serviceComboBox.setEnabled(False)
 
-class QubesFirewallRulesModel(QAbstractItemModel):
+class QubesFirewallRulesModel(QtCore.QAbstractItemModel):
     def __init__(self, parent=None):
-        QAbstractItemModel.__init__(self, parent)
-
-        self.__columnValues = {
-            0: lambda x: "*" if self.children[x]["address"] == "0.0.0.0" and
-                                self.children[x]["netmask"] == 0  else
-            self.children[x]["address"] + ("" if self.children[x][ "netmask"] == 32  else
-                                           " /{0}".format(self.children[x][
-                                               "netmask"])),
-            1: lambda x: "any" if self.children[x]["portBegin"] == 0  else
-            "{0}-{1}".format(self.children[x]["portBegin"], self.children[x][
-                "portEnd"]) if self.children[x]["portEnd"] is not None  else \
-                self.get_service_name(self.children[x]["portBegin"]),
-            2: lambda x: self.children[x]["proto"], }
-        self.__columnNames = {0: "Address", 1: "Service", 2: "Protocol", }
+        QtCore.QAbstractItemModel.__init__(self, parent)
+
+        self.__column_names = {0: "Address", 1: "Service", 2: "Protocol", }
         self.__services = list()
-        pattern = re.compile("(?P<name>[a-z][a-z0-9-]+)\s+(?P<port>[0-9]+)/(?P<protocol>[a-z]+)", re.IGNORECASE)
-        f = open('/etc/services', 'r')
-        for line in f:
-            match = pattern.match(line)
-            if match is not None:
-                service = match.groupdict()
-                self.__services.append( (service["name"], int(service["port"]),) )
-        f.close()
+        pattern = re.compile(
+            r"(?P<name>[a-z][a-z0-9-]+)\s+(?P<port>[0-9]+)/"
+            r"(?P<protocol>[a-z]+)",
+            re.IGNORECASE)
+        with open('/etc/services', 'r') as file:
+            for line in file:
+                match = pattern.match(line)
+                if match is not None:
+                    service = match.groupdict()
+                    self.__services.append(
+                        (service["name"], int(service["port"]),))
 
         self.fw_changed = False
+        self.allow = None # is the default policy allow or deny
+        self.temp_full_access_expire_time = None # temporary full access time
+        self.__vm = None # VM that the model applies to
+        self.__children = None # list of rules in the FW
 
     def sort(self, idx, order):
-        from operator import attrgetter
-
-        rev = (order == Qt.AscendingOrder)
-        if idx==0:
-            self.children.sort(key=lambda x: x['address'], reverse = rev)
-        if idx==1:
-            self.children.sort(key=lambda x: self.get_service_name(x[
-                "portBegin"]) if x["portEnd"] == None else x["portBegin"],
-                               reverse = rev)
-        if idx==2:
-            self.children.sort(key=lambda x: x['proto'], reverse
-            = rev)
+        rev = (order == QtCore.Qt.AscendingOrder)
+        self.children.sort(key=lambda x: self.get_column_string(idx, x),
+                           reverse=rev)
+
         index1 = self.createIndex(0, 0)
-        index2 = self.createIndex(len(self)-1, len(self.__columnValues)-1)
+        index2 = self.createIndex(len(self) - 1, len(self.__column_names) - 1)
         self.dataChanged.emit(index1, index2)
 
 
     def get_service_name(self, port):
         for service in self.__services:
-            if service[1] == port:
+            if str(service[1]) == str(port):
                 return service[0]
         return str(port)
 
@@ -197,256 +184,292 @@ class QubesFirewallRulesModel(QAbstractItemModel):
                 return service[1]
         return None
 
-    def get_column_string(self, col, row):
-        return self.__columnValues[col](row)
-
-
-    def rule_to_dict(self, rule):
-        if rule.dsthost is None:
-            raise FirewallModifiedOutsideError('no dsthost')
-
-        d = {}
-
-        if not rule.proto:
-            d['proto'] = 'any'
-            d['portBegin'] = 'any'
-            d['portEnd'] = None
-
-        else:
-            d['proto'] = rule.proto
+    def get_column_string(self, col, rule):
+        # pylint: disable=too-many-return-statements
+        # Address
+        if col == 0:
+            if rule.dsthost is None:
+                return "*"
+            if rule.dsthost.type == 'dst4' and rule.dsthost.prefixlen == '32':
+                return str(rule.dsthost)[:-3]
+            if rule.dsthost.type == 'dst6' and rule.dsthost.prefixlen == '128':
+                return str(rule.dsthost)[:-4]
+            return str(rule.dsthost)
+
+        # Service
+        if col == 1:
             if rule.dstports is None:
-                raise FirewallModifiedOutsideError('no dstport')
-            d['portBegin'] = rule.dstports.range[0]
-            d['portEnd'] = rule.dstports.range[1] \
-                if rule.dstports.range[0] != rule.dstports.range[1] \
-                else None
-
-        if rule.dsthost.type == 'dsthost':
-            d['address'] = str(rule.dsthost)
-            d['netmask'] = 32
-        elif rule.dsthost.type == 'dst4':
-            network = ipaddress.IPv4Network(rule.dsthost)
-            d['address'] = str(network.network_address)
-            d['netmask'] = int(network.prefixlen)
-        else:
-            raise FirewallModifiedOutsideError(
-                'cannot map dsthost.type={!s}'.format(rule.dsthost))
-
-        if rule.expire is not None:
-            d['expire'] = int(rule.expire)
-
-        return d
-
-    def get_firewall_conf(self, vm):
+                return "any"
+            if rule.dstports.range[0] != rule.dstports.range[1]:
+                return str(rule.dstports)
+            return self.get_service_name(rule.dstports)
+
+        # Protocol
+        if col == 2:
+            if rule.proto is None:
+                return "any"
+            return str(rule.proto)
+        return "unknown"
+
+    @staticmethod
+    def get_firewall_conf(vm):
         conf = {
             'allow': None,
-            'allowDns': False,
-            'allowIcmp': False,
-            'allowYumProxy': False,
+            'expire': 0,
             'rules': [],
         }
 
+        allow_dns = False
+        allow_icmp = False
         common_action = None
-        tentative_action = None
 
         reversed_rules = list(reversed(vm.firewall.rules))
+        last_rule = reversed_rules.pop(0)
+
+        if last_rule == qubesadmin.firewall.Rule('action=accept') \
+                or last_rule == qubesadmin.firewall.Rule('action=drop'):
+            common_action = last_rule.action
+        else:
+            FirewallModifiedOutsideError('Last rule must be either '
+                                         'drop all or accept all.')
 
+        dns_rule = qubesadmin.firewall.Rule(None,
+                                        action='accept', specialtarget='dns')
+        icmp_rule = qubesadmin.firewall.Rule(None,
+                                        action='accept', proto='icmp')
         while reversed_rules:
-            rule = reversed_rules[0]
-            if rule.dsthost is not None or rule.proto is not None:
-                break
-            tentative_action = reversed_rules.pop(0).action
-
-        if not reversed_rules:
-            conf['allow'] = tentative_action == 'accept'
-            return conf
-
-        for rule in reversed_rules:
-            if rule.specialtarget == 'dns':
-                conf['allowDns'] = (rule.action == 'accept')
+            rule = reversed_rules.pop(0)
+
+            if rule == dns_rule:
+                allow_dns = True
                 continue
 
-            if rule.proto == 'icmp':
-                if rule.icmptype is not None:
-                    raise FirewallModifiedOutsideError(
-                        'cannot map icmptype != None')
-                conf['allowIcmp'] = (rule.action == 'accept')
+            if rule == icmp_rule:
+                allow_icmp = True
                 continue
 
-            if common_action is None:
-                common_action = rule.action
-            elif common_action != rule.action:
-                raise FirewallModifiedOutsideError('incoherent action')
+            if rule.specialtarget is not None or rule.icmptype is not None:
+                raise FirewallModifiedOutsideError("Rule type unknown!")
 
-            conf['rules'].insert(0, self.rule_to_dict(rule))
+            if (rule.dsthost is not None or rule.proto is not None) \
+                    and rule.expire is None:
+                if rule.action == 'accept':
+                    conf['rules'].insert(0, rule)
+                    continue
+                else:
+                    raise FirewallModifiedOutsideError('No blacklist support.')
 
-        if common_action is None or common_action != tentative_action:
-            # we've got only specialtarget and/or icmp
-            conf['allow'] = tentative_action == 'accept'
-            return conf
+            if rule.expire is not None and rule.dsthost is None \
+                    and rule.proto is None:
+                conf['expire'] = int(str(rule.expire))
+                continue
 
-        raise FirewallModifiedOutsideError('it does not add up')
+            raise FirewallModifiedOutsideError('it does not add up.')
 
-    def write_firewall_conf(self, vm, conf):
-        common_action = qubesadmin.firewall.Action(
-            'drop' if conf['allow'] else 'accept')
+        conf['allow'] = (common_action == 'accept')
 
-        rules = []
+        if not allow_icmp and not conf['allow']:
+            raise FirewallModifiedOutsideError('ICMP must be allowed.')
 
-        for rule in conf['rules']:
-            kwargs = {}
-            if rule['proto'] != 'any':
-                kwargs['proto'] = rule['proto']
-                if rule['portBegin'] != 'any':
-                    kwargs['dstports'] = '-'.join(map(str, filter((lambda x: x),
-                        (rule['portBegin'], rule['portEnd']))))
+        if not allow_dns and not conf['allow']:
+            raise FirewallModifiedOutsideError('DNS must be allowed')
 
-            netmask = str(rule['netmask']) if rule['netmask'] != 32 else None
+        return conf
 
-            rules.append(qubesadmin.firewall.Rule(None,
-                action=common_action,
-                dsthost='/'.join(map(str, filter((lambda x: x),
-                    (rule['address'], netmask)))),
-                **kwargs))
+    @staticmethod
+    def write_firewall_conf(vm, conf):
+        rules = []
+
+        for rule in conf['rules']:
+            rules.append(rule)
 
-        if conf['allowDns']:
+        if not conf['allow']:
             rules.append(qubesadmin.firewall.Rule(None,
                 action='accept', specialtarget='dns'))
 
-        if conf['allowIcmp']:
+        if not conf['allow']:
             rules.append(qubesadmin.firewall.Rule(None,
                 action='accept', proto='icmp'))
 
-        if common_action == 'drop':
+        if conf['allow']:
             rules.append(qubesadmin.firewall.Rule(None,
                 action='accept'))
+        else:
+            rules.append(qubesadmin.firewall.Rule(None,
+                action='drop'))
 
         vm.firewall.rules = rules
 
     def set_vm(self, vm):
         self.__vm = vm
 
-        self.clearChildren()
+        self.clear_children()
 
         conf = self.get_firewall_conf(vm)
 
         self.allow = conf["allow"]
-        self.allowDns = conf["allowDns"]
-        self.allowIcmp = conf["allowIcmp"]
-        self.allowYumProxy = conf["allowYumProxy"]
-        self.tempFullAccessExpireTime = 0
+
+        self.temp_full_access_expire_time = conf['expire']
 
         for rule in conf["rules"]:
-            self.appendChild(rule)
-            if "expire" in rule and rule["address"] == "0.0.0.0":
-                self.tempFullAccessExpireTime = rule["expire"]
+            self.append_child(rule)
 
     def get_vm_name(self):
         return self.__vm.name
 
-    def apply_rules(self, allow, dns, icmp, yumproxy, tempFullAccess=False,
-                    tempFullAccessTime=None):
+    def apply_rules(self, allow, temp_full_access=False,
+                    temp_full_access_time=None):
         assert self.__vm is not None
 
-        if self.allow != allow or self.allowDns != dns or \
-                self.allowIcmp != icmp or self.allowYumProxy != yumproxy or \
-                (self.tempFullAccessExpireTime != 0) != tempFullAccess:
+        if self.allow != allow or \
+                (self.temp_full_access_expire_time != 0) != temp_full_access:
             self.fw_changed = True
 
-        conf = { "allow": allow,
-                "allowDns": dns,
-                "allowIcmp": icmp,
-                "allowYumProxy": yumproxy,
+        conf = {"allow": allow,
                 "rules": list()
             }
 
-        for rule in self.children:
-            if "expire" in rule and rule["address"] == "0.0.0.0" and \
-                    rule["netmask"] == 0 and rule["proto"] == "any":
-                # rule already present, update its time
-                if tempFullAccess:
-                    rule["expire"] = \
-                        int(datetime.datetime.now().strftime("%s")) + \
-                        tempFullAccessTime*60
-                tempFullAccess = False
-            conf["rules"].append(rule)
-
-        if tempFullAccess and not allow:
-            conf["rules"].append({"address": "0.0.0.0",
-                                  "netmask": 0,
-                                  "proto": "any",
-                                  "expire": int(
-                                      datetime.datetime.now().strftime("%s"))+\
-                                        tempFullAccessTime*60
-                                  })
+        conf['rules'].extend(self.children)
+
+        if temp_full_access and not allow:
+            conf["rules"].append(qubesadmin.firewall.Rule(
+                None,
+                action='accept',
+                expire=int(datetime.datetime.now().strftime("%s")) +
+                       temp_full_access_time * 60))
 
         if self.fw_changed:
             self.write_firewall_conf(self.__vm, conf)
 
-    def index(self, row, column, parent=QModelIndex()):
+    def populate_edit_dialog(self, dialog, row):
+        address = self.get_column_string(0, self.children[row])
+        dialog.addressComboBox.setItemText(0, address)
+        dialog.addressComboBox.setCurrentIndex(0)
+        service = self.get_column_string(1, self.children[row])
+        if service == "any":
+            service = ""
+        dialog.serviceComboBox.setItemText(0, service)
+        dialog.serviceComboBox.setCurrentIndex(0)
+        protocol = self.get_column_string(2, self.children[row])
+        if protocol == "tcp":
+            dialog.tcp_radio.setChecked(True)
+        elif protocol == "udp":
+            dialog.udp_radio.setChecked(True)
+        else:
+            dialog.any_radio.setChecked(True)
+
+    def run_rule_dialog(self, dialog, row=None):
+        if dialog.exec_():
+
+            address = str(dialog.addressComboBox.currentText())
+            service = str(dialog.serviceComboBox.currentText())
+
+            rule = qubesadmin.firewall.Rule(None, action='accept')
+
+            if address is not None and address != "*":
+                try:
+                    rule.dsthost = address
+                except ValueError:
+                    QtGui.QMessageBox.warning(None, self.tr("Invalid address"),
+                        self.tr("Address '{0}' is invalid.").format(address))
+
+            if dialog.tcp_radio.isChecked():
+                rule.proto = 'tcp'
+            elif dialog.udp_radio.isChecked():
+                rule.proto = 'udp'
+
+            if '-' in service:
+                try:
+                    rule.dstports = service
+                except ValueError:
+                    QtGui.QMessageBox.warning(
+                        None,
+                        self.tr("Invalid port or service"),
+                        self.tr("Port number or service '{0}' is invalid.")
+                                        .format(service))
+            elif service:
+                try:
+                    rule.dstports = service
+                except (TypeError, ValueError):
+                    if self.get_service_port(service) is not None:
+                        rule.dstports = self.get_service_port(service)
+                    else:
+                        QtGui.QMessageBox.warning(None,
+                            self.tr("Invalid port or service"),
+                            self.tr("Port number or service '{0}' is invalid.")
+                                            .format(service))
+
+            if row is not None:
+                self.set_child(row, rule)
+            else:
+                self.append_child(rule)
+
+    def index(self, row, column, parent=QtCore.QModelIndex()):
         if not self.hasIndex(row, column, parent):
-            return QModelIndex()
+            return QtCore.QModelIndex()
 
         return self.createIndex(row, column, self.children[row])
 
-    def parent(self, child):
-        return QModelIndex()
+    def parent(self, child): # pylint: disable=unused-argument,no-self-use
+        return QtCore.QModelIndex()
 
-    def rowCount(self, parent=QModelIndex()):
+    # pylint: disable=invalid-name,unused-argument
+    def rowCount(self, parent=QtCore.QModelIndex()):
         return len(self)
 
-    def columnCount(self, parent=QModelIndex()):
-        return len(self.__columnValues)
+    # pylint: disable=invalid-name,unused-argument
+    def columnCount(self, parent=QtCore.QModelIndex()):
+        return len(self.__column_names)
 
-    def hasChildren(self, index=QModelIndex()):
-        parentItem = index.internalPointer()
-        if parentItem is not None:
-            return False
-        else:
-            return True
+    # pylint: disable=invalid-name,no-self-use
+    def hasChildren(self, index=QtCore.QModelIndex()):
+        parent_item = index.internalPointer()
+        return parent_item is None
 
-    def data(self, index, role=Qt.DisplayRole):
-        if index.isValid() and role == Qt.DisplayRole:
-            return self.__columnValues[index.column()](index.row())
+    def data(self, index, role=QtCore.Qt.DisplayRole):
+        if index.isValid() and role == QtCore.Qt.DisplayRole:
+            return self.get_column_string(index.column(),
+                                          self.children[index.row()])
 
-    def headerData(self, section, orientation, role=Qt.DisplayRole):
-        if section < len(self.__columnNames) \
-                and orientation == Qt.Horizontal and role == Qt.DisplayRole:
-                    return self.__columnNames[section]
+    # pylint: disable=invalid-name
+    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
+        if section < len(self.__column_names) \
+                and orientation == QtCore.Qt.Horizontal \
+                and role == QtCore.Qt.DisplayRole:
+            return self.__column_names[section]
 
     @property
     def children(self):
         return self.__children
 
-    def appendChild(self, child):
+    def append_child(self, child):
         row = len(self)
-        self.beginInsertRows(QModelIndex(), row, row)
+        self.beginInsertRows(QtCore.QModelIndex(), row, row)
         self.children.append(child)
         self.endInsertRows()
         index = self.createIndex(row, 0, child)
         self.dataChanged.emit(index, index)
         self.fw_changed = True
 
-    def removeChild(self, i):
+    def remove_child(self, i):
         if i >= len(self):
             return
 
-        self.beginRemoveRows(QModelIndex(), i, i)
+        self.beginRemoveRows(QtCore.QModelIndex(), i, i)
         del self.children[i]
         self.endRemoveRows()
         index = self.createIndex(i, 0)
         self.dataChanged.emit(index, index)
         self.fw_changed = True
 
-    def setChild(self, i, child):
+    def set_child(self, i, child):
         self.children[i] = child
         index = self.createIndex(i, 0, child)
         self.dataChanged.emit(index, index)
         self.fw_changed = True
 
-    def clearChildren(self):
+    def clear_children(self):
         self.__children = list()
 
     def __len__(self):
         return len(self.children)
-

+ 75 - 62
qubesmanager/global_settings.py

@@ -15,28 +15,29 @@
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
 #
 #
 
 import sys
 import os
-from PyQt4.QtCore import *
-from PyQt4.QtGui import *
+import os.path
+import traceback
+from PyQt4 import QtCore, QtGui  # pylint: disable=import-error
 
 from qubesadmin import Qubes
+from qubesadmin.utils import parse_size, updates_vms_status
 
-from .ui_globalsettingsdlg import *
+from . import ui_globalsettingsdlg  # pylint: disable=no-name-in-module
 
 from configparser import ConfigParser
-from qubesadmin.utils import parse_size, updates_vms_status
 
 qmemman_config_path = '/etc/qubes/qmemman.conf'
 
 
-class GlobalSettingsWindow(Ui_GlobalSettings, QDialog):
+class GlobalSettingsWindow(ui_globalsettingsdlg.Ui_GlobalSettings,
+                           QtGui.QDialog):
 
     def __init__(self, app, qvm_collection, parent=None):
         super(GlobalSettingsWindow, self).__init__(parent)
@@ -45,10 +46,13 @@ class GlobalSettingsWindow(Ui_GlobalSettings, QDialog):
         self.qvm_collection = qvm_collection
 
         self.setupUi(self)
- 
-        self.connect(self.buttonBox, SIGNAL("accepted()"), self.save_and_apply)
-        self.connect(self.buttonBox, SIGNAL("rejected()"), self.reject)
-       
+
+        self.connect(
+            self.buttonBox,
+            QtCore.SIGNAL("accepted()"),
+            self.save_and_apply)
+        self.connect(self.buttonBox, QtCore.SIGNAL("rejected()"), self.reject)
+
         self.__init_system_defaults__()
         self.__init_kernel_defaults__()
         self.__init_mem_defaults__()
@@ -89,7 +93,8 @@ class GlobalSettingsWindow(Ui_GlobalSettings, QDialog):
         self.clock_vm_combo.setCurrentIndex(self.clockvm_idx)
 
         # default netvm
-        netvms = [vm for vm in all_vms if getattr(vm, 'provides_network', False)]
+        netvms = [vm for vm in all_vms
+                  if getattr(vm, 'provides_network', False)]
         self.netvm_idx = -1
 
         current_netvm = self.qvm_collection.default_netvm
@@ -122,7 +127,7 @@ class GlobalSettingsWindow(Ui_GlobalSettings, QDialog):
             updatevm_name = str(self.update_vm_combo.currentText())
             updatevm_name = updatevm_name.split(' ')[0]
             updatevm = self.qvm_collection.domains[updatevm_name]
-            
+
             self.qvm_collection.updatevm = updatevm
 
         #clockvm
@@ -130,7 +135,7 @@ class GlobalSettingsWindow(Ui_GlobalSettings, QDialog):
             clockvm_name = str(self.clock_vm_combo.currentText())
             clockvm_name = clockvm_name.split(' ')[0]
             clockvm = self.qvm_collection.domains[clockvm_name]
-            
+
             self.qvm_collection.clockvm = clockvm
 
         #default netvm
@@ -138,7 +143,7 @@ class GlobalSettingsWindow(Ui_GlobalSettings, QDialog):
             name = str(self.default_netvm_combo.currentText())
             name = name.split(' ')[0]
             vm = self.qvm_collection.domains[name]
-            
+
             self.qvm_collection.default_netvm = vm
 
         #default template
@@ -146,13 +151,14 @@ class GlobalSettingsWindow(Ui_GlobalSettings, QDialog):
             name = str(self.default_template_combo.currentText())
             name = name.split(' ')[0]
             vm = self.qvm_collection.domains[name]
-            
+
             self.qvm_collection.default_template = vm
 
 
     def __init_kernel_defaults__(self):
         kernel_list = []
-        # TODO system_path["qubes_kernels_base_dir"] idea: qubes.pulls['linux-kernel'].volumes
+        # TODO system_path["qubes_kernels_base_dir"]
+        # idea: qubes.pools['linux-kernel'].volumes
         for k in os.listdir('/var/lib/qubes/vm-kernels'):
             kernel_list.append(k)
 
@@ -170,21 +176,22 @@ class GlobalSettingsWindow(Ui_GlobalSettings, QDialog):
         if self.default_kernel_combo.currentIndex() != self.kernel_idx:
             kernel = str(self.default_kernel_combo.currentText())
             kernel = kernel.split(' ')[0]
-            
+
             self.qvm_collection.default_kernel = kernel
 
-        
-    def __init_mem_defaults__(self):
 
+    def __init_mem_defaults__(self):
         #qmemman settings
         self.qmemman_config = ConfigParser()
-        self.vm_min_mem_val =  '200MiB'  #str(qmemman_algo.MIN_PREFMEM)
+        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)
         if self.qmemman_config.has_section('global'):
-            self.vm_min_mem_val = self.qmemman_config.get('global', 'vm-min-mem')
-            self.dom0_mem_boost_val = self.qmemman_config.get('global', 'dom0-mem-boost')
+            self.vm_min_mem_val = \
+                self.qmemman_config.get('global', 'vm-min-mem')
+            self.dom0_mem_boost_val = \
+                self.qmemman_config.get('global', 'dom0-mem-boost')
 
         self.vm_min_mem_val = parse_size(self.vm_min_mem_val)
         self.dom0_mem_boost_val = parse_size(self.dom0_mem_boost_val)
@@ -199,7 +206,8 @@ class GlobalSettingsWindow(Ui_GlobalSettings, QDialog):
         current_min_vm_mem = self.min_vm_mem.value()
         current_dom0_mem_boost = self.dom0_mem_boost.value()
 
-        if current_min_vm_mem*1024*1024 != self.vm_min_mem_val or current_dom0_mem_boost*1024*1024 != self.dom0_mem_boost_val:
+        if current_min_vm_mem*1024*1024 != self.vm_min_mem_val \
+                or current_dom0_mem_boost*1024*1024 != self.dom0_mem_boost_val:
 
             current_min_vm_mem = str(current_min_vm_mem)+'M'
             current_dom0_mem_boost = str(current_dom0_mem_boost)+'M'
@@ -207,38 +215,46 @@ class GlobalSettingsWindow(Ui_GlobalSettings, QDialog):
             if not self.qmemman_config.has_section('global'):
                 #add the whole section
                 self.qmemman_config.add_section('global')
-                self.qmemman_config.set('global', 'vm-min-mem', current_min_vm_mem)
-                self.qmemman_config.set('global', 'dom0-mem-boost', current_dom0_mem_boost)
-                self.qmemman_config.set('global', 'cache-margin-factor', str(1.3)) # removed qmemman_algo.CACHE_FACTOR
+                self.qmemman_config.set(
+                    'global', 'vm-min-mem', current_min_vm_mem)
+                self.qmemman_config.set(
+                    'global', 'dom0-mem-boost', current_dom0_mem_boost)
+                self.qmemman_config.set(
+                    'global', 'cache-margin-factor', str(1.3))
+                # removed qmemman_algo.CACHE_FACTOR
 
                 qmemman_config_file = open(qmemman_config_path, 'a')
                 self.qmemman_config.write(qmemman_config_file)
                 qmemman_config_file.close()
 
             else:
-                #If there already is a 'global' section, we don't use SafeConfigParser.write() - it would get rid of all the comments...
-                
+                #If there already is a 'global' section, we don't use
+                # SafeConfigParser.write() - it would get rid of
+                # all the comments...
+
                 lines_to_add = {}
-                lines_to_add['vm-min-mem'] = "vm-min-mem = " + current_min_vm_mem + "\n"
-                lines_to_add['dom0-mem-boost'] = "dom0-mem-boost = " + current_dom0_mem_boost +"\n"
+                lines_to_add['vm-min-mem'] = \
+                    "vm-min-mem = " + current_min_vm_mem + "\n"
+                lines_to_add['dom0-mem-boost'] = \
+                    "dom0-mem-boost = " + current_dom0_mem_boost +"\n"
 
                 config_lines = []
 
                 qmemman_config_file = open(qmemman_config_path, 'r')
-                for l in qmemman_config_file:
-                    if l.strip().startswith('vm-min-mem'):
+                for line in qmemman_config_file:
+                    if line.strip().startswith('vm-min-mem'):
                         config_lines.append(lines_to_add['vm-min-mem'])
                         del lines_to_add['vm-min-mem']
-                    elif l.strip().startswith('dom0-mem-boost'):
+                    elif line.strip().startswith('dom0-mem-boost'):
                         config_lines.append(lines_to_add['dom0-mem-boost'])
                         del lines_to_add['dom0-mem-boost']
                     else:
-                        config_lines.append(l)
-                
+                        config_lines.append(line)
+
                 qmemman_config_file.close()
 
-                for l in lines_to_add:
-                    config_lines.append(l)
+                for line in lines_to_add:
+                    config_lines.append(line)
 
                 qmemman_config_file = open(qmemman_config_path, 'w')
                 qmemman_config_file.writelines(config_lines)
@@ -252,24 +268,26 @@ class GlobalSettingsWindow(Ui_GlobalSettings, QDialog):
         self.updates_dom0.setChecked(self.updates_dom0_val)
         updates_vms = updates_vms_status(self.qvm_collection)
         if updates_vms is None:
-            self.updates_vm.setCheckState(Qt.PartiallyChecked)
+            self.updates_vm.setCheckState(QtCore.Qt.PartiallyChecked)
         else:
             self.updates_vm.setCheckState(updates_vms)
 
     def __apply_updates__(self):
         if self.updates_dom0.isChecked() != self.updates_dom0_val:
-            # TODO updates_dom0_toggle(self.qvm_collection, self.updates_dom0.isChecked())
+            # TODO updates_dom0_toggle(
+            # self.qvm_collection, self.updates_dom0.isChecked())
             raise NotImplementedError('Toggle dom0 updates not implemented')
-        if self.updates_vm.checkState() != Qt.PartiallyChecked:
+        if self.updates_vm.checkState() != QtCore.Qt.PartiallyChecked:
             for vm in self.qvm_collection.domains:
-                vm.features['check-updates'] = bool(self.updates_vm.checkState())
+                vm.features['check-updates'] = \
+                    bool(self.updates_vm.checkState())
 
     def reject(self):
         self.done(0)
 
     def save_and_apply(self):
 
-        self.__apply_system_defaults__()        
+        self.__apply_system_defaults__()
         self.__apply_kernel_defaults__()
         self.__apply_mem_defaults__()
         self.__apply_updates__()
@@ -279,26 +297,22 @@ class GlobalSettingsWindow(Ui_GlobalSettings, QDialog):
 # Bases on the original code by:
 # Copyright (c) 2002-2007 Pascal Varet <p.varet@gmail.com>
 
-def handle_exception( exc_type, exc_value, exc_traceback ):
-    import os.path
-    import traceback
+def handle_exception(exc_type, exc_value, exc_traceback):
+    filename, line, dummy, dummy = traceback.extract_tb(exc_traceback).pop()
+    filename = os.path.basename(filename)
+    error = "%s: %s" % (exc_type.__name__, exc_value)
 
-    filename, line, dummy, dummy = traceback.extract_tb( exc_traceback ).pop()
-    filename = os.path.basename( filename )
-    error    = "%s: %s" % ( exc_type.__name__, exc_value )
-
-    QMessageBox.critical(None, "Houston, we have a problem...",
-                         "Whoops. A critical error has occured. This is most likely a bug "
-                         "in Qubes Global Settings application.<br><br>"
-                         "<b><i>%s</i></b>" % error +
-                         "at <b>line %d</b> of file <b>%s</b>.<br/><br/>"
-                         % ( line, filename ))
+    QtGui.QMessageBox.critical(
+        None,
+        "Houston, we have a problem...",
+        "Whoops. A critical error has occured. This is most likely a bug "
+        "in Qubes Global Settings application.<br><br><b><i>%s</i></b>" %
+        error + "at <b>line %d</b> of file <b>%s</b>.<br/><br/>"
+        % (line, filename))
 
 
 def main():
-
-    global qtapp
-    qtapp = QApplication(sys.argv)
+    qtapp = QtGui.QApplication(sys.argv)
     qtapp.setOrganizationName("The Qubes Project")
     qtapp.setOrganizationDomain("http://qubes-os.org")
     qtapp.setApplicationName("Qubes Global Settings")
@@ -307,7 +321,6 @@ def main():
 
     app = Qubes()
 
-    global global_window
     global_window = GlobalSettingsWindow(qtapp, app)
 
     global_window.show()

+ 11 - 10
qubesmanager/informationnotes.py

@@ -1,4 +1,4 @@
-#!/usr/bin/python2
+#!/usr/bin/python3
 # coding=utf-8
 #
 # The Qubes OS Project, http://www.qubes-os.org
@@ -16,22 +16,23 @@
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
 #
 #
-from PyQt4.QtCore import SIGNAL
-from PyQt4.QtGui import QDialog, QIcon
+from PyQt4.QtGui import QDialog  # pylint: disable=import-error
 
-from .ui_informationnotes import *
+from . import ui_informationnotes  # pylint: disable=no-name-in-module
 import subprocess
 
 
-class InformationNotesDialog(Ui_InformationNotesDialog, QDialog):
+class InformationNotesDialog(ui_informationnotes.Ui_InformationNotesDialog,
+                             QDialog):
+    # pylint: disable=too-few-public-methods
     def __init__(self):
         super(InformationNotesDialog, self).__init__()
 
         self.setupUi(self)
-        details = subprocess.check_output(['/usr/libexec/qubes-manager/qvm_about.sh'])
-        self.informationNotes.setText(details)
+        details = subprocess.check_output(
+            ['/usr/libexec/qubes-manager/qvm_about.sh'])
+        self.informationNotes.setText(details.decode())

+ 20 - 22
qubesmanager/log_dialog.py

@@ -1,4 +1,4 @@
-#!/usr/bin/python2
+#!/usr/bin/python3
 #
 # The Qubes OS Project, http://www.qubes-os.org
 #
@@ -15,28 +15,24 @@
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
 #
 #
 
-import sys
-import os
-from PyQt4.QtCore import *
-from PyQt4.QtGui import *
-
-from qubes.qubes import QubesException
+from PyQt4 import QtCore  # pylint: disable=import-error
+from PyQt4 import QtGui  # pylint: disable=import-error
 
-import qubesmanager.resources_rc
-
-from .ui_logdlg import *
-from .clipboard import *
+from . import ui_logdlg   # pylint: disable=no-name-in-module
+from . import clipboard
+import os
 
 # Display only this size of log
 LOG_DISPLAY_SIZE = 1024*1024
 
-class LogDialog(Ui_LogDialog, QDialog):
+
+class LogDialog(ui_logdlg.Ui_LogDialog, QtGui.QDialog):
+    # pylint: disable=too-few-public-methods
 
     def __init__(self, app, log_path, parent=None):
         super(LogDialog, self).__init__(parent)
@@ -46,9 +42,11 @@ class LogDialog(Ui_LogDialog, QDialog):
 
         self.setupUi(self)
         self.setWindowTitle(log_path)
- 
-        self.connect(self.copy_to_qubes_clipboard, SIGNAL("clicked()"), self.copy_to_qubes_clipboard_triggered)
-       
+
+        self.connect(self.copy_to_qubes_clipboard,
+                     QtCore.SIGNAL("clicked()"),
+                     self.copy_to_clipboard_triggered)
+
         self.__init_log_text__()
 
     def __init_log_text__(self):
@@ -56,7 +54,8 @@ class LogDialog(Ui_LogDialog, QDialog):
         log = open(self.log_path)
         log.seek(0, os.SEEK_END)
         if log.tell() > LOG_DISPLAY_SIZE:
-            self.displayed_text = self.tr("(Showing only last %d bytes of file)\n") % LOG_DISPLAY_SIZE
+            self.displayed_text = self.tr(
+                "(Showing only last %d bytes of file)\n") % LOG_DISPLAY_SIZE
             log.seek(-LOG_DISPLAY_SIZE, os.SEEK_END)
         else:
             log.seek(0, os.SEEK_SET)
@@ -64,6 +63,5 @@ class LogDialog(Ui_LogDialog, QDialog):
         log.close()
         self.log_text.setPlainText(self.displayed_text)
 
-
-    def copy_to_qubes_clipboard_triggered(self):
-        copy_text_to_qubes_clipboard(self.displayed_text)
+    def copy_to_clipboard_triggered(self):
+        clipboard.copy_text_to_qubes_clipboard(self.displayed_text)

+ 21 - 21
qubesmanager/multiselectwidget.py

@@ -1,46 +1,47 @@
-import sys
-from PyQt4.QtCore import *
-from PyQt4.QtGui import *
-from .ui_multiselectwidget import *
+from PyQt4 import QtCore, QtGui  # pylint: disable=import-error
+from . import ui_multiselectwidget # pylint: disable=no-name-in-module
 
-class MultiSelectWidget(Ui_MultiSelectWidget, QWidget):
+class MultiSelectWidget(
+    ui_multiselectwidget.Ui_MultiSelectWidget, QtGui.QWidget):
 
     __pyqtSignals__ = ("selected_changed()",)
     __pyqtSignals__ = ("items_added(PyQt_PyObject)",)
     __pyqtSignals__ = ("items_removed(PyQt_PyObject)",)
 
     def __init__(self, parent=None):
-        super(MultiSelectWidget, self).__init__()
+        super(MultiSelectWidget, self).__init__(parent)
         self.setupUi(self)
         self.add_selected_button.clicked.connect(self.add_selected)
         self.add_all_button.clicked.connect(self.add_all)
         self.remove_selected_button.clicked.connect(self.remove_selected)
         self.remove_all_button.clicked.connect(self.remove_all)
-        self.available_list.setSelectionMode(QAbstractItemView.ExtendedSelection)
-        self.selected_list.setSelectionMode(QAbstractItemView.ExtendedSelection)
+        self.available_list.setSelectionMode(
+            QtGui.QAbstractItemView.ExtendedSelection)
+        self.selected_list.setSelectionMode(
+            QtGui.QAbstractItemView.ExtendedSelection)
 
     def switch_selected(self, src, dst):
         selected = src.selectedItems()
         items = []
 
-        for s in selected:
-            row = src.indexFromItem(s).row()
+        for selected_item in selected:
+            row = src.indexFromItem(selected_item).row()
             item = src.takeItem(row)
             dst.addItem(item)
             items.append(item)
         dst.sortItems()
-        self.emit(SIGNAL("selected_changed()"))
-        if src is self.selected_list:    
-            self.emit(SIGNAL("items_removed(PyQt_PyObject)"), items)
+        self.emit(QtCore.SIGNAL("selected_changed()"))
+        if src is self.selected_list:
+            self.emit(QtCore.SIGNAL("items_removed(PyQt_PyObject)"), items)
         else:
-            self.emit(SIGNAL("items_added(PyQt_PyObject)"), items)
+            self.emit(QtCore.SIGNAL("items_added(PyQt_PyObject)"), items)
 
     def add_selected(self):
         self.switch_selected(self.available_list, self.selected_list)
 
     def remove_selected(self):
-        self.switch_selected(self.selected_list, self.available_list)    
-   
+        self.switch_selected(self.selected_list, self.available_list)
+
     def move_all(self, src, dst):
         items = []
         while src.count() > 0:
@@ -48,11 +49,11 @@ class MultiSelectWidget(Ui_MultiSelectWidget, QWidget):
             dst.addItem(item)
             items.append(item)
         dst.sortItems()
-        self.emit(SIGNAL("selected_changed()"))
-        if src is self.selected_list:    
-            self.emit(SIGNAL("items_removed(PyQt_PyObject)"), items)
+        self.emit(QtCore.SIGNAL("selected_changed()"))
+        if src is self.selected_list:
+            self.emit(QtCore.SIGNAL("items_removed(PyQt_PyObject)"), items)
         else:
-            self.emit(SIGNAL("items_added(PyQt_PyObject)"), items)
+            self.emit(QtCore.SIGNAL("items_added(PyQt_PyObject)"), items)
 
 
     def add_all(self):
@@ -64,4 +65,3 @@ class MultiSelectWidget(Ui_MultiSelectWidget, QWidget):
     def clear(self):
         self.available_list.clear()
         self.selected_list.clear()
-

+ 1188 - 0
qubesmanager/qube_manager.py

@@ -0,0 +1,1188 @@
+#!/usr/bin/python3
+#
+# The Qubes OS Project, http://www.qubes-os.org
+#
+# Copyright (C) 2012  Agnieszka Kostrzewa <agnieszka.kostrzewa@gmail.com>
+# Copyright (C) 2012  Marek Marczykowski-Górecki
+#                       <marmarek@invisiblethingslab.com>
+# Copyright (C) 2017  Wojtek Porczyk <woju@invisiblethingslab.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import sys
+import os
+import os.path
+import subprocess
+import time
+from datetime import datetime, timedelta
+import traceback
+
+from qubesadmin import Qubes
+from qubesadmin import exc
+
+from PyQt4 import QtGui  # pylint: disable=import-error
+from PyQt4 import QtCore  # pylint: disable=import-error
+
+from . import ui_qubemanager  # pylint: disable=no-name-in-module
+from . import thread_monitor
+from . import table_widgets
+from . import settings
+from . import global_settings
+from . import restore
+from . import backup
+from . import log_dialog
+import threading
+
+from qubesmanager.about import AboutDialog
+
+
+class SearchBox(QtGui.QLineEdit):
+    def __init__(self, parent=None):
+        super(SearchBox, self).__init__(parent)
+        self.focusing = False
+
+    def focusInEvent(self, e):  # pylint: disable=invalid-name
+        super(SearchBox, self).focusInEvent(e)
+        self.selectAll()
+        self.focusing = True
+
+    def mousePressEvent(self, e):  # pylint: disable=invalid-name
+        super(SearchBox, self).mousePressEvent(e)
+        if self.focusing:
+            self.selectAll()
+            self.focusing = False
+
+
+class VmRowInTable(object):
+    # pylint: disable=too-few-public-methods
+
+    def __init__(self, vm, row_no, table):
+        self.vm = vm
+        self.row_no = row_no
+        # TODO: replace a various different widgets with a more generic
+        # VmFeatureWidget or VMPropertyWidget
+
+        table_widgets.row_height = VmManagerWindow.row_height
+        table.setRowHeight(row_no, VmManagerWindow.row_height)
+
+        self.type_widget = table_widgets.VmTypeWidget(vm)
+        table.setCellWidget(row_no, VmManagerWindow.columns_indices['Type'],
+                            self.type_widget)
+        table.setItem(row_no, VmManagerWindow.columns_indices['Type'],
+                      self.type_widget.table_item)
+
+        self.label_widget = table_widgets.VmLabelWidget(vm)
+        table.setCellWidget(row_no, VmManagerWindow.columns_indices['Label'],
+                            self.label_widget)
+        table.setItem(row_no, VmManagerWindow.columns_indices['Label'],
+                      self.label_widget.table_item)
+
+        self.name_widget = table_widgets.VmNameItem(vm)
+        table.setItem(row_no, VmManagerWindow.columns_indices['Name'],
+                      self.name_widget)
+
+        self.info_widget = table_widgets.VmInfoWidget(vm)
+        table.setCellWidget(row_no, VmManagerWindow.columns_indices['State'],
+                            self.info_widget)
+        table.setItem(row_no, VmManagerWindow.columns_indices['State'],
+                      self.info_widget.table_item)
+
+        self.template_widget = table_widgets.VmTemplateItem(vm)
+        table.setItem(row_no, VmManagerWindow.columns_indices['Template'],
+                      self.template_widget)
+
+        self.netvm_widget = table_widgets.VmNetvmItem(vm)
+        table.setItem(row_no, VmManagerWindow.columns_indices['NetVM'],
+                      self.netvm_widget)
+
+        self.size_widget = table_widgets.VmSizeOnDiskItem(vm)
+        table.setItem(row_no, VmManagerWindow.columns_indices['Size'],
+                      self.size_widget)
+
+        self.internal_widget = table_widgets.VmInternalItem(vm)
+        table.setItem(row_no, VmManagerWindow.columns_indices['Internal'],
+                      self.internal_widget)
+
+        self.ip_widget = table_widgets.VmIPItem(vm)
+        table.setItem(row_no, VmManagerWindow.columns_indices['IP'],
+                      self.ip_widget)
+
+        self.include_in_backups_widget = \
+            table_widgets.VmIncludeInBackupsItem(vm)
+        table.setItem(row_no, VmManagerWindow.columns_indices[
+            'Backups'], self.include_in_backups_widget)
+
+        self.last_backup_widget = table_widgets.VmLastBackupItem(vm)
+        table.setItem(row_no, VmManagerWindow.columns_indices[
+            'Last backup'], self.last_backup_widget)
+
+    def update(self, update_size_on_disk=False):
+        """
+        Update info in a single VM row
+        :param update_size_on_disk: should disk utilization be updated? the
+        widget will extract the data from VM object
+        :return: None
+        """
+        self.info_widget.update_vm_state(self.vm)
+        if update_size_on_disk:
+            self.size_widget.update()
+
+
+vm_shutdown_timeout = 20000  # in msec
+vm_restart_check_timeout = 1000  # in msec
+
+
+class VmShutdownMonitor(QtCore.QObject):
+    def __init__(self, vm, shutdown_time=vm_shutdown_timeout,
+                 check_time=vm_restart_check_timeout,
+                 and_restart=False, caller=None):
+        QtCore.QObject.__init__(self)
+        self.vm = vm
+        self.shutdown_time = shutdown_time
+        self.check_time = check_time
+        self.and_restart = and_restart
+        self.shutdown_started = datetime.now()
+        self.caller = caller
+
+    def restart_vm_if_needed(self):
+        if self.and_restart and self.caller:
+            self.caller.start_vm(self.vm)
+
+    def check_again_later(self):
+        # noinspection PyTypeChecker,PyCallByClass
+        QtCore.QTimer.singleShot(self.check_time, self.check_if_vm_has_shutdown)
+
+    def timeout_reached(self):
+        actual = datetime.now() - self.shutdown_started
+        allowed = timedelta(milliseconds=self.shutdown_time)
+
+        return actual > allowed
+
+    def check_if_vm_has_shutdown(self):
+        vm = self.vm
+        vm_is_running = vm.is_running()
+        try:
+            vm_start_time = datetime.fromtimestamp(float(vm.start_time))
+        except AttributeError:
+            vm_start_time = None
+
+        if vm_is_running and vm_start_time \
+                and vm_start_time < self.shutdown_started:
+            if self.timeout_reached():
+                reply = QtGui.QMessageBox.question(
+                    None, self.tr("Qube Shutdown"),
+                    self.tr(
+                        "The Qube <b>'{0}'</b> hasn't shutdown within the last "
+                        "{1} seconds, do you want to kill it?<br>").format(
+                        vm.name, self.shutdown_time / 1000),
+                    self.tr("Kill it!"),
+                    self.tr("Wait another {0} seconds...").format(
+                        self.shutdown_time / 1000))
+                if reply == 0:
+                    vm.force_shutdown()
+                    self.restart_vm_if_needed()
+                else:
+                    self.shutdown_started = datetime.now()
+                    self.check_again_later()
+            else:
+                self.check_again_later()
+        else:
+            if vm_is_running:
+                # Due to unknown reasons, Xen sometimes reports that a domain
+                # is running even though its start-up timestamp is not valid.
+                # Make sure that "restart_vm_if_needed" is not called until
+                # the domain has been completely shut down according to Xen.
+                self.check_again_later()
+                return
+
+            self.restart_vm_if_needed()
+
+
+class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
+    # pylint: disable=too-many-instance-attributes
+    row_height = 30
+    column_width = 200
+    min_visible_rows = 10
+    search = ""
+    # suppress saving settings while initializing widgets
+    settings_loaded = False
+    columns_indices = {"Type": 0,
+                       "Label": 1,
+                       "Name": 2,
+                       "State": 3,
+                       "Template": 4,
+                       "NetVM": 5,
+                       "Size": 6,
+                       "Internal": 7,
+                       "IP": 8,
+                       "Backups": 9,
+                       "Last backup": 10,
+                       }
+
+    def __init__(self, qubes_app, qt_app, parent=None):
+        # pylint: disable=unused-argument
+        super(VmManagerWindow, self).__init__()
+        self.setupUi(self)
+
+        self.manager_settings = QtCore.QSettings(self)
+
+        self.qubes_app = qubes_app
+        self.qt_app = qt_app
+
+        self.searchbox = SearchBox()
+        self.searchbox.setValidator(QtGui.QRegExpValidator(
+            QtCore.QRegExp("[a-zA-Z0-9-]*", QtCore.Qt.CaseInsensitive), None))
+        self.searchContainer.addWidget(self.searchbox)
+
+        self.connect(self.table, QtCore.SIGNAL("itemSelectionChanged()"),
+                     self.table_selection_changed)
+
+        self.table.setColumnWidth(0, self.column_width)
+
+        self.sort_by_column = "Type"
+        self.sort_order = QtCore.Qt.AscendingOrder
+
+        self.vms_list = []
+        self.vms_in_table = {}
+        self.reload_table = False
+
+        self.frame_width = 0
+        self.frame_height = 0
+
+        self.move(self.x(), 0)
+
+        self.columns_actions = {
+            self.columns_indices["Type"]: self.action_vm_type,
+            self.columns_indices["Label"]: self.action_label,
+            self.columns_indices["Name"]: self.action_name,
+            self.columns_indices["State"]: self.action_state,
+            self.columns_indices["Template"]: self.action_template,
+            self.columns_indices["NetVM"]: self.action_netvm,
+            self.columns_indices["Size"]: self.action_size_on_disk,
+            self.columns_indices["Internal"]: self.action_internal,
+            self.columns_indices["IP"]: self
+                .action_ip, self.columns_indices["Backups"]: self
+                .action_backups, self.columns_indices["Last backup"]: self
+            .action_last_backup
+        }
+
+        self.visible_columns_count = len(self.columns_indices)
+
+        self.table.setColumnWidth(self.columns_indices["State"], 80)
+        self.table.setColumnWidth(self.columns_indices["Name"], 150)
+        self.table.setColumnWidth(self.columns_indices["Label"], 40)
+        self.table.setColumnWidth(self.columns_indices["Type"], 40)
+        self.table.setColumnWidth(self.columns_indices["Size"], 100)
+        self.table.setColumnWidth(self.columns_indices["Internal"], 60)
+        self.table.setColumnWidth(self.columns_indices["IP"], 100)
+        self.table.setColumnWidth(self.columns_indices["Backups"], 60)
+        self.table.setColumnWidth(self.columns_indices["Last backup"], 90)
+
+        self.table.horizontalHeader().setResizeMode(QtGui.QHeaderView.Fixed)
+
+        self.table.sortItems(self.columns_indices[self.sort_by_column],
+                             self.sort_order)
+
+        self.context_menu = QtGui.QMenu(self)
+
+        self.context_menu.addAction(self.action_settings)
+        self.context_menu.addAction(self.action_editfwrules)
+        self.context_menu.addAction(self.action_appmenus)
+        self.context_menu.addAction(self.action_set_keyboard_layout)
+        self.context_menu.addSeparator()
+
+        self.context_menu.addAction(self.action_updatevm)
+        self.context_menu.addAction(self.action_run_command_in_vm)
+        self.context_menu.addAction(self.action_resumevm)
+        self.context_menu.addAction(self.action_startvm_tools_install)
+        self.context_menu.addAction(self.action_pausevm)
+        self.context_menu.addAction(self.action_shutdownvm)
+        self.context_menu.addAction(self.action_restartvm)
+        self.context_menu.addAction(self.action_killvm)
+        self.context_menu.addSeparator()
+
+        self.context_menu.addAction(self.action_clonevm)
+        self.context_menu.addAction(self.action_removevm)
+        self.context_menu.addSeparator()
+
+        self.context_menu.addMenu(self.logs_menu)
+        self.context_menu.addSeparator()
+
+        self.tools_context_menu = QtGui.QMenu(self)
+        self.tools_context_menu.addAction(self.action_toolbar)
+        self.tools_context_menu.addAction(self.action_menubar)
+
+        self.connect(
+            self.table.horizontalHeader(),
+            QtCore.SIGNAL("sortIndicatorChanged(int, Qt::SortOrder)"),
+            self.sort_indicator_changed)
+        self.connect(self.table,
+                     QtCore.SIGNAL("customContextMenuRequested(const QPoint&)"),
+                     self.open_context_menu)
+        self.connect(self.menubar,
+                     QtCore.SIGNAL("customContextMenuRequested(const QPoint&)"),
+                     lambda pos: self.open_tools_context_menu(self.menubar,
+                                                              pos))
+        self.connect(self.toolbar,
+                     QtCore.SIGNAL("customContextMenuRequested(const QPoint&)"),
+                     lambda pos: self.open_tools_context_menu(self.toolbar,
+                                                              pos))
+        self.connect(self.logs_menu, QtCore.SIGNAL("triggered(QAction *)"),
+                     self.show_log)
+
+        self.connect(self.searchbox,
+                     QtCore.SIGNAL("textChanged(const QString&)"),
+                     self.do_search)
+
+        self.table.setContentsMargins(0, 0, 0, 0)
+        self.centralwidget.layout().setContentsMargins(0, 0, 0, 0)
+        self.layout().setContentsMargins(0, 0, 0, 0)
+
+        self.connect(self.action_menubar, QtCore.SIGNAL("toggled(bool)"),
+                     self.showhide_menubar)
+        self.connect(self.action_toolbar, QtCore.SIGNAL("toggled(bool)"),
+                     self.showhide_toolbar)
+
+        self.load_manager_settings()
+
+        self.fill_table()
+
+        self.counter = 0
+        self.update_size_on_disk = False
+        self.shutdown_monitor = {}
+
+    def load_manager_settings(self):
+        # visible columns
+        self.visible_columns_count = 0
+        for col in self.columns_indices:
+            col_no = self.columns_indices[col]
+            visible = self.manager_settings.value(
+                'columns/%s' % col,
+                defaultValue="true")
+            self.columns_actions[col_no].setChecked(visible == "true")
+            self.visible_columns_count += 1
+
+        self.sort_by_column = str(
+            self.manager_settings.value("view/sort_column",
+                                        defaultValue=self.sort_by_column))
+        self.sort_order = QtCore.Qt.SortOrder(
+            self.manager_settings.value("view/sort_order",
+                                        defaultValue=self.sort_order))
+        self.table.sortItems(self.columns_indices[self.sort_by_column],
+                             self.sort_order)
+        if not self.manager_settings.value("view/menubar_visible",
+                                           defaultValue=True):
+            self.action_menubar.setChecked(False)
+        if not self.manager_settings.value("view/toolbar_visible",
+                                           defaultValue=True):
+            self.action_toolbar.setChecked(False)
+        self.settings_loaded = True
+
+    def get_vms_list(self):
+        return [vm for vm in self.qubes_app.domains]
+
+    def fill_table(self):
+        # save current selection
+        row_index = self.table.currentRow()
+        selected_qid = -1
+        if row_index != -1:
+            vm_item = self.table.item(row_index, self.columns_indices["Name"])
+            if vm_item:
+                selected_qid = vm_item.qid
+
+        self.table.setSortingEnabled(False)
+        self.table.clearContents()
+        vms_list = self.get_vms_list()
+
+        vms_in_table = {}
+
+        row_no = 0
+        for vm in vms_list:
+            vm_row = VmRowInTable(vm, row_no, self.table)
+            vms_in_table[vm.qid] = vm_row
+
+            row_no += 1
+
+        self.table.setRowCount(row_no)
+        self.vms_list = vms_list
+        self.vms_in_table = vms_in_table
+        self.reload_table = False
+        if selected_qid in vms_in_table.keys():
+            self.table.setCurrentItem(
+                self.vms_in_table[selected_qid].name_widget)
+        self.table.setSortingEnabled(True)
+
+        self.showhide_vms()
+
+    def showhide_vms(self):
+        if not self.search:
+            for row_no in range(self.table.rowCount()):
+                self.table.setRowHidden(row_no, False)
+        else:
+            for row_no in range(self.table.rowCount()):
+                widget = self.table.cellWidget(row_no,
+                                               self.columns_indices["State"])
+                show = (self.search in widget.vm.name)
+                self.table.setRowHidden(row_no, not show)
+
+    @QtCore.pyqtSlot(str)
+    def do_search(self, search):
+        self.search = str(search)
+        self.showhide_vms()
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_search_triggered')
+    def action_search_triggered(self):
+        self.searchbox.setFocus()
+
+    def mark_table_for_update(self):
+        self.reload_table = True
+
+    def update_table(self):
+
+        self.fill_table()
+        # TODO: instead of manually refreshing the entire table, use dbus events
+
+        # reapply sorting
+        if self.sort_by_column:
+            self.table.sortByColumn(self.columns_indices[self.sort_by_column])
+
+        self.table_selection_changed()
+
+    # noinspection PyPep8Naming
+    def sort_indicator_changed(self, column, order):
+        self.sort_by_column = [name for name in self.columns_indices if
+                               self.columns_indices[name] == column][0]
+        self.sort_order = order
+        if self.settings_loaded:
+            self.manager_settings.setValue('view/sort_column',
+                                           self.sort_by_column)
+            self.manager_settings.setValue('view/sort_order', self.sort_order)
+            self.manager_settings.sync()
+
+    def table_selection_changed(self):
+
+        vm = self.get_selected_vm()
+
+        if vm is not None and vm in self.qubes_app.domains:
+
+            #  TODO: add boot from device to menu and add windows tools there
+            # Update available actions:
+            self.action_settings.setEnabled(vm.klass != 'AdminVM')
+            self.action_removevm.setEnabled(
+                vm.klass != 'AdminVM' and not vm.is_running())
+            self.action_clonevm.setEnabled(vm.klass != 'AdminVM')
+            self.action_resumevm.setEnabled(
+                not vm.is_running() or vm.get_power_state() == "Paused")
+            self.action_pausevm.setEnabled(
+                vm.is_running() and vm.get_power_state() != "Paused"
+                and vm.klass != 'AdminVM')
+            self.action_shutdownvm.setEnabled(
+                vm.is_running() and vm.get_power_state() != "Paused"
+                and vm.klass != 'AdminVM')
+            self.action_restartvm.setEnabled(
+                vm.is_running() and vm.get_power_state() != "Paused"
+                and vm.klass != 'AdminVM' and vm.klass != 'DispVM')
+            self.action_killvm.setEnabled(
+                (vm.get_power_state() == "Paused" or vm.is_running())
+                and vm.klass != 'AdminVM')
+
+            self.action_appmenus.setEnabled(
+                vm.klass != 'AdminVM' and vm.klass != 'DispVM'
+                and not vm.features.get('internal', False))
+            self.action_editfwrules.setEnabled(vm.klass != 'AdminVM')
+            self.action_updatevm.setEnabled(getattr(vm, 'updateable', False)
+                                            or vm.qid == 0)
+            self.action_run_command_in_vm.setEnabled(
+                not vm.get_power_state() == "Paused" and vm.qid != 0)
+            self.action_set_keyboard_layout.setEnabled(
+                vm.qid != 0 and
+                vm.get_power_state() != "Paused" and vm.is_running())
+        else:
+            self.action_settings.setEnabled(False)
+            self.action_removevm.setEnabled(False)
+            self.action_clonevm.setEnabled(False)
+            self.action_resumevm.setEnabled(False)
+            self.action_pausevm.setEnabled(False)
+            self.action_shutdownvm.setEnabled(False)
+            self.action_restartvm.setEnabled(False)
+            self.action_killvm.setEnabled(False)
+            self.action_appmenus.setEnabled(False)
+            self.action_editfwrules.setEnabled(False)
+            self.action_updatevm.setEnabled(False)
+            self.action_run_command_in_vm.setEnabled(False)
+            self.action_set_keyboard_layout.setEnabled(False)
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_createvm_triggered')
+    def action_createvm_triggered(self):  # pylint: disable=no-self-use
+        subprocess.check_call('qubes-vm-create')
+
+    def get_selected_vm(self):
+        # vm selection relies on the VmInfo widget's value used
+        # for sorting by VM name
+        row_index = self.table.currentRow()
+        if row_index != -1:
+            vm_item = self.table.item(row_index, self.columns_indices["Name"])
+            # here is possible race with update_table timer so check
+            # if really got the item
+            if vm_item is None:
+                return None
+            qid = vm_item.qid
+            assert self.vms_in_table[qid] is not None
+            vm = self.vms_in_table[qid].vm
+            return vm
+        else:
+            return None
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_removevm_triggered')
+    def action_removevm_triggered(self):
+
+        vm = self.get_selected_vm()
+
+        if vm.klass == 'TemplateVM':
+            dependent_vms = 0
+            for single_vm in self.qubes_app.domains:
+                if getattr(single_vm, 'template', None) == vm:
+                    dependent_vms += 1
+            if dependent_vms > 0:
+                QtGui.QMessageBox.warning(
+                    None, self.tr("Warning!"),
+                    self.tr("This Template Qube cannot be removed, "
+                            "because there is at least one Qube that is based "
+                            "on it.<br><small>If you want to remove this "
+                            "Template Qube and all the Qubes based on it, you "
+                            "should first remove each individual Qube that "
+                            "uses this template.</small>"))
+                return
+
+        (requested_name, ok) = QtGui.QInputDialog.getText(
+            None, self.tr("Qube Removal Confirmation"),
+            self.tr("Are you sure you want to remove the Qube <b>'{0}'</b>"
+                    "?<br> All data on this Qube's private storage will be "
+                    "lost!<br><br>Type the name of the Qube (<b>{1}</b>) below "
+                    "to confirm:").format(vm.name, vm.name))
+
+        if not ok:
+            # user clicked cancel
+            return
+
+        elif requested_name != vm.name:
+            # name did not match
+            QtGui.QMessageBox.warning(
+                None,
+                self.tr("Qube removal confirmation failed"),
+                self.tr(
+                    "Entered name did not match! Not removing "
+                    "{0}.").format(vm.name))
+            return
+
+        else:
+            # remove the VM
+            t_monitor = thread_monitor.ThreadMonitor()
+            thread = threading.Thread(target=self.do_remove_vm,
+                                      args=(vm, self.qubes_app, t_monitor))
+            thread.daemon = True
+            thread.start()
+
+            progress = QtGui.QProgressDialog(
+                self.tr(
+                    "Removing Qube: <b>{0}</b>...").format(vm.name), "", 0, 0)
+            progress.setCancelButton(None)
+            progress.setModal(True)
+            progress.show()
+
+            while not t_monitor.is_finished():
+                self.qt_app.processEvents()
+                time.sleep(0.1)
+
+            progress.hide()
+
+            if t_monitor.success:
+                pass
+            else:
+                QtGui.QMessageBox.warning(None, self.tr("Error removing Qube!"),
+                                          self.tr("ERROR: {0}").format(
+                                              t_monitor.error_msg))
+
+            self.update_table()
+
+    @staticmethod
+    def do_remove_vm(vm, qubes_app, t_monitor):
+        try:
+            del qubes_app.domains[vm.name]
+        except exc.QubesException as ex:
+            t_monitor.set_error_msg(str(ex))
+
+        t_monitor.set_finished()
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_clonevm_triggered')
+    def action_clonevm_triggered(self):
+        vm = self.get_selected_vm()
+
+        name_number = 1
+        name_format = vm.name + '-clone-%d'
+        while name_format % name_number in self.qubes_app.domains.keys():
+            name_number += 1
+
+        (clone_name, ok) = QtGui.QInputDialog.getText(
+            self, self.tr('Qubes clone Qube'),
+            self.tr('Enter name for Qube <b>{}</b> clone:').format(vm.name),
+            text=(name_format % name_number))
+        if not ok or clone_name == "":
+            return
+
+        t_monitor = thread_monitor.ThreadMonitor()
+        thread = threading.Thread(target=self.do_clone_vm,
+                                  args=(vm, self.qubes_app,
+                                        clone_name, t_monitor))
+        thread.daemon = True
+        thread.start()
+
+        progress = QtGui.QProgressDialog(
+            self.tr("Cloning Qube <b>{0}</b> to <b>{1}</b>...").format(
+                vm.name, clone_name), "", 0, 0)
+        progress.setCancelButton(None)
+        progress.setModal(True)
+        progress.show()
+
+        while not t_monitor.is_finished():
+            self.qt_app.processEvents()
+            time.sleep(0.2)
+
+        progress.hide()
+
+        if not t_monitor.success:
+            QtGui.QMessageBox.warning(
+                None,
+                self.tr("Error while cloning Qube"),
+                self.tr("Exception while cloning:<br>{0}").format(
+                    t_monitor.error_msg))
+
+        self.update_table()
+
+    @staticmethod
+    def do_clone_vm(src_vm, qubes_app, dst_name, t_monitor):
+        dst_vm = None
+        try:
+            dst_vm = qubes_app.clone_vm(src_vm, dst_name)
+        except exc.QubesException as ex:
+            t_monitor.set_error_msg(str(ex))
+            if dst_vm:
+                pass
+        t_monitor.set_finished()
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_resumevm_triggered')
+    def action_resumevm_triggered(self):
+        vm = self.get_selected_vm()
+
+        if vm.get_power_state() in ["Paused", "Suspended"]:
+            try:
+                vm.unpause()
+            except exc.QubesException as ex:
+                QtGui.QMessageBox.warning(
+                    None, self.tr("Error unpausing Qube!"),
+                    self.tr("ERROR: {0}").format(ex))
+            return
+
+        self.start_vm(vm)
+        self.update_table()
+
+    def start_vm(self, vm):
+        if vm.is_running():
+            return
+        t_monitor = thread_monitor.ThreadMonitor()
+        thread = threading.Thread(target=self.do_start_vm,
+                                  args=(vm, t_monitor))
+        thread.daemon = True
+        thread.start()
+
+        while not t_monitor.is_finished():
+            self.qt_app.processEvents()
+            time.sleep(0.1)
+
+        if not t_monitor.success:
+            QtGui.QMessageBox.warning(
+                None,
+                self.tr("Error starting Qube!"),
+                self.tr("ERROR: {0}").format(t_monitor.error_msg))
+
+        self.update_table()
+
+    @staticmethod
+    def do_start_vm(vm, t_monitor):
+        try:
+            vm.start()
+        except exc.QubesException as ex:
+            t_monitor.set_error_msg(str(ex))
+            t_monitor.set_finished()
+            return
+
+        t_monitor.set_finished()
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_startvm_tools_install_triggered')
+    # TODO: replace with boot from device
+    def action_startvm_tools_install_triggered(self):
+        # pylint: disable=invalid-name
+        pass
+
+    @QtCore.pyqtSlot(name='on_action_pausevm_triggered')
+    def action_pausevm_triggered(self):
+        vm = self.get_selected_vm()
+        assert vm.is_running()
+        try:
+            vm.pause()
+            self.update_table()
+        except exc.QubesException as ex:
+            QtGui.QMessageBox.warning(
+                None,
+                self.tr("Error pausing Qube!"),
+                self.tr("ERROR: {0}").format(ex))
+            return
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_shutdownvm_triggered')
+    def action_shutdownvm_triggered(self):
+        vm = self.get_selected_vm()
+        assert vm.is_running()
+
+        reply = QtGui.QMessageBox.question(
+            None, self.tr("Qube Shutdown Confirmation"),
+            self.tr("Are you sure you want to power down the Qube"
+                    " <b>'{0}'</b>?<br><small>This will shutdown all the "
+                    "running applications within this Qube.</small>").format(
+                vm.name), QtGui.QMessageBox.Yes | QtGui.QMessageBox.Cancel)
+
+        self.qt_app.processEvents()
+
+        if reply == QtGui.QMessageBox.Yes:
+            self.shutdown_vm(vm)
+
+        self.update_table()
+
+    def shutdown_vm(self, vm, shutdown_time=vm_shutdown_timeout,
+                    check_time=vm_restart_check_timeout, and_restart=False):
+        try:
+            vm.shutdown()
+        except exc.QubesException as ex:
+            QtGui.QMessageBox.warning(
+                None,
+                self.tr("Error shutting down Qube!"),
+                self.tr("ERROR: {0}").format(ex))
+            return
+
+        self.shutdown_monitor[vm.qid] = VmShutdownMonitor(vm, shutdown_time,
+                                                          check_time,
+                                                          and_restart, self)
+        # noinspection PyCallByClass,PyTypeChecker
+        QtCore.QTimer.singleShot(check_time, self.shutdown_monitor[
+            vm.qid].check_if_vm_has_shutdown)
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_restartvm_triggered')
+    def action_restartvm_triggered(self):
+        vm = self.get_selected_vm()
+        assert vm.is_running()
+
+        reply = QtGui.QMessageBox.question(
+            None, self.tr("Qube Restart Confirmation"),
+            self.tr("Are you sure you want to restart the Qube <b>'{0}'</b>?"
+                    "<br><small>This will shutdown all the running "
+                    "applications within this Qube.</small>").format(vm.name),
+            QtGui.QMessageBox.Yes | QtGui.QMessageBox.Cancel)
+
+        self.qt_app.processEvents()
+
+        if reply == QtGui.QMessageBox.Yes:
+            self.shutdown_vm(vm, and_restart=True)
+
+        self.update_table()
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_killvm_triggered')
+    def action_killvm_triggered(self):
+        vm = self.get_selected_vm()
+        assert vm.is_running() or vm.is_paused()
+
+        reply = QtGui.QMessageBox.question(
+            None, self.tr("Qube Kill Confirmation"),
+            self.tr("Are you sure you want to kill the Qube <b>'{0}'</b>?<br>"
+                    "<small>This will end <b>(not shutdown!)</b> all the "
+                    "running applications within this Qube.</small>").format(
+                vm.name),
+            QtGui.QMessageBox.Yes | QtGui.QMessageBox.Cancel,
+            QtGui.QMessageBox.Cancel)
+
+        self.qt_app.processEvents()
+
+        if reply == QtGui.QMessageBox.Yes:
+            try:
+                vm.force_shutdown()
+            except exc.QubesException as ex:
+                QtGui.QMessageBox.critical(
+                    None, self.tr("Error while killing Qube!"),
+                    self.tr(
+                        "<b>An exception ocurred while killing {0}.</b><br>"
+                        "ERROR: {1}").format(vm.name, ex))
+                return
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_settings_triggered')
+    def action_settings_triggered(self):
+        vm = self.get_selected_vm()
+        if vm:
+            settings_window = settings.VMSettingsWindow(
+                vm, self.qt_app, "basic")
+            settings_window.exec_()
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_appmenus_triggered')
+    def action_appmenus_triggered(self):
+        vm = self.get_selected_vm()
+        if vm:
+            settings_window = settings.VMSettingsWindow(
+                vm, self.qt_app, "applications")
+            settings_window.exec_()
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_refresh_list_triggered')
+    def action_refresh_list_triggered(self):
+        self.update_table()
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_updatevm_triggered')
+    def action_updatevm_triggered(self):
+        vm = self.get_selected_vm()
+
+        if not vm.is_running():
+            reply = QtGui.QMessageBox.question(
+                None, self.tr("Qube Update Confirmation"),
+                self.tr(
+                    "<b>{0}</b><br>The Qube has to be running to be updated."
+                    "<br>Do you want to start it?<br>").format(vm.name),
+                QtGui.QMessageBox.Yes | QtGui.QMessageBox.Cancel)
+            if reply != QtGui.QMessageBox.Yes:
+                return
+
+        self.qt_app.processEvents()
+
+        t_monitor = thread_monitor.ThreadMonitor()
+        thread = threading.Thread(target=self.do_update_vm,
+                                  args=(vm, t_monitor))
+        thread.daemon = True
+        thread.start()
+
+        progress = QtGui.QProgressDialog(
+                self.tr(
+                    "<b>{0}</b><br>Please wait for the updater to "
+                    "launch...").format(vm.name), "", 0, 0)
+        progress.setCancelButton(None)
+        progress.setModal(True)
+        progress.show()
+
+        while not t_monitor.is_finished():
+            self.qt_app.processEvents()
+            time.sleep(0.2)
+
+        progress.hide()
+
+        if vm.qid != 0:
+            if not t_monitor.success:
+                QtGui.QMessageBox.warning(
+                    None,
+                    self.tr("Error on Qube update!"),
+                    self.tr("ERROR: {0}").format(t_monitor.error_msg))
+
+        self.update_table()
+
+    @staticmethod
+    def do_update_vm(vm, t_monitor):
+        try:
+            if vm.qid == 0:
+                subprocess.check_call(
+                    ["/usr/bin/qubes-dom0-update", "--clean", "--gui"])
+            else:
+                if not vm.is_running():
+                    vm.start()
+                vm.run_service("qubes.InstallUpdatesGUI",
+                               user="root", wait=False)
+        except (ChildProcessError, exc.QubesException) as ex:
+            t_monitor.set_error_msg(str(ex))
+            t_monitor.set_finished()
+            return
+        t_monitor.set_finished()
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_run_command_in_vm_triggered')
+    def action_run_command_in_vm_triggered(self):
+        # pylint: disable=invalid-name
+        vm = self.get_selected_vm()
+
+        (command_to_run, ok) = QtGui.QInputDialog.getText(
+            self, self.tr('Qubes command entry'),
+            self.tr('Run command in <b>{}</b>:').format(vm.name))
+        if not ok or command_to_run == "":
+            return
+        t_monitor = thread_monitor.ThreadMonitor()
+        thread = threading.Thread(target=self.do_run_command_in_vm, args=(
+            vm, command_to_run, t_monitor))
+        thread.daemon = True
+        thread.start()
+
+        while not t_monitor.is_finished():
+            self.qt_app.processEvents()
+            time.sleep(0.2)
+
+        if not t_monitor.success:
+            QtGui.QMessageBox.warning(
+                None, self.tr("Error while running command"),
+                self.tr("Exception while running command:<br>{0}").format(
+                    t_monitor.error_msg))
+
+    @staticmethod
+    def do_run_command_in_vm(vm, command_to_run, t_monitor):
+        try:
+            vm.run(command_to_run)
+        except (ChildProcessError, exc.QubesException) as ex:
+            t_monitor.set_error_msg(str(ex))
+        t_monitor.set_finished()
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_set_keyboard_layout_triggered')
+    def action_set_keyboard_layout_triggered(self):
+        # pylint: disable=invalid-name
+        vm = self.get_selected_vm()
+        vm.run('qubes-change-keyboard-layout')
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_editfwrules_triggered')
+    def action_editfwrules_triggered(self):
+        vm = self.get_selected_vm()
+        settings_window = settings.VMSettingsWindow(vm, self.qt_app, "firewall")
+        settings_window.exec_()
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_global_settings_triggered')
+    def action_global_settings_triggered(self):  # pylint: disable=invalid-name
+        global_settings_window = global_settings.GlobalSettingsWindow(
+            self.qt_app,
+            self.qubes_app)
+        global_settings_window.exec_()
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_show_network_triggered')
+    def action_show_network_triggered(self):
+        pass
+        # TODO: revive for 4.1
+        # network_notes_dialog = NetworkNotesDialog()
+        # network_notes_dialog.exec_()
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_restore_triggered')
+    def action_restore_triggered(self):
+        restore_window = restore.RestoreVMsWindow(self.qt_app, self.qubes_app)
+        restore_window.exec_()
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_backup_triggered')
+    def action_backup_triggered(self):
+        backup_window = backup.BackupVMsWindow(self.qt_app, self.qubes_app)
+        backup_window.exec_()
+
+    def showhide_menubar(self, checked):
+        self.menubar.setVisible(checked)
+        if not checked:
+            self.context_menu.addAction(self.action_menubar)
+        else:
+            self.context_menu.removeAction(self.action_menubar)
+        if self.settings_loaded:
+            self.manager_settings.setValue('view/menubar_visible', checked)
+            self.manager_settings.sync()
+
+    def showhide_toolbar(self, checked):
+        self.toolbar.setVisible(checked)
+        if not checked:
+            self.context_menu.addAction(self.action_toolbar)
+        else:
+            self.context_menu.removeAction(self.action_toolbar)
+        if self.settings_loaded:
+            self.manager_settings.setValue('view/toolbar_visible', checked)
+            self.manager_settings.sync()
+
+    def showhide_column(self, col_num, show):
+        self.table.setColumnHidden(col_num, not show)
+
+        val = 1 if show else -1
+        self.visible_columns_count += val
+
+        if self.visible_columns_count == 1:
+            # disable hiding the last one
+            for col in self.columns_actions:
+                if self.columns_actions[col].isChecked():
+                    self.columns_actions[col].setEnabled(False)
+                    break
+        elif self.visible_columns_count == 2 and val == 1:
+            # enable hiding previously disabled column
+            for col in self.columns_actions:
+                if not self.columns_actions[col].isEnabled():
+                    self.columns_actions[col].setEnabled(True)
+                    break
+
+        if self.settings_loaded:
+            col_name = [name for name in self.columns_indices if
+                        self.columns_indices[name] == col_num][0]
+            self.manager_settings.setValue('columns/%s' % col_name, show)
+            self.manager_settings.sync()
+
+    def on_action_vm_type_toggled(self, checked):
+        self.showhide_column(self.columns_indices['Type'], checked)
+
+    def on_action_label_toggled(self, checked):
+        self.showhide_column(self.columns_indices['Label'], checked)
+
+    def on_action_name_toggled(self, checked):
+        self.showhide_column(self.columns_indices['Name'], checked)
+
+    def on_action_state_toggled(self, checked):
+        self.showhide_column(self.columns_indices['State'], checked)
+
+    def on_action_internal_toggled(self, checked):
+        self.showhide_column(self.columns_indices['Internal'], checked)
+
+    def on_action_ip_toggled(self, checked):
+        self.showhide_column(self.columns_indices['IP'], checked)
+
+    def on_action_backups_toggled(self, checked):
+        self.showhide_column(self.columns_indices['Backups'], checked)
+
+    def on_action_last_backup_toggled(self, checked):
+        self.showhide_column(self.columns_indices['Last backup'], checked)
+
+    def on_action_template_toggled(self, checked):
+        self.showhide_column(self.columns_indices['Template'], checked)
+
+    def on_action_netvm_toggled(self, checked):
+        self.showhide_column(self.columns_indices['NetVM'], checked)
+
+    def on_action_size_on_disk_toggled(self, checked):
+        self.showhide_column(self.columns_indices['Size'], checked)
+
+    # noinspection PyArgumentList
+    @QtCore.pyqtSlot(name='on_action_about_qubes_triggered')
+    def action_about_qubes_triggered(self):  # pylint: disable=no-self-use
+        about = AboutDialog()
+        about.exec_()
+
+    def createPopupMenu(self):  # pylint: disable=invalid-name
+        menu = QtGui.QMenu()
+        menu.addAction(self.action_toolbar)
+        menu.addAction(self.action_menubar)
+        return menu
+
+    def open_tools_context_menu(self, widget, point):
+        self.tools_context_menu.exec_(widget.mapToGlobal(point))
+
+    @QtCore.pyqtSlot('const QPoint&')
+    def open_context_menu(self, point):
+        vm = self.get_selected_vm()
+
+        # logs menu
+        self.logs_menu.clear()
+
+        if vm.qid == 0:
+            logfiles = ["/var/log/xen/console/hypervisor.log"]
+        else:
+            logfiles = [
+                "/var/log/xen/console/guest-" + vm.name + ".log",
+                "/var/log/xen/console/guest-" + vm.name + "-dm.log",
+                "/var/log/qubes/guid." + vm.name + ".log",
+                "/var/log/qubes/qrexec." + vm.name + ".log",
+            ]
+
+        menu_empty = True
+        for logfile in logfiles:
+            if os.path.exists(logfile):
+                action = self.logs_menu.addAction(QtGui.QIcon(":/log.png"),
+                                                  logfile)
+                action.setData(logfile)
+                menu_empty = False
+
+        self.logs_menu.setEnabled(not menu_empty)
+        self.context_menu.exec_(self.table.mapToGlobal(point))
+
+    @QtCore.pyqtSlot('QAction *')
+    def show_log(self, action):
+        log = str(action.data())
+        log_dlg = log_dialog.LogDialog(self.qt_app, log)
+        log_dlg.exec_()
+
+
+# Bases on the original code by:
+# Copyright (c) 2002-2007 Pascal Varet <p.varet@gmail.com>
+
+def handle_exception(exc_type, exc_value, exc_traceback):
+
+    filename, line, dummy, dummy = traceback.extract_tb(exc_traceback).pop()
+    filename = os.path.basename(filename)
+    error = "%s: %s" % (exc_type.__name__, exc_value)
+
+    strace = ""
+    stacktrace = traceback.extract_tb(exc_traceback)
+    while stacktrace:
+        (filename, line, func, txt) = stacktrace.pop()
+        strace += "----\n"
+        strace += "line: %s\n" % txt
+        strace += "func: %s\n" % func
+        strace += "line no.: %d\n" % line
+        strace += "file: %s\n" % filename
+
+    msg_box = QtGui.QMessageBox()
+    msg_box.setDetailedText(strace)
+    msg_box.setIcon(QtGui.QMessageBox.Critical)
+    msg_box.setWindowTitle("Houston, we have a problem...")
+    msg_box.setText("Whoops. A critical error has occured. "
+                    "This is most likely a bug in Qubes Manager.<br><br>"
+                    "<b><i>%s</i></b>" % error +
+                    "<br/>at line <b>%d</b><br/>of file %s.<br/><br/>"
+                    % (line, filename))
+
+    msg_box.exec_()
+
+
+def main():
+    qt_app = QtGui.QApplication(sys.argv)
+    qt_app.setOrganizationName("The Qubes Project")
+    qt_app.setOrganizationDomain("http://qubes-os.org")
+    qt_app.setApplicationName("Qube Manager")
+    qt_app.setWindowIcon(QtGui.QIcon.fromTheme("qubes-manager"))
+
+    sys.excepthook = handle_exception
+
+    qubes_app = Qubes()
+
+    manager_window = VmManagerWindow(qubes_app, qt_app)
+
+    manager_window.show()
+    manager_window.update_table()
+    qt_app.exec_()
+
+
+if __name__ == "__main__":
+    main()

+ 7 - 8
qubesmanager/releasenotes.py

@@ -1,4 +1,4 @@
-#!/usr/bin/python2
+#!/usr/bin/python3
 # coding=utf-8
 #
 # The Qubes OS Project, http://www.qubes-os.org
@@ -16,18 +16,17 @@
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
 #
 #
-from PyQt4.QtCore import SIGNAL
-from PyQt4.QtGui import QDialog, QIcon
+from PyQt4.QtGui import QDialog  # pylint: disable=import-error
 
-from .ui_releasenotes import *
+from . import ui_releasenotes  # pylint: disable=no-name-in-module
 
 
-class ReleaseNotesDialog(Ui_ReleaseNotesDialog, QDialog):
+class ReleaseNotesDialog(ui_releasenotes.Ui_ReleaseNotesDialog, QDialog):
+    # pylint: disable=too-few-public-methods
     def __init__(self):
         super(ReleaseNotesDialog, self).__init__()
 

+ 159 - 214
qubesmanager/restore.py

@@ -15,104 +15,90 @@
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
 #
 #
 
 import sys
+from PyQt4 import QtCore  # pylint: disable=import-error
+from PyQt4 import QtGui  # pylint: disable=import-error
+import threading
+import time
 import os
-import shutil
-from PyQt4.QtCore import *
-from PyQt4.QtGui import *
-
-from qubes.qubes import QubesVmCollection
-from qubes.qubes import QubesException
-from qubes.qubes import QubesDaemonPidfile
-from qubes.qubes import QubesHost
-from qubes.qubes import qubes_base_dir
-import qubesmanager.resources_rc
-import signal
-
-from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent
+import os.path
+import traceback
+import logging
+import logging.handlers
 
-import time
-from operator import itemgetter
-from .thread_monitor import *
+import signal
 
 from qubes import backup
-from qubes import qubesutils
 
-from .ui_restoredlg import *
-from .multiselectwidget import *
+from . import ui_restoredlg  # pylint: disable=no-name-in-module
+from . import multiselectwidget
+from . import backup_utils
+from . import thread_monitor
 
-from .backup_utils import *
 from multiprocessing import Queue, Event
 from multiprocessing.queues import Empty
+from qubesadmin import Qubes, exc
+from qubesadmin.backup import restore
 
-class RestoreVMsWindow(Ui_Restore, QWizard):
 
-    __pyqtSignals__ = ("restore_progress(int)","backup_progress(int)")
+class RestoreVMsWindow(ui_restoredlg.Ui_Restore, QtGui.QWizard):
 
-    def __init__(self, app, qvm_collection, blk_manager, parent=None):
+    def __init__(self, qt_app, qubes_app, parent=None):
         super(RestoreVMsWindow, self).__init__(parent)
 
-        self.app = app
-        self.qvm_collection = qvm_collection
-        self.blk_manager = blk_manager
+        self.qt_app = qt_app
+        self.qubes_app = qubes_app
 
-        self.restore_options = None
         self.vms_to_restore = None
         self.func_output = []
+
+        # Set up logging
         self.feedback_queue = Queue()
+        handler = logging.handlers.QueueHandler(self.feedback_queue)
+        logger = logging.getLogger('qubesadmin.backup')
+        logger.addHandler(handler)
+        logger.setLevel(logging.INFO)
+
         self.canceled = False
-        self.tmpdir_to_remove = None
         self.error_detected = Event()
-
-        self.excluded = {}
-
-        self.vm = self.qvm_collection[0]
-
-        assert self.vm != None
+        self.thread_monitor = None
+        self.backup_restore = None
+        self.target_appvm = None
 
         self.setupUi(self)
 
-        self.select_vms_widget = MultiSelectWidget(self)
+        self.select_vms_widget = multiselectwidget.MultiSelectWidget(self)
         self.select_vms_layout.insertWidget(1, self.select_vms_widget)
 
-        self.connect(self, SIGNAL("currentIdChanged(int)"), self.current_page_changed)
-        self.connect(self, SIGNAL("restore_progress(QString)"), self.commit_text_edit.append)
-        self.connect(self, SIGNAL("backup_progress(int)"), self.progress_bar.setValue)
-        self.dir_line_edit.connect(self.dir_line_edit, SIGNAL("textChanged(QString)"), self.backup_location_changed)
-        self.connect(self.verify_only, SIGNAL("stateChanged(int)"),
-                     self.on_verify_only_toogled)
+        self.connect(self,
+                     QtCore.SIGNAL("currentIdChanged(int)"),
+                     self.current_page_changed)
+        self.dir_line_edit.connect(self.dir_line_edit,
+                                   QtCore.SIGNAL("textChanged(QString)"),
+                                   self.backup_location_changed)
 
         self.select_dir_page.isComplete = self.has_selected_dir
         self.select_vms_page.isComplete = self.has_selected_vms
         self.confirm_page.isComplete = self.all_vms_good
-        #FIXME
-        #this causes to run isComplete() twice, I don't know why
-        self.select_vms_page.connect(self.select_vms_widget, SIGNAL("selected_changed()"), SIGNAL("completeChanged()"))
+        # FIXME
+        # this causes to run isComplete() twice, I don't know why
+        self.select_vms_page.connect(
+            self.select_vms_widget,
+            QtCore.SIGNAL("selected_changed()"),
+            QtCore.SIGNAL("completeChanged()"))
 
-        fill_appvms_list(self)
-        self.__init_restore_options__()
+        backup_utils.fill_appvms_list(self)
 
-    @pyqtSlot(name='on_select_path_button_clicked')
+    @QtCore.pyqtSlot(name='on_select_path_button_clicked')
     def select_path_button_clicked(self):
-        select_path_button_clicked(self, True)
-
-    def on_ignore_missing_toggled(self, checked):
-        self.restore_options['use-default-template'] = checked
-        self.restore_options['use-default-netvm'] = checked
-
-    def on_ignore_uname_mismatch_toggled(self, checked):
-        self.restore_options['ignore-username-mismatch'] = checked
+        backup_utils.select_path_button_clicked(self, True)
 
-    def on_verify_only_toogled(self, checked):
-        self.restore_options['verify-only'] = bool(checked)
-
-    def cleanupPage(self, p_int):
+    def cleanupPage(self, p_int):  # pylint: disable=invalid-name
         if self.page(p_int) is self.select_vms_page:
             self.vms_to_restore = None
         else:
@@ -126,119 +112,95 @@ class RestoreVMsWindow(Ui_Restore, QWizard):
         self.select_vms_widget.available_list.clear()
 
         self.target_appvm = None
-        if self.appvm_combobox.currentIndex() != 0:   #An existing appvm chosen
-            self.target_appvm = self.qvm_collection.get_vm_by_name(
-                    str(self.appvm_combobox.currentText()))
+        if self.appvm_combobox.currentIndex() != 0:   # An existing appvm chosen
+            self.target_appvm = self.qubes_app.domains[
+                str(self.appvm_combobox.currentText())]
 
         try:
-            self.vms_to_restore = backup.backup_restore_prepare(
-                    self.dir_line_edit.text(),
-                    self.passphrase_line_edit.text(),
-                    options=self.restore_options,
-                    host_collection=self.qvm_collection,
-                    encrypted=self.encryption_checkbox.isChecked(),
-                    appvm=self.target_appvm)
+            self.backup_restore = restore.BackupRestore(
+                self.qubes_app,
+                self.dir_line_edit.text(),
+                self.target_appvm,
+                self.passphrase_line_edit.text()
+            )
+
+            if self.ignore_missing.isChecked():
+                self.backup_restore.options.use_default_template = True
+                self.backup_restore.options.use_default_netvm = True
+
+            if self.ignore_uname_mismatch.isChecked():
+                self.backup_restore.options.ignore_username_mismatch = True
+
+            if self.verify_only.isChecked():
+                self.backup_restore.options.verify_only = True
+
+            self.vms_to_restore = self.backup_restore.get_restore_info()
 
             for vmname in self.vms_to_restore:
                 if vmname.startswith('$'):
                     # Internal info
                     continue
                 self.select_vms_widget.available_list.addItem(vmname)
-        except QubesException as ex:
-            QMessageBox.warning (None, self.tr("Restore error!"), str(ex))
-
-    def __init_restore_options__(self):
-        if not self.restore_options:
-            self.restore_options = {}
-            backup.backup_restore_set_defaults(self.restore_options)
+        except exc.QubesException as ex:
+            QtGui.QMessageBox.warning(None, self.tr("Restore error!"), str(ex))
 
-        if 'use-default-template' in self.restore_options and 'use-default-netvm' in self.restore_options:
-            val = self.restore_options['use-default-template'] and self.restore_options['use-default-netvm']
-            self.ignore_missing.setChecked(val)
-        else:
-            self.ignore_missing.setChecked(False)
-
-        if 'ignore-username-mismatch' in self.restore_options:
-            self.ignore_uname_mismatch.setChecked(self.restore_options['ignore-username-mismatch'])
-
-    def gather_output(self, s):
-        self.func_output.append(s)
-
-    def restore_error_output(self, s):
-        self.error_detected.set()
-        self.feedback_queue.put((SIGNAL("restore_progress(QString)"),
-                                 u'<font color="red">{0}</font>'.format(s)))
-
-    def restore_output(self, s):
-        self.feedback_queue.put((SIGNAL("restore_progress(QString)"),
-                                 u'<font color="black">{0}</font>'.format(s)))
+    def append_output(self, text):
+        self.commit_text_edit.append(text)
 
-    def update_progress_bar(self, value):
-        self.feedback_queue.put((SIGNAL("backup_progress(int)"), value))
-
-    def __do_restore__(self, thread_monitor):
+    def __do_restore__(self, t_monitor):
         err_msg = []
-        self.qvm_collection.lock_db_for_writing()
         try:
-            backup.backup_restore_do(self.vms_to_restore,
-                                     self.qvm_collection,
-                                     print_callback=self.restore_output,
-                                     error_callback=self.restore_error_output,
-                                     progress_callback=self.update_progress_bar)
+            self.backup_restore.restore_do(self.vms_to_restore)
+
         except backup.BackupCanceledError as ex:
             self.canceled = True
-            self.tmpdir_to_remove = ex.tmpdir
             err_msg.append(str(ex))
-        except Exception as ex:
-            print ("Exception:", ex)
+        except Exception as ex:  # pylint: disable=broad-except
             err_msg.append(str(ex))
             err_msg.append(
-                self.tr("Partially restored files left in "
-                   "/var/tmp/restore_*, investigate them and/or clean them up"))
+                self.tr("Partially restored files left in /var/tmp/restore_*, "
+                        "investigate them and/or clean them up"))
 
-        self.qvm_collection.unlock_db()
         if self.canceled:
-            self.emit(SIGNAL("restore_progress(QString)"),
-                      '<b><font color="red">{0}</font></b>'
-                      .format(self.tr("Restore aborted!")))
-        elif len(err_msg) > 0 or self.error_detected.is_set():
-            if len(err_msg) > 0:
-                thread_monitor.set_error_msg('\n'.join(err_msg))
-            self.emit(SIGNAL("restore_progress(QString)"),
-                      '<b><font color="red">{0}</font></b>'
-                      .format(self.tr("Finished with errors!")))
+            self.append_output('<b><font color="red">{0}</font></b>'.format(
+                self.tr("Restore aborted!")))
+        elif err_msg or self.error_detected.is_set():
+            if err_msg:
+                t_monitor.set_error_msg('\n'.join(err_msg))
+            self.append_output('<b><font color="red">{0}</font></b>'.format(
+                self.tr("Finished with errors!")))
         else:
-            self.emit(SIGNAL("restore_progress(QString)"),
-                      '<font color="green">{0}</font>'
-                      .format(self.tr("Finished successfully!")))
+            self.append_output('<font color="green">{0}</font>'.format(
+                self.tr("Finished successfully!")))
 
-        thread_monitor.set_finished()
+        t_monitor.set_finished()
 
-    def current_page_changed(self, id):
+    def current_page_changed(self, page_id):  # pylint: disable=unused-argument
 
         old_sigchld_handler = signal.signal(signal.SIGCHLD, signal.SIG_DFL)
         if self.currentPage() is self.select_vms_page:
             self.__fill_vms_list__()
 
         elif self.currentPage() is self.confirm_page:
-            for v in self.excluded:
-                self.vms_to_restore[v] = self.excluded[v]
-            self.excluded = {}
+
+            self.vms_to_restore = self.backup_restore.get_restore_info()
+
             for i in range(self.select_vms_widget.available_list.count()):
-                vmname =  self.select_vms_widget.available_list.item(i).text()
-                self.excluded[str(vmname)] = self.vms_to_restore[str(vmname)]
+                vmname = self.select_vms_widget.available_list.item(i).text()
                 del self.vms_to_restore[str(vmname)]
 
-            del self.func_output[:]
-            self.vms_to_restore = backup.restore_info_verify(self.vms_to_restore,
-                                                             self.qvm_collection)
-            backup.backup_restore_print_summary(
-                    self.vms_to_restore, print_callback = self.gather_output)
+            self.vms_to_restore = self.backup_restore.restore_info_verify(
+                self.vms_to_restore)
+
+            self.func_output = self.backup_restore.get_restore_summary(
+                self.vms_to_restore
+            )
+
             self.confirm_text_edit.setReadOnly(True)
             self.confirm_text_edit.setFontFamily("Monospace")
-            self.confirm_text_edit.setText("\n".join(self.func_output))
+            self.confirm_text_edit.setText(self.func_output)
 
-            self.confirm_page.emit(SIGNAL("completeChanged()"))
+            self.confirm_page.emit(QtCore.SIGNAL("completeChanged()"))
 
         elif self.currentPage() is self.commit_page:
             self.button(self.FinishButton).setDisabled(True)
@@ -247,50 +209,46 @@ class RestoreVMsWindow(Ui_Restore, QWizard):
                                            and str(self.dir_line_edit.text())
                                            .count("media/") > 0)
 
-            self.thread_monitor = ThreadMonitor()
-            thread = threading.Thread (target= self.__do_restore__ , args=(self.thread_monitor,))
+            self.thread_monitor = thread_monitor.ThreadMonitor()
+            thread = threading.Thread(target=self.__do_restore__,
+                                      args=(self.thread_monitor,))
             thread.daemon = True
             thread.start()
-
             while not self.thread_monitor.is_finished():
-                self.app.processEvents()
-                time.sleep (0.1)
+                self.qt_app.processEvents()
+                time.sleep(0.1)
                 try:
-                    for (signal_to_emit,data) in iter(self.feedback_queue.get_nowait,None):
-                        self.emit(signal_to_emit,data)
+                    log_record = self.feedback_queue.get_nowait()
+                    while log_record:
+                        if log_record.levelno == logging.ERROR or\
+                                        log_record.levelno == logging.CRITICAL:
+                            output = '<font color="red">{0}</font>'.format(
+                                log_record.getMessage())
+                        else:
+                            output = log_record.getMessage()
+                        self.append_output(output)
+                        log_record = self.feedback_queue.get_nowait()
                 except Empty:
                     pass
 
             if not self.thread_monitor.success:
-                if self.canceled:
-                    if self.tmpdir_to_remove and \
-                        QMessageBox.warning(None, self.tr("Restore aborted"),
-                            self.tr("Do you want to remove temporary files "
-                                    "from %s?") % self.tmpdir_to_remove,
-                            QMessageBox.Yes, QMessageBox.No) == \
-                            QMessageBox.Yes:
-                        shutil.rmtree(self.tmpdir_to_remove)
-                else:
-                    QMessageBox.warning(None,
-                        self.tr("Backup error!"), self.tr("ERROR: {0}")
-                                      .format(self.thread_monitor.error_msg))
+                if not self.canceled:
+                    QtGui.QMessageBox.warning(
+                        None,
+                        self.tr("Backup error!"),
+                        self.tr("ERROR: {0}").format(
+                            self.thread_monitor.error_msg))
+            self.progress_bar.setMaximum(100)
+            self.progress_bar.setValue(100)
 
             if self.showFileDialog.isChecked():
-                self.emit(SIGNAL("restore_progress(QString)"),
-                          '<b><font color="black">{0}</font></b>'.format(
-                              self.tr(
-                                  "Please unmount your backup volume and cancel"
-                                  " the file selection dialog.")))
-                if self.target_appvm:
-                    self.target_appvm.run("QUBESRPC %s dom0" %
-                                          "qubes.SelectDirectory")
-                else:
-                    file_dialog = QFileDialog()
-                    file_dialog.setReadOnly(True)
-                    file_dialog.getExistingDirectory(
-                        self, self.tr("Detach backup device"),
-                        os.path.dirname(self.dir_line_edit.text()))
-            self.progress_bar.setValue(100)
+                self.append_output(
+                    '<b><font color="black">{0}</font></b>'.format(
+                        self.tr("Please unmount your backup volume and cancel "
+                                "the file selection dialog.")))
+                self.qt_app.processEvents()
+                backup_utils.select_path_button_clicked(self, False, True)
+
             self.button(self.FinishButton).setEnabled(True)
             self.button(self.CancelButton).setEnabled(False)
             self.showFileDialog.setEnabled(False)
@@ -298,20 +256,19 @@ class RestoreVMsWindow(Ui_Restore, QWizard):
         signal.signal(signal.SIGCHLD, old_sigchld_handler)
 
     def all_vms_good(self):
-        for vminfo in self.vms_to_restore.values():
-            if not vminfo.has_key('vm'):
+        for vm_info in self.vms_to_restore.values():
+            if not vm_info.vm:
                 continue
-            if not vminfo['good-to-go']:
+            if not vm_info.good_to_go:
                 return False
         return True
 
     def reject(self):
         if self.currentPage() is self.commit_page:
-            if backup.backup_cancel():
-                self.emit(SIGNAL("restore_progress(QString)"),
-                          '<font color="red">{0}</font>'
-                          .format(self.tr("Aborting the operation...")))
-                self.button(self.CancelButton).setDisabled(True)
+            self.backup_restore.canceled = True
+            self.append_output('<font color="red">{0}</font>'.format(
+                self.tr("Aborting the operation...")))
+            self.button(self.CancelButton).setDisabled(True)
         else:
             self.done(0)
 
@@ -331,58 +288,46 @@ class RestoreVMsWindow(Ui_Restore, QWizard):
     def has_selected_vms(self):
         return self.select_vms_widget.selected_list.count() > 0
 
-    def backup_location_changed(self, new_dir = None):
-        self.select_dir_page.emit(SIGNAL("completeChanged()"))
+    def backup_location_changed(self, new_dir=None):
+        # pylint: disable=unused-argument
+        self.select_dir_page.emit(QtCore.SIGNAL("completeChanged()"))
 
 
 # Bases on the original code by:
 # Copyright (c) 2002-2007 Pascal Varet <p.varet@gmail.com>
 
-def handle_exception( exc_type, exc_value, exc_traceback ):
-    import sys
-    import os.path
-    import traceback
+def handle_exception(exc_type, exc_value, exc_traceback):
 
-    filename, line, dummy, dummy = traceback.extract_tb( exc_traceback ).pop()
-    filename = os.path.basename( filename )
-    error    = "%s: %s" % ( exc_type.__name__, exc_value )
+    filename, line, dummy, dummy = traceback.extract_tb(exc_traceback).pop()
+    filename = os.path.basename(filename)
+    error = "%s: %s" % (exc_type.__name__, exc_value)
 
-    QMessageBox.critical(None, "Houston, we have a problem...",
-                         "Whoops. A critical error has occured. This is most likely a bug "
+    QtGui.QMessageBox.critical(None, "Houston, we have a problem...",
+                         "Whoops. A critical error has occured. "
+                         "This is most likely a bug "
                          "in Qubes Restore VMs application.<br><br>"
                          "<b><i>%s</i></b>" % error +
                          "at <b>line %d</b> of file <b>%s</b>.<br/><br/>"
-                         % ( line, filename ))
-
-
+                                      % (line, filename))
 
 
 def main():
 
-    global qubes_host
-    qubes_host = QubesHost()
-
-    global app
-    app = QApplication(sys.argv)
-    app.setOrganizationName("The Qubes Project")
-    app.setOrganizationDomain("http://qubes-os.org")
-    app.setApplicationName("Qubes Restore VMs")
+    qt_app = QtGui.QApplication(sys.argv)
+    qt_app.setOrganizationName("The Qubes Project")
+    qt_app.setOrganizationDomain("http://qubes-os.org")
+    qt_app.setApplicationName("Qubes Restore VMs")
 
     sys.excepthook = handle_exception
 
-    qvm_collection = QubesVmCollection()
-    qvm_collection.lock_db_for_reading()
-    qvm_collection.load()
-    qvm_collection.unlock_db()
+    qubes_app = Qubes()
 
-    global restore_window
-    restore_window = RestoreVMsWindow()
+    restore_window = RestoreVMsWindow(qt_app, qubes_app)
 
     restore_window.show()
 
-    app.exec_()
-    app.exit()
-
+    qt_app.exec_()
+    qt_app.exit()
 
 
 if __name__ == "__main__":

+ 325 - 265
qubesmanager/settings.py

@@ -17,39 +17,37 @@
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
 #
 #
 
 
 import collections
-import copy
-import os
 import os.path
 import re
 import subprocess
-import sys
 import threading
 import time
 import traceback
-
-import qubesadmin
-import qubesadmin.tools
+import os
+import sys
+from qubesadmin.tools import QubesArgumentParser
+from qubesadmin import devices
+import qubesadmin.exc
 
 from . import utils
 from . import multiselectwidget
 from . import thread_monitor
 
 from .appmenu_select import AppmenuSelectManager
-from .backup_utils import get_path_for_vm
-from .firewall import *
+from . import firewall
+from PyQt4 import QtCore, QtGui  # pylint: disable=import-error
 
-from .ui_settingsdlg import *
-from .bootfromdevice import main as bootfromdevice
+from . import ui_settingsdlg  #pylint: disable=no-name-in-module
 
-class VMSettingsWindow(Ui_SettingsDialog, QDialog):
+# pylint: disable=too-many-instance-attributes
+class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtGui.QDialog):
     tabs_indices = collections.OrderedDict((
             ('basic', 0),
             ('advanced', 1),
@@ -73,42 +71,59 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
         self.setWindowTitle(self.tr("Settings: {vm}").format(vm=self.vm.name))
         if init_page in self.tabs_indices:
             idx = self.tabs_indices[init_page]
-            assert (idx in range(self.tabWidget.count()))
+            assert idx in range(self.tabWidget.count())
             self.tabWidget.setCurrentIndex(idx)
 
-        self.connect(self.buttonBox, SIGNAL("accepted()"), self.save_and_apply)
-        self.connect(self.buttonBox, SIGNAL("rejected()"), self.reject)
+        self.connect(self.buttonBox,
+                     QtCore.SIGNAL("accepted()"),
+                     self.save_and_apply)
+        self.connect(self.buttonBox, QtCore.SIGNAL("rejected()"), self.reject)
 
         self.tabWidget.currentChanged.connect(self.current_tab_changed)
 
-#       self.tabWidget.setTabEnabled(self.tabs_indices["firewall"], vm.is_networked() and not vm.provides_network)
-
         ###### basic tab
         self.__init_basic_tab__()
         self.rename_vm_button.clicked.connect(self.rename_vm)
+        self.delete_vm_button.clicked.connect(self.remove_vm)
+        self.clone_vm_button.clicked.connect(self.clone_vm)
 
         ###### advanced tab
         self.__init_advanced_tab__()
-        self.include_in_balancing.stateChanged.connect(self.include_in_balancing_state_changed)
-        self.connect(self.init_mem, SIGNAL("editingFinished()"), self.check_mem_changes)
-        self.connect(self.max_mem_size, SIGNAL("editingFinished()"), self.check_mem_changes)
-        self.bootFromDeviceButton.clicked.connect(self.boot_from_cdrom_button_pressed)
+        self.include_in_balancing.stateChanged.connect(
+            self.include_in_balancing_changed)
+        self.connect(self.init_mem,
+                     QtCore.SIGNAL("editingFinished()"),
+                     self.check_mem_changes)
+        self.connect(self.max_mem_size,
+                     QtCore.SIGNAL("editingFinished()"),
+                     self.check_mem_changes)
+        self.boot_from_device_button.clicked.connect(
+            self.boot_from_cdrom_button_pressed)
 
         ###### firewall tab
         if self.tabWidget.isTabEnabled(self.tabs_indices['firewall']):
-            model = QubesFirewallRulesModel()
-            model.set_vm(vm)
-            self.set_fw_model(model)
-
-            self.newRuleButton.clicked.connect(self.new_rule_button_pressed)
-            self.editRuleButton.clicked.connect(self.edit_rule_button_pressed)
-            self.deleteRuleButton.clicked.connect(self.delete_rule_button_pressed)
-            self.policyDenyRadioButton.clicked.connect(self.policy_changed)
-            self.policyAllowRadioButton.clicked.connect(self.policy_changed)
+            model = firewall.QubesFirewallRulesModel()
+            try:
+                model.set_vm(vm)
+                self.set_fw_model(model)
+                self.firewall_modified_outside_label.setVisible(False)
+            except firewall.FirewallModifiedOutsideError:
+                self.disable_all_fw_conf()
+
+            self.new_rule_button.clicked.connect(self.new_rule_button_pressed)
+            self.edit_rule_button.clicked.connect(self.edit_rule_button_pressed)
+            self.delete_rule_button.clicked.connect(
+                self.delete_rule_button_pressed)
+            self.policy_deny_radio_button.clicked.connect(self.policy_changed)
+            self.policy_allow_radio_button.clicked.connect(self.policy_changed)
+            if init_page == 'firewall':
+                self.check_network_availability()
 
         ####### devices tab
         self.__init_devices_tab__()
-        self.connect(self.dev_list, SIGNAL("selected_changed()"), self.devices_selection_changed)
+        self.connect(self.dev_list,
+                     QtCore.SIGNAL("selected_changed()"),
+                     self.devices_selection_changed)
 
         ####### services tab
         self.__init_services_tab__()
@@ -119,8 +134,9 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
         if self.tabWidget.isTabEnabled(self.tabs_indices["applications"]):
             self.app_list = multiselectwidget.MultiSelectWidget(self)
             self.apps_layout.addWidget(self.app_list)
-            self.AppListManager = AppmenuSelectManager(self.vm, self.app_list)
-            self.refresh_apps_button.clicked.connect(self.refresh_apps_button_pressed)
+            self.app_list_manager = AppmenuSelectManager(self.vm, self.app_list)
+            self.refresh_apps_button.clicked.connect(
+                self.refresh_apps_button_pressed)
 
     def reject(self):
         self.done(0)
@@ -131,91 +147,112 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
 
     def save_and_apply(self):
         t_monitor = thread_monitor.ThreadMonitor()
-        thread = threading.Thread(target=self.__save_changes__, args=(t_monitor,))
+        thread = threading.Thread(target=self.__save_changes__,
+                                  args=(t_monitor,))
         thread.daemon = True
         thread.start()
 
-        progress = QProgressDialog(
-            self.tr("Applying settings to <b>{0}</b>...").format(self.vm.name), "", 0, 0)
+        progress = QtGui.QProgressDialog(
+            self.tr("Applying settings to <b>{0}</b>...").format(self.vm.name),
+            "", 0, 0)
         progress.setCancelButton(None)
         progress.setModal(True)
         progress.show()
 
         while not t_monitor.is_finished():
             self.qapp.processEvents()
-            time.sleep (0.1)
+            time.sleep(0.1)
 
         progress.hide()
 
         if not t_monitor.success:
-            QMessageBox.warning(None,
-                self.tr("Error while changing settings for {0}!").format(self.vm.name),
-                    self.tr("ERROR: {0}").format(t_monitor.error_msg))
+            QtGui.QMessageBox.warning(
+                None,
+                self.tr("Error while changing settings for {0}!"
+                        ).format(self.vm.name),
+                self.tr("ERROR: {0}").format(t_monitor.error_msg))
 
         self.done(0)
 
     def __save_changes__(self, t_monitor):
 
-        self.anything_changed = False
-
         ret = []
         try:
             ret_tmp = self.__apply_basic_tab__()
-            if len(ret_tmp) > 0:
+            if ret_tmp:
                 ret += ["Basic tab:"] + ret_tmp
             ret_tmp = self.__apply_advanced_tab__()
-            if len(ret_tmp) > 0:
+            if ret_tmp:
                 ret += ["Advanced tab:"] + ret_tmp
             ret_tmp = self.__apply_devices_tab__()
-            if len(ret_tmp) > 0:
+            if ret_tmp:
                 ret += ["Devices tab:"] + ret_tmp
             ret_tmp = self.__apply_services_tab__()
-            if len(ret_tmp) > 0:
+            if ret_tmp:
                 ret += ["Sevices tab:"] + ret_tmp
-        except Exception as ex:
-            ret.append(self.tr('Error while saving changes: ') + str(ex))
+        except qubesadmin.exc.QubesException as qex:
+            ret.append(self.tr('Error while saving changes: ') + str(qex))
+        except Exception as ex:  # pylint: disable=broad-except
+            ret.append(repr(ex))
 
         try:
-            if self.tabWidget.isTabEnabled(self.tabs_indices["firewall"]):
-                self.fw_model.apply_rules(self.policyAllowRadioButton.isChecked(),
-                        self.dnsCheckBox.isChecked(),
-                        self.icmpCheckBox.isChecked(),
-                        self.yumproxyCheckBox.isChecked(),
-                        self.tempFullAccess.isChecked(),
-                        self.tempFullAccessTime.value())
-                if self.fw_model.fw_changed:
-                    # might modified vm.services
-                    self.anything_changed = True
-        except Exception as ex:
-            ret += [self.tr("Firewall tab:"), str(ex)]
+            if self.policy_allow_radio_button.isEnabled():
+                self.fw_model.apply_rules(
+                    self.policy_allow_radio_button.isChecked(),
+                    self.temp_full_access.isChecked(),
+                    self.temp_full_access_time.value())
+        except qubesadmin.exc.QubesException as qex:
+            ret += [self.tr("Firewall tab:"), str(qex)]
+        except Exception as ex:  # pylint: disable=broad-except
+            ret += [self.tr("Firewall tab:"), repr(ex)]
 
         try:
             if self.tabWidget.isTabEnabled(self.tabs_indices["applications"]):
-                self.AppListManager.save_appmenu_select_changes()
-        except Exception as ex:
-            ret += [self.tr("Applications tab:"), str(ex)]
+                self.app_list_manager.save_appmenu_select_changes()
+        except qubesadmin.exc.QubesException as qex:
+            ret += [self.tr("Applications tab:"), str(qex)]
+        except Exception as ex:  # pylint: disable=broad-except
+            ret += [self.tr("Applications tab:"), repr(ex)]
 
-        if len(ret) > 0 :
+        if ret:
             t_monitor.set_error_msg('\n'.join(ret))
 
         utils.debug('\n'.join(ret))
 
         t_monitor.set_finished()
 
-    def current_tab_changed(self, idx):
-        if idx == self.tabs_indices["firewall"]:
-            netvm = self.vm.netvm
-            if netvm is not None and \
-                    not netvm.features.check_with_template('qubes-firewall', False):
-                QMessageBox.warning(None,
-                    self.tr("VM configuration problem!"),
-                    self.tr("The '{vm}' AppVM is network connected to "
+    def check_network_availability(self):
+        netvm = self.vm.netvm
+        self.no_netvm_label.setVisible(netvm is None)
+        self.netvm_no_firewall_label.setVisible(
+            netvm is not None and
+            not netvm.features.check_with_template('qubes-firewall', False))
+        if netvm is None:
+            QtGui.QMessageBox.warning(
+                None,
+                self.tr("Qube configuration problem!"),
+                self.tr('This qube has networking disabled '
+                        '(Basic -> Networking) - network will be disabled. '
+                        'If you want to use firewall, '
+                        'please enable networking.')
+            )
+        if netvm is not None and \
+                not netvm.features.check_with_template(
+                    'qubes-firewall',
+                    False):
+            QtGui.QMessageBox.warning(
+                None,
+                self.tr("Qube configuration problem!"),
+                self.tr("The '{vm}' AppVM is network connected to "
                         "'{netvm}', which does not support firewall!<br/>"
                         "You may edit the '{vm}' VM firewall rules, but these "
                         "will not take any effect until you connect it to "
                         "a working Firewall VM.").format(
-                            vm=self.vm.name, netvm=netvm.name))
+                    vm=self.vm.name, netvm=netvm.name))
 
+    def current_tab_changed(self, idx):
+        if idx == self.tabs_indices["firewall"]:
+            self.check_network_availability()
 
     ######### basic tab
 
@@ -233,9 +270,17 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
 
     def __init_basic_tab__(self):
         self.vmname.setText(self.vm.name)
-        self.vmname.setValidator(QRegExpValidator(QRegExp("[a-zA-Z0-9-]*", Qt.CaseInsensitive), None))
+        self.vmname.setValidator(
+            QtGui.QRegExpValidator(
+                QtCore.QRegExp("[a-zA-Z0-9-]*",
+                               QtCore.Qt.CaseInsensitive), None))
         self.vmname.setEnabled(False)
         self.rename_vm_button.setEnabled(not self.vm.is_running())
+        self.delete_vm_button.setEnabled(not self.vm.is_running())
+
+        if self.vm.is_running():
+            self.delete_vm_button.setText(self.tr('Delete VM '
+                                            '(cannot delete a running VM)'))
 
         if self.vm.qid == 0:
             self.vmlabel.setVisible(False)
@@ -282,7 +327,7 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
             self.autostart_vm.setVisible(False)
 
         #type
-        self.type_label.setText(type(self.vm).__name__)
+        self.type_label.setText(self.vm.klass)
 
         #installed by rpm
         self.rpm_label.setText('Yes' if self.vm.installed_by_rpm else 'No')
@@ -316,8 +361,7 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
                 if self.vmlabel.currentIndex() != self.label_idx:
                     label = self.label_list[self.vmlabel.currentIndex()]
                     self.vm.label = label
-                    self.anything_changed = True
-        except Exception as ex:
+        except qubesadmin.exc.QubesException as ex:
             msg.append(str(ex))
 
         #vm template changed
@@ -325,24 +369,22 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
             if self.template_name.currentIndex() != self.template_idx:
                 self.vm.template = \
                     self.template_list[self.template_name.currentIndex()]
-                self.anything_changed = True
-        except Exception as ex:
+        except qubesadmin.exc.QubesException as ex:
             msg.append(str(ex))
 
         #vm netvm changed
         try:
             if self.netVM.currentIndex() != self.netvm_idx:
                 self.vm.netvm = self.netvm_list[self.netVM.currentIndex()]
-                self.anything_changed = True
-        except Exception as ex:
+        except qubesadmin.exc.QubesException as ex:
             msg.append(str(ex))
 
         #include in backups
         try:
-            if self.vm.include_in_backups != self.include_in_backups.isChecked():
+            if self.vm.include_in_backups != \
+                    self.include_in_backups.isChecked():
                 self.vm.include_in_backups = self.include_in_backups.isChecked()
-                self.anything_changed = True
-        except Exception as ex:
+        except qubesadmin.exc.QubesException as ex:
             msg.append(str(ex))
 
         #run_in_debug_mode
@@ -350,8 +392,7 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
             if self.run_in_debug_mode.isVisible():
                 if self.vm.debug != self.run_in_debug_mode.isChecked():
                     self.vm.debug = self.run_in_debug_mode.isChecked()
-                    self.anything_changed = True
-        except Exception as ex:
+        except qubesadmin.exc.QubesException as ex:
             msg.append(str(ex))
 
         #autostart_vm
@@ -359,8 +400,7 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
             if self.autostart_vm.isVisible():
                 if self.vm.autostart != self.autostart_vm.isChecked():
                     self.vm.autostart = self.autostart_vm.isChecked()
-                    self.anything_changed = True
-        except Exception as ex:
+        except qubesadmin.exc.QubesException as ex:
             msg.append(str(ex))
 
         #max priv storage
@@ -368,8 +408,7 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
         if self.priv_img_size != priv_size:
             try:
                 self.vm.volumes['private'].resize(priv_size * 1024**2)
-                self.anything_changed = True
-            except Exception as ex:
+            except qubesadmin.exc.QubesException as ex:
                 msg.append(str(ex))
 
         #max sys storage
@@ -377,8 +416,7 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
         if self.root_img_size != sys_size:
             try:
                 self.vm.volumes['root'].resize(sys_size * 1024**2)
-                self.anything_changed = True
-            except Exception as ex:
+            except qubesadmin.exc.QubesException as ex:
                 msg.append(str(ex))
 
         return msg
@@ -386,54 +424,122 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
 
     def check_mem_changes(self):
         if self.max_mem_size.value() < self.init_mem.value():
-            QMessageBox.warning(None,
+            QtGui.QMessageBox.warning(
+                None,
                 self.tr("Warning!"),
                 self.tr("Max memory can not be less than initial memory.<br>"
                         "Setting max memory to equal initial memory."))
             self.max_mem_size.setValue(self.init_mem.value())
-        # Linux specific limit: init memory must not be below max_mem_size/10.79 in order to allow scaling up to max_mem_size (or else "add_memory() failed: -17" problem)
+        # Linux specific limit: init memory must not be below
+        # max_mem_size/10.79 in order to allow scaling up to
+        # max_mem_size (or else "add_memory() failed: -17" problem)
         if self.init_mem.value() * 10 < self.max_mem_size.value():
-            QMessageBox.warning(None,
+            QtGui.QMessageBox.warning(
+                None,
                 self.tr("Warning!"),
                 self.tr("Initial memory can not be less than one tenth "
                         "Max memory.<br>Setting initial memory to the minimum "
                         "allowed value."))
             self.init_mem.setValue(self.max_mem_size.value() / 10)
 
+    def _run_in_thread(self, func, *args):
+        t_monitor = thread_monitor.ThreadMonitor()
+        thread = threading.Thread(target=func, args=(t_monitor, *args,))
+        thread.daemon = True
+        thread.start()
+
+        while not t_monitor.is_finished():
+            self.qapp.processEvents()
+            time.sleep(0.1)
+
+        if not t_monitor.success:
+            QtGui.QMessageBox.warning(None,
+                                         self.tr("Error!"),
+                                         self.tr("ERROR: {}").format(
+                                    t_monitor.error_msg))
+
+
     def _rename_vm(self, t_monitor, name):
         try:
             self.vm.app.clone_vm(self.vm, name)
             del self.vm.app.domains[self.vm.name]
 
-        except Exception as ex:
-            t_monitor.set_error_msg(str(ex))
+        except qubesadmin.exc.QubesException as qex:
+            t_monitor.set_error_msg(str(qex))
+        except Exception as ex:  # pylint: disable=broad-except
+            t_monitor.set_error_msg(repr(ex))
 
         t_monitor.set_finished()
 
 
     def rename_vm(self):
 
-        new_vm_name, ok = QInputDialog.getText(self, self.tr('Rename VM'), self.tr('New name: (WARNING: all other changes will be discarded)'))
+        new_vm_name, ok = QtGui.QInputDialog.getText(
+            self,
+            self.tr('Rename VM'),
+            self.tr('New name: (WARNING: all other changes will be discarded)'))
 
         if ok:
+            self._run_in_thread(self._rename_vm, new_vm_name)
+            self.done(0)
 
-            t_monitor = thread_monitor.ThreadMonitor()
-            thread = threading.Thread(target=self._rename_vm, args=(t_monitor, new_vm_name,))
-            thread.daemon = True
-            thread.start()
+    def _remove_vm(self, t_monitor):
+        try:
+            del self.vm.app.domains[self.vm.name]
 
-            while not t_monitor.is_finished():
-                self.qapp.processEvents()
-                time.sleep (0.1)
+        except qubesadmin.exc.QubesException as qex:
+            t_monitor.set_error_msg(str(qex))
+        except Exception as ex:  # pylint: disable=broad-except
+            t_monitor.set_error_msg(repr(ex))
 
-            if not t_monitor.success:
-                QMessageBox.warning(None,
-                    self.tr("Error renaming the VM!"),
-                    self.tr("ERROR: {}").format(
-                        t_monitor.error_msg))
+        t_monitor.set_finished()
 
+    def remove_vm(self):
+
+        answer, ok = QtGui.QInputDialog.getText(
+            self,
+            self.tr('Delete VM'),
+            self.tr('Are you absolutely sure you want to delete this VM? '
+                      '<br/> All VM settings and data will be irrevocably'
+                      ' deleted. <br/> If you are sure, please enter this '
+                      'VM\'s name below.'))
+
+
+        if ok and answer == self.vm.name:
+            self._run_in_thread(self._remove_vm)
             self.done(0)
 
+        elif ok:
+            QtGui.QMessageBox.warning(
+                None,
+                self.tr("Removal cancelled"),
+                self.tr("The VM will not be removed."))
+
+    def _clone_vm(self, t_monitor, name):
+        try:
+            self.vm.app.clone_vm(self.vm, name)
+
+        except qubesadmin.exc.QubesException as qex:
+            t_monitor.set_error_msg(str(qex))
+        except Exception as ex:  # pylint: disable=broad-except
+            t_monitor.set_error_msg(repr(ex))
+
+        t_monitor.set_finished()
+
+    def clone_vm(self):
+
+        cloned_vm_name, ok = QtGui.QInputDialog.getText(
+            self,
+            self.tr('Clone VM'),
+            self.tr('Name for the cloned VM:'))
+
+        if ok:
+            self._run_in_thread(self._clone_vm, cloned_vm_name)
+            QtGui.QMessageBox.warning(
+                None,
+                self.tr("Success"),
+                self.tr("The VM was cloned successfully."))
+
     ######### advanced tab
 
     def __init_advanced_tab__(self):
@@ -453,7 +559,7 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
 
         self.include_in_balancing.setEnabled(True)
         self.include_in_balancing.setChecked(
-            self.vm.features.get('services.meminfo-writer', True))
+            bool(self.vm.features.get('service.meminfo-writer', True)))
         self.max_mem_size.setEnabled(self.include_in_balancing.isChecked())
 
         #in case VM is HVM
@@ -490,16 +596,13 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
         try:
             if self.init_mem.value() != int(self.vm.memory):
                 self.vm.memory = self.init_mem.value()
-                self.anything_changed = True
 
             if self.max_mem_size.value() != int(self.vm.maxmem):
                 self.vm.maxmem = self.max_mem_size.value()
-                self.anything_changed = True
 
             if self.vcpus.value() != int(self.vm.vcpus):
                 self.vm.vcpus = self.vcpus.value()
-                self.anything_changed = True
-        except Exception as ex:
+        except qubesadmin.exc.QubesException as ex:
             msg.append(str(ex))
 
         #include_in_memory_balancing applied in services tab
@@ -508,9 +611,9 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
         if hasattr(self.vm, "kernel") and self.kernel_groupbox.isVisible():
             try:
                 if self.kernel.currentIndex() != self.kernel_idx:
-                    self.vm.kernel = self.kernel_list[self.kernel.currentIndex()]
-                    self.anything_changed = True
-            except Exception as ex:
+                    self.vm.kernel = self.kernel_list[
+                        self.kernel.currentIndex()]
+            except qubesadmin.exc.QubesException as ex:
                 msg.append(str(ex))
 
         #vm default_dispvm changed
@@ -518,8 +621,7 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
             if self.default_dispvm.currentIndex() != self.default_dispvm_idx:
                 self.vm.default_dispvm = \
                     self.default_dispvm_list[self.default_dispvm.currentIndex()]
-                self.anything_changed = True
-        except Exception as ex:
+        except qubesadmin.exc.QubesException as ex:
             msg.append(str(ex))
 
         try:
@@ -632,11 +734,11 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
         for dev in lspci.splitlines():
             devs.append((dev.rstrip(), dev.split(' ')[0]))
 
-        class DevListWidgetItem(QListWidgetItem):
-            def __init__(self, name, ident, parent = None):
+        # pylint: disable=too-few-public-methods
+        class DevListWidgetItem(QtGui.QListWidgetItem):
+            def __init__(self, name, ident, parent=None):
                 super(DevListWidgetItem, self).__init__(name, parent)
                 self.ident = ident
-                self.Type
 
         persistent = [ass.ident.replace('_', ':')
             for ass in self.vm.devices['pci'].persistent()]
@@ -649,7 +751,8 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
                 self.dev_list.available_list.addItem(
                     DevListWidgetItem(name, ident))
 
-        if self.dev_list.selected_list.count() > 0 and self.include_in_balancing.isChecked():
+        if self.dev_list.selected_list.count() > 0\
+                and self.include_in_balancing.isChecked():
             self.dmm_warning_adv.show()
             self.dmm_warning_dev.show()
         else:
@@ -677,7 +780,7 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
                     for i in range(self.dev_list.selected_list.count())]
             for ident in new:
                 if ident not in old:
-                    ass = qubesadmin.devices.DeviceAssignment(
+                    ass = devices.DeviceAssignment(
                         self.vm.app.domains['dom0'],
                         ident.replace(':', '_'),
                         persistent=True)
@@ -686,24 +789,22 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
                 if ass.ident.replace('_', ':') not in new:
                     self.vm.devices['pci'].detach(ass)
 
-            self.anything_changed = True
-
-        except Exception as ex:
+        except qubesadmin.exc.QubesException as ex:
             if utils.is_debug():
                 traceback.print_exc()
             msg.append(str(ex))
 
         return msg
 
-    def include_in_balancing_state_changed(self, state):
-        for r in range (self.services_list.count()):
-            item = self.services_list.item(r)
+    def include_in_balancing_changed(self, state):
+        for i in range(self.services_list.count()):
+            item = self.services_list.item(i)
             if str(item.text()) == 'meminfo-writer':
                 item.setCheckState(state)
                 break
 
         if self.dev_list.selected_list.count() > 0:
-            if state == QtCore.Qt.Checked:
+            if state == ui_settingsdlg.QtCore.Qt.Checked:
                 self.dmm_warning_adv.show()
                 self.dmm_warning_dev.show()
             else:
@@ -713,7 +814,7 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
 
     def devices_selection_changed(self):
         if self.include_in_balancing.isChecked():
-            if self.dev_list.selected_list.count() > 0 :
+            if self.dev_list.selected_list.count() > 0:
                 self.dmm_warning_adv.show()
                 self.dmm_warning_dev.show()
             else:
@@ -757,15 +858,17 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
         self.refresh_apps_button.setText(self.tr('Refresh in progress...'))
 
         t_monitor = thread_monitor.ThreadMonitor()
-        thread = threading.Thread(target=self.refresh_apps_in_vm, args=(t_monitor,))
+        thread = threading.Thread(
+            target=self.refresh_apps_in_vm,
+            args=(t_monitor,))
         thread.daemon = True
         thread.start()
 
         while not t_monitor.is_finished():
             self.qapp.processEvents()
-            time.sleep (0.1)
+            time.sleep(0.1)
 
-        self.AppListManager = AppmenuSelectManager(self.vm, self.app_list)
+        self.app_list_manager = AppmenuSelectManager(self.vm, self.app_list)
 
         self.refresh_apps_button.setEnabled(True)
         self.refresh_apps_button.setText(self.tr('Refresh Applications'))
@@ -778,23 +881,29 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
             if not feature.startswith('service.'):
                 continue
             service = feature[len('service.'):]
-            item = QListWidgetItem(service)
-            item.setCheckState(QtCore.Qt.Checked
-                if self.vm.features[feature] else QtCore.Qt.Unchecked)
+            item = QtGui.QListWidgetItem(service)
+            item.setCheckState(ui_settingsdlg.QtCore.Qt.Checked
+                if self.vm.features[feature]
+                               else ui_settingsdlg.QtCore.Qt.Unchecked)
             self.services_list.addItem(item)
             self.new_srv_dict[service] = self.vm.features[feature]
 
-        self.connect(self.services_list, SIGNAL("itemClicked(QListWidgetItem *)"), self.services_item_clicked)
+        self.connect(
+            self.services_list,
+            QtCore.SIGNAL("itemClicked(QListWidgetItem *)"),
+            self.services_item_clicked)
 
     def __add_service__(self):
         srv = str(self.service_line_edit.text()).strip()
         if srv != "":
             if srv in self.new_srv_dict:
-                QMessageBox.information(None, '',
+                QtGui.QMessageBox.information(
+                    None,
+                    '',
                     self.tr('Service already on the list!'))
             else:
-                item = QListWidgetItem(srv)
-                item.setCheckState(QtCore.Qt.Checked)
+                item = QtGui.QListWidgetItem(srv)
+                item.setCheckState(ui_settingsdlg.QtCore.Qt.Checked)
                 self.services_list.addItem(item)
                 self.new_srv_dict[srv] = True
 
@@ -804,9 +913,11 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
         if not item:
             return
         if str(item.text()) == 'meminfo-writer':
-            QMessageBox.information(None,
+            QtGui.QMessageBox.information(
+                None,
                 self.tr('Service can not be removed'),
-                self.tr('Service meminfo-writer can not be removed from the list.'))
+                self.tr('Service meminfo-writer can not '
+                        'be removed from the list.'))
             return
 
         row = self.services_list.currentRow()
@@ -816,10 +927,10 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
 
     def services_item_clicked(self, item):
         if str(item.text()) == 'meminfo-writer':
-            if item.checkState() == QtCore.Qt.Checked:
+            if item.checkState() == ui_settingsdlg.QtCore.Qt.Checked:
                 if not self.include_in_balancing.isChecked():
                     self.include_in_balancing.setChecked(True)
-            elif item.checkState() == QtCore.Qt.Unchecked:
+            elif item.checkState() == ui_settingsdlg.QtCore.Qt.Unchecked:
                 if self.include_in_balancing.isChecked():
                     self.include_in_balancing.setChecked(False)
 
@@ -828,13 +939,16 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
         msg = []
 
         try:
-            for r in range(self.services_list.count()):
-                item = self.services_list.item(r)
-                self.new_srv_dict[str(item.text())] = (item.checkState() == QtCore.Qt.Checked)
+            for i in range(self.services_list.count()):
+                item = self.services_list.item(i)
+                self.new_srv_dict[str(item.text())] = \
+                    (item.checkState() == ui_settingsdlg.QtCore.Qt.Checked)
 
-            balancing_was_checked = self.vm.features.get('service.meminfo-writer', True)
+            balancing_was_checked = self.vm.features.get(
+                'service.meminfo-writer', True)
             balancing_is_checked = self.include_in_balancing.isChecked()
-            meminfo_writer_checked = self.new_srv_dict.get('meminfo-writer', True)
+            meminfo_writer_checked = self.new_srv_dict.get(
+                'meminfo-writer', True)
 
             if balancing_is_checked != meminfo_writer_checked:
                 if balancing_is_checked != balancing_was_checked:
@@ -844,7 +958,6 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
                 feature = 'service.' + service
                 if v != self.vm.features.get(feature, object()):
                     self.vm.features[feature] = v
-                    self.anything_changed = True
 
             for feature in self.vm.features:
                 if not feature.startswith('service.'):
@@ -852,7 +965,7 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
                 service = feature[len('service.'):]
                 if service not in self.new_srv_dict:
                     del self.vm.features[feature]
-        except Exception as ex:
+        except qubesadmin.exc.QubesException as ex:
             msg.append(str(ex))
 
         return msg
@@ -863,116 +976,65 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog):
     def set_fw_model(self, model):
         self.fw_model = model
         self.rulesTreeView.setModel(model)
-        self.rulesTreeView.header().setResizeMode(QHeaderView.ResizeToContents)
-        self.rulesTreeView.header().setResizeMode(0, QHeaderView.Stretch)
+        self.rulesTreeView.header().setResizeMode(
+            QtGui.QHeaderView.ResizeToContents)
+        self.rulesTreeView.header().setResizeMode(0, QtGui.QHeaderView.Stretch)
         self.set_allow(model.allow)
-        self.dnsCheckBox.setChecked(model.allowDns)
-        self.icmpCheckBox.setChecked(model.allowIcmp)
-        self.yumproxyCheckBox.setChecked(model.allowYumProxy)
-        if model.tempFullAccessExpireTime:
-            self.tempFullAccess.setChecked(True)
-            self.tempFullAccessTime.setValue(
-                (model.tempFullAccessExpireTime -
-                int(datetime.datetime.now().strftime("%s")))/60)
+        if model.temp_full_access_expire_time:
+            self.temp_full_access.setChecked(True)
+            self.temp_full_access_time.setValue(
+                (model.temp_full_access_expire_time -
+                 int(firewall.datetime.datetime.now().strftime("%s"))) / 60)
+
+    def disable_all_fw_conf(self):
+        self.firewall_modified_outside_label.setVisible(True)
+        self.policy_allow_radio_button.setEnabled(False)
+        self.policy_deny_radio_button.setEnabled(False)
+        self.rulesTreeView.setEnabled(False)
+        self.new_rule_button.setEnabled(False)
+        self.edit_rule_button.setEnabled(False)
+        self.delete_rule_button.setEnabled(False)
+        self.firewal_rules_label.setEnabled(False)
+        self.tempFullAccessWidget.setEnabled(False)
 
     def set_allow(self, allow):
-        self.policyAllowRadioButton.setChecked(allow)
-        self.policyDenyRadioButton.setChecked(not allow)
-        self.policy_changed(allow)
-
-    def policy_changed(self, checked):
-        self.tempFullAccessWidget.setEnabled(self.policyDenyRadioButton.isChecked())
+        self.policy_allow_radio_button.setChecked(allow)
+        self.policy_deny_radio_button.setChecked(not allow)
+        self.policy_changed()
+
+    def policy_changed(self):
+        self.rulesTreeView.setEnabled(
+            self.policy_deny_radio_button.isChecked())
+        self.new_rule_button.setEnabled(
+            self.policy_deny_radio_button.isChecked())
+        self.edit_rule_button.setEnabled(
+            self.policy_deny_radio_button.isChecked())
+        self.delete_rule_button.setEnabled(
+            self.policy_deny_radio_button.isChecked())
+        self.firewal_rules_label.setEnabled(
+            self.policy_deny_radio_button.isChecked())
+        self.tempFullAccessWidget.setEnabled(
+            self.policy_deny_radio_button.isChecked())
 
     def new_rule_button_pressed(self):
-        dialog = NewFwRuleDlg()
-        self.run_rule_dialog(dialog)
+        dialog = firewall.NewFwRuleDlg()
+        self.fw_model.run_rule_dialog(dialog)
 
     def edit_rule_button_pressed(self):
-        dialog = NewFwRuleDlg()
-        dialog.set_ok_enabled(True)
+
         selected = self.rulesTreeView.selectedIndexes()
-        if len(selected) > 0:
-            row = self.rulesTreeView.selectedIndexes().pop().row()
-            address = self.fw_model.get_column_string(0, row).replace(' ', '')
-            dialog.addressComboBox.setItemText(0, address)
-            dialog.addressComboBox.setCurrentIndex(0)
-            service = self.fw_model.get_column_string(1, row)
-            if service == "any":
-                service = ""
-            dialog.serviceComboBox.setItemText(0, service)
-            dialog.serviceComboBox.setCurrentIndex(0)
-            protocol = self.fw_model.get_column_string(2, row)
-            if protocol == "tcp":
-                dialog.tcp_radio.setChecked(True)
-            elif protocol == "udp":
-                dialog.udp_radio.setChecked(True)
-            else:
-                dialog.any_radio.setChecked(True)
 
-            self.run_rule_dialog(dialog, row)
+        if selected:
+            dialog = firewall.NewFwRuleDlg()
+            dialog.set_ok_state(True)
+            row = self.rulesTreeView.selectedIndexes().pop().row()
+            self.fw_model.populate_edit_dialog(dialog, row)
+            self.fw_model.run_rule_dialog(dialog, row)
 
     def delete_rule_button_pressed(self):
-        for i in set([index.row() for index in self.rulesTreeView.selectedIndexes()]):
-            self.fw_model.removeChild(i)
-
-    def run_rule_dialog(self, dialog, row = None):
-        if dialog.exec_():
-            address = str(dialog.addressComboBox.currentText())
-            service = str(dialog.serviceComboBox.currentText())
-            port = None
-            port2 = None
-
-            unmask = address.split("/", 1)
-            if len(unmask) == 2:
-                address = unmask[0]
-                netmask = int(unmask[1])
-            else:
-                netmask = 32
-
-            if address == "*":
-                address = "0.0.0.0"
-                netmask = 0
-
-            if dialog.any_radio.isChecked():
-                protocol = "any"
-                port = 0
-            else:
-                if dialog.tcp_radio.isChecked():
-                    protocol = "tcp"
-                elif dialog.udp_radio.isChecked():
-                    protocol = "udp"
-                else:
-                    protocol = "any"
-
-                try:
-                    range = service.split("-", 1)
-                    if len(range) == 2:
-                        port = int(range[0])
-                        port2 = int(range[1])
-                    else:
-                        port = int(service)
-                except (TypeError, ValueError) as ex:
-                    port = self.fw_model.get_service_port(service)
-
-            if port is not None:
-                if port2 is not None and port2 <= port:
-                    QMessageBox.warning(None, self.tr("Invalid service ports range"),
-                        self.tr("Port {0} is lower than port {1}.").format(
-                            port2, port))
-                else:
-                    item = {"address": address,
-                            "netmask": netmask,
-                            "portBegin": port,
-                            "portEnd": port2,
-                            "proto": protocol,
-                    }
-                    if row is not None:
-                        self.fw_model.setChild(row, item)
-                    else:
-                        self.fw_model.appendChild(item)
-            else:
-                QMessageBox.warning(None, self.tr("Invalid service name"),
-                    self.tr("Service '{0}' is unknown.").format(service))
+        for i in set([index.row() for index
+                      in self.rulesTreeView.selectedIndexes()]):
+            self.fw_model.remove_child(i)
 
 
 # Bases on the original code by:
@@ -986,7 +1048,7 @@ def handle_exception(exc_type, exc_value, exc_traceback):
 
     strace = ""
     stacktrace = traceback.extract_tb(exc_traceback)
-    while len(stacktrace) > 0:
+    while stacktrace:
         (filename, line, func, txt) = stacktrace.pop()
         strace += "----\n"
         strace += "line: %s\n" %txt
@@ -994,20 +1056,20 @@ def handle_exception(exc_type, exc_value, exc_traceback):
         strace += "line no.: %d\n" %line
         strace += "file: %s\n" %filename
 
-    msg_box = QMessageBox()
+    msg_box = QtGui.QMessageBox()
     msg_box.setDetailedText(strace)
-    msg_box.setIcon(QMessageBox.Critical)
+    msg_box.setIcon(QtGui.QMessageBox.Critical)
     msg_box.setWindowTitle("Houston, we have a problem...")
-    msg_box.setText("Whoops. A critical error has occured. This is most likely a bug "
-                    "in Qubes Manager.<br><br>"
+    msg_box.setText("Whoops. A critical error has occured. "
+                    "This is most likely a bug in Qubes Manager.<br><br>"
                     "<b><i>%s</i></b>" % error +
                     "<br/>at line <b>%d</b><br/>of file %s.<br/><br/>"
-                    % ( line, filename ))
+                    % (line, filename))
 
     msg_box.exec_()
 
 
-parser = qubesadmin.tools.QubesArgumentParser(vmname_nargs=1)
+parser = QubesArgumentParser(vmname_nargs=1)
 
 parser.add_argument('--tab', metavar='TAB',
     action='store',
@@ -1018,12 +1080,10 @@ parser.set_defaults(
 )
 
 def main(args=None):
-    global settings_window
-
     args = parser.parse_args(args)
     vm = args.domains.pop()
 
-    qapp = QApplication(sys.argv)
+    qapp = QtGui.QApplication(sys.argv)
     qapp.setOrganizationName('Invisible Things Lab')
     qapp.setOrganizationDomain("https://www.qubes-os.org/")
     qapp.setApplicationName("Qubes VM Settings")

+ 184 - 390
qubesmanager/table_widgets.py

@@ -1,9 +1,10 @@
-#!/usr/bin/python2
+#!/usr/bin/python3
 # -*- coding: utf8 -*-
 #
 # The Qubes OS Project, http://www.qubes-os.org
 #
-# Copyright (C) 2014 Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
+# Copyright (C) 2014 Marek Marczykowski-Górecki
+#                       <marmarek@invisiblethingslab.com>
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
@@ -15,61 +16,54 @@
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
 
-import os
+from PyQt4 import QtGui  # pylint: disable=import-error
+from PyQt4 import QtCore  # pylint: disable=import-error
+# pylint: disable=too-few-public-methods
 
-from PyQt4 import QtGui
-
-from PyQt4.QtCore import QSize, Qt
-
-from PyQt4.QtGui import QTableWidgetItem, QHBoxLayout, QIcon, QLabel, QWidget, \
-    QSizePolicy, QSpacerItem, QFont, QColor, QProgressBar, QPainter, QPen
-import time
-from qubes.qubes import vm_files
-import main
-
-qubes_dom0_updates_stat_file = '/var/lib/qubes/updates/dom0-updates-available'
-power_order = Qt.DescendingOrder
-update_order = Qt.AscendingOrder
+power_order = QtCore.Qt.DescendingOrder
+update_order = QtCore.Qt.AscendingOrder
 
 
 row_height = 30
 
 
-class VmIconWidget (QWidget):
+class VmIconWidget(QtGui.QWidget):
     def __init__(self, icon_path, enabled=True, size_multiplier=0.7,
-                 tooltip  = None, parent=None, icon_sz = (32, 32)):
+                 tooltip=None, parent=None, icon_sz=(32, 32)):
         super(VmIconWidget, self).__init__(parent)
 
-        self.label_icon = QLabel()
+        self.label_icon = QtGui.QLabel()
         if icon_path[0] in ':/':
-            icon = QIcon (icon_path)
+            icon = QtGui.QIcon(icon_path)
         else:
-            icon = QIcon.fromTheme(icon_path)
-        icon_sz = QSize (row_height * size_multiplier, row_height * size_multiplier)
-        icon_pixmap = icon.pixmap(icon_sz, QIcon.Disabled if not enabled else QIcon.Normal)
-        self.label_icon.setPixmap (icon_pixmap)
-        self.label_icon.setFixedSize (icon_sz)
-        if tooltip != None:
+            icon = QtGui.QIcon.fromTheme(icon_path)
+        icon_sz = QtCore.QSize(row_height * size_multiplier,
+                               row_height * size_multiplier)
+        icon_pixmap = icon.pixmap(
+            icon_sz,
+            QtGui.QIcon.Disabled if not enabled else QtGui.QIcon.Normal)
+        self.label_icon.setPixmap(icon_pixmap)
+        self.label_icon.setFixedSize(icon_sz)
+        if tooltip is not None:
             self.label_icon.setToolTip(tooltip)
 
-        layout = QHBoxLayout()
+        layout = QtGui.QHBoxLayout()
         layout.addWidget(self.label_icon)
-        layout.setContentsMargins(0,0,0,0)
+        layout.setContentsMargins(0, 0, 0, 0)
         self.setLayout(layout)
 
-    def setToolTip(self, tooltip):
+    def setToolTip(self, tooltip):  # pylint: disable=invalid-name
         if tooltip is not None:
             self.label_icon.setToolTip(tooltip)
         else:
             self.label_icon.setToolTip('')
 
-class VmTypeWidget(VmIconWidget):
 
-    class VmTypeItem(QTableWidgetItem):
+class VmTypeWidget(VmIconWidget):
+    class VmTypeItem(QtGui.QTableWidgetItem):
         def __init__(self, value, vm):
             super(VmTypeWidget.VmTypeItem, self).__init__()
             self.value = value
@@ -84,40 +78,39 @@ class VmTypeWidget(VmIconWidget):
             elif other.vm.qid == 0:
                 return False
             elif self.value == other.value:
-                return self.vm.qid < other.vm.qid
-            else:
-                return self.value < other.value
+                return self.vm.name < other.vm.name
+            return self.value < other.value
 
     def __init__(self, vm, parent=None):
         (icon_path, tooltip) = self.get_vm_icon(vm)
-        super (VmTypeWidget, self).__init__(icon_path, True, 0.8, tooltip, parent)
+        super(VmTypeWidget, self).__init__(
+            icon_path, True, 0.8, tooltip, parent)
         self.vm = vm
-        self.tableItem = self.VmTypeItem(self.value, vm)
+        self.table_item = self.VmTypeItem(self.value, vm)
+        self.value = None
+
+    # TODO: add "provides network" column
 
     def get_vm_icon(self, vm):
-        if vm.qid == 0:
+        if vm.klass == 'AdminVM':
             self.value = 0
-            return (":/dom0.png", "Dom0")
-        elif vm.is_netvm() and not vm.is_proxyvm():
-            self.value = 1
-            return (":/netvm.png", "NetVM")
-        elif vm.is_proxyvm():
-            self.value = 2
-            return (":/proxyvm.png", "ProxyVM")
-        elif vm.is_appvm() and vm.template is None:
-            self.value = 4
-            return (":/standalonevm.png", "StandaloneVM")
-        elif vm.is_template():
+            icon_name = "dom0"
+        elif vm.klass == 'TemplateVM':
             self.value = 3
-            return (":/templatevm.png", "TemplateVM")
-        elif vm.is_appvm() or vm.is_disposablevm():
+            icon_name = "templatevm"
+        elif vm.klass == 'StandaloneVM':
+            self.value = 4
+            icon_name = "standalonevm"
+        else:
             self.value = 5 + vm.label.index
-            return (":/appvm.png", "AppVM")
+            icon_name = "appvm"
+
+        return ":/" + icon_name + ".png", vm.klass
 
 
 class VmLabelWidget(VmIconWidget):
 
-    class VmLabelItem(QTableWidgetItem):
+    class VmLabelItem(QtGui.QTableWidgetItem):
         def __init__(self, value, vm):
             super(VmLabelWidget.VmLabelItem, self).__init__()
             self.value = value
@@ -132,32 +125,27 @@ class VmLabelWidget(VmIconWidget):
             elif other.vm.qid == 0:
                 return False
             elif self.value == other.value:
-                return self.vm.qid < other.vm.qid
-            else:
-                return self.value < other.value
+                return self.vm.name < other.vm.name
+            return self.value < other.value
 
     def __init__(self, vm, parent=None):
         icon_path = self.get_vm_icon_path(vm)
-        super (VmLabelWidget, self).__init__(icon_path, True, 0.8, None, parent)
+        super(VmLabelWidget, self).__init__(icon_path, True, 0.8, None, parent)
         self.vm = vm
-        self.tableItem = self.VmLabelItem(self.value, vm)
+        self.table_item = self.VmLabelItem(self.value, vm)
+        self.value = None
 
     def get_vm_icon_path(self, vm):
-        if vm.qid == 0:
-            self.value = 100
-            return ":/off.png"
-        else:
-            self.value = vm.label.index
-            return vm.label.icon
-
+        self.value = vm.label.index
+        return vm.label.icon
 
 
-class VmNameItem (QTableWidgetItem):
+class VmNameItem(QtGui.QTableWidgetItem):
     def __init__(self, vm):
         super(VmNameItem, self).__init__()
-        self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
+        self.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
         self.setText(vm.name)
-        self.setTextAlignment(Qt.AlignVCenter)
+        self.setTextAlignment(QtCore.Qt.AlignVCenter)
         self.qid = vm.qid
 
     def __lt__(self, other):
@@ -168,44 +156,43 @@ class VmNameItem (QTableWidgetItem):
         return super(VmNameItem, self).__lt__(other)
 
 
-class VmStatusIcon(QLabel):
+class VmStatusIcon(QtGui.QLabel):
     def __init__(self, vm, parent=None):
-        super (VmStatusIcon, self).__init__(parent)
+        super(VmStatusIcon, self).__init__(parent)
         self.vm = vm
         self.set_on_icon()
-        self.previous_power_state = vm.last_power_state
+        self.previous_power_state = self.vm.get_power_state()
 
     def update(self):
-        if self.previous_power_state != self.vm.last_power_state:
+        if self.previous_power_state != self.vm.get_power_state():
             self.set_on_icon()
-            self.previous_power_state = self.vm.last_power_state
+            self.previous_power_state = self.vm.get_power_state()
 
     def set_on_icon(self):
-        if self.vm.last_power_state == "Running":
-            icon = QIcon (":/on.png")
-        elif self.vm.last_power_state in ["Paused", "Suspended"]:
-            icon = QIcon (":/paused.png")
-        elif self.vm.last_power_state in ["Transient", "Halting", "Dying"]:
-            icon = QIcon (":/transient.png")
+        if self.vm.get_power_state() == "Running":
+            icon = QtGui.QIcon(":/on.png")
+        elif self.vm.get_power_state() in ["Paused", "Suspended"]:
+            icon = QtGui.QIcon(":/paused.png")
+        elif self.vm.get_power_state() in ["Transient", "Halting", "Dying"]:
+            icon = QtGui.QIcon(":/transient.png")
         else:
-            icon = QIcon (":/off.png")
+            icon = QtGui.QIcon(":/off.png")
 
-        icon_sz = QSize (row_height * 0.5, row_height *0.5)
+        icon_sz = QtCore.QSize(row_height * 0.5, row_height * 0.5)
         icon_pixmap = icon.pixmap(icon_sz)
-        self.setPixmap (icon_pixmap)
-        self.setFixedSize (icon_sz)
+        self.setPixmap(icon_pixmap)
+        self.setFixedSize(icon_sz)
 
 
-
-class VmInfoWidget (QWidget):
-
-    class VmInfoItem (QTableWidgetItem):
+class VmInfoWidget(QtGui.QWidget):
+    class VmInfoItem(QtGui.QTableWidgetItem):
         def __init__(self, upd_info_item, vm):
             super(VmInfoWidget.VmInfoItem, self).__init__()
             self.upd_info_item = upd_info_item
             self.vm = vm
 
         def __lt__(self, other):
+            # pylint: disable=too-many-return-statements
             if self.vm.qid == 0:
                 return True
             elif other.vm.qid == 0:
@@ -213,30 +200,34 @@ class VmInfoWidget (QWidget):
 
             self_val = self.upd_info_item.value
             other_val = other.upd_info_item.value
-            if self.tableWidget().horizontalHeader().sortIndicatorOrder() == update_order:
+
+            if self.tableWidget().\
+                    horizontalHeader().sortIndicatorOrder() == update_order:
                 # the result will be sorted by upd, sorting order: Ascending
                 self_val += 1 if self.vm.is_running() else 0
                 other_val += 1 if other.vm.is_running() else 0
                 if self_val == other_val:
-                    return self.vm.qid < other.vm.qid
-                else:
-                    return self_val > other_val
-            elif self.tableWidget().horizontalHeader().sortIndicatorOrder() == power_order:
-                #the result will be sorted by power state, sorting order: Descending
-                self_val = -(self_val/10 + 10*(1 if self.vm.is_running() else 0))
-                other_val = -(other_val/10 + 10*(1 if other.vm.is_running() else 0))
+                    return self.vm.name < other.vm.name
+                return self_val > other_val
+            elif self.tableWidget().\
+                    horizontalHeader().sortIndicatorOrder() == power_order:
+                # the result will be sorted by power state,
+                # sorting order: Descending
+                self_val = -(self_val/10 +
+                             10*(1 if self.vm.is_running() else 0))
+                other_val = -(other_val/10 +
+                              10*(1 if other.vm.is_running() else 0))
                 if self_val == other_val:
-                    return self.vm.qid < other.vm.qid
-                else:
-                    return self_val > other_val
+                    return self.vm.name < other.vm.name
+                return self_val > other_val
             else:
-                #it would be strange if this happened
+                # it would be strange if this happened
                 return
 
-    def __init__(self, vm, parent = None):
-        super (VmInfoWidget, self).__init__(parent)
+    def __init__(self, vm, parent=None):
+        super(VmInfoWidget, self).__init__(parent)
         self.vm = vm
-        layout = QHBoxLayout ()
+        layout = QtGui.QHBoxLayout()
 
         self.on_icon = VmStatusIcon(vm)
         self.upd_info = VmUpdateInfoWidget(vm, show_text=False)
@@ -247,58 +238,43 @@ class VmInfoWidget (QWidget):
         layout.addWidget(self.on_icon)
         layout.addWidget(self.upd_info)
         layout.addWidget(self.error_icon)
-        layout.addItem(QSpacerItem(0, 10, QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding))
+        layout.addItem(QtGui.QSpacerItem(0, 10,
+                                         QtGui.QSizePolicy.Expanding,
+                                         QtGui.QSizePolicy.Expanding))
         layout.addWidget(self.blk_icon)
         layout.addWidget(self.rec_icon)
 
-        layout.setContentsMargins(5,0,5,0)
+        layout.setContentsMargins(5, 0, 5, 0)
         self.setLayout(layout)
 
         self.rec_icon.setVisible(False)
         self.blk_icon.setVisible(False)
         self.error_icon.setVisible(False)
 
-        self.tableItem = self.VmInfoItem(self.upd_info.tableItem, vm)
+        self.table_item = self.VmInfoItem(self.upd_info.table_item, vm)
 
-    def update_vm_state(self, vm, blk_visible, rec_visible=None):
+    def update_vm_state(self, vm):
         self.on_icon.update()
         self.upd_info.update_outdated(vm)
-        if blk_visible != None:
-            self.blk_icon.setVisible(blk_visible)
-        if rec_visible != None:
-            self.rec_icon.setVisible(rec_visible)
-        self.error_icon.setToolTip(vm.qubes_manager_state[main.QMVmState
-            .ErrorMsg])
-        self.error_icon.setVisible(vm.qubes_manager_state[main.QMVmState
-                                   .ErrorMsg] is not None)
 
 
-class VmTemplateItem (QTableWidgetItem):
+class VmTemplateItem(QtGui.QTableWidgetItem):
     def __init__(self, vm):
         super(VmTemplateItem, self).__init__()
-        self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
+        self.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
         self.vm = vm
 
-        if vm.template is not None:
+        if getattr(vm, 'template', None) is not None:
             self.setText(vm.template.name)
         else:
-            font = QFont()
-            font.setStyle(QFont.StyleItalic)
+            font = QtGui.QFont()
+            font.setStyle(QtGui.QFont.StyleItalic)
             self.setFont(font)
-            self.setTextColor(QColor("gray"))
-
-            if vm.is_appvm(): # and vm.template is None
-                self.setText("StandaloneVM")
-            elif vm.is_template():
-                self.setText("TemplateVM")
-            elif vm.qid == 0:
-                self.setText("AdminVM")
-            elif vm.is_netvm():
-                self.setText("NetVM")
-            else:
-                self.setText("---")
+            self.setTextColor(QtGui.QColor("gray"))
+
+            self.setText(vm.klass)
 
-        self.setTextAlignment(Qt.AlignVCenter)
+        self.setTextAlignment(QtCore.Qt.AlignVCenter)
 
     def __lt__(self, other):
         if self.vm.qid == 0:
@@ -306,27 +282,22 @@ class VmTemplateItem (QTableWidgetItem):
         elif other.vm.qid == 0:
             return False
         elif self.text() == other.text():
-            return self.vm.qid < other.vm.qid
-        else:
-            return super(VmTemplateItem, self).__lt__(other)
-
+            return self.vm.name < other.vm.name
+        return super(VmTemplateItem, self).__lt__(other)
 
 
-
-class VmNetvmItem (QTableWidgetItem):
+class VmNetvmItem(QtGui.QTableWidgetItem):
     def __init__(self, vm):
         super(VmNetvmItem, self).__init__()
-        self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
+        self.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
         self.vm = vm
 
-        if vm.is_netvm() and not vm.is_proxyvm():
+        if getattr(vm, 'netvm', None) is None:
             self.setText("n/a")
-        elif vm.netvm is not None:
-            self.setText(vm.netvm.name)
         else:
-            self.setText("---")
+            self.setText(vm.netvm.name)
 
-        self.setTextAlignment(Qt.AlignVCenter)
+        self.setTextAlignment(QtCore.Qt.AlignVCenter)
 
     def __lt__(self, other):
         if self.vm.qid == 0:
@@ -334,22 +305,19 @@ class VmNetvmItem (QTableWidgetItem):
         elif other.vm.qid == 0:
             return False
         elif self.text() == other.text():
-            return self.vm.qid < other.vm.qid
-        else:
-            return super(VmNetvmItem, self).__lt__(other)
+            return self.vm.name < other.vm.name
+        return super(VmNetvmItem, self).__lt__(other)
+
 
-class VmInternalItem(QTableWidgetItem):
+class VmInternalItem(QtGui.QTableWidgetItem):
     def __init__(self, vm):
         super(VmInternalItem, self).__init__()
-        self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
+        self.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
 
         self.vm = vm
-        self.internal = self.vm.internal
+        self.internal = vm.features.get('internal', False)
 
-        if self.internal:
-            self.setText("Yes")
-        else:
-            self.setText("")
+        self.setText("Yes" if self.internal else "")
 
     def __lt__(self, other):
         if self.vm.qid == 0:
@@ -359,144 +327,10 @@ class VmInternalItem(QTableWidgetItem):
         return super(VmInternalItem, self).__lt__(other)
 
 
-class VmUsageBarWidget (QWidget):
+# features man qvm-features
+class VmUpdateInfoWidget(QtGui.QWidget):
 
-    class VmUsageBarItem (QTableWidgetItem):
-        def __init__(self, value, vm):
-            super(VmUsageBarWidget.VmUsageBarItem, self).__init__()
-            self.value = value
-            self.vm = vm
-
-        def set_value(self, value):
-            self.value = value
-
-        def __lt__(self, other):
-            if self.vm.qid == 0:
-                return True
-            elif other.vm.qid == 0:
-                return False
-            elif self.value == other.value:
-                return self.vm.qid < other.vm.qid
-            else:
-                return int(self.value) < int(other.value)
-
-    def __init__(self, min, max, format, update_func, vm, load, hue=210, parent = None):
-        super (VmUsageBarWidget, self).__init__(parent)
-
-
-        self.min = min
-        self.max = max
-        self.update_func = update_func
-        self.value = min
-
-        self.widget = QProgressBar()
-        self.widget.setMinimum(min)
-        self.widget.setMaximum(max)
-        self.widget.setFormat(format)
-
-        self.widget.setStyleSheet(
-                                    "QProgressBar:horizontal{" +\
-                                        "border: 1px solid hsv({0}, 100, 250);".format(hue) +\
-                                        "border-radius: 4px;\
-                                        background: transparent;\
-                                        text-align: center;\
-                                    }\
-                                    QProgressBar::chunk:horizontal {\
-                                        background: qlineargradient(x1: 1, y1: 0.5, x2: 1, y2: 0.5, " +\
-                                        "stop: 0 hsv({0}, 170, 207),".format(hue) +
-                                        " stop: 1 white); \
-                                    }"
-            )
-
-        layout = QHBoxLayout()
-        layout.addWidget(self.widget)
-
-        self.setLayout(layout)
-        self.tableItem = self.VmUsageBarItem(min, vm)
-
-        self.update_load(vm, load)
-
-
-
-    def update_load(self, vm, load):
-        self.value = self.update_func(vm, load)
-        self.widget.setValue(self.value)
-        self.tableItem.set_value(self.value)
-
-class ChartWidget (QWidget):
-
-    class ChartItem (QTableWidgetItem):
-        def __init__(self, value, vm):
-            super(ChartWidget.ChartItem, self).__init__()
-            self.value = value
-            self.vm = vm
-
-        def set_value(self, value):
-            self.value = value
-
-        def __lt__(self, other):
-            if self.vm.qid == 0:
-                return True
-            elif other.vm.qid == 0:
-                return False
-            elif self.value == other.value:
-                return self.vm.qid < other.vm.qid
-            else:
-                return self.value < other.value
-
-    def __init__(self, vm, update_func, hue, load = 0, parent = None):
-        super (ChartWidget, self).__init__(parent)
-        self.update_func = update_func
-        self.hue = hue
-        if hue < 0 or hue > 255:
-            self.hue = 255
-        self.load = load
-        assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load)
-        self.load_history = [self.load]
-        self.tableItem = ChartWidget.ChartItem(self.load, vm)
-
-    def update_load (self, vm, load):
-        self.load = self.update_func(vm, load)
-
-        assert self.load >= 0, "load = {0}".format(self.load)
-        # assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load)
-        if self.load > 100:
-            # FIXME: This is an ugly workaround for cpu_load:/
-            self.load = 100
-
-        self.load_history.append (self.load)
-        self.tableItem.set_value(self.load)
-        self.repaint()
-
-    def paintEvent (self, Event = None):
-        p = QPainter (self)
-        dx = 4
-
-        W = self.width()
-        H = self.height() - 5
-        N = len(self.load_history)
-        if N > W/dx:
-            tail = N - W/dx
-            N = W/dx
-            self.load_history = self.load_history[tail:]
-
-        assert len(self.load_history) == N
-
-        for i in range (0, N-1):
-            val = self.load_history[N- i - 1]
-            sat = 70 + val*(255-70)/100
-            color = QColor.fromHsv (self.hue, sat, 255)
-            pen = QPen (color)
-            pen.setWidth(dx-1)
-            p.setPen(pen)
-            if val > 0:
-                p.drawLine (W - i*dx - dx, H , W - i*dx - dx, H - (H - 5) * val/100)
-
-
-
-class VmUpdateInfoWidget(QWidget):
-
-    class VmUpdateInfoItem (QTableWidgetItem):
+    class VmUpdateInfoItem(QtGui.QTableWidgetItem):
         def __init__(self, value, vm):
             super(VmUpdateInfoWidget.VmUpdateInfoItem, self).__init__()
             self.value = 0
@@ -517,91 +351,53 @@ class VmUpdateInfoWidget(QWidget):
             elif other.vm.qid == 0:
                 return False
             elif self.value == other.value:
-                return self.vm.qid < other.vm.qid
-            else:
-                return self.value < other.value
+                return self.vm.name < other.vm.name
+            return self.value < other.value
 
-    def __init__(self, vm, show_text=True, parent = None):
-        super (VmUpdateInfoWidget, self).__init__(parent)
-        layout = QHBoxLayout ()
+    def __init__(self, vm, show_text=True, parent=None):
+        super(VmUpdateInfoWidget, self).__init__(parent)
+        layout = QtGui.QHBoxLayout()
         self.show_text = show_text
         if self.show_text:
-            self.label=QLabel("")
-            layout.addWidget(self.label, alignment=Qt.AlignCenter)
+            self.label = QtGui.QLabel("")
+            layout.addWidget(self.label, alignment=QtCore.Qt.AlignCenter)
         else:
-            self.icon =  QLabel("")
-            layout.addWidget(self.icon, alignment=Qt.AlignCenter)
+            self.icon = QtGui.QLabel("")
+            layout.addWidget(self.icon, alignment=QtCore.Qt.AlignCenter)
         self.setLayout(layout)
 
         self.previous_outdated_state = None
         self.previous_update_recommended = None
         self.value = None
-        self.tableItem = VmUpdateInfoWidget.VmUpdateInfoItem(self.value, vm)
+        self.table_item = VmUpdateInfoWidget.VmUpdateInfoItem(self.value, vm)
 
     def update_outdated(self, vm):
-        if vm.type == "HVM":
-            return
-
-        if vm.is_outdated():
-            outdated_state = "outdated"
-        # During TemplateVM shutdown, there's an interval of a few seconds
-        # during which vm.template.is_running() returns false but
-        # vm.is_outdated() does not yet return true, so the icon disappears.
-        # This looks goofy, but we've decided not to fix it at this time
-        # (2015-02-09).
-        elif vm.template and vm.template.is_running():
-            outdated_state = "to-be-outdated"
-        else:
-            outdated_state = None
 
+        outdated_state = False
+
+        try:
+            for vol in vm.volumes:
+                if vol.is_outdated():
+                    outdated_state = "outdated"
+                    break
+        except AttributeError:
+            pass
+
+        if not outdated_state and getattr(vm, 'template', None)\
+                and vm.template.is_running():
+            outdated_state = "to-be-outdated"
         if outdated_state != self.previous_outdated_state:
             self.update_status_widget(outdated_state)
-
         self.previous_outdated_state = outdated_state
 
-        if not vm.is_updateable():
-            return
-
-        if vm.qid == 0:
-            update_recommended = self.previous_update_recommended
-            if os.path.exists(qubes_dom0_updates_stat_file):
-                update_recommended = True
-            else:
-                update_recommended = False
-
-        else:
-            update_recommended = self.previous_update_recommended
-            stat_file_path = vm.dir_path + '/' + vm_files["updates_stat_file"]
-            if not os.path.exists(stat_file_path):
-                update_recommended = False
-            else:
-                if (not hasattr(vm, "updates_stat_file_read_time")) or vm.updates_stat_file_read_time <= os.path.getmtime(stat_file_path):
-
-                        stat_file = open(stat_file_path, "r")
-                        updates = stat_file.read().strip()
-                        stat_file.close()
-                        if updates.isdigit():
-                            updates = int(updates)
-                        else:
-                            updates = 0
-
-                        if updates == 0:
-                            update_recommended = False
-                        else:
-                            update_recommended = True
-                        vm.updates_stat_file_read_time = time.time()
-
-        if update_recommended and not self.previous_update_recommended:
-            self.update_status_widget("update")
-        elif self.previous_update_recommended and not update_recommended:
-            self.update_status_widget(None)
-
-        self.previous_update_recommended = update_recommended
-
+        updates_available = vm.features.get('updates-available', False)
+        if updates_available != self.previous_update_recommended:
+            self.update_status_widget("update" if updates_available else None)
+        self.previous_update_recommended = updates_available
 
     def update_status_widget(self, state):
         self.value = state
-        self.tableItem.set_value(state)
+        self.table_item.set_value(state)
         if state == "update":
             label_text = "<font color=\"#CCCC00\">Check updates</font>"
             icon_path = ":/update-recommended.png"
@@ -618,7 +414,7 @@ class VmUpdateInfoWidget(QWidget):
             tooltip_text = self.tr(
                 "The TemplateVM must be stopped before changes from its "
                 "current session can be picked up by this VM.")
-        elif state is None:
+        else:
             label_text = ""
             icon_path = None
             tooltip_text = None
@@ -632,26 +428,27 @@ class VmUpdateInfoWidget(QWidget):
                 self.icon = VmIconWidget(icon_path, True, 0.7)
                 self.icon.setToolTip(tooltip_text)
             else:
-                self.icon = QLabel(label_text)
-            self.layout().addWidget(self.icon, alignment=Qt.AlignCenter)
+                self.icon = QtGui.QLabel(label_text)
+            self.layout().addWidget(self.icon, alignment=QtCore.Qt.AlignCenter)
 
 
-class VmSizeOnDiskItem (QTableWidgetItem):
+class VmSizeOnDiskItem(QtGui.QTableWidgetItem):
     def __init__(self, vm):
         super(VmSizeOnDiskItem, self).__init__()
-        self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
+        self.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
 
         self.vm = vm
         self.value = 0
         self.update()
-        self.setTextAlignment(Qt.AlignVCenter)
+        self.setTextAlignment(QtCore.Qt.AlignVCenter)
 
     def update(self):
         if self.vm.qid == 0:
             self.setText("n/a")
         else:
-            self.value = self.vm.get_disk_utilization()/(1024*1024)
-            self.setText( str(self.value) + " MiB")
+            self.value = 10
+            self.value = round(self.vm.get_disk_utilization()/(1024*1024), 2)
+            self.setText(str(self.value) + " MiB")
 
     def __lt__(self, other):
         if self.vm.qid == 0:
@@ -659,21 +456,18 @@ class VmSizeOnDiskItem (QTableWidgetItem):
         elif other.vm.qid == 0:
             return False
         elif self.value == other.value:
-            return self.vm.qid < other.vm.qid
-        else:
-            return self.value < other.value
+            return self.vm.name < other.vm.name
+        return self.value < other.value
+
 
-class VmIPItem(QTableWidgetItem):
+class VmIPItem(QtGui.QTableWidgetItem):
     def __init__(self, vm):
         super(VmIPItem, self).__init__()
-        self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
+        self.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
 
         self.vm = vm
-        self.ip = self.vm.ip
-        if self.ip:
-            self.setText(self.ip)
-        else:
-            self.setText("n/a")
+        self.ip = getattr(self.vm, 'ip', None)
+        self.setText(self.ip if self.ip is not None else 'n/a')
 
     def __lt__(self, other):
         if self.vm.qid == 0:
@@ -682,13 +476,14 @@ class VmIPItem(QTableWidgetItem):
             return False
         return super(VmIPItem, self).__lt__(other)
 
-class VmIncludeInBackupsItem(QTableWidgetItem):
+
+class VmIncludeInBackupsItem(QtGui.QTableWidgetItem):
     def __init__(self, vm):
         super(VmIncludeInBackupsItem, self).__init__()
-        self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
+        self.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
 
         self.vm = vm
-        if self.vm.include_in_backups:
+        if getattr(self.vm, 'include_in_backups', None):
             self.setText("Yes")
         else:
             self.setText("")
@@ -699,18 +494,18 @@ class VmIncludeInBackupsItem(QTableWidgetItem):
         elif other.vm.qid == 0:
             return False
         elif self.vm.include_in_backups == other.vm.include_in_backups:
-            return self.vm.qid < other.vm.qid
-        else:
-            return self.vm.include_in_backups < other.vm.include_in_backups
+            return self.vm.name < other.vm.name
+        return self.vm.include_in_backups < other.vm.include_in_backups
+
 
-class VmLastBackupItem(QTableWidgetItem):
+class VmLastBackupItem(QtGui.QTableWidgetItem):
     def __init__(self, vm):
         super(VmLastBackupItem, self).__init__()
-        self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
+        self.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
 
         self.vm = vm
-        if self.vm.backup_timestamp:
-            self.setText(str(self.vm.backup_timestamp.date()))
+        if getattr(self.vm, 'backup_timestamp', None):
+            self.setText(self.vm.backup_timestamp)
         else:
             self.setText("")
 
@@ -720,10 +515,9 @@ class VmLastBackupItem(QTableWidgetItem):
         elif other.vm.qid == 0:
             return False
         elif self.vm.backup_timestamp == other.vm.backup_timestamp:
-            return self.vm.qid < other.vm.qid
+            return self.vm.name < other.vm.name
         elif not self.vm.backup_timestamp:
             return False
         elif not other.vm.backup_timestamp:
             return True
-        else:
-            return self.vm.backup_timestamp < other.vm.backup_timestamp
+        return self.vm.backup_timestamp < other.vm.backup_timestamp

+ 4 - 6
qubesmanager/thread_monitor.py

@@ -14,18 +14,17 @@
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
 #
 #
 
 
-from PyQt4.QtCore import *
+import PyQt4.QtCore  # pylint: disable=import-error
 
 import threading
 
-class ThreadMonitor(QObject):
+class ThreadMonitor(PyQt4.QtCore.QObject):
     def __init__(self):
         self.success = True
         self.error_msg = None
@@ -41,4 +40,3 @@ class ThreadMonitor(QObject):
 
     def set_finished(self):
         self.event_finished.set()
-

+ 3 - 4
qubesmanager/utils.py

@@ -20,12 +20,11 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 
-import functools
 import os
 import re
 import qubesadmin
 
-from PyQt4.QtGui import QIcon
+from PyQt4.QtGui import QIcon  # pylint: disable=import-error
 
 def _filter_internal(vm):
     return (not vm.klass == 'AdminVM'
@@ -153,11 +152,11 @@ def get_path_from_vm(vm, service_name):
 
     if not vm:
         return None
-    stdout, stderr = vm.run_service_for_stdio(service_name)
+    stdout, _stderr = vm.run_service_for_stdio(service_name)
 
     untrusted_path = stdout.decode(encoding='ascii')[:path_max_len]
 
-    if len(untrusted_path) == 0:
+    if not untrusted_path:
         return None
     if path_re.match(untrusted_path):
         assert '../' not in untrusted_path

+ 11 - 1
rpm_spec/qmgr.spec

@@ -43,7 +43,9 @@ cp qubesmanager/qvm_about.sh $RPM_BUILD_ROOT/usr/libexec/qubes-manager/
 mkdir -p $RPM_BUILD_ROOT/usr/share/applications
 cp qubes-global-settings.desktop $RPM_BUILD_ROOT/usr/share/applications/
 cp qubes-vm-create.desktop $RPM_BUILD_ROOT/usr/share/applications/
-
+cp qubes-backup.desktop $RPM_BUILD_ROOT/usr/share/applications/
+cp qubes-backup-restore.desktop $RPM_BUILD_ROOT/usr/share/applications/
+cp qubes-qube-manager.desktop $RPM_BUILD_ROOT/usr/share/applications/
 
 %post
 update-desktop-database &> /dev/null || :
@@ -60,6 +62,9 @@ rm -rf $RPM_BUILD_ROOT
 /usr/bin/qubes-vm-settings
 /usr/bin/qubes-vm-create
 /usr/bin/qubes-vm-boot-from-device
+/usr/bin/qubes-backup
+/usr/bin/qubes-backup-restore
+/usr/bin/qubes-qube-manager
 /usr/libexec/qubes-manager/mount_for_backup.sh
 /usr/libexec/qubes-manager/qvm_about.sh
 
@@ -83,6 +88,7 @@ rm -rf $RPM_BUILD_ROOT
 %{python3_sitelib}/qubesmanager/informationnotes.py
 %{python3_sitelib}/qubesmanager/create_new_vm.py
 %{python3_sitelib}/qubesmanager/thread_monitor.py
+%{python3_sitelib}/qubesmanager/qube_manager.py
 %{python3_sitelib}/qubesmanager/utils.py
 %{python3_sitelib}/qubesmanager/bootfromdevice.py
 
@@ -100,6 +106,7 @@ rm -rf $RPM_BUILD_ROOT
 %{python3_sitelib}/qubesmanager/ui_about.py
 %{python3_sitelib}/qubesmanager/ui_releasenotes.py
 %{python3_sitelib}/qubesmanager/ui_informationnotes.py
+%{python3_sitelib}/qubesmanager/ui_qubemanager.py
 %{python3_sitelib}/qubesmanager/i18n/qubesmanager_*.qm
 %{python3_sitelib}/qubesmanager/i18n/qubesmanager_*.ts
 
@@ -108,3 +115,6 @@ rm -rf $RPM_BUILD_ROOT
 
 /usr/share/applications/qubes-global-settings.desktop
 /usr/share/applications/qubes-vm-create.desktop
+/usr/share/applications/qubes-backup.desktop
+/usr/share/applications/qubes-backup-restore.desktop
+/usr/share/applications/qubes-qube-manager.desktop

+ 4 - 1
setup.py

@@ -21,6 +21,9 @@ if __name__ == '__main__':
                 'qubes-global-settings = qubesmanager.global_settings:main',
                 'qubes-vm-settings = qubesmanager.settings:main',
                 'qubes-vm-create = qubesmanager.create_new_vm:main',
-                'qubes-vm-boot-from-device = qubesmanager.bootfromdevice:main'
+                'qubes-vm-boot-from-device = qubesmanager.bootfromdevice:main',
+                'qubes-backup = qubesmanager.backup:main',
+                'qubes-backup-restore = qubesmanager.restore:main',
+                'qubes-qube-manager = qubesmanager.qube_manager:main'
             ],
         })

+ 5 - 0
test-packages/qubes/backup/__init__.py

@@ -0,0 +1,5 @@
+class BackupCanceledError(BaseException):
+
+    tmpdir = None
+
+    pass

+ 1 - 0
test-packages/qubes/storage/__init__.py

@@ -0,0 +1 @@
+pass

+ 2 - 0
test-packages/qubes/storage/file.py

@@ -0,0 +1,2 @@
+def get_disk_usage(*args):
+    return 0

+ 4 - 0
test-packages/qubesadmin/__init__.py

@@ -0,0 +1,4 @@
+class Qubes(object):
+    pass
+
+DEFAULT = object()

+ 1 - 0
test-packages/qubesadmin/backup/__init__.py

@@ -0,0 +1 @@
+pass

+ 19 - 0
test-packages/qubesadmin/backup/restore/__init__.py

@@ -0,0 +1,19 @@
+class BackupRestore(object):
+
+    options = object()
+    canceled = None
+
+    def get_restore_info(self, *args):
+        pass
+
+    def restore_do(self, *args):
+        pass
+
+    def get_restore_info(self, *args):
+        pass
+
+    def restore_info_verify(self, *args):
+        pass
+
+    def get_restore_summary(self, *args):
+        pass

+ 4 - 0
test-packages/qubesadmin/devices.py

@@ -0,0 +1,4 @@
+### mock qubesadmin.devices module
+
+class DeviceAssignment(object):
+    pass

+ 5 - 0
test-packages/qubesadmin/events.py

@@ -0,0 +1,5 @@
+class EventsDispatcher(object):
+
+    def add_handler(self, *args):
+        pass
+    pass

+ 6 - 0
test-packages/qubesadmin/exc.py

@@ -0,0 +1,6 @@
+### mock qubesadmin.exc module
+# pylint: disable=unused-variable
+
+
+class QubesException(BaseException):
+    pass

+ 4 - 0
test-packages/qubesadmin/firewall.py

@@ -0,0 +1,4 @@
+### mock qubesadmin.firewall module
+
+class Rule(object):
+    pass

+ 9 - 0
test-packages/qubesadmin/tools/__init__.py

@@ -0,0 +1,9 @@
+### mock qubesadmin.tools module
+
+class QubesArgumentParser(object):
+
+    def add_argument(self, *args, **kwargs):
+        pass
+
+    def set_defaults(self, *args, **kwargs):
+        pass

+ 4 - 0
test-packages/qubesadmin/tools/qvm_start.py

@@ -0,0 +1,4 @@
+### mock qvm_start module
+
+def main(*args, **kwargs):
+    pass

+ 10 - 0
test-packages/qubesadmin/utils.py

@@ -0,0 +1,10 @@
+### mock qubesadmin.utils module
+
+def parse_size(*args, **kwargs):
+    return args[0]
+
+def updates_vms_status(*args, **kwargs):
+    return args[0]
+
+def size_to_human(*args, **kwargs):
+    return args[0]

+ 145 - 107
ui/backupdlg.ui

@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>650</width>
-    <height>399</height>
+    <width>737</width>
+    <height>420</height>
    </rect>
   </property>
   <property name="windowTitle">
@@ -23,73 +23,8 @@
    <layout class="QVBoxLayout" name="verticalLayout">
     <item>
      <layout class="QGridLayout" name="gridLayout_2">
-      <item row="1" column="1">
-       <widget class="QPushButton" name="shutdown_running_vms_button">
-        <property name="sizePolicy">
-         <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
-          <horstretch>0</horstretch>
-          <verstretch>0</verstretch>
-         </sizepolicy>
-        </property>
-        <property name="text">
-         <string>Shutdown all running selected VMs</string>
-        </property>
-        <property name="icon">
-         <iconset resource="../resources.qrc">
-          <normaloff>:/shutdownvm.png</normaloff>:/shutdownvm.png</iconset>
-        </property>
-       </widget>
-      </item>
-      <item row="2" column="1">
-       <widget class="QPushButton" name="refresh_button">
-        <property name="sizePolicy">
-         <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
-          <horstretch>0</horstretch>
-          <verstretch>0</verstretch>
-         </sizepolicy>
-        </property>
-        <property name="text">
-         <string>Refresh running states.</string>
-        </property>
-       </widget>
-      </item>
-      <item row="1" column="0" rowspan="2">
-       <widget class="QLabel" name="running_vms_warning">
-        <property name="sizePolicy">
-         <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
-          <horstretch>0</horstretch>
-          <verstretch>0</verstretch>
-         </sizepolicy>
-        </property>
-        <property name="font">
-         <font>
-          <weight>75</weight>
-          <italic>true</italic>
-          <bold>true</bold>
-         </font>
-        </property>
-        <property name="styleSheet">
-         <string notr="true">color:rgb(255, 0, 0)</string>
-        </property>
-        <property name="text">
-         <string>Some of the selected VMs are running (red). Running VMs cannot be backed up!</string>
-        </property>
-        <property name="wordWrap">
-         <bool>true</bool>
-        </property>
-       </widget>
-      </item>
       <item row="0" column="0">
        <widget class="QLabel" name="label_4">
-        <property name="font">
-         <font>
-          <pointsize>9</pointsize>
-          <weight>50</weight>
-          <italic>false</italic>
-          <bold>false</bold>
-          <underline>false</underline>
-         </font>
-        </property>
         <property name="text">
          <string>Select VMs to backup:</string>
         </property>
@@ -145,12 +80,79 @@
       </item>
      </layout>
     </item>
+    <item>
+     <widget class="QCheckBox" name="compress_checkbox">
+      <property name="text">
+       <string>Compress the backup</string>
+      </property>
+      <property name="checked">
+       <bool>true</bool>
+      </property>
+     </widget>
+    </item>
+    <item>
+     <widget class="QLabel" name="unrecognized_config_label">
+      <property name="palette">
+       <palette>
+        <active>
+         <colorrole role="WindowText">
+          <brush brushstyle="SolidPattern">
+           <color alpha="255">
+            <red>255</red>
+            <green>0</green>
+            <blue>0</blue>
+           </color>
+          </brush>
+         </colorrole>
+        </active>
+        <inactive>
+         <colorrole role="WindowText">
+          <brush brushstyle="SolidPattern">
+           <color alpha="255">
+            <red>255</red>
+            <green>0</green>
+            <blue>0</blue>
+           </color>
+          </brush>
+         </colorrole>
+        </inactive>
+        <disabled>
+         <colorrole role="WindowText">
+          <brush brushstyle="SolidPattern">
+           <color alpha="255">
+            <red>139</red>
+            <green>142</green>
+            <blue>142</blue>
+           </color>
+          </brush>
+         </colorrole>
+        </disabled>
+       </palette>
+      </property>
+      <property name="font">
+       <font>
+        <weight>75</weight>
+        <italic>true</italic>
+        <bold>true</bold>
+       </font>
+      </property>
+      <property name="text">
+       <string>Warning: unrecognized data found in configuration files. </string>
+      </property>
+     </widget>
+    </item>
    </layout>
   </widget>
   <widget class="QWizardPage" name="select_dir_page">
    <layout class="QGridLayout" name="gridLayout_5">
     <item row="0" column="0">
      <widget class="QGroupBox" name="groupBox">
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
       <property name="title">
        <string>Backup destination directory</string>
       </property>
@@ -188,73 +190,106 @@
       </layout>
      </widget>
     </item>
-    <item row="1" column="0">
+    <item row="3" column="0">
+     <widget class="QGroupBox" name="groupBox_3">
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <property name="title">
+       <string>Save backup profile</string>
+      </property>
+      <widget class="QLabel" name="label_8">
+       <property name="geometry">
+        <rect>
+         <x>10</x>
+         <y>30</y>
+         <width>267</width>
+         <height>25</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Save settings as default backup profile:</string>
+       </property>
+      </widget>
+      <widget class="QCheckBox" name="save_profile_checkbox">
+       <property name="geometry">
+        <rect>
+         <x>270</x>
+         <y>30</y>
+         <width>539</width>
+         <height>25</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string/>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+     </widget>
+    </item>
+    <item row="2" column="0">
      <widget class="QGroupBox" name="groupBox_2">
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
       <property name="title">
        <string>Backup security</string>
       </property>
       <layout class="QFormLayout" name="formLayout">
-       <item row="2" column="0">
+       <property name="fieldGrowthPolicy">
+        <enum>QFormLayout::AllNonFixedFieldsGrow</enum>
+       </property>
+       <item row="1" column="0">
         <widget class="QLabel" name="label_12">
          <property name="text">
           <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Encryption / Verification&lt;br/&gt;passphrase:&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
          </property>
         </widget>
        </item>
-       <item row="0" column="0">
-        <widget class="QLabel" name="label_10">
-         <property name="text">
-          <string>Encrypt backup:</string>
-         </property>
-        </widget>
-       </item>
-       <item row="0" column="1">
-        <widget class="QCheckBox" name="encryption_checkbox">
-         <property name="layoutDirection">
-          <enum>Qt::LeftToRight</enum>
-         </property>
-         <property name="text">
-          <string/>
-         </property>
-         <property name="checked">
-          <bool>true</bool>
+       <item row="1" column="1">
+        <widget class="QLineEdit" name="passphrase_line_edit">
+         <property name="echoMode">
+          <enum>QLineEdit::Password</enum>
          </property>
         </widget>
        </item>
-       <item row="5" column="0">
+       <item row="4" column="0">
         <widget class="QLabel" name="label_11">
          <property name="text">
           <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Reenter passphrase:&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
          </property>
         </widget>
        </item>
-       <item row="2" column="1">
-        <widget class="QLineEdit" name="passphrase_line_edit">
-         <property name="echoMode">
-          <enum>QLineEdit::Password</enum>
-         </property>
-        </widget>
-       </item>
-       <item row="5" column="1">
+       <item row="4" column="1">
         <widget class="QLineEdit" name="passphrase_line_edit_verify">
          <property name="echoMode">
           <enum>QLineEdit::Password</enum>
          </property>
         </widget>
        </item>
-       <item row="6" column="1">
-        <widget class="QLabel" name="label">
-         <property name="text">
-          <string>&lt;b&gt;Note:&lt;/b&gt; Even if you choose not to encrypt your backup, a password is still required in order to provide authenticated verification of the data. See the emergency restore instructions for more info.</string>
-         </property>
-         <property name="wordWrap">
-          <bool>true</bool>
-         </property>
-        </widget>
-       </item>
       </layout>
      </widget>
     </item>
+    <item row="1" column="0">
+     <widget class="QLabel" name="label">
+      <property name="font">
+       <font>
+        <italic>true</italic>
+       </font>
+      </property>
+      <property name="text">
+       <string>NOTE: Only running VMs are listed.</string>
+      </property>
+     </widget>
+    </item>
    </layout>
   </widget>
   <widget class="QWizardPage" name="confirm_page">
@@ -281,8 +316,8 @@
        <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
 &lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
 p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Cantarell'; font-size:11pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans Serif'; font-size:9pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
       </property>
      </widget>
     </item>
@@ -324,6 +359,9 @@ p, li { white-space: pre-wrap; }
     </item>
     <item>
      <widget class="QProgressBar" name="progress_bar">
+      <property name="maximum">
+       <number>0</number>
+      </property>
       <property name="value">
        <number>0</number>
       </property>

+ 45 - 42
ui/newfwruledlg.ui

@@ -31,10 +31,22 @@
      <property name="verticalSpacing">
       <number>6</number>
      </property>
-     <item row="2" column="0">
-      <widget class="QLabel" name="label">
+     <item row="2" column="2">
+      <widget class="QRadioButton" name="udp_radio">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="minimumSize">
+        <size>
+         <width>0</width>
+         <height>0</height>
+        </size>
+       </property>
        <property name="text">
-        <string>Protocol</string>
+        <string>UDP</string>
        </property>
       </widget>
      </item>
@@ -45,13 +57,6 @@
        </property>
       </widget>
      </item>
-     <item row="1" column="1" colspan="3">
-      <widget class="QComboBox" name="serviceComboBox">
-       <property name="editable">
-        <bool>true</bool>
-       </property>
-      </widget>
-     </item>
      <item row="0" column="0">
       <widget class="QLabel" name="label_2">
        <property name="text">
@@ -59,15 +64,8 @@
        </property>
       </widget>
      </item>
-     <item row="0" column="1" colspan="3">
-      <widget class="QComboBox" name="addressComboBox">
-       <property name="editable">
-        <bool>true</bool>
-       </property>
-      </widget>
-     </item>
-     <item row="2" column="1">
-      <widget class="QRadioButton" name="tcp_radio">
+     <item row="2" column="3">
+      <widget class="QRadioButton" name="any_radio">
        <property name="sizePolicy">
         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
          <horstretch>0</horstretch>
@@ -76,36 +74,37 @@
        </property>
        <property name="minimumSize">
         <size>
-         <width>0</width>
+         <width>71</width>
          <height>0</height>
         </size>
        </property>
+       <property name="toolTip">
+        <string/>
+       </property>
        <property name="text">
-        <string>TCP</string>
+        <string> Any   </string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
        </property>
       </widget>
      </item>
-     <item row="2" column="2">
-      <widget class="QRadioButton" name="udp_radio">
-       <property name="sizePolicy">
-        <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
-         <horstretch>0</horstretch>
-         <verstretch>0</verstretch>
-        </sizepolicy>
-       </property>
-       <property name="minimumSize">
-        <size>
-         <width>0</width>
-         <height>0</height>
-        </size>
+     <item row="0" column="1" colspan="3">
+      <widget class="QComboBox" name="addressComboBox">
+       <property name="editable">
+        <bool>true</bool>
        </property>
-       <property name="text">
-        <string>UDP</string>
+      </widget>
+     </item>
+     <item row="1" column="1" colspan="3">
+      <widget class="QComboBox" name="serviceComboBox">
+       <property name="editable">
+        <bool>true</bool>
        </property>
       </widget>
      </item>
-     <item row="2" column="3">
-      <widget class="QRadioButton" name="any_radio">
+     <item row="2" column="1">
+      <widget class="QRadioButton" name="tcp_radio">
        <property name="sizePolicy">
         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
          <horstretch>0</horstretch>
@@ -114,15 +113,19 @@
        </property>
        <property name="minimumSize">
         <size>
-         <width>71</width>
+         <width>0</width>
          <height>0</height>
         </size>
        </property>
        <property name="text">
-        <string> Any   </string>
+        <string>TCP</string>
        </property>
-       <property name="checked">
-        <bool>true</bool>
+      </widget>
+     </item>
+     <item row="2" column="0">
+      <widget class="QLabel" name="label">
+       <property name="text">
+        <string>Protocol</string>
        </property>
       </widget>
      </item>

+ 841 - 0
ui/qubemanager.ui

@@ -0,0 +1,841 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>VmManagerWindow</class>
+ <widget class="QMainWindow" name="VmManagerWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>769</width>
+    <height>385</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="Fixed" vsizetype="Maximum">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="contextMenuPolicy">
+   <enum>Qt::DefaultContextMenu</enum>
+  </property>
+  <property name="windowTitle">
+   <string>Qube Manager</string>
+  </property>
+  <property name="windowIcon">
+   <iconset theme="qubes-manager">
+    <normaloff/>
+   </iconset>
+  </property>
+  <property name="locale">
+   <locale language="English" country="UnitedStates"/>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <property name="enabled">
+    <bool>true</bool>
+   </property>
+   <property name="sizePolicy">
+    <sizepolicy hsizetype="Fixed" vsizetype="Expanding">
+     <horstretch>0</horstretch>
+     <verstretch>0</verstretch>
+    </sizepolicy>
+   </property>
+   <property name="acceptDrops">
+    <bool>false</bool>
+   </property>
+   <property name="autoFillBackground">
+    <bool>true</bool>
+   </property>
+   <property name="locale">
+    <locale language="English" country="UnitedStates"/>
+   </property>
+   <layout class="QGridLayout" name="gridLayout">
+    <property name="sizeConstraint">
+     <enum>QLayout::SetDefaultConstraint</enum>
+    </property>
+    <property name="margin">
+     <number>0</number>
+    </property>
+    <property name="spacing">
+     <number>0</number>
+    </property>
+    <item row="0" column="0">
+     <layout class="QHBoxLayout" name="searchContainer">
+      <property name="spacing">
+       <number>6</number>
+      </property>
+      <property name="leftMargin">
+       <number>6</number>
+      </property>
+      <item>
+       <widget class="QLabel" name="label">
+        <property name="text">
+         <string>Search:</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </item>
+    <item row="1" column="0">
+     <widget class="QTableWidget" name="table">
+      <property name="minimumSize">
+       <size>
+        <width>0</width>
+        <height>0</height>
+       </size>
+      </property>
+      <property name="maximumSize">
+       <size>
+        <width>16777215</width>
+        <height>16777215</height>
+       </size>
+      </property>
+      <property name="sizeIncrement">
+       <size>
+        <width>200</width>
+        <height>30</height>
+       </size>
+      </property>
+      <property name="contextMenuPolicy">
+       <enum>Qt::CustomContextMenu</enum>
+      </property>
+      <property name="acceptDrops">
+       <bool>false</bool>
+      </property>
+      <property name="lineWidth">
+       <number>0</number>
+      </property>
+      <property name="verticalScrollBarPolicy">
+       <enum>Qt::ScrollBarAsNeeded</enum>
+      </property>
+      <property name="horizontalScrollBarPolicy">
+       <enum>Qt::ScrollBarAsNeeded</enum>
+      </property>
+      <property name="alternatingRowColors">
+       <bool>true</bool>
+      </property>
+      <property name="selectionMode">
+       <enum>QAbstractItemView::SingleSelection</enum>
+      </property>
+      <property name="selectionBehavior">
+       <enum>QAbstractItemView::SelectRows</enum>
+      </property>
+      <property name="showGrid">
+       <bool>false</bool>
+      </property>
+      <property name="gridStyle">
+       <enum>Qt::NoPen</enum>
+      </property>
+      <property name="sortingEnabled">
+       <bool>true</bool>
+      </property>
+      <property name="cornerButtonEnabled">
+       <bool>false</bool>
+      </property>
+      <property name="rowCount">
+       <number>10</number>
+      </property>
+      <property name="columnCount">
+       <number>11</number>
+      </property>
+      <attribute name="horizontalHeaderCascadingSectionResizes">
+       <bool>false</bool>
+      </attribute>
+      <attribute name="horizontalHeaderDefaultSectionSize">
+       <number>150</number>
+      </attribute>
+      <attribute name="horizontalHeaderMinimumSectionSize">
+       <number>150</number>
+      </attribute>
+      <attribute name="verticalHeaderVisible">
+       <bool>false</bool>
+      </attribute>
+      <attribute name="verticalHeaderCascadingSectionResizes">
+       <bool>false</bool>
+      </attribute>
+      <row>
+       <property name="text">
+        <string>Nowy wiersz</string>
+       </property>
+      </row>
+      <row/>
+      <row/>
+      <row/>
+      <row/>
+      <row/>
+      <row/>
+      <row/>
+      <row/>
+      <row/>
+      <column>
+       <property name="text">
+        <string/>
+       </property>
+      </column>
+      <column>
+       <property name="text">
+        <string/>
+       </property>
+      </column>
+      <column>
+       <property name="text">
+        <string>Name</string>
+       </property>
+       <property name="toolTip">
+        <string>VM name</string>
+       </property>
+      </column>
+      <column>
+       <property name="text">
+        <string>State</string>
+       </property>
+       <property name="toolTip">
+        <string>Update info</string>
+       </property>
+      </column>
+      <column>
+       <property name="text">
+        <string>Template</string>
+       </property>
+       <property name="toolTip">
+        <string>VM's template</string>
+       </property>
+      </column>
+      <column>
+       <property name="text">
+        <string>NetVM</string>
+       </property>
+       <property name="toolTip">
+        <string>VM's netVM</string>
+       </property>
+      </column>
+      <column>
+       <property name="text">
+        <string>Size</string>
+       </property>
+      </column>
+      <column>
+       <property name="text">
+        <string>Internal</string>
+       </property>
+      </column>
+      <column>
+       <property name="text">
+        <string>IP</string>
+       </property>
+      </column>
+      <column>
+       <property name="text">
+        <string>Backups</string>
+       </property>
+      </column>
+      <column>
+       <property name="text">
+        <string>Last backup</string>
+       </property>
+      </column>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QMenuBar" name="menubar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>769</width>
+     <height>28</height>
+    </rect>
+   </property>
+   <property name="contextMenuPolicy">
+    <enum>Qt::CustomContextMenu</enum>
+   </property>
+   <widget class="QMenu" name="menu_system">
+    <property name="title">
+     <string>&amp;System</string>
+    </property>
+    <addaction name="action_global_settings"/>
+    <addaction name="action_show_network"/>
+    <addaction name="action_backup"/>
+    <addaction name="action_restore"/>
+   </widget>
+   <widget class="QMenu" name="menu_view">
+    <property name="title">
+     <string>&amp;View</string>
+    </property>
+    <addaction name="action_vm_type"/>
+    <addaction name="action_label"/>
+    <addaction name="action_name"/>
+    <addaction name="action_state"/>
+    <addaction name="action_template"/>
+    <addaction name="action_netvm"/>
+    <addaction name="action_size_on_disk"/>
+    <addaction name="action_internal"/>
+    <addaction name="action_ip"/>
+    <addaction name="action_backups"/>
+    <addaction name="action_last_backup"/>
+    <addaction name="separator"/>
+    <addaction name="action_toolbar"/>
+    <addaction name="action_menubar"/>
+    <addaction name="separator"/>
+    <addaction name="separator"/>
+    <addaction name="action_search"/>
+    <addaction name="action_refresh_list"/>
+   </widget>
+   <widget class="QMenu" name="menu_vm">
+    <property name="title">
+     <string>&amp;Qube</string>
+    </property>
+    <widget class="QMenu" name="logs_menu">
+     <property name="title">
+      <string>&amp;Logs</string>
+     </property>
+     <property name="icon">
+      <iconset resource="../resources.qrc">
+       <normaloff>:/log.png</normaloff>:/log.png</iconset>
+     </property>
+    </widget>
+    <widget class="QMenu" name="blk_menu">
+     <property name="title">
+      <string>Attach/detach &amp;block devices</string>
+     </property>
+     <property name="icon">
+      <iconset resource="../resources.qrc">
+       <normaloff>:/mount.png</normaloff>:/mount.png</iconset>
+     </property>
+    </widget>
+    <addaction name="action_createvm"/>
+    <addaction name="action_removevm"/>
+    <addaction name="action_clonevm"/>
+    <addaction name="separator"/>
+    <addaction name="action_resumevm"/>
+    <addaction name="action_pausevm"/>
+    <addaction name="action_shutdownvm"/>
+    <addaction name="action_restartvm"/>
+    <addaction name="action_killvm"/>
+    <addaction name="separator"/>
+    <addaction name="action_settings"/>
+    <addaction name="action_editfwrules"/>
+    <addaction name="action_appmenus"/>
+    <addaction name="action_updatevm"/>
+    <addaction name="action_run_command_in_vm"/>
+    <addaction name="action_set_keyboard_layout"/>
+    <addaction name="separator"/>
+    <addaction name="logs_menu"/>
+    <addaction name="blk_menu"/>
+   </widget>
+   <widget class="QMenu" name="menu_about">
+    <property name="title">
+     <string>&amp;About</string>
+    </property>
+    <addaction name="action_about_qubes"/>
+   </widget>
+   <addaction name="menu_system"/>
+   <addaction name="menu_vm"/>
+   <addaction name="menu_view"/>
+   <addaction name="menu_about"/>
+  </widget>
+  <widget class="QToolBar" name="toolbar">
+   <property name="contextMenuPolicy">
+    <enum>Qt::CustomContextMenu</enum>
+   </property>
+   <property name="windowTitle">
+    <string>toolBar</string>
+   </property>
+   <property name="allowedAreas">
+    <set>Qt::BottomToolBarArea|Qt::TopToolBarArea</set>
+   </property>
+   <property name="floatable">
+    <bool>false</bool>
+   </property>
+   <attribute name="toolBarArea">
+    <enum>TopToolBarArea</enum>
+   </attribute>
+   <attribute name="toolBarBreak">
+    <bool>false</bool>
+   </attribute>
+   <addaction name="action_createvm"/>
+   <addaction name="action_removevm"/>
+   <addaction name="separator"/>
+   <addaction name="action_resumevm"/>
+   <addaction name="action_pausevm"/>
+   <addaction name="action_shutdownvm"/>
+   <addaction name="action_restartvm"/>
+   <addaction name="separator"/>
+   <addaction name="action_settings"/>
+   <addaction name="action_editfwrules"/>
+   <addaction name="action_appmenus"/>
+   <addaction name="action_updatevm"/>
+   <addaction name="action_set_keyboard_layout"/>
+   <addaction name="separator"/>
+   <addaction name="action_global_settings"/>
+   <addaction name="action_backup"/>
+   <addaction name="action_restore"/>
+   <addaction name="separator"/>
+   <addaction name="action_refresh_list"/>
+  </widget>
+  <action name="action_createvm">
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/createvm.png</normaloff>:/createvm.png</iconset>
+   </property>
+   <property name="text">
+    <string>Create &amp;New Qube</string>
+   </property>
+   <property name="toolTip">
+    <string>Create a new Qube</string>
+   </property>
+  </action>
+  <action name="action_removevm">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/removevm.png</normaloff>:/removevm.png</iconset>
+   </property>
+   <property name="text">
+    <string>&amp;Delete Qube</string>
+   </property>
+   <property name="toolTip">
+    <string>Remove an existing Qube (must be stopped first)</string>
+   </property>
+  </action>
+  <action name="action_resumevm">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/resumevm.png</normaloff>:/resumevm.png</iconset>
+   </property>
+   <property name="text">
+    <string>Start/Resu&amp;me Qube</string>
+   </property>
+   <property name="toolTip">
+    <string>Start/Resume selected Qube</string>
+   </property>
+  </action>
+  <action name="action_pausevm">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/pausevm.png</normaloff>:/pausevm.png</iconset>
+   </property>
+   <property name="text">
+    <string>&amp;Pause Qube</string>
+   </property>
+   <property name="toolTip">
+    <string>Pause selected Qube</string>
+   </property>
+  </action>
+  <action name="action_shutdownvm">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/shutdownvm.png</normaloff>:/shutdownvm.png</iconset>
+   </property>
+   <property name="text">
+    <string>&amp;Shutdown Qube</string>
+   </property>
+   <property name="toolTip">
+    <string>Shutdown selected Qube</string>
+   </property>
+  </action>
+  <action name="action_restartvm">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/restartvm.png</normaloff>:/restartvm.png</iconset>
+   </property>
+   <property name="text">
+    <string>Restar&amp;t Qube</string>
+   </property>
+   <property name="toolTip">
+    <string>Restart selected Qube</string>
+   </property>
+  </action>
+  <action name="action_appmenus">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/apps.png</normaloff>:/apps.png</iconset>
+   </property>
+   <property name="text">
+    <string>Add/remove app s&amp;hortcuts</string>
+   </property>
+   <property name="toolTip">
+    <string>Add/remove app shortcuts for this Qube</string>
+   </property>
+  </action>
+  <action name="action_updatevm">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/updateable.png</normaloff>:/updateable.png</iconset>
+   </property>
+   <property name="text">
+    <string>&amp;Update Qube</string>
+   </property>
+   <property name="toolTip">
+    <string>Update Qube system</string>
+   </property>
+  </action>
+  <action name="action_editfwrules">
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/firewall.png</normaloff>:/firewall.png</iconset>
+   </property>
+   <property name="text">
+    <string>Edit Qube &amp;firewall rules</string>
+   </property>
+   <property name="toolTip">
+    <string>Edit Qube firewall rules</string>
+   </property>
+  </action>
+  <action name="action_showgraphs">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/showcpuload.png</normaloff>:/showcpuload.png</iconset>
+   </property>
+   <property name="text">
+    <string>Show graphs</string>
+   </property>
+   <property name="toolTip">
+    <string>Show Graphs</string>
+   </property>
+  </action>
+  <action name="action_options">
+   <property name="text">
+    <string>Options</string>
+   </property>
+  </action>
+  <action name="action_view">
+   <property name="text">
+    <string>View</string>
+   </property>
+  </action>
+  <action name="action_template">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="checked">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>&amp;Template</string>
+   </property>
+  </action>
+  <action name="action_netvm">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="checked">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>&amp;NetVM</string>
+   </property>
+  </action>
+  <action name="action_settings">
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/settings.png</normaloff>:/settings.png</iconset>
+   </property>
+   <property name="text">
+    <string>Qube s&amp;ettings</string>
+   </property>
+   <property name="toolTip">
+    <string>Qube Settings</string>
+   </property>
+  </action>
+  <action name="action_restore">
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/restore.png</normaloff>:/restore.png</iconset>
+   </property>
+   <property name="text">
+    <string>&amp;Restore Qubes from backup</string>
+   </property>
+   <property name="toolTip">
+    <string>Restore Qubes from backup</string>
+   </property>
+  </action>
+  <action name="action_backup">
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/backup.png</normaloff>:/backup.png</iconset>
+   </property>
+   <property name="text">
+    <string>&amp;Backup Qubes</string>
+   </property>
+   <property name="toolTip">
+    <string>Backup Qubes</string>
+   </property>
+  </action>
+  <action name="action_global_settings">
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/global-settings.png</normaloff>:/global-settings.png</iconset>
+   </property>
+   <property name="text">
+    <string>&amp;Global settings</string>
+   </property>
+  </action>
+  <action name="action_show_network">
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/networking.png</normaloff>:/networking.png</iconset>
+   </property>
+   <property name="text">
+    <string>&amp;Qubes Network</string>
+   </property>
+   <property name="visible">
+    <bool>false</bool>
+   </property>
+  </action>
+  <action name="action_state">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="checked">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>&amp;State</string>
+   </property>
+  </action>
+  <action name="action_killvm">
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/killvm.png</normaloff>:/killvm.png</iconset>
+   </property>
+   <property name="text">
+    <string>&amp;Kill Qube</string>
+   </property>
+   <property name="toolTip">
+    <string>Kill selected Qube</string>
+   </property>
+  </action>
+  <action name="action_set_keyboard_layout">
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/kbd-layout.png</normaloff>:/kbd-layout.png</iconset>
+   </property>
+   <property name="text">
+    <string>Set keyboard la&amp;yout</string>
+   </property>
+   <property name="toolTip">
+    <string>Set keyboard layout per Qube</string>
+   </property>
+  </action>
+  <action name="action_vm_type">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="checked">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>T&amp;ype</string>
+   </property>
+   <property name="toolTip">
+    <string>Qube Type</string>
+   </property>
+  </action>
+  <action name="action_label">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="checked">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>&amp;Label</string>
+   </property>
+  </action>
+  <action name="action_name">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="checked">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>N&amp;ame</string>
+   </property>
+  </action>
+  <action name="action_toolbar">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="checked">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Show tool bar</string>
+   </property>
+  </action>
+  <action name="action_menubar">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="checked">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Show menu bar</string>
+   </property>
+  </action>
+  <action name="action_about_qubes">
+   <property name="icon">
+    <iconset theme="qubes-manager">
+     <normaloff/>
+    </iconset>
+   </property>
+   <property name="text">
+    <string>&amp;Qubes OS</string>
+   </property>
+  </action>
+  <action name="action_size_on_disk">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="checked">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Si&amp;ze</string>
+   </property>
+   <property name="toolTip">
+    <string>Size on Disk</string>
+   </property>
+  </action>
+  <action name="action_run_command_in_vm">
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/run-command.png</normaloff>:/run-command.png</iconset>
+   </property>
+   <property name="text">
+    <string>&amp;Run command in Qube</string>
+   </property>
+   <property name="toolTip">
+    <string>Run command in the specified Qube</string>
+   </property>
+  </action>
+  <action name="action_clonevm">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/templatevm.png</normaloff>:/templatevm.png</iconset>
+   </property>
+   <property name="text">
+    <string>&amp;Clone Qube</string>
+   </property>
+   <property name="toolTip">
+    <string>Clone Qube</string>
+   </property>
+  </action>
+  <action name="action_internal">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="checked">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Inte&amp;rnal</string>
+   </property>
+   <property name="toolTip">
+    <string>Is an internal Qube</string>
+   </property>
+  </action>
+  <action name="action_startvm_tools_install">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/resumevm.png</normaloff>:/resumevm.png</iconset>
+   </property>
+   <property name="text">
+    <string>Start Qube for Window Tools installation</string>
+   </property>
+   <property name="toolTip">
+    <string>Start Qube for Window Tools installation</string>
+   </property>
+  </action>
+  <action name="action_ip">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="checked">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>&amp;IP</string>
+   </property>
+  </action>
+  <action name="action_backups">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="checked">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Include in &amp;backups</string>
+   </property>
+  </action>
+  <action name="action_last_backup">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="checked">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Last back&amp;up</string>
+   </property>
+  </action>
+  <action name="action_search">
+   <property name="text">
+    <string>Search</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+F</string>
+   </property>
+  </action>
+  <action name="action_refresh_list">
+   <property name="icon">
+    <iconset resource="../resources.qrc">
+     <normaloff>:/outdated.png</normaloff>:/outdated.png</iconset>
+   </property>
+   <property name="text">
+    <string>Refresh Qube List</string>
+   </property>
+   <property name="toolTip">
+    <string>Refresh Qube List</string>
+   </property>
+  </action>
+ </widget>
+ <resources>
+  <include location="../resources.qrc"/>
+ </resources>
+ <connections/>
+</ui>

+ 9 - 3
ui/restoredlg.ui

@@ -52,7 +52,7 @@
           <string>Ignore missing templates or netvms, restore VMs anyway.</string>
          </property>
          <property name="text">
-          <string>ignore missing</string>
+          <string>ignore missing templates and net VMs</string>
          </property>
         </widget>
        </item>
@@ -147,6 +147,9 @@
          <property name="text">
           <string/>
          </property>
+         <property name="checked">
+          <bool>true</bool>
+         </property>
         </widget>
        </item>
        <item row="0" column="0">
@@ -211,8 +214,8 @@
        <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
 &lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
 p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Sans Serif'; font-size:10pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:9pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Cantarell'; font-size:11pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans Serif'; font-size:9pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
       </property>
      </widget>
     </item>
@@ -241,6 +244,9 @@ p, li { white-space: pre-wrap; }
     </item>
     <item>
      <widget class="QProgressBar" name="progress_bar">
+      <property name="maximum">
+       <number>0</number>
+      </property>
       <property name="value">
        <number>0</number>
       </property>

+ 237 - 49
ui/settingsdlg.ui

@@ -29,7 +29,7 @@
         <locale language="English" country="UnitedStates"/>
        </property>
        <property name="currentIndex">
-        <number>1</number>
+        <number>2</number>
        </property>
        <widget class="QWidget" name="basic_tab">
         <property name="locale">
@@ -170,7 +170,7 @@
            </layout>
           </widget>
          </item>
-         <item row="5" column="0">
+         <item row="6" column="0">
           <spacer name="verticalSpacer">
            <property name="orientation">
             <enum>Qt::Vertical</enum>
@@ -373,6 +373,26 @@
            </layout>
           </widget>
          </item>
+         <item row="4" column="1">
+          <widget class="QPushButton" name="delete_vm_button">
+           <property name="styleSheet">
+            <string notr="true">background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:1, y2:0, stop:0 rgba(255, 179, 179, 255), stop:1 rgba(255, 108, 108, 255));
+border-color: rgb(170, 0, 0);
+border-style: solid;
+border-width: 1px;</string>
+           </property>
+           <property name="text">
+            <string>Delete VM</string>
+           </property>
+          </widget>
+         </item>
+         <item row="5" column="1">
+          <widget class="QPushButton" name="clone_vm_button">
+           <property name="text">
+            <string>Clone VM</string>
+           </property>
+          </widget>
+         </item>
         </layout>
        </widget>
        <widget class="QWidget" name="advanced_tab">
@@ -580,7 +600,7 @@
              <widget class="QComboBox" name="default_dispvm"/>
             </item>
             <item row="2" column="0" colspan="2">
-             <widget class="QPushButton" name="bootFromDeviceButton">
+             <widget class="QPushButton" name="boot_from_device_button">
               <property name="text">
                <string>Boot qube from CDROM</string>
               </property>
@@ -769,47 +789,198 @@
         </attribute>
         <layout class="QGridLayout" name="gridLayout_8">
          <item row="0" column="0">
-          <widget class="QRadioButton" name="policyAllowRadioButton">
-           <property name="text">
-            <string>Allow network access except...</string>
+          <widget class="QLabel" name="no_netvm_label">
+           <property name="palette">
+            <palette>
+             <active>
+              <colorrole role="WindowText">
+               <brush brushstyle="SolidPattern">
+                <color alpha="255">
+                 <red>255</red>
+                 <green>0</green>
+                 <blue>0</blue>
+                </color>
+               </brush>
+              </colorrole>
+             </active>
+             <inactive>
+              <colorrole role="WindowText">
+               <brush brushstyle="SolidPattern">
+                <color alpha="255">
+                 <red>255</red>
+                 <green>0</green>
+                 <blue>0</blue>
+                </color>
+               </brush>
+              </colorrole>
+             </inactive>
+             <disabled>
+              <colorrole role="WindowText">
+               <brush brushstyle="SolidPattern">
+                <color alpha="255">
+                 <red>139</red>
+                 <green>142</green>
+                 <blue>142</blue>
+                </color>
+               </brush>
+              </colorrole>
+             </disabled>
+            </palette>
            </property>
-          </widget>
-         </item>
-         <item row="0" column="1">
-          <widget class="QCheckBox" name="icmpCheckBox">
-           <property name="minimumSize">
-            <size>
-             <width>323</width>
-             <height>0</height>
-            </size>
+           <property name="font">
+            <font>
+             <weight>75</weight>
+             <italic>true</italic>
+             <bold>true</bold>
+            </font>
            </property>
            <property name="text">
-            <string>Allow ICMP traffic</string>
+            <string>This qube has no networking - it will not have any network access anyway.</string>
            </property>
-           <property name="checked">
+           <property name="wordWrap">
             <bool>true</bool>
            </property>
           </widget>
          </item>
          <item row="1" column="0">
-          <widget class="QRadioButton" name="policyDenyRadioButton">
+          <widget class="QLabel" name="netvm_no_firewall_label">
+           <property name="palette">
+            <palette>
+             <active>
+              <colorrole role="WindowText">
+               <brush brushstyle="SolidPattern">
+                <color alpha="255">
+                 <red>255</red>
+                 <green>0</green>
+                 <blue>0</blue>
+                </color>
+               </brush>
+              </colorrole>
+             </active>
+             <inactive>
+              <colorrole role="WindowText">
+               <brush brushstyle="SolidPattern">
+                <color alpha="255">
+                 <red>255</red>
+                 <green>0</green>
+                 <blue>0</blue>
+                </color>
+               </brush>
+              </colorrole>
+             </inactive>
+             <disabled>
+              <colorrole role="WindowText">
+               <brush brushstyle="SolidPattern">
+                <color alpha="255">
+                 <red>139</red>
+                 <green>142</green>
+                 <blue>142</blue>
+                </color>
+               </brush>
+              </colorrole>
+             </disabled>
+            </palette>
+           </property>
+           <property name="font">
+            <font>
+             <weight>75</weight>
+             <italic>true</italic>
+             <bold>true</bold>
+            </font>
+           </property>
            <property name="text">
-            <string>Deny network access except...</string>
+            <string>Networking qube does not support 'qubes-firewall' - firewall restrictions will not be applied.</string>
            </property>
           </widget>
          </item>
-         <item row="1" column="1">
-          <widget class="QCheckBox" name="dnsCheckBox">
+         <item row="2" column="0">
+          <widget class="QLabel" name="firewall_modified_outside_label">
+           <property name="palette">
+            <palette>
+             <active>
+              <colorrole role="WindowText">
+               <brush brushstyle="SolidPattern">
+                <color alpha="255">
+                 <red>255</red>
+                 <green>0</green>
+                 <blue>0</blue>
+                </color>
+               </brush>
+              </colorrole>
+             </active>
+             <inactive>
+              <colorrole role="WindowText">
+               <brush brushstyle="SolidPattern">
+                <color alpha="255">
+                 <red>255</red>
+                 <green>0</green>
+                 <blue>0</blue>
+                </color>
+               </brush>
+              </colorrole>
+             </inactive>
+             <disabled>
+              <colorrole role="WindowText">
+               <brush brushstyle="SolidPattern">
+                <color alpha="255">
+                 <red>139</red>
+                 <green>142</green>
+                 <blue>142</blue>
+                </color>
+               </brush>
+              </colorrole>
+             </disabled>
+            </palette>
+           </property>
+           <property name="font">
+            <font>
+             <weight>75</weight>
+             <italic>true</italic>
+             <bold>true</bold>
+            </font>
+           </property>
            <property name="text">
-            <string>Allow DNS queries</string>
+            <string>Firewall has been modified manually - please use qvm-firewall for any further configuration.</string>
            </property>
-           <property name="checked">
-            <bool>true</bool>
+          </widget>
+         </item>
+         <item row="3" column="0">
+          <layout class="QHBoxLayout" name="horizontalLayout_5">
+           <item>
+            <widget class="QRadioButton" name="policy_allow_radio_button">
+             <property name="text">
+              <string>Allow all outgoing Internet connections</string>
+             </property>
+            </widget>
+           </item>
+           <item>
+            <widget class="QRadioButton" name="policy_deny_radio_button">
+             <property name="toolTip">
+              <string>Changing firewall settings does NOT affect existing connections.</string>
+             </property>
+             <property name="text">
+              <string>Limit outgoing Internet connections to ...</string>
+             </property>
+            </widget>
+           </item>
+          </layout>
+         </item>
+         <item row="5" column="0" colspan="2">
+          <widget class="Line" name="line">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
            </property>
           </widget>
          </item>
-         <item row="3" column="0" colspan="2">
-          <layout class="QHBoxLayout" name="horizontalLayout_2">
+         <item row="6" column="0">
+          <widget class="QLabel" name="firewal_rules_label">
+           <property name="text">
+            <string>List of allowed (whitelisted) addresses:</string>
+           </property>
+          </widget>
+         </item>
+         <item row="7" column="0" colspan="2">
+          <layout class="QHBoxLayout" name="firewallRulesLayout">
            <property name="sizeConstraint">
             <enum>QLayout::SetMaximumSize</enum>
            </property>
@@ -851,7 +1022,7 @@
            <item>
             <layout class="QVBoxLayout" name="verticalLayout_6">
              <item>
-              <widget class="QPushButton" name="newRuleButton">
+              <widget class="QPushButton" name="new_rule_button">
                <property name="text">
                 <string/>
                </property>
@@ -868,7 +1039,7 @@
               </widget>
              </item>
              <item>
-              <widget class="QPushButton" name="editRuleButton">
+              <widget class="QPushButton" name="edit_rule_button">
                <property name="text">
                 <string/>
                </property>
@@ -885,7 +1056,10 @@
               </widget>
              </item>
              <item>
-              <widget class="QPushButton" name="deleteRuleButton">
+              <widget class="QPushButton" name="delete_rule_button">
+               <property name="toolTip">
+                <string>Changing firewall settings does NOT affect existing connections.</string>
+               </property>
                <property name="text">
                 <string/>
                </property>
@@ -918,14 +1092,7 @@
            </item>
           </layout>
          </item>
-         <item row="2" column="1">
-          <widget class="QCheckBox" name="yumproxyCheckBox">
-           <property name="text">
-            <string>Allow connections to Updates Proxy</string>
-           </property>
-          </widget>
-         </item>
-         <item row="2" column="0">
+         <item row="8" column="0">
           <widget class="QWidget" name="tempFullAccessWidget" native="true">
            <property name="enabled">
             <bool>true</bool>
@@ -940,15 +1107,15 @@
             <property name="bottomMargin">
              <number>0</number>
             </property>
-            <item row="0" column="0">
-             <widget class="QCheckBox" name="tempFullAccess">
+            <item row="1" column="0">
+             <widget class="QCheckBox" name="temp_full_access">
               <property name="text">
                <string>Allow full access for </string>
               </property>
              </widget>
             </item>
-            <item row="0" column="1">
-             <widget class="QSpinBox" name="tempFullAccessTime">
+            <item row="1" column="1">
+             <widget class="QSpinBox" name="temp_full_access_time">
               <property name="suffix">
                <string> min</string>
               </property>
@@ -957,9 +1124,35 @@
               </property>
              </widget>
             </item>
+            <item row="1" column="2">
+             <spacer name="horizontalSpacer_2">
+              <property name="orientation">
+               <enum>Qt::Horizontal</enum>
+              </property>
+              <property name="sizeHint" stdset="0">
+               <size>
+                <width>40</width>
+                <height>20</height>
+               </size>
+              </property>
+             </spacer>
+            </item>
            </layout>
           </widget>
          </item>
+         <item row="9" column="0" colspan="2">
+          <widget class="QLabel" name="label_22">
+           <property name="text">
+            <string>NOTE:  To block all network access, set Networking to (none) on the Basic settings tab. This tab provides a very simplified firewall configuration. All DNS requests and ICMP (pings) will be allowed. For more granular control, use the command line tool qvm-firewall.</string>
+           </property>
+           <property name="alignment">
+            <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+           </property>
+           <property name="wordWrap">
+            <bool>true</bool>
+           </property>
+          </widget>
+         </item>
         </layout>
        </widget>
        <widget class="QWidget" name="devices_tab">
@@ -1162,15 +1355,10 @@
   <tabstop>vcpus</tabstop>
   <tabstop>include_in_balancing</tabstop>
   <tabstop>kernel</tabstop>
-  <tabstop>policyAllowRadioButton</tabstop>
-  <tabstop>policyDenyRadioButton</tabstop>
-  <tabstop>icmpCheckBox</tabstop>
-  <tabstop>dnsCheckBox</tabstop>
-  <tabstop>yumproxyCheckBox</tabstop>
-  <tabstop>newRuleButton</tabstop>
+  <tabstop>new_rule_button</tabstop>
   <tabstop>rulesTreeView</tabstop>
-  <tabstop>editRuleButton</tabstop>
-  <tabstop>deleteRuleButton</tabstop>
+  <tabstop>edit_rule_button</tabstop>
+  <tabstop>delete_rule_button</tabstop>
   <tabstop>service_line_edit</tabstop>
   <tabstop>add_srv_button</tabstop>
   <tabstop>services_list</tabstop>

+ 1 - 1
version

@@ -1 +1 @@
-4.0.6
+4.0.10