diff --git a/.travis.yml b/.travis.yml index 66bde29..e868fbe 100644 --- a/.travis.yml +++ b/.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 diff --git a/ci/pylintrc b/ci/pylintrc new file mode 100644 index 0000000..85eefb5 --- /dev/null +++ b/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 diff --git a/ci/requirements.txt b/ci/requirements.txt new file mode 100644 index 0000000..ac54cec --- /dev/null +++ b/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 \ No newline at end of file diff --git a/qubes-backup-restore.desktop b/qubes-backup-restore.desktop new file mode 100644 index 0000000..3983f9f --- /dev/null +++ b/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; diff --git a/qubes-backup.desktop b/qubes-backup.desktop new file mode 100644 index 0000000..7e93247 --- /dev/null +++ b/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; diff --git a/qubes-global-settings.desktop b/qubes-global-settings.desktop index 110d418..74a0d53 100644 --- a/qubes-global-settings.desktop +++ b/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 diff --git a/qubes-qube-manager.desktop b/qubes-qube-manager.desktop new file mode 100644 index 0000000..220f8d5 --- /dev/null +++ b/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; diff --git a/qubesmanager/about.py b/qubesmanager/about.py index fb06102..b12ae36 100644 --- a/qubesmanager/about.py +++ b/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 . # # -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__() diff --git a/qubesmanager/appmenu_select.py b/qubesmanager/appmenu_select.py index c161f56..f042011 100755 --- a/qubesmanager/appmenu_select.py +++ b/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 . # # -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() diff --git a/qubesmanager/backup.py b/qubesmanager/backup.py index bde407b..27dcea1 100644 --- a/qubesmanager/backup.py +++ b/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 . # # +import traceback + +import signal + +from qubesadmin import Qubes, exc +from qubesadmin import utils as admin_utils +from qubes.storage.file import get_disk_usage + +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 -import signal -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 import backup -from qubes import qubesutils - -import qubesmanager.resources_rc - -from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent - +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_Backup, QWizard): +class BackupVMsWindow(ui_backupdlg.Ui_Backup, multiselectwidget.QtGui.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) + """ + 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 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 '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 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()) + if 'destination_path' in profile_data: + dest_path = profile_data['destination_path'] + self.dir_line_edit.setText(dest_path) - 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()) + 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']) - 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) + if 'compression' in profile_data: + self.compress_checkbox.setChecked(profile_data['compression']) - class VmListItem(QListWidgetItem): + 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) + ")") + self.size = vm.get_disk_utilization() + super(BackupVMsWindow.VmListItem, self).__init__( + vm.name + " (" + admin_utils.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) - - 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)) + self.total_size_label.setText( + admin_utils.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) - - 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: " - "{0}?
" - "This will shutdown all the running applications " - "within them.").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 {0}...".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 -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 " - "in Qubes Restore VMs application.

" - "%s" % error + - "at line %d of file %s.

" - % ( 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.

%s" % + error + "at line %d of file %s.

" + % (line, filename)) -def app_main(): +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 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() diff --git a/qubesmanager/backup_utils.py b/qubesmanager/backup_utils.py index 38b5931..9efd8b2 100644 --- a/qubesmanager/backup_utils.py +++ b/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 . # # 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 or new_appvm) and len(backup_location) > 0: - dialog.select_dir_page.emit(SIGNAL("completeChanged()")) + if new_path and not read_only: + dialog.dir_line_edit.setText(new_path) -def simulate_long_lasting_proces(period, progress_callback): - for i in range(period): - progress_callback((i*100)/period) - time.sleep(1) + if new_path and backup_location and not read_only: + dialog.select_dir_page.emit(QtCore.SIGNAL("completeChanged()")) - progress_callback(100) - return 0 + +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'] + + profile_data = {key: value for key, value in args.items() + if key in acceptable_fields} + + path = get_profile_path(use_temp) + + with open(path, 'w') as profile_file: + yaml.safe_dump(profile_data, profile_file) diff --git a/qubesmanager/block.py b/qubesmanager/block.py index a826a1e..ed4ff0a 100644 --- a/qubesmanager/block.py +++ b/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 . import threading import time diff --git a/qubesmanager/bootfromdevice.py b/qubesmanager/bootfromdevice.py index d17e8fc..458abef 100644 --- a/qubesmanager/bootfromdevice.py +++ b/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 . # # +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 - 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 diff --git a/qubesmanager/clipboard.py b/qubesmanager/clipboard.py index 00fe9f7..f4c5785 100644 --- a/qubesmanager/clipboard.py +++ b/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 . # # diff --git a/qubesmanager/create_new_vm.py b/qubesmanager/create_new_vm.py index f75debc..d31c5db 100644 --- a/qubesmanager/create_new_vm.py +++ b/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 . # # -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 {} 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 {}...").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') diff --git a/qubesmanager/firewall.py b/qubesmanager/firewall.py index d482b3f..14b69dc 100644 --- a/qubesmanager/firewall.py +++ b/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 . # # 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}(?[a-z][a-z0-9-]+)\s+(?P[0-9]+)/(?P[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[a-z][a-z0-9-]+)\s+(?P[0-9]+)/" + r"(?P[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 == QtCore.Qt.AscendingOrder) + self.children.sort(key=lambda x: self.get_column_string(idx, x), + reverse=rev) - 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) 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 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) - - 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 + # 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 + return "any" + if rule.dstports.range[0] != rule.dstports.range[1]: + return str(rule.dstports) + return self.get_service_name(rule.dstports) - 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)) + # Protocol + if col == 2: + if rule.proto is None: + return "any" + return str(rule.proto) + return "unknown" - if rule.expire is not None: - d['expire'] = int(rule.expire) - - return d - - def get_firewall_conf(self, vm): + @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 + rule = reversed_rules.pop(0) - 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') + 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') + if not allow_icmp and not conf['allow']: + raise FirewallModifiedOutsideError('ICMP must be allowed.') + + if not allow_dns and not conf['allow']: + raise FirewallModifiedOutsideError('DNS must be allowed') + + return conf + + @staticmethod + def write_firewall_conf(vm, conf): rules = [] 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'])))) + rules.append(rule) - netmask = str(rule['netmask']) if rule['netmask'] != 32 else None - - rules.append(qubesadmin.firewall.Rule(None, - action=common_action, - dsthost='/'.join(map(str, filter((lambda x: x), - (rule['address'], netmask)))), - **kwargs)) - - 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) + conf['rules'].extend(self.children) - 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 - }) + 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) - diff --git a/qubesmanager/global_settings.py b/qubesmanager/global_settings.py index eaf009f..c779326 100644 --- a/qubesmanager/global_settings.py +++ b/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 . # # 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 -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.

" - "%s" % error + - "at line %d of file %s.

" - % ( 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.

%s" % + error + "at line %d of file %s.

" + % (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() diff --git a/qubesmanager/informationnotes.py b/qubesmanager/informationnotes.py index b6aa946..b512d3f 100644 --- a/qubesmanager/informationnotes.py +++ b/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 . # # -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()) diff --git a/qubesmanager/log_dialog.py b/qubesmanager/log_dialog.py index 6e7ced9..0f4232b 100644 --- a/qubesmanager/log_dialog.py +++ b/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 . # # -import sys +from PyQt4 import QtCore # pylint: disable=import-error +from PyQt4 import QtGui # pylint: disable=import-error + +from . import ui_logdlg # pylint: disable=no-name-in-module +from . import clipboard import os -from PyQt4.QtCore import * -from PyQt4.QtGui import * - -from qubes.qubes import QubesException - -import qubesmanager.resources_rc - -from .ui_logdlg import * -from .clipboard import * # 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) diff --git a/qubesmanager/multiselectwidget.py b/qubesmanager/multiselectwidget.py index a1ab7c2..c733889 100644 --- a/qubesmanager/multiselectwidget.py +++ b/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() - diff --git a/qubesmanager/qube_manager.py b/qubesmanager/qube_manager.py new file mode 100755 index 0000000..9cb6ef1 --- /dev/null +++ b/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 +# Copyright (C) 2012 Marek Marczykowski-Górecki +# +# Copyright (C) 2017 Wojtek Porczyk +# +# 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 . +# +# + +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 '{0}' hasn't shutdown within the last " + "{1} seconds, do you want to kill it?
").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.
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.")) + return + + (requested_name, ok) = QtGui.QInputDialog.getText( + None, self.tr("Qube Removal Confirmation"), + self.tr("Are you sure you want to remove the Qube '{0}'" + "?
All data on this Qube's private storage will be " + "lost!

Type the name of the Qube ({1}) 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: {0}...").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 {} 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 {0} to {1}...").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:
{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" + " '{0}'?
This will shutdown all the " + "running applications within this Qube.").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 '{0}'?" + "
This will shutdown all the running " + "applications within this Qube.").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 '{0}'?
" + "This will end (not shutdown!) all the " + "running applications within this Qube.").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( + "An exception ocurred while killing {0}.
" + "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( + "{0}
The Qube has to be running to be updated." + "
Do you want to start it?
").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( + "{0}
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 {}:').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:
{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 + +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.

" + "%s" % error + + "
at line %d
of file %s.

" + % (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() diff --git a/qubesmanager/releasenotes.py b/qubesmanager/releasenotes.py index 53b8752..fd781d5 100644 --- a/qubesmanager/releasenotes.py +++ b/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 . # # -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__() diff --git a/qubesmanager/restore.py b/qubesmanager/restore.py index 552e3eb..b36ec52 100644 --- a/qubesmanager/restore.py +++ b/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 . # # 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 * +import os.path +import traceback +import logging +import logging.handlers -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 time -from operator import itemgetter -from .thread_monitor import * - 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) + backup_utils.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 - - 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)) + except exc.QubesException as ex: + QtGui.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) + def append_output(self, text): + self.commit_text_edit.append(text) - 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'{0}'.format(s))) - - def restore_output(self, s): - self.feedback_queue.put((SIGNAL("restore_progress(QString)"), - u'{0}'.format(s))) - - 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)"), - '{0}' - .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)"), - '{0}' - .format(self.tr("Finished with errors!"))) + self.append_output('{0}'.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('{0}'.format( + self.tr("Finished with errors!"))) else: - self.emit(SIGNAL("restore_progress(QString)"), - '{0}' - .format(self.tr("Finished successfully!"))) + self.append_output('{0}'.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 = '{0}'.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)"), - '{0}'.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( + '{0}'.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)"), - '{0}' - .format(self.tr("Aborting the operation..."))) - self.button(self.CancelButton).setDisabled(True) + self.backup_restore.canceled = True + self.append_output('{0}'.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 -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.

" "%s" % error + "at line %d of file %s.

" - % ( 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__": diff --git a/qubesmanager/settings.py b/qubesmanager/settings.py index 2c4ed15..64765e3 100755 --- a/qubesmanager/settings.py +++ b/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 . # # 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) + 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.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) + 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 {0}...").format(self.vm.name), "", 0, 0) + progress = QtGui.QProgressDialog( + self.tr("Applying settings to {0}...").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!
" "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.
" "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.
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: - - t_monitor = thread_monitor.ThreadMonitor() - thread = threading.Thread(target=self._rename_vm, args=(t_monitor, new_vm_name,)) - thread.daemon = True - thread.start() - - while not t_monitor.is_finished(): - self.qapp.processEvents() - time.sleep (0.1) - - if not t_monitor.success: - QMessageBox.warning(None, - self.tr("Error renaming the VM!"), - self.tr("ERROR: {}").format( - t_monitor.error_msg)) - + self._run_in_thread(self._rename_vm, new_vm_name) self.done(0) + def _remove_vm(self, t_monitor): + try: + del self.vm.app.domains[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 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? ' + '
All VM settings and data will be irrevocably' + ' deleted.
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) + self.policy_allow_radio_button.setChecked(allow) + self.policy_deny_radio_button.setChecked(not allow) + self.policy_changed() - def policy_changed(self, checked): - self.tempFullAccessWidget.setEnabled(self.policyDenyRadioButton.isChecked()) + 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) + selected = self.rulesTreeView.selectedIndexes() + + 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.

" + msg_box.setText("Whoops. A critical error has occured. " + "This is most likely a bug in Qubes Manager.

" "%s" % error + "
at line %d
of file %s.

" - % ( 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") diff --git a/qubesmanager/table_widgets.py b/qubesmanager/table_widgets.py index b5a662a..06ec486 100644 --- a/qubesmanager/table_widgets.py +++ b/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 +# Copyright (C) 2014 Marek Marczykowski-Górecki +# # # 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 . -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")) + self.setTextColor(QtGui.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.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 = 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" - else: - outdated_state = None - 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 = "Check updates" 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 diff --git a/qubesmanager/thread_monitor.py b/qubesmanager/thread_monitor.py index 07fdca3..8515aff 100644 --- a/qubesmanager/thread_monitor.py +++ b/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 . # # -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() - diff --git a/qubesmanager/utils.py b/qubesmanager/utils.py index 25e9f8f..a8b68f9 100644 --- a/qubesmanager/utils.py +++ b/qubesmanager/utils.py @@ -20,12 +20,11 @@ # along with this program. If not, see . # -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 diff --git a/rpm_spec/qmgr.spec b/rpm_spec/qmgr.spec index 7c473fa..877caaa 100644 --- a/rpm_spec/qmgr.spec +++ b/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 diff --git a/setup.py b/setup.py index cba28dd..28d2c07 100644 --- a/setup.py +++ b/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' ], }) diff --git a/test-packages/qubes/backup/__init__.py b/test-packages/qubes/backup/__init__.py new file mode 100644 index 0000000..dc24fbb --- /dev/null +++ b/test-packages/qubes/backup/__init__.py @@ -0,0 +1,5 @@ +class BackupCanceledError(BaseException): + + tmpdir = None + + pass diff --git a/test-packages/qubes/storage/__init__.py b/test-packages/qubes/storage/__init__.py new file mode 100644 index 0000000..2ae2839 --- /dev/null +++ b/test-packages/qubes/storage/__init__.py @@ -0,0 +1 @@ +pass diff --git a/test-packages/qubes/storage/file.py b/test-packages/qubes/storage/file.py new file mode 100644 index 0000000..1503cca --- /dev/null +++ b/test-packages/qubes/storage/file.py @@ -0,0 +1,2 @@ +def get_disk_usage(*args): + return 0 diff --git a/test-packages/qubesadmin/__init__.py b/test-packages/qubesadmin/__init__.py new file mode 100644 index 0000000..1801453 --- /dev/null +++ b/test-packages/qubesadmin/__init__.py @@ -0,0 +1,4 @@ +class Qubes(object): + pass + +DEFAULT = object() diff --git a/test-packages/qubesadmin/backup/__init__.py b/test-packages/qubesadmin/backup/__init__.py new file mode 100644 index 0000000..fc80254 --- /dev/null +++ b/test-packages/qubesadmin/backup/__init__.py @@ -0,0 +1 @@ +pass \ No newline at end of file diff --git a/test-packages/qubesadmin/backup/restore/__init__.py b/test-packages/qubesadmin/backup/restore/__init__.py new file mode 100644 index 0000000..aa328db --- /dev/null +++ b/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 diff --git a/test-packages/qubesadmin/devices.py b/test-packages/qubesadmin/devices.py new file mode 100644 index 0000000..be5de38 --- /dev/null +++ b/test-packages/qubesadmin/devices.py @@ -0,0 +1,4 @@ +### mock qubesadmin.devices module + +class DeviceAssignment(object): + pass diff --git a/test-packages/qubesadmin/events.py b/test-packages/qubesadmin/events.py new file mode 100644 index 0000000..68c1b58 --- /dev/null +++ b/test-packages/qubesadmin/events.py @@ -0,0 +1,5 @@ +class EventsDispatcher(object): + + def add_handler(self, *args): + pass + pass diff --git a/test-packages/qubesadmin/exc.py b/test-packages/qubesadmin/exc.py new file mode 100644 index 0000000..22a3a27 --- /dev/null +++ b/test-packages/qubesadmin/exc.py @@ -0,0 +1,6 @@ +### mock qubesadmin.exc module +# pylint: disable=unused-variable + + +class QubesException(BaseException): + pass diff --git a/test-packages/qubesadmin/firewall.py b/test-packages/qubesadmin/firewall.py new file mode 100644 index 0000000..e83c19f --- /dev/null +++ b/test-packages/qubesadmin/firewall.py @@ -0,0 +1,4 @@ +### mock qubesadmin.firewall module + +class Rule(object): + pass diff --git a/test-packages/qubesadmin/tools/__init__.py b/test-packages/qubesadmin/tools/__init__.py new file mode 100644 index 0000000..c448ad0 --- /dev/null +++ b/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 diff --git a/test-packages/qubesadmin/tools/qvm_start.py b/test-packages/qubesadmin/tools/qvm_start.py new file mode 100644 index 0000000..f04be20 --- /dev/null +++ b/test-packages/qubesadmin/tools/qvm_start.py @@ -0,0 +1,4 @@ +### mock qvm_start module + +def main(*args, **kwargs): + pass diff --git a/test-packages/qubesadmin/utils.py b/test-packages/qubesadmin/utils.py new file mode 100644 index 0000000..6915104 --- /dev/null +++ b/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] diff --git a/ui/backupdlg.ui b/ui/backupdlg.ui index 31760d5..5330fb3 100644 --- a/ui/backupdlg.ui +++ b/ui/backupdlg.ui @@ -6,8 +6,8 @@ 0 0 - 650 - 399 + 737 + 420 @@ -23,73 +23,8 @@ - - - - - 0 - 0 - - - - Shutdown all running selected VMs - - - - :/shutdownvm.png:/shutdownvm.png - - - - - - - - 0 - 0 - - - - Refresh running states. - - - - - - - - 0 - 0 - - - - - 75 - true - true - - - - color:rgb(255, 0, 0) - - - Some of the selected VMs are running (red). Running VMs cannot be backed up! - - - true - - - - - - 9 - 50 - false - false - false - - Select VMs to backup: @@ -145,12 +80,79 @@ + + + + Compress the backup + + + true + + + + + + + + + + + + 255 + 0 + 0 + + + + + + + + + 255 + 0 + 0 + + + + + + + + + 139 + 142 + 142 + + + + + + + + + 75 + true + true + + + + Warning: unrecognized data found in configuration files. + + + + + + 0 + 0 + + Backup destination directory @@ -188,73 +190,106 @@ - + + + + + 0 + 0 + + + + Save backup profile + + + + + 10 + 30 + 267 + 25 + + + + Save settings as default backup profile: + + + + + + 270 + 30 + 539 + 25 + + + + + + + true + + + + + + + + 0 + 0 + + Backup security - + + QFormLayout::AllNonFixedFieldsGrow + + <html><head/><body><p>Encryption / Verification<br/>passphrase:</p></body></html> - - - - Encrypt backup: - - - - - - - Qt::LeftToRight - - - - - - true - - - - - - - <html><head/><body><p>Reenter passphrase:</p></body></html> - - - - + QLineEdit::Password - + + + + <html><head/><body><p>Reenter passphrase:</p></body></html> + + + + QLineEdit::Password - - - - <b>Note:</b> 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. - - - true - - - + + + + + true + + + + NOTE: Only running VMs are listed. + + + @@ -281,8 +316,8 @@ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> +</style></head><body style=" font-family:'Cantarell'; font-size:11pt; font-weight:400; font-style:normal;"> +<p style="-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;"><br /></p></body></html> @@ -324,6 +359,9 @@ p, li { white-space: pre-wrap; } + + 0 + 0 diff --git a/ui/newfwruledlg.ui b/ui/newfwruledlg.ui index 11da4ed..391e16c 100644 --- a/ui/newfwruledlg.ui +++ b/ui/newfwruledlg.ui @@ -31,10 +31,22 @@ 6 - - + + + + + 0 + 0 + + + + + 0 + 0 + + - Protocol + UDP @@ -45,13 +57,6 @@ - - - - true - - - @@ -59,6 +64,31 @@ + + + + + 0 + 0 + + + + + 71 + 0 + + + + + + + Any + + + true + + + @@ -66,6 +96,13 @@ + + + + true + + + @@ -85,44 +122,10 @@ - - - - - 0 - 0 - - - - - 0 - 0 - - + + - UDP - - - - - - - - 0 - 0 - - - - - 71 - 0 - - - - Any - - - true + Protocol diff --git a/ui/qubemanager.ui b/ui/qubemanager.ui new file mode 100644 index 0000000..4a2cdc9 --- /dev/null +++ b/ui/qubemanager.ui @@ -0,0 +1,841 @@ + + + VmManagerWindow + + + + 0 + 0 + 769 + 385 + + + + + 0 + 0 + + + + Qt::DefaultContextMenu + + + Qube Manager + + + + + + + + + + + + true + + + + 0 + 0 + + + + false + + + true + + + + + + + QLayout::SetDefaultConstraint + + + 0 + + + 0 + + + + + 6 + + + 6 + + + + + Search: + + + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + + 200 + 30 + + + + Qt::CustomContextMenu + + + false + + + 0 + + + Qt::ScrollBarAsNeeded + + + Qt::ScrollBarAsNeeded + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + Qt::NoPen + + + true + + + false + + + 10 + + + 11 + + + false + + + 150 + + + 150 + + + false + + + false + + + + Nowy wiersz + + + + + + + + + + + + + + + + + + + + + + + + Name + + + VM name + + + + + State + + + Update info + + + + + Template + + + VM's template + + + + + NetVM + + + VM's netVM + + + + + Size + + + + + Internal + + + + + IP + + + + + Backups + + + + + Last backup + + + + + + + + + + 0 + 0 + 769 + 28 + + + + Qt::CustomContextMenu + + + + &System + + + + + + + + + &View + + + + + + + + + + + + + + + + + + + + + + + &Qube + + + + &Logs + + + + :/log.png:/log.png + + + + + Attach/detach &block devices + + + + :/mount.png:/mount.png + + + + + + + + + + + + + + + + + + + + + + + + + &About + + + + + + + + + + + Qt::CustomContextMenu + + + toolBar + + + Qt::BottomToolBarArea|Qt::TopToolBarArea + + + false + + + TopToolBarArea + + + false + + + + + + + + + + + + + + + + + + + + + + + + + :/createvm.png:/createvm.png + + + Create &New Qube + + + Create a new Qube + + + + + false + + + + :/removevm.png:/removevm.png + + + &Delete Qube + + + Remove an existing Qube (must be stopped first) + + + + + false + + + + :/resumevm.png:/resumevm.png + + + Start/Resu&me Qube + + + Start/Resume selected Qube + + + + + false + + + + :/pausevm.png:/pausevm.png + + + &Pause Qube + + + Pause selected Qube + + + + + false + + + + :/shutdownvm.png:/shutdownvm.png + + + &Shutdown Qube + + + Shutdown selected Qube + + + + + false + + + + :/restartvm.png:/restartvm.png + + + Restar&t Qube + + + Restart selected Qube + + + + + false + + + + :/apps.png:/apps.png + + + Add/remove app s&hortcuts + + + Add/remove app shortcuts for this Qube + + + + + false + + + + :/updateable.png:/updateable.png + + + &Update Qube + + + Update Qube system + + + + + + :/firewall.png:/firewall.png + + + Edit Qube &firewall rules + + + Edit Qube firewall rules + + + + + true + + + + :/showcpuload.png:/showcpuload.png + + + Show graphs + + + Show Graphs + + + + + Options + + + + + View + + + + + true + + + true + + + &Template + + + + + true + + + true + + + &NetVM + + + + + + :/settings.png:/settings.png + + + Qube s&ettings + + + Qube Settings + + + + + + :/restore.png:/restore.png + + + &Restore Qubes from backup + + + Restore Qubes from backup + + + + + + :/backup.png:/backup.png + + + &Backup Qubes + + + Backup Qubes + + + + + + :/global-settings.png:/global-settings.png + + + &Global settings + + + + + + :/networking.png:/networking.png + + + &Qubes Network + + + false + + + + + true + + + true + + + &State + + + + + + :/killvm.png:/killvm.png + + + &Kill Qube + + + Kill selected Qube + + + + + + :/kbd-layout.png:/kbd-layout.png + + + Set keyboard la&yout + + + Set keyboard layout per Qube + + + + + true + + + true + + + T&ype + + + Qube Type + + + + + true + + + true + + + &Label + + + + + true + + + true + + + N&ame + + + + + true + + + true + + + Show tool bar + + + + + true + + + true + + + Show menu bar + + + + + + + + + + &Qubes OS + + + + + true + + + true + + + Si&ze + + + Size on Disk + + + + + + :/run-command.png:/run-command.png + + + &Run command in Qube + + + Run command in the specified Qube + + + + + false + + + + :/templatevm.png:/templatevm.png + + + &Clone Qube + + + Clone Qube + + + + + true + + + true + + + Inte&rnal + + + Is an internal Qube + + + + + false + + + + :/resumevm.png:/resumevm.png + + + Start Qube for Window Tools installation + + + Start Qube for Window Tools installation + + + + + true + + + true + + + &IP + + + + + true + + + true + + + Include in &backups + + + + + true + + + true + + + Last back&up + + + + + Search + + + Ctrl+F + + + + + + :/outdated.png:/outdated.png + + + Refresh Qube List + + + Refresh Qube List + + + + + + + + diff --git a/ui/restoredlg.ui b/ui/restoredlg.ui index cc37988..284b867 100644 --- a/ui/restoredlg.ui +++ b/ui/restoredlg.ui @@ -52,7 +52,7 @@ Ignore missing templates or netvms, restore VMs anyway. - ignore missing + ignore missing templates and net VMs @@ -147,6 +147,9 @@ + + true + @@ -211,8 +214,8 @@ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Sans Serif'; font-size:10pt; font-weight:400; font-style:normal;"> -<p style="-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;"><br /></p></body></html> +</style></head><body style=" font-family:'Cantarell'; font-size:11pt; font-weight:400; font-style:normal;"> +<p style="-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;"><br /></p></body></html> @@ -241,6 +244,9 @@ p, li { white-space: pre-wrap; } + + 0 + 0 diff --git a/ui/settingsdlg.ui b/ui/settingsdlg.ui index 73257e5..8593eea 100644 --- a/ui/settingsdlg.ui +++ b/ui/settingsdlg.ui @@ -29,7 +29,7 @@ - 1 + 2 @@ -170,7 +170,7 @@ - + Qt::Vertical @@ -373,6 +373,26 @@ + + + + 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; + + + Delete VM + + + + + + + Clone VM + + + @@ -580,7 +600,7 @@ - + Boot qube from CDROM @@ -769,47 +789,198 @@ - - - Allow network access except... + + + + + + + + 255 + 0 + 0 + + + + + + + + + 255 + 0 + 0 + + + + + + + + + 139 + 142 + 142 + + + + + - - - - - - - 323 - 0 - + + + 75 + true + true + - Allow ICMP traffic + This qube has no networking - it will not have any network access anyway. - + true - + + + + + + + + 255 + 0 + 0 + + + + + + + + + 255 + 0 + 0 + + + + + + + + + 139 + 142 + 142 + + + + + + + + + 75 + true + true + + - Deny network access except... + Networking qube does not support 'qubes-firewall' - firewall restrictions will not be applied. - - - - Allow DNS queries + + + + + + + + + 255 + 0 + 0 + + + + + + + + + 255 + 0 + 0 + + + + + + + + + 139 + 142 + 142 + + + + + - - true + + + 75 + true + true + + + + Firewall has been modified manually - please use qvm-firewall for any further configuration. - - + + + + + + Allow all outgoing Internet connections + + + + + + + Changing firewall settings does NOT affect existing connections. + + + Limit outgoing Internet connections to ... + + + + + + + + + Qt::Horizontal + + + + + + + List of allowed (whitelisted) addresses: + + + + + QLayout::SetMaximumSize @@ -851,7 +1022,7 @@ - + @@ -868,7 +1039,7 @@ - + @@ -885,7 +1056,10 @@ - + + + Changing firewall settings does NOT affect existing connections. + @@ -918,14 +1092,7 @@ - - - - Allow connections to Updates Proxy - - - - + true @@ -940,15 +1107,15 @@ 0 - - + + Allow full access for - - + + min @@ -957,9 +1124,35 @@ + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + 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. + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + @@ -1162,15 +1355,10 @@ vcpus include_in_balancing kernel - policyAllowRadioButton - policyDenyRadioButton - icmpCheckBox - dnsCheckBox - yumproxyCheckBox - newRuleButton + new_rule_button rulesTreeView - editRuleButton - deleteRuleButton + edit_rule_button + delete_rule_button service_line_edit add_srv_button services_list diff --git a/version b/version index d13e837..2d2d681 100644 --- a/version +++ b/version @@ -1 +1 @@ -4.0.6 +4.0.10