diff --git a/ci/pylintrc b/ci/pylintrc index 85eefb5..1360ac3 100644 --- a/ci/pylintrc +++ b/ci/pylintrc @@ -14,6 +14,7 @@ ignore=tests, ui_restoredlg.py, ui_settingsdlg.py, resources_rc.py +extension-pkg-whitelist=PyQt5 [MESSAGES CONTROL] # abstract-class-little-used: see http://www.logilab.org/ticket/111138 diff --git a/qubesmanager.pro b/qubesmanager.pro index ce1577d..bcaa865 100644 --- a/qubesmanager.pro +++ b/qubesmanager.pro @@ -33,7 +33,6 @@ SOURCES = \ qubesmanager/resources_rc.py \ qubesmanager/restore.py \ qubesmanager/settings.py \ - qubesmanager/table_widgets.py \ qubesmanager/template_manager.py \ qubesmanager/ui_about.py \ qubesmanager/ui_backupdlg.py \ diff --git a/qubesmanager/qube_manager.py b/qubesmanager/qube_manager.py index 807b88e..db58de8 100644 --- a/qubesmanager/qube_manager.py +++ b/qubesmanager/qube_manager.py @@ -25,16 +25,26 @@ import os import os.path import subprocess from datetime import datetime, timedelta +from functools import partial from qubesadmin import exc from qubesadmin import utils -from PyQt5 import QtWidgets, QtCore, QtGui # pylint: disable=import-error +# pylint: disable=import-error +from PyQt5.QtCore import (Qt, QAbstractTableModel, QObject, pyqtSlot, QEvent, + QSettings, QRegExp, QSortFilterProxyModel, QSize, QPoint, QTimer) + +# pylint: disable=import-error +from PyQt5.QtWidgets import (QLineEdit, QStyledItemDelegate, QToolTip, + QMenu, QInputDialog, QMainWindow, QProgressDialog, QStyleOptionViewItem, + QAbstractItemView, QMessageBox) + +# pylint: disable=import-error +from PyQt5.QtGui import (QIcon, QPixmap, QRegExpValidator, QFont, QColor) from qubesmanager.about import AboutDialog from . import ui_qubemanager # pylint: disable=no-name-in-module -from . import table_widgets from . import settings from . import global_settings from . import restore @@ -45,7 +55,7 @@ from . import utils as manager_utils from . import common_threads -class SearchBox(QtWidgets.QLineEdit): +class SearchBox(QLineEdit): def __init__(self, parent=None): super(SearchBox, self).__init__(parent) self.focusing = False @@ -61,120 +71,211 @@ class SearchBox(QtWidgets.QLineEdit): self.selectAll() self.focusing = False +icon_size = QSize(24, 24) -class VmRowInTable: - # pylint: disable=too-few-public-methods,too-many-instance-attributes - def __init__(self, vm, row_no, table): +# pylint: disable=invalid-name +class StateIconDelegate(QStyledItemDelegate): + lastIndex = None + def __init__(self): + super(StateIconDelegate, self).__init__() + self.stateIcons = { + "Running" : QIcon(":/on.png"), + "Paused" : QIcon(":/paused.png"), + "Suspended" : QIcon(":/paused.png"), + "Transient" : QIcon(":/transient.png"), + "Halting" : QIcon(":/transient.png"), + "Dying" : QIcon(":/transient.png"), + "Halted" : QIcon(":/off.png") + } + self.outdatedIcons = { + "update" : QIcon(":/update-recommended.png"), + "outdated" : QIcon(":/outdated.png"), + "to-be-outdated" : QIcon(":/to-be-outdated.png"), + } + self.outdatedTooltips = { + "update" : self.tr("Updates pending!"), + "outdated" : self.tr( + "The qube must be restarted for its filesystem to reflect" + " the template's recent committed changes."), + "to-be-outdated" : self.tr( + "The Template must be stopped before changes from its " + "current session can be picked up by this qube."), + } + + def sizeHint(self, option, index): + hint = super(StateIconDelegate, self).sizeHint(option, index) + option = QStyleOptionViewItem(option) + option.features |= option.HasDecoration + widget = option.widget + style = widget.style() + iconRect = style.subElementRect(style.SE_ItemViewItemDecoration, + option, widget) + width = iconRect.width() * 3 # Nº of possible icons + hint.setWidth(width) + return hint + + def paint(self, qp, option, index): + # create a new QStyleOption (*never* use the one given in arguments) + option = QStyleOptionViewItem(option) + + widget = option.widget + style = widget.style() + + # paint the base item (borders, gradients, selection colors, etc) + style.drawControl(style.CE_ItemViewItem, option, qp, widget) + + # "lie" about the decoration, to get a valid icon rectangle (even if we + # don't have any "real" icon set for the item) + option.features |= option.HasDecoration + iconRect = style.subElementRect(style.SE_ItemViewItemDecoration, + option, widget) + iconSize = iconRect.size() + margin = iconRect.left() - option.rect.left() + + qp.save() + # ensure that we do not draw outside the item rectangle (and add some + # fancy margin on the right + qp.setClipRect(option.rect.adjusted(0, 0, -margin, 0)) + + # draw the main state icon, assuming all items have one + qp.drawPixmap(iconRect, + self.stateIcons[index.data()['power']].pixmap(iconSize)) + + left = delta = margin + iconRect.width() + if index.data()['outdated']: + qp.drawPixmap(iconRect.translated(left, 0), + self.outdatedIcons[index.data()['outdated']]\ + .pixmap(iconSize)) + left += delta + + qp.restore() + + def helpEvent(self, event, view, option, index): + if event.type() != QEvent.ToolTip: + return super(StateIconDelegate, self).helpEvent(event, view, + option, index) + option = QStyleOptionViewItem(option) + widget = option.widget + style = widget.style() + option.features |= option.HasDecoration + + iconRect = style.subElementRect(style.SE_ItemViewItemDecoration, + option, widget) + iconRect.setTop(option.rect.y()) + iconRect.setHeight(option.rect.height()) + + # similar to what we do in the paint() method + if event.pos() in iconRect: + # (*) clear any existing tooltip; a single space is better , as + # sometimes it's not enough to use an empty string + if index != self.lastIndex: + QToolTip.showText(QPoint(), ' ') + QToolTip.showText(event.globalPos(), + index.data()['power'], view) + else: + margin = iconRect.left() - option.rect.left() + left = delta = margin + iconRect.width() + + if index.data()['outdated']: + if event.pos() in iconRect.translated(left, 0): + # see above (*) + if index != self.lastIndex: + QToolTip.showText(QPoint(), ' ') + QToolTip.showText(event.globalPos(), + self.outdatedTooltips[index.data()['outdated']], + view) + # shift the left *only* if the role is True, otherwise we + # can assume that that icon doesn't exist at all + left += delta + self.lastIndex = index + return True + + +# pylint: disable=too-many-instance-attributes +# pylint: disable=too-few-public-methods +class VmInfo(): + def __init__(self, vm): self.vm = vm - - 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.VMPropertyItem(vm, "name") - 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.VMPropertyItem(vm, "netvm", - check_default=True) - 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.VMPropertyItem(vm, "ip") - table.setItem(row_no, VmManagerWindow.columns_indices['IP'], - self.ip_widget) - - self.include_in_backups_widget = table_widgets.VMPropertyItem( - vm, "include_in_backups", - empty_function=(lambda x: not bool(x))) - table.setItem(row_no, VmManagerWindow.columns_indices[ - 'Include in backups'], self.include_in_backups_widget) - - self.last_backup_widget = table_widgets.VmLastBackupItem( - vm, "backup_timestamp") - table.setItem(row_no, VmManagerWindow.columns_indices[ - 'Last backup'], self.last_backup_widget) - - self.dvm_template_widget = table_widgets.VMPropertyItem( - vm, "default_dispvm", check_default=True) - table.setItem(row_no, VmManagerWindow.columns_indices['Default DispVM'], - self.dvm_template_widget) - - self.is_dispvm_template_widget = table_widgets.VMPropertyItem( - vm, "template_for_dispvms", empty_function=(lambda x: not x)) - table.setItem( - row_no, VmManagerWindow.columns_indices['Is DVM Template'], - self.is_dispvm_template_widget) - - self.virt_mode_widget = table_widgets.VMPropertyItem(vm, 'virt_mode') - table.setItem(row_no, VmManagerWindow.columns_indices[ - 'Virtualization Mode'], self.virt_mode_widget) - - self.table = table + self.qid = vm.qid + self.name = self.vm.name + self.label = self.vm.label + self.klass = self.vm.klass + self.state = {'power': "", 'outdated': ""} + self.updateable = getattr(vm, 'updateable', False) + self.update(True) def update(self, update_size_on_disk=False, event=None): """ - 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 + Update VmInfo + :param update_size_on_disk: should disk utilization be updated? :param event: name of the event that caused the update, to avoid updating unnecessary properties; if event is none, update everything :return: None """ try: - self.info_widget.update_vm_state() + self.state['power'] = self.vm.get_power_state() + + if self.vm.is_running(): + if hasattr(self.vm, 'template') and \ + self.vm.template.is_running(): + self.state['outdated'] = "to-be-outdated" + else: + for vol in self.vm.volumes.values(): + if vol.is_outdated(): + self.state['outdated'] = "outdated" + break + else: + self.state['outdated'] = "" + + if self.vm.klass in {'TemplateVM', 'StandaloneVM'} and \ + self.vm.features.get('updates-available', False): + self.state['outdated'] = 'update' + if not event or event.endswith(':label'): - self.label_widget.update() + self.label = self.vm.label if not event or event.endswith(':template'): - self.template_widget.update() + try: + self.template = self.vm.template.name + except AttributeError: + self.template = None if not event or event.endswith(':netvm'): - self.netvm_widget.update() + self.netvm = getattr(self.vm, 'netvm', None) + if self.netvm: + self.netvm = self.netvm.name + else: + self.netvm = "n/a" + if self.qid != 0 and self.vm.property_is_default("netvm"): + self.netvm = "default (" + self.netvm + ")" if not event or event.endswith(':internal'): # this is a feature, not a property; TODO: fix event handling - self.internal_widget.update() + self.internal = self.vm.features.get('internal', False) if not event or event.endswith(':ip'): - self.ip_widget.update() + self.ip = getattr(self.vm, 'ip', "n/a") if not event or event.endswith(':include_in_backups'): - self.include_in_backups_widget.update() + self.inc_backup = getattr(self.vm, 'include_in_backups', None) if not event or event.endswith(':backup_timestamp'): - self.last_backup_widget.update() + self.last_backup = getattr(self.vm, 'backup_timestamp', None) + if self.last_backup: + self.last_backup = str(datetime.fromtimestamp( + self.last_backup)) if not event or event.endswith(':default_dispvm'): - self.dvm_template_widget.update() + self.dvm = getattr(self.vm, 'default_dispvm', None) + if self.vm.property_is_default("default_dispvm"): + self.dvm = "default (" + str(self.dvm) + ")" + elif self.dvm is not None: + self.dvm = self.dvm.name if not event or event.endswith(':template_for_dispvms'): - self.is_dispvm_template_widget.update() - if not event or event.endswith(':virt_mode'): - self.virt_mode_widget.update() - if update_size_on_disk: - self.size_widget.update() + self.dvm_template = getattr(self.vm, 'template_for_dispvms', + None) + if self.qid != 0 and update_size_on_disk: + self.disk_float = float(self.vm.get_disk_utilization()) + self.disk = str(round(self.disk_float/(1024*1024), 2)) + " MiB" + + if self.qid != 0: + self.virt_mode = self.vm.virt_mode + else: + self.virt_mode = None + self.disk = "n/a" except exc.QubesPropertyAccessError: pass except exc.QubesDaemonNoResponseError: @@ -182,19 +283,179 @@ class VmRowInTable: # AdminAPI pass - # force re-sorting - self.table.setSortingEnabled(True) +class QubesCache(QAbstractTableModel): + def __init__(self, qubes_app): + QAbstractTableModel.__init__(self) + self._qubes_app = qubes_app + self._info_list = [] + self._info_by_id = {} + + def add_vm(self, vm): + vm_info = VmInfo(vm) + self._info_list.append(vm_info) + self._info_by_id[vm.qid] = vm_info + + def remove_vm(self, name): + vm_info = self.get_vm(name=name) + self._info_list.remove(vm_info) + del self._info_by_id[vm_info.qid] + + def get_vm(self, row=None, qid=None, name=None): + if row is not None: + return self._info_list[row] + if qid is not None: + return self._info_by_id[qid] + return next(x for x in self._info_list if x.name == name) + + def __len__(self): + return len(self._info_list) + + def __iter__(self): + return iter(self._info_list) + +class QubesTableModel(QAbstractTableModel): + def __init__(self, qubes_cache): + QAbstractTableModel.__init__(self) + self.qubes_cache = qubes_cache + self.template = {} + self.klass_pixmap = {} + self.label_pixmap = {} + self.columns_indices = [ + "Type", + "Label", + "Name", + "State", + "Template", + "NetVM", + "Disk Usage", + "Internal", + "IP", + "Include in backups", + "Last backup", + "Default DispVM", + "Is DVM Template", + "Virt Mode" + ] + + # pylint: disable=invalid-name + def rowCount(self, _): + return len(self.qubes_cache) + + # pylint: disable=invalid-name + def columnCount(self, _): + return len(self.columns_indices) + + # pylint: disable=too-many-return-statements + def data(self, index, role): + if not index.isValid(): + return None + + col = index.column() + row = index.row() + + col_name = self.columns_indices[col] + vm = self.qubes_cache.get_vm(row) + + if role == Qt.DisplayRole: + if col in [0, 1]: + return None + if col_name == "Name": + return vm.name + if col_name == "State": + return vm.state + if col_name == "Template": + if vm.template is None: + return vm.klass + return vm.template + if col_name == "NetVM": + return vm.netvm + if col_name == "Disk Usage": + return vm.disk + if col_name == "Internal": + return "Yes" if vm.internal else "" + if col_name == "IP": + return vm.ip + if col_name == "Include in backups": + return "Yes" if vm.inc_backup else "" + if col_name == "Last backup": + return vm.last_backup + if col_name == "Default DispVM": + return vm.dvm + if col_name == "Is DVM Template": + return "Yes" if vm.dvm_template else "" + if col_name == "Virt Mode": + return vm.virt_mode + if role == Qt.DecorationRole: + if col_name == "Type": + try: + return self.klass_pixmap[vm.klass] + except KeyError: + pixmap = QPixmap() + icon_name = ":/"+vm.klass.lower()+".png" + icon_name = icon_name.replace("adminvm", "dom0") + icon_name = icon_name.replace("dispvm", "appvm") + pixmap.load(icon_name) + self.klass_pixmap[vm.klass] = pixmap.scaled(icon_size) + return self.klass_pixmap[vm.klass] + + if col_name == "Label": + try: + return self.label_pixmap[vm.label] + except KeyError: + icon = QIcon.fromTheme(vm.label.icon) + self.label_pixmap[vm.label] = icon.pixmap(icon_size) + return self.label_pixmap[vm.label] + + if role == Qt.FontRole: + if col_name == "Template": + if vm.template is None: + font = QFont() + font.setItalic(True) + return font + + if role == Qt.ForegroundRole: + if col_name == "Template": + if vm.template is None: + return QColor("gray") + + # Used for get VM Object + if role == Qt.UserRole: + return vm + + # Used for sorting + if role == Qt.UserRole + 1: + if vm.qid == 0: + return "" + if col_name == "Type": + return vm.klass + if col_name == "Label": + return vm.label.name + if col_name == "State": + return str(vm.state) + if col_name == "Disk Usage": + return vm.disk_float + + return self.data(index, Qt.DisplayRole) + + # pylint: disable=invalid-name + def headerData(self, col, orientation, role): + if col < 2: + return None + if orientation == Qt.Horizontal and role == Qt.DisplayRole: + return self.columns_indices[col] + return None + vm_shutdown_timeout = 20000 # in msec vm_restart_check_timeout = 1000 # in msec -class VmShutdownMonitor(QtCore.QObject): +class VmShutdownMonitor(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) + QObject.__init__(self) self.vm = vm self.shutdown_time = shutdown_time self.check_time = check_time @@ -208,7 +469,7 @@ class VmShutdownMonitor(QtCore.QObject): def check_again_later(self): # noinspection PyTypeChecker,PyCallByClass - QtCore.QTimer.singleShot(self.check_time, self.check_if_vm_has_shutdown) + QTimer.singleShot(self.check_time, self.check_if_vm_has_shutdown) def timeout_reached(self): actual = datetime.now() - self.shutdown_started @@ -228,19 +489,19 @@ class VmShutdownMonitor(QtCore.QObject): and vm_start_time < self.shutdown_started: if self.timeout_reached(): - msgbox = QtWidgets.QMessageBox(self.caller) - msgbox.setIcon(QtWidgets.QMessageBox.Question) + msgbox = QMessageBox(self.caller) + msgbox.setIcon(QMessageBox.Question) msgbox.setWindowTitle(self.tr("Qube Shutdown")) msgbox.setText(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)) kill_button = msgbox.addButton( - self.tr("Kill it!"), QtWidgets.QMessageBox.YesRole) + self.tr("Kill it!"), QMessageBox.YesRole) wait_button = msgbox.addButton( self.tr("Wait another {0} seconds...").format( self.shutdown_time / 1000), - QtWidgets.QMessageBox.NoRole) + QMessageBox.NoRole) msgbox.setDefaultButton(wait_button) msgbox.exec_() msgbox.deleteLater() @@ -325,90 +586,41 @@ class RunCommandThread(common_threads.QubesThread): except (ChildProcessError, exc.QubesException) as ex: self.msg = (self.tr("Error while running command!"), str(ex)) +class QubesProxyModel(QSortFilterProxyModel): + def lessThan(self, left, right): + if left.data(self.sortRole()) != right.data(self.sortRole()): + return super().lessThan(left, right) -class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtWidgets.QMainWindow): - # pylint: disable=too-many-instance-attributes - row_height = 30 - column_width = 200 - search = "" + left_vm = left.data(Qt.UserRole) + right_vm = right.data(Qt.UserRole) + + return left_vm.name.lower() < right_vm.name.lower() + +class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QMainWindow): # 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, - "Include in backups": 9, - "Last backup": 10, - "Default DispVM": 11, - "Is DVM Template": 12, - "Virtualization Mode": 13 - } - def __init__(self, qt_app, qubes_app, dispatcher, parent=None): - # pylint: disable=unused-argument + def __init__(self, qt_app, qubes_app, dispatcher, _parent=None): super(VmManagerWindow, self).__init__() self.setupUi(self) - self.manager_settings = QtCore.QSettings(self) + self.manager_settings = 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.searchbox.setValidator(QRegExpValidator( + QRegExp("[a-zA-Z0-9_-]*", Qt.CaseInsensitive), None)) + self.searchbox.textChanged.connect(self.do_search) self.searchContainer.addWidget(self.searchbox) - self.table.itemSelectionChanged.connect(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.settings_windows = {} self.frame_width = 0 self.frame_height = 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["Include in backups"]: self.action_backups, - self.columns_indices["Last backup"]: self.action_last_backup, - self.columns_indices["Default DispVM"]: self.action_dispvm_template, - self.columns_indices["Is DVM Template"]: - self.action_is_dvm_template, - self.columns_indices["Virtualization Mode"]: self.action_virt_mode - } - - self.visible_columns_count = len(self.columns_indices) - - # Other columns get sensible default sizes, but those have only - # icon content, and thus PyQt makes them too wide - self.table.setColumnWidth(self.columns_indices["State"], 80) - self.table.setColumnWidth(self.columns_indices["Label"], 40) - self.table.setColumnWidth(self.columns_indices["Type"], 40) - - self.table.horizontalHeader().setSectionResizeMode( - QtWidgets.QHeaderView.Interactive) - self.table.horizontalHeader().setStretchLastSection(True) - self.table.horizontalHeader().setMinimumSectionSize(40) - - self.context_menu = QtWidgets.QMenu(self) + self.context_menu = QMenu(self) self.context_menu.addAction(self.action_settings) self.context_menu.addAction(self.action_editfwrules) @@ -434,40 +646,61 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtWidgets.QMainWindow): self.context_menu.addMenu(self.logs_menu) self.context_menu.addSeparator() - self.tools_context_menu = QtWidgets.QMenu(self) + self.tools_context_menu = QMenu(self) self.tools_context_menu.addAction(self.action_toolbar) self.tools_context_menu.addAction(self.action_menubar) - self.dom0_context_menu = QtWidgets.QMenu(self) - self.dom0_context_menu.addAction(self.action_global_settings) - self.dom0_context_menu.addAction(self.action_updatevm) - self.dom0_context_menu.addSeparator() - - self.dom0_context_menu.addMenu(self.logs_menu) - self.dom0_context_menu.addSeparator() - - self.table.horizontalHeader().sortIndicatorChanged.connect( - self.sort_indicator_changed) - self.table.customContextMenuRequested.connect(self.open_context_menu) self.menubar.customContextMenuRequested.connect( - lambda pos: self.open_tools_context_menu(self.menubar, pos)) + lambda pos: self.open_tools_context_menu(self.menubar, pos)) self.toolbar.customContextMenuRequested.connect( - lambda pos: self.open_tools_context_menu(self.toolbar, pos)) - self.logs_menu.triggered.connect(self.show_log) - - self.searchbox.textChanged.connect(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) - + lambda pos: self.open_tools_context_menu(self.toolbar, pos)) self.action_menubar.toggled.connect(self.showhide_menubar) self.action_toolbar.toggled.connect(self.showhide_toolbar) + self.logs_menu.triggered.connect(self.show_log) + + self.table.resizeColumnsToContents() + + self.update_size_on_disk = False + self.shutdown_monitor = {} + + self.qubes_cache = QubesCache(qubes_app) + self.fill_cache() + self.qubes_model = QubesTableModel(self.qubes_cache) + + self.proxy = QubesProxyModel() + self.proxy.setSourceModel(self.qubes_model) + self.proxy.setSortRole(Qt.UserRole + 1) + self.proxy.setSortCaseSensitivity(Qt.CaseInsensitive) + self.proxy.setFilterKeyColumn(2) + self.proxy.setFilterCaseSensitivity(Qt.CaseInsensitive) + self.proxy.layoutChanged.connect(self.save_sorting) + + self.table.setModel(self.proxy) + self.table.setItemDelegateForColumn(3, StateIconDelegate()) + self.table.resizeColumnsToContents() + self.table.setSelectionMode(QAbstractItemView.ExtendedSelection) + selection_model = self.table.selectionModel() + selection_model.selectionChanged.connect(self.table_selection_changed) + + self.table.setContextMenuPolicy(Qt.CustomContextMenu) + self.table.customContextMenuRequested.connect(self.open_context_menu) + + # Create view menu + for col_no in range(len(self.qubes_model.columns_indices)): + column = self.qubes_model.columns_indices[col_no] + action = self.menu_view.addAction(column) + action.setData(column) + action.setCheckable(True) + action.toggled.connect(partial(self.showhide_column, col_no)) + + self.menu_view.addSeparator() + self.menu_view.addAction(self.action_toolbar) + self.menu_view.addAction(self.action_menubar) try: self.load_manager_settings() except Exception as ex: # pylint: disable=broad-except - QtWidgets.QMessageBox.warning( + QMessageBox.warning( self, self.tr("Manager settings unreadable"), self.tr("Qube Manager settings cannot be parsed. Previously " @@ -476,13 +709,6 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtWidgets.QMainWindow): self.settings_loaded = True - self.fill_table() - - self.table.resizeColumnsToContents() - - self.update_size_on_disk = False - self.shutdown_monitor = {} - # Connect events self.dispatcher = dispatcher dispatcher.add_handler('domain-pre-start', @@ -518,16 +744,36 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtWidgets.QMainWindow): self.check_updates() - # select the first row of the table to make sure menu actions are - # correctly initialized - self.table.selectRow(0) + def save_sorting(self): + self.manager_settings.setValue('view/sort_column', + self.proxy.sortColumn()) + self.manager_settings.setValue('view/sort_order', + self.proxy.sortOrder()) + + def fill_cache(self): + progress = QProgressDialog( + self.tr( + "Loading Qube Manager..."), "", 0, + len(self.qubes_app.domains.keys())) + progress.setWindowTitle(self.tr("Qube Manager")) + progress.setMinimumDuration(1000) + progress.setWindowModality(Qt.WindowModal) + progress.setCancelButton(None) + + row_no = 0 + for vm in self.qubes_app.domains: + progress.setValue(row_no) + self.qubes_cache.add_vm(vm) + row_no += 1 + + progress.setValue(row_no) def setup_application(self): self.qt_app.setApplicationName(self.tr("Qube Manager")) - self.qt_app.setWindowIcon(QtGui.QIcon.fromTheme("qubes-manager")) + self.qt_app.setWindowIcon(QIcon.fromTheme("qubes-manager")) def keyPressEvent(self, event): # pylint: disable=invalid-name - if event.key() == QtCore.Qt.Key_Escape: + if event.key() == Qt.Key_Escape: self.searchbox.clear() super(VmManagerWindow, self).keyPressEvent(event) @@ -541,12 +787,12 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtWidgets.QMainWindow): if thread.msg: (title, msg) = thread.msg if thread.msg_is_success: - QtWidgets.QMessageBox.information( + QMessageBox.information( self, title, msg) else: - QtWidgets.QMessageBox.warning( + QMessageBox.warning( self, title, msg) @@ -556,115 +802,90 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtWidgets.QMainWindow): raise RuntimeError(self.tr('No finished thread found')) - def closeEvent(self, event): - # pylint: disable=invalid-name - # save window size at close - self.manager_settings.setValue("window_size", self.size()) - event.accept() + # pylint: disable=invalid-name + def resizeEvent(self, event): + self.manager_settings.setValue("window_size", event.size()) - def check_updates(self, vm=None): - if vm is None: - for vm_iter in self.qubes_app.domains: - self.check_updates(vm_iter) + def check_updates(self, info=None): + if info is None: + for info_iter in self.qubes_cache: + self.check_updates(info_iter) return - if vm.klass in {'TemplateVM', 'StandaloneVM'}: - try: - self.vms_in_table[vm.qid].info_widget.update_vm_state() - except (exc.QubesException, KeyError): - # the VM might have vanished in the meantime or - # the signal might have been handled in the wrong order - pass + if info.vm.klass in {'TemplateVM', 'StandaloneVM'} and \ + info.vm.features.get('updates-available', False): + info.state['outdated'] = 'update' def on_domain_added(self, _submitter, _event, vm, **_kwargs): - row_no = 0 - self.table.setSortingEnabled(False) try: domain = self.qubes_app.domains[vm] - row_no = self.table.rowCount() - self.table.setRowCount(row_no + 1) - vm_row = VmRowInTable(domain, row_no, self.table) - self.vms_in_table[domain.qid] = vm_row + self.qubes_cache.add_vm(domain) + self.proxy.invalidate() except (exc.QubesException, KeyError): - if row_no != 0: - self.table.removeRow(row_no) - self.table.setSortingEnabled(True) - self.showhide_vms() + pass def on_domain_removed(self, _submitter, _event, **kwargs): - row_to_delete = None - qid_to_delete = None - for qid, row in self.vms_in_table.items(): - if row.vm.name == kwargs['vm']: - row_to_delete = row - qid_to_delete = qid - if not row_to_delete: - return # for some reason, the VM was removed in some other way + self.qubes_cache.remove_vm(name=kwargs['vm']) + self.proxy.invalidate() - del self.vms_in_table[qid_to_delete] - self.table.removeRow(row_to_delete.name_widget.row()) - - def on_domain_status_changed(self, vm, _event, **_kwargs): + def on_domain_status_changed(self, vm, event, **_kwargs): try: - self.vms_in_table[vm.qid].info_widget.update_vm_state() + self.qubes_cache.get_vm(qid=vm.qid).update(event=event) + if vm.klass in {'TemplateVM'}: + for appvm in vm.appvms: + self.qubes_cache.get_vm(qid=appvm.qid).\ + update(event="outdated") + self.proxy.invalidate() + self.table_selection_changed() except exc.QubesPropertyAccessError: return # the VM was deleted before its status could be updated except KeyError: # adding the VM failed for some reason self.on_domain_added(None, None, vm) - if vm == self.get_selected_vm(): - self.table_selection_changed() - - if vm.klass == 'TemplateVM': - for row in self.vms_in_table.values(): - if getattr(row.vm, 'template', None) == vm: - row.info_widget.update_vm_state() - def on_domain_updates_available(self, vm, _event, **_kwargs): - self.check_updates(vm) + self.check_updates(self.qubes_cache.get_vm(qid=vm.qid)) def on_domain_changed(self, vm, event, **_kwargs): if not vm: # change of global properties occured if event.endswith(':default_netvm'): - for vm_row in self.vms_in_table.values(): - vm_row.update(event='property-set:netvm') + for vm_info in self.qubes_cache: + vm_info.update(event='property-set:netvm') if event.endswith(':default_dispvm'): - for vm_row in self.vms_in_table.values(): - vm_row.update(event='property-set:default_dispvm') + for vm_info in self.qubes_cache: + vm_info.update(event='property-set:default_dispvm') return try: - self.vms_in_table[vm.qid].update(event=event) + self.qubes_cache.get_vm(qid=vm.qid).update(event=event) + self.proxy.invalidate() except exc.QubesPropertyAccessError: return # the VM was deleted before its status could be updated def load_manager_settings(self): - for col in self.columns_indices: - col_no = self.columns_indices[col] - if col == 'Name': - # 'Name' column should be always visible - self.columns_actions[col_no].setChecked(True) - else: - visible = self.manager_settings.value( - 'columns/%s' % col, - defaultValue="true") - self.columns_actions[col_no].setChecked(visible == "true") + # Load view menu settings + for action in self.menu_view.actions(): + column = action.data() + if column is not None: + col_no = self.qubes_model.columns_indices.index(column) + if column == 'Name': + # 'Name' column should be always visible + action.setChecked(True) + else: + visible = self.manager_settings.value('columns/%s' % column, + defaultValue="true") + action.setChecked(visible == "true") + self.showhide_column(col_no, visible == "true") - 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)) + # Restore sorting + sort_column = int(self.manager_settings.value("view/sort_column", + defaultValue=2)) + order = Qt.SortOrder(self.manager_settings.value("view/sort_order", + defaultValue=Qt.AscendingOrder)) - try: - self.table.sortItems(self.columns_indices[self.sort_by_column], - self.sort_order) - except KeyError: - # the manager was sorted on a column that does not exist in the - # current version; possible only with downgrades - self.table.sortItems(self.columns_indices["Name"], - self.sort_order) + if not sort_column: # Default sort by name + self.table.sortByColumn(2, Qt.AscendingOrder) + else: + self.table.sortByColumn(sort_column, order) if not self.manager_settings.value("view/menubar_visible", defaultValue=True): @@ -675,259 +896,204 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtWidgets.QMainWindow): # load last window size self.resize(self.manager_settings.value("window_size", - QtCore.QSize(1100, 600))) + QSize(1100, 600))) - def get_vms_list(self): - return list(self.qubes_app.domains) - - def fill_table(self): - self.table.setSortingEnabled(False) - vms_list = self.get_vms_list() - - vms_in_table = {} - - self.table.setRowCount(len(vms_list)) - - progress = QtWidgets.QProgressDialog( - self.tr( - "Loading Qube Manager..."), "", 0, len(vms_list)) - progress.setWindowTitle(self.tr("Qube Manager")) - progress.setMinimumDuration(1000) - progress.setCancelButton(None) - - row_no = 0 - for vm in vms_list: - progress.setValue(row_no) - vm_row = VmRowInTable(vm, row_no, self.table) - vms_in_table[vm.qid] = vm_row - row_no += 1 - - progress.setValue(row_no) - - self.vms_list = vms_list - self.vms_in_table = vms_in_table - self.table.setSortingEnabled(True) - - 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) + @pyqtSlot(str) def do_search(self, search): - self.search = str(search) - self.showhide_vms() + self.proxy.setFilterFixedString(search) # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_search_triggered') + @pyqtSlot(name='on_action_search_triggered') def action_search_triggered(self): self.searchbox.setFocus() - # 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 get_selected_vms(self): + vms = [] + + selection = self.table.selectionModel().selection() + indexes = self.proxy.mapSelectionToSource(selection).indexes() + + for index in indexes: + if index.column() != 0: + continue + vms.append(index.data(Qt.UserRole)) + + return vms def table_selection_changed(self): - vm = self.get_selected_vm() + # Since selection could have multiple domains + # enable all first and then filter them + for action in self.toolbar.actions() + self.context_menu.actions(): + action.setEnabled(True) - if vm is not None and vm in self.qubes_app.domains: + for vm in self.get_selected_vms(): # 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' or not vm.auto_cleanup)) - self.action_killvm.setEnabled( - (vm.get_power_state() == "Paused" or vm.is_running()) - and vm.klass != 'AdminVM') + if vm.state['power'] in \ + ['Running', 'Transient', 'Halting', 'Dying']: + self.action_resumevm.setEnabled(False) + self.action_removevm.setEnabled(False) + elif vm.state['power'] == 'Paused': + self.action_removevm.setEnabled(False) + self.action_pausevm.setEnabled(False) + self.action_set_keyboard_layout.setEnabled(False) + self.action_restartvm.setEnabled(False) + self.action_open_console.setEnabled(False) + elif vm.state['power'] == 'Suspend': + self.action_set_keyboard_layout.setEnabled(False) + self.action_removevm.setEnabled(False) + self.action_pausevm.setEnabled(False) + self.action_open_console.setEnabled(False) + elif vm.state['power'] == 'Halted': + self.action_set_keyboard_layout.setEnabled(False) + self.action_pausevm.setEnabled(False) + self.action_shutdownvm.setEnabled(False) + self.action_restartvm.setEnabled(False) + self.action_killvm.setEnabled(False) + self.action_open_console.setEnabled(False) - 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_open_console.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_open_console.setEnabled(False) - self.action_set_keyboard_layout.setEnabled(False) + if vm.klass == 'AdminVM': + self.action_open_console.setEnabled(False) + self.action_settings.setEnabled(False) + self.action_resumevm.setEnabled(False) + self.action_removevm.setEnabled(False) + self.action_clonevm.setEnabled(False) + self.action_pausevm.setEnabled(False) + self.action_restartvm.setEnabled(False) + self.action_killvm.setEnabled(False) + self.action_shutdownvm.setEnabled(False) + self.action_appmenus.setEnabled(False) + self.action_editfwrules.setEnabled(False) + self.action_set_keyboard_layout.setEnabled(False) + self.action_run_command_in_vm.setEnabled(False) + elif vm.klass == 'DispVM': + self.action_appmenus.setEnabled(False) + self.action_restartvm.setEnabled(False) + + if vm.vm.features.get('internal', False): + self.action_appmenus.setEnabled(False) + + if not vm.updateable and vm.qid != 0: + self.action_updatevm.setEnabled(False) self.update_logs_menu() # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_createvm_triggered') - def action_createvm_triggered(self): # pylint: disable=no-self-use + @pyqtSlot(name='on_action_createvm_triggered') + def action_createvm_triggered(self): with common_threads.busy_cursor(): create_window = create_new_vm.NewVmDlg(self.qt_app, self.qubes_app) create_window.exec_() - 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 - return None - # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_removevm_triggered') + @pyqtSlot(name='on_action_removevm_triggered') def action_removevm_triggered(self): - # pylint: disable=no-else-return + remove_vms = [] - vm = self.get_selected_vm() + for vm_info in self.get_selected_vms(): + vm = vm_info.vm - dependencies = utils.vm_dependencies(self.qubes_app, vm) + dependencies = utils.vm_dependencies(self.qubes_app, vm) - if dependencies: - list_text = "
" + \ - manager_utils.format_dependencies_list(dependencies) + \ - "
" + if dependencies: + list_deps = manager_utils.format_dependencies_list(dependencies) + list_text = "
" + list_deps + "
" - info_dialog = QtWidgets.QMessageBox(self) - info_dialog.setWindowTitle(self.tr("Warning!")) - info_dialog.setText( - self.tr("This qube cannot be removed. It is used as:" - "
{} If you want to remove this qube, " - "you should remove or change settings of each qube " - "or setting that uses it.").format(list_text)) - info_dialog.setModal(False) - info_dialog.show() + info_dialog = QMessageBox(self) + info_dialog.setWindowTitle(self.tr("Warning!")) + info_dialog.setText( + self.tr("This qube cannot be removed. It is used as:
" + "{} If you want to remove this qube, you " + "should remove or change settings of each qube or " + "setting that uses it.").format(list_text)) + info_dialog.setModal(False) + info_dialog.show() - return + return - (requested_name, ok) = QtWidgets.QInputDialog.getText( - self, 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)) + (requested_name, ok) = QInputDialog.getText( + self, 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}) be" + "low to confirm:").format(vm.name, vm.name)) - if not ok: - # user clicked cancel - return + if not ok: + # user clicked cancel + continue - if requested_name != vm.name: - # name did not match - QtWidgets.QMessageBox.warning( - self, - self.tr("Qube removal confirmation failed"), - self.tr( - "Entered name did not match! Not removing " - "{0}.").format(vm.name)) - return + if requested_name == vm.name: + remove_vms.append(vm) + else: + # name did not match + QMessageBox.warning( + self, + self.tr("Qube removal confirmation failed"), + self.tr( + "Entered name did not match! Not removing " + "{0}.").format(vm.name)) - else: - # remove the VM + # remove the VMs + for vm in remove_vms: thread = common_threads.RemoveVMThread(vm) self.threads_list.append(thread) thread.finished.connect(self.clear_threads) thread.start() # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_clonevm_triggered') + @pyqtSlot(name='on_action_clonevm_triggered') def action_clonevm_triggered(self): - vm = self.get_selected_vm() + for vm_info in self.get_selected_vms(): + vm = vm_info.vm + name_number = 1 + name_format = vm.name + '-clone-%d' + while name_format % name_number in self.qubes_app.domains.keys(): + name_number += 1 - 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) = 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 - (clone_name, ok) = QtWidgets.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 + name_in_use = clone_name in self.qubes_app.domains - name_in_use = clone_name in self.qubes_app.domains + if name_in_use: + QMessageBox.warning( + self, self.tr("Name already in use!"), + self.tr("There already exists a qube called '{}'. " + "Cloning aborted.").format(clone_name)) + return - if name_in_use: - QtWidgets.QMessageBox.warning( - self, self.tr("Name already in use!"), - self.tr("There already exists a qube called '{}'. " - "Cloning aborted.").format(clone_name)) - return + self.progress = QProgressDialog( + self.tr( + "Cloning Qube..."), "", 0, 0) + self.progress.setCancelButton(None) + self.progress.setModal(True) + self.progress.setWindowTitle(self.tr("Cloning qube...")) + self.progress.show() - self.progress = QtWidgets.QProgressDialog( - self.tr( - "Cloning Qube..."), "", 0, 0) - self.progress.setCancelButton(None) - self.progress.setModal(True) - self.progress.setWindowTitle(self.tr("Cloning qube...")) - self.progress.show() - - thread = common_threads.CloneVMThread(vm, clone_name) - thread.finished.connect(self.clear_threads) - self.threads_list.append(thread) - thread.start() + thread = common_threads.CloneVMThread(vm, clone_name) + thread.finished.connect(self.clear_threads) + self.threads_list.append(thread) + thread.start() # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_resumevm_triggered') + @pyqtSlot(name='on_action_resumevm_triggered') def action_resumevm_triggered(self): - vm = self.get_selected_vm() + for vm_info in self.get_selected_vms(): + vm = vm_info.vm + if vm.get_power_state() in ["Paused", "Suspended"]: + try: + vm.unpause() + except exc.QubesException as ex: + QMessageBox.warning( + self, self.tr("Error unpausing Qube!"), + self.tr("ERROR: {0}").format(ex)) + return - if vm.get_power_state() in ["Paused", "Suspended"]: - try: - vm.unpause() - except exc.QubesException as ex: - QtWidgets.QMessageBox.warning( - self, self.tr("Error unpausing Qube!"), - self.tr("ERROR: {0}").format(ex)) - return - - self.start_vm(vm) + self.start_vm(vm) def start_vm(self, vm): if vm.is_running(): @@ -939,46 +1105,46 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtWidgets.QMainWindow): thread.start() # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_startvm_tools_install_triggered') + @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') + @pyqtSlot(name='on_action_pausevm_triggered') def action_pausevm_triggered(self): - vm = self.get_selected_vm() - try: - vm.pause() - except exc.QubesException as ex: - QtWidgets.QMessageBox.warning( - self, - self.tr("Error pausing Qube!"), - self.tr("ERROR: {0}").format(ex)) - return + for vm_info in self.get_selected_vms(): + try: + vm_info.vm.pause() + except exc.QubesException as ex: + QMessageBox.warning( + self, + self.tr("Error pausing Qube!"), + self.tr("ERROR: {0}").format(ex)) + return # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_shutdownvm_triggered') + @pyqtSlot(name='on_action_shutdownvm_triggered') def action_shutdownvm_triggered(self): - vm = self.get_selected_vm() + for vm_info in self.get_selected_vms(): + vm = vm_info.vm + reply = QMessageBox.question( + self, 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), + QMessageBox.Yes | QMessageBox.Cancel) - reply = QtWidgets.QMessageBox.question( - self, 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), - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Cancel) - - if reply == QtWidgets.QMessageBox.Yes: - self.shutdown_vm(vm) + if reply == QMessageBox.Yes: + self.shutdown_vm(vm) 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: - QtWidgets.QMessageBox.warning( + QMessageBox.warning( self, self.tr("Error shutting down Qube!"), self.tr("ERROR: {0}").format(ex)) @@ -988,68 +1154,70 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtWidgets.QMainWindow): check_time, and_restart, self) # noinspection PyCallByClass,PyTypeChecker - QtCore.QTimer.singleShot(check_time, self.shutdown_monitor[ + QTimer.singleShot(check_time, self.shutdown_monitor[ vm.qid].check_if_vm_has_shutdown) # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_restartvm_triggered') + @pyqtSlot(name='on_action_restartvm_triggered') def action_restartvm_triggered(self): - vm = self.get_selected_vm() + for vm_info in self.get_selected_vms(): + vm = vm_info.vm + reply = QMessageBox.question( + self, self.tr("Qube Restart Confirmation"), + self.tr("Are you sure you want to restart the Qube '{0}'" + "?
This will shutdown all the running applica" + "tions within this Qube.").format(vm.name), + QMessageBox.Yes | QMessageBox.Cancel) - reply = QtWidgets.QMessageBox.question( - self, 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), - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Cancel) - - if reply == QtWidgets.QMessageBox.Yes: - # in case the user shut down the VM in the meantime - if vm.is_running(): - self.shutdown_vm(vm, and_restart=True) - else: - self.start_vm(vm) + if reply == QMessageBox.Yes: + # in case the user shut down the VM in the meantime + if vm.is_running(): + self.shutdown_vm(vm, and_restart=True) + else: + self.start_vm(vm) # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_killvm_triggered') + @pyqtSlot(name='on_action_killvm_triggered') def action_killvm_triggered(self): - vm = self.get_selected_vm() - if not (vm.is_running() or vm.is_paused()): - info = self.tr("Qube '{0}' is not running. Are you " - "absolutely sure you want to try to kill it?
" - "This will end (not shutdown!) all " - "the running applications within this " - "Qube.").format(vm.name) - else: - info = 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) + for vm_info in self.get_selected_vms(): + vm = vm_info.vm + if not (vm.is_running() or vm.is_paused()): + info = self.tr("Qube '{0}' is not running. Are you " + "absolutely sure you want to try to kill it?
" + "This will end (not shutdown!) " + "all the running applications within this " + "Qube.").format(vm.name) + else: + info = 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) - reply = QtWidgets.QMessageBox.question( - self, self.tr("Qube Kill Confirmation"), info, - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Cancel, - QtWidgets.QMessageBox.Cancel) + reply = QMessageBox.question( + self, self.tr("Qube Kill Confirmation"), info, + QMessageBox.Yes | QMessageBox.Cancel, + QMessageBox.Cancel) - if reply == QtWidgets.QMessageBox.Yes: - try: - vm.kill() - except exc.QubesException as ex: - QtWidgets.QMessageBox.critical( - self, self.tr("Error while killing Qube!"), - self.tr( - "An exception ocurred while killing {0}.
" - "ERROR: {1}").format(vm.name, ex)) - return + if reply == QMessageBox.Yes: + try: + vm.kill() + except exc.QubesException as ex: + QMessageBox.critical( + self, self.tr("Error while killing Qube!"), + self.tr( + "An exception ocurred while killing {0}.
" + "ERROR: {1}").format(vm.name, ex)) + return def open_settings(self, vm, tab='basic'): try: with common_threads.busy_cursor(): settings_window = settings.VMSettingsWindow( vm, self.qt_app, tab) - settings_window.exec_() + settings_window.show() + self.settings_windows[vm.name] = settings_window except exc.QubesException as ex: - QtWidgets.QMessageBox.warning( + QMessageBox.warning( self, self.tr("Qube settings unavailable"), self.tr( @@ -1058,113 +1226,95 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtWidgets.QMainWindow): "\nError: {}".format(str(ex)))) return - vm_deleted = False - - try: - # the VM might not exist after running Settings - it might - # have been cloned or removed - self.vms_in_table[vm.qid].update() - except exc.QubesException: - vm_deleted = True - - if vm_deleted: - for row in self.vms_in_table: - try: - self.vms_in_table[row].update() - except exc.QubesException: - pass - # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_settings_triggered') + @pyqtSlot(name='on_action_settings_triggered') def action_settings_triggered(self): - vm = self.get_selected_vm() - if vm: - self.open_settings(vm, "basic") + for vm_info in self.get_selected_vms(): + self.open_settings(vm_info.vm, "basic") # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_appmenus_triggered') + @pyqtSlot(name='on_action_appmenus_triggered') def action_appmenus_triggered(self): - vm = self.get_selected_vm() - if vm: - self.open_settings(vm, "applications") + for vm_info in self.get_selected_vms(): + self.open_settings(vm_info.vm, "applications") # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_updatevm_triggered') + @pyqtSlot(name='on_action_updatevm_triggered') def action_updatevm_triggered(self): - vm = self.get_selected_vm() + for vm_info in self.get_selected_vms(): + vm = vm_info.vm + if not vm.is_running(): + reply = QMessageBox.question( + self, 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), + QMessageBox.Yes | QMessageBox.Cancel) + if reply != QMessageBox.Yes: + return - if not vm.is_running(): - reply = QtWidgets.QMessageBox.question( - self, 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), - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Cancel) - if reply != QtWidgets.QMessageBox.Yes: - return - - thread = UpdateVMThread(vm) - self.threads_list.append(thread) - thread.finished.connect(self.clear_threads) - thread.start() + thread = UpdateVMThread(vm) + self.threads_list.append(thread) + thread.finished.connect(self.clear_threads) + thread.start() # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_run_command_in_vm_triggered') + @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() + for vm_info in self.get_selected_vms(): + (command_to_run, ok) = QInputDialog.getText( + self, self.tr('Qubes command entry'), + self.tr('Run command in {}:').format(vm_info.name)) + if not ok or command_to_run == "": + return - (command_to_run, ok) = QtWidgets.QInputDialog.getText( - self, self.tr('Qubes command entry'), - self.tr('Run command in {}:').format(vm.name)) - if not ok or command_to_run == "": - return - - thread = RunCommandThread(vm, command_to_run) - self.threads_list.append(thread) - thread.finished.connect(self.clear_threads) - thread.start() + thread = RunCommandThread(vm_info.vm, command_to_run) + self.threads_list.append(thread) + thread.finished.connect(self.clear_threads) + thread.start() # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_open_console_triggered') + @pyqtSlot(name='on_action_open_console_triggered') def action_open_console_triggered(self): # pylint: disable=invalid-name - - vm = self.get_selected_vm() - subprocess.Popen(['qvm-console-dispvm', vm.name], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + for vm in self.get_selected_vms(): + subprocess.Popen(['qvm-console-dispvm', vm.name], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_set_keyboard_layout_triggered') + @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') + for vm_info in self.get_selected_vms(): + vm_info.vm.run('qubes-change-keyboard-layout') # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_editfwrules_triggered') + @pyqtSlot(name='on_action_editfwrules_triggered') def action_editfwrules_triggered(self): - vm = self.get_selected_vm() - if vm: - self.open_settings(vm, "firewall") + for vm_info in self.get_selected_vms(): + self.open_settings(vm_info.vm, "firewall") # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_global_settings_triggered') + @pyqtSlot(name='on_action_global_settings_triggered') def action_global_settings_triggered(self): # pylint: disable=invalid-name with common_threads.busy_cursor(): global_settings_window = global_settings.GlobalSettingsWindow( self.qt_app, self.qubes_app) - global_settings_window.exec_() + global_settings_window.show() + self.settings_windows['global_settings_window'] = global_settings_window # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_manage_templates_triggered') + @pyqtSlot(name='on_action_manage_templates_triggered') def action_manage_templates_triggered(self): - # pylint: disable=invalid-name, no-self-use + # pylint: disable=no-self-use subprocess.check_call('qubes-template-manager') # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_show_network_triggered') + @pyqtSlot(name='on_action_show_network_triggered') def action_show_network_triggered(self): pass # TODO: revive for 4.1 @@ -1172,7 +1322,7 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtWidgets.QMainWindow): # network_notes_dialog.exec_() # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_restore_triggered') + @pyqtSlot(name='on_action_restore_triggered') def action_restore_triggered(self): with common_threads.busy_cursor(): restore_window = restore.RestoreVMsWindow(self.qt_app, @@ -1180,7 +1330,7 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtWidgets.QMainWindow): restore_window.exec_() # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_backup_triggered') + @pyqtSlot(name='on_action_backup_triggered') def action_backup_triggered(self): with common_threads.busy_cursor(): backup_window = backup.BackupVMsWindow( @@ -1188,7 +1338,7 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtWidgets.QMainWindow): backup_window.show() # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_exit_triggered') + @pyqtSlot(name='on_action_exit_triggered') def action_exit_triggered(self): self.close() @@ -1200,7 +1350,6 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtWidgets.QMainWindow): 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) @@ -1210,71 +1359,20 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtWidgets.QMainWindow): 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) - - 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['Include in 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) - - def on_action_virt_mode_toggled(self, checked): - self.showhide_column(self.columns_indices['Virtualization Mode'], - checked) - - # pylint: disable=invalid-name - def on_action_dispvm_template_toggled(self, checked): - self.showhide_column(self.columns_indices['Default DispVM'], checked) - - # pylint: disable=invalid-name - def on_action_is_dvm_template_toggled(self, checked): - self.showhide_column(self.columns_indices['Is DVM Template'], checked) + col_name = self.qubes_model.columns_indices[col_num] + self.manager_settings.setValue('columns/%s' % col_name, show) # noinspection PyArgumentList - @QtCore.pyqtSlot(name='on_action_about_qubes_triggered') + @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 = QtWidgets.QMenu() + menu = QMenu() menu.addAction(self.action_toolbar) menu.addAction(self.action_menubar) return menu @@ -1283,47 +1381,42 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtWidgets.QMainWindow): self.tools_context_menu.exec_(widget.mapToGlobal(point)) def update_logs_menu(self): + self.logs_menu.clear() + menu_empty = True + try: - vm = self.get_selected_vm() + vm_info = self.get_selected_vms() - # logs menu - self.logs_menu.clear() + if len(vm_info) == 1: + vm = vm_info[0].vm - 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", - ] + 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 + for logfile in logfiles: + if os.path.exists(logfile): + action = self.logs_menu.addAction(QIcon(":/log.png"), + logfile) + action.setData(logfile) + menu_empty = False self.logs_menu.setEnabled(not menu_empty) - except exc.QubesPropertyAccessError: pass - @QtCore.pyqtSlot('const QPoint&') + @pyqtSlot('const QPoint&') def open_context_menu(self, point): - vm = self.get_selected_vm() + self.context_menu.exec_(self.table.mapToGlobal( + point + QPoint(10, 0))) - if vm.qid == 0: - self.dom0_context_menu.exec_(self.table.mapToGlobal( - point + QtCore.QPoint(10, 0))) - else: - self.context_menu.exec_(self.table.mapToGlobal( - point + QtCore.QPoint(10, 0))) - - @QtCore.pyqtSlot('QAction *') + @pyqtSlot('QAction *') def show_log(self, action): log = str(action.data()) log_dlg = log_dialog.LogDialog(self.qt_app, log) diff --git a/qubesmanager/table_widgets.py b/qubesmanager/table_widgets.py deleted file mode 100644 index 4d508bd..0000000 --- a/qubesmanager/table_widgets.py +++ /dev/null @@ -1,510 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf8 -*- -# -# The Qubes OS Project, http://www.qubes-os.org -# -# 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 -# 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 datetime - -from PyQt5 import QtWidgets, QtCore, QtGui # pylint: disable=import-error -# pylint: disable=too-few-public-methods - -power_order = QtCore.Qt.DescendingOrder -update_order = QtCore.Qt.AscendingOrder - - -row_height = 30 - - -class VmIconWidget(QtWidgets.QWidget): - def __init__(self, icon_path, enabled=True, size_multiplier=0.7, - tooltip=None, parent=None, - icon_sz=(32, 32)): # pylint: disable=unused-argument - super(VmIconWidget, self).__init__(parent) - - self.enabled = enabled - self.size_multiplier = size_multiplier - self.label_icon = QtWidgets.QLabel() - self.set_icon(icon_path) - - if tooltip is not None: - self.label_icon.setToolTip(tooltip) - - layout = QtWidgets.QHBoxLayout() - layout.addWidget(self.label_icon) - layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(layout) - - def setToolTip(self, tooltip): # pylint: disable=invalid-name - if tooltip is not None: - self.label_icon.setToolTip(tooltip) - else: - self.label_icon.setToolTip('') - - def set_icon(self, icon_path): - - if icon_path[0] in ':/': - icon = QtGui.QIcon(icon_path) - else: - icon = QtGui.QIcon.fromTheme(icon_path) - icon_sz = QtCore.QSize(row_height * self.size_multiplier, - row_height * self.size_multiplier) - icon_pixmap = icon.pixmap( - icon_sz, - QtGui.QIcon.Disabled if not self.enabled else QtGui.QIcon.Normal) - self.label_icon.setPixmap(icon_pixmap) - self.label_icon.setFixedSize(icon_sz) - - -class VmTypeWidget(VmIconWidget): - class VmTypeItem(QtWidgets.QTableWidgetItem): - def __init__(self, value, vm): - super(VmTypeWidget.VmTypeItem, self).__init__() - self.value = value - self.qid = vm.qid - self.name = vm.name - - def set_value(self, value): - self.value = value - - # pylint: disable=too-many-return-statements - def __lt__(self, other): - if self.qid == 0: - return True - if other.qid == 0: - return False - if self.value == other.value: - return self.name < other.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) - self.vm = 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.klass == 'AdminVM': - self.value = 0 - icon_name = "dom0" - elif vm.klass == 'TemplateVM': - self.value = 3 - icon_name = "templatevm" - elif vm.klass == 'StandaloneVM': - self.value = 4 - icon_name = "standalonevm" - else: - self.value = 5 + vm.label.index - icon_name = "appvm" - - return ":/" + icon_name + ".png", vm.klass - - -class VmLabelWidget(VmIconWidget): - class VmLabelItem(QtWidgets.QTableWidgetItem): - def __init__(self, value, vm): - super(VmLabelWidget.VmLabelItem, self).__init__() - self.value = value - self.qid = vm.qid - self.name = vm.name - - def set_value(self, value): - self.value = value - - # pylint: disable=too-many-return-statements - def __lt__(self, other): - if self.qid == 0: - return True - if other.qid == 0: - return False - if self.value == other.value: - return self.name < other.name - return self.value < other.value - - def __init__(self, vm, parent=None): - self.icon_path = self.get_vm_icon_path(vm) - super(VmLabelWidget, self).__init__(self.icon_path, - True, 0.8, None, parent) - self.vm = vm - self.table_item = self.VmLabelItem(self.value, vm) - self.value = None - - def get_vm_icon_path(self, vm): - self.value = vm.label.index - return vm.label.icon - - def update(self): - icon_path = self.get_vm_icon_path(self.vm) - if icon_path != self.icon_path: - self.icon_path = icon_path - self.set_icon(icon_path) - - -class VmStatusIcon(QtWidgets.QLabel): - def __init__(self, vm, parent=None): - super(VmStatusIcon, self).__init__(parent) - self.vm = vm - self.status = None - self.set_on_icon() - self.previous_power_state = self.vm.get_power_state() - - def update(self): - if self.previous_power_state != self.vm.get_power_state(): - self.set_on_icon() - self.previous_power_state = self.vm.get_power_state() - - def set_on_icon(self): - if self.vm.get_power_state() == "Running": - icon = QtGui.QIcon(":/on.png") - self.status = 3 - elif self.vm.get_power_state() in ["Paused", "Suspended"]: - icon = QtGui.QIcon(":/paused.png") - self.status = 2 - elif self.vm.get_power_state() in ["Transient", "Halting", "Dying"]: - icon = QtGui.QIcon(":/transient.png") - self.status = 1 - else: - icon = QtGui.QIcon(":/off.png") - self.status = 0 - - 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) - - -class VmInfoWidget(QtWidgets.QWidget): - class VmInfoItem(QtWidgets.QTableWidgetItem): - def __init__(self, on_icon, upd_info_item, vm): - super(VmInfoWidget.VmInfoItem, self).__init__() - self.on_icon = on_icon - self.upd_info_item = upd_info_item - self.vm = vm - self.qid = vm.qid - self.name = vm.name - - def __lt__(self, other): - # pylint: disable=too-many-return-statements - if self.qid == 0: - return True - if other.qid == 0: - return False - - self_val = self.upd_info_item.value - other_val = other.upd_info_item.value - - if self.tableWidget().\ - horizontalHeader().sortIndicatorOrder() == update_order: - # the result will be sorted by upd, sorting order: Ascending - self_val += 1 if self.on_icon.status > 0 else 0 - other_val += 1 if other.on_icon.status > 0 else 0 - if self_val == other_val: - return self.name < other.name - return self_val > other_val - if self.tableWidget().\ - horizontalHeader().sortIndicatorOrder() == power_order: - # the result will be sorted by power state, - # sorting order: Descending - if self.on_icon.status == other.on_icon.status: - return self.name < other.name - return self_val > other_val - # it would be strange if this happened - return - - def __init__(self, vm, parent=None): - super(VmInfoWidget, self).__init__(parent) - self.vm = vm - layout = QtWidgets.QHBoxLayout() - - self.on_icon = VmStatusIcon(vm) - self.upd_info = VmUpdateInfoWidget(vm, show_text=False) - self.error_icon = VmIconWidget(":/warning.png") - self.blk_icon = VmIconWidget(":/mount.png") - self.rec_icon = VmIconWidget(":/mic.png") - - layout.addWidget(self.on_icon) - layout.addWidget(self.upd_info) - layout.addWidget(self.error_icon) - layout.addItem(QtWidgets.QSpacerItem(0, 10, - QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding)) - layout.addWidget(self.blk_icon) - layout.addWidget(self.rec_icon) - - 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.table_item = self.VmInfoItem(self.on_icon, - self.upd_info.table_item, vm) - - def update_vm_state(self): - self.on_icon.update() - self.upd_info.update_outdated() - - -class VMPropertyItem(QtWidgets.QTableWidgetItem): - def __init__(self, vm, property_name, empty_function=(lambda x: False), - check_default=False): - """ - Class used to represent Qube Manager table widget. - :param vm: vm object - :param property_name: name of the property the widget represents - :param empty_function: a function that, when applied to values of - vm.property_name, returns True when the property value should be - represented as an empty string and False otherwise; by default this - function always returns false (vm.property_name is represented by an - empty string only when it actually is one) - :param check_default: if True, the widget will prepend its text with - "default" if the if the property is set to DEFAULT - """ - super(VMPropertyItem, self).__init__() - self.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) - self.setTextAlignment(QtCore.Qt.AlignVCenter) - self.vm = vm - self.qid = vm.qid - self.property_name = property_name - self.name = vm.name - self.empty_function = empty_function - self.check_default = check_default - self.update() - - def update(self): - val = getattr(self.vm, self.property_name, None) - if self.empty_function(val): - text = "" - elif val is None: - text = QtCore.QCoreApplication.translate("QubeManager", "n/a") - elif val is True: - text = QtCore.QCoreApplication.translate("QubeManager", "Yes") - else: - text = str(val) - - if self.check_default and hasattr(self.vm, self.property_name) and \ - self.vm.property_is_default(self.property_name): - text = QtCore.QCoreApplication.translate( - "QubeManager", 'default ({})').format(text) - self.setText(text) - - def __lt__(self, other): - if self.qid == 0: - return True - if other.qid == 0: - return False - if self.text() == other.text(): - return self.name < other.name - return super(VMPropertyItem, self).__lt__(other) - - -class VmTemplateItem(VMPropertyItem): - def __init__(self, vm): - super(VmTemplateItem, self).__init__(vm, "template") - - def update(self): - if getattr(self.vm, 'template', None) is not None: - self.setText(self.vm.template.name) - else: - font = QtGui.QFont() - font.setStyle(QtGui.QFont.StyleItalic) - self.setFont(font) - self.setForeground(QtGui.QBrush(QtGui.QColor("gray"))) - - self.setText(self.vm.klass) - - -class VmInternalItem(VMPropertyItem): - def __init__(self, vm): - super(VmInternalItem, self).__init__(vm, None) - - def update(self): - internal = self.vm.features.get('internal', False) - self.setText(QtCore.QCoreApplication.translate( - "QubeManager", "Yes") if internal else "") - - -# features man qvm-features -class VmUpdateInfoWidget(QtWidgets.QWidget): - class VmUpdateInfoItem(QtWidgets.QTableWidgetItem): - def __init__(self, value, vm): - super(VmUpdateInfoWidget.VmUpdateInfoItem, self).__init__() - self.value = 0 - self.vm = vm - self.qid = vm.qid - self.name = vm.name - self.set_value(value) - - def set_value(self, value): - if value in ("outdated", "to-be-outdated"): - self.value = 30 - elif value == "update": - self.value = 20 - else: - self.value = 0 - - def __lt__(self, other): - if self.qid == 0: - return True - if other.qid == 0: - return False - if self.value == other.value: - return self.name < other.name - return self.value < other.value - - def __init__(self, vm, show_text=True, parent=None): - super(VmUpdateInfoWidget, self).__init__(parent) - layout = QtWidgets.QHBoxLayout() - self.show_text = show_text - if self.show_text: - self.label = QtWidgets.QLabel("") - layout.addWidget(self.label, alignment=QtCore.Qt.AlignCenter) - else: - self.icon = QtWidgets.QLabel("") - layout.addWidget(self.icon, alignment=QtCore.Qt.AlignCenter) - self.setLayout(layout) - - self.vm = vm - - self.previous_outdated_state = None - self.previous_update_recommended = None - self.value = None - self.table_item = VmUpdateInfoWidget.VmUpdateInfoItem(self.value, vm) - self.update_outdated() - - def update_outdated(self): - outdated_state = False - is_disposable = getattr(self.vm, 'auto_cleanup', False) - - if not is_disposable and self.vm.is_running(): - if hasattr(self.vm, 'template') and self.vm.template.is_running(): - outdated_state = "to-be-outdated" - - if not outdated_state: - for vol in self.vm.volumes.values(): - if vol.is_outdated(): - outdated_state = "outdated" - break - - if not is_disposable and \ - self.vm.klass in {'TemplateVM', 'StandaloneVM'} and \ - self.vm.features.get('updates-available', False): - outdated_state = 'update' - - self.update_status_widget(outdated_state) - - def update_status_widget(self, state): - if state == self.previous_outdated_state: - return - - self.previous_outdated_state = state - self.value = state - self.table_item.set_value(state) - if state == "update": - label_text = "{}".format( - QtCore.QCoreApplication.translate("QubeManager", - "Check updates")) - icon_path = ":/update-recommended.png" - tooltip_text = QtCore.QCoreApplication.translate("QubeManager", - "Updates pending!") - elif state == "outdated": - label_text = "{}".format( - QtCore.QCoreApplication.translate("QubeManager", - "Qube outdated")) - icon_path = ":/outdated.png" - tooltip_text = QtCore.QCoreApplication.translate( - "QubeManager", - "The qube must be restarted for its filesystem to reflect the " - "template's recent committed changes.") - elif state == "to-be-outdated": - label_text = "{}".format( - QtCore.QCoreApplication.translate("QubeManager", - "Template running")) - icon_path = ":/to-be-outdated.png" - tooltip_text = QtCore.QCoreApplication.translate( - "QubeManager", - "The Template must be stopped before changes from its " - "current session can be picked up by this qube.") - else: - label_text = None - tooltip_text = None - icon_path = None - - if hasattr(self, 'icon'): - self.icon.setVisible(False) - self.layout().removeWidget(self.icon) - del self.icon - - if self.show_text: - self.label.setText(label_text) - else: - if icon_path is not None: - self.icon = VmIconWidget(icon_path, True, 0.7) - self.icon.setToolTip(tooltip_text) - self.layout().addWidget(self.icon, - alignment=QtCore.Qt.AlignCenter) - self.icon.setVisible(True) - - -class VmSizeOnDiskItem(QtWidgets.QTableWidgetItem): - def __init__(self, vm): - super(VmSizeOnDiskItem, self).__init__() - self.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) - - self.vm = vm - self.qid = vm.qid - self.name = vm.name - self.value = 0 - self.update() - self.setTextAlignment(QtCore.Qt.AlignVCenter) - - def update(self): - if self.vm.qid == 0: - self.setText(QtCore.QCoreApplication.translate("QubeManager", - "n/a")) - else: - 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.qid == 0: - return True - if other.qid == 0: - return False - if self.value == other.value: - return self.name < other.name - return self.value < other.value - - -class VmLastBackupItem(VMPropertyItem): - def __init__(self, vm, property_name): - super(VmLastBackupItem, self).__init__(vm, property_name) - - def update(self): - backup_timestamp = getattr(self.vm, 'backup_timestamp', None) - - if backup_timestamp: - self.setText( - str(datetime.datetime.fromtimestamp(backup_timestamp))) - else: - self.setText("") diff --git a/qubesmanager/tests/test_qube_manager.py b/qubesmanager/tests/test_qube_manager.py index 4f1ebf8..a02a508 100644 --- a/qubesmanager/tests/test_qube_manager.py +++ b/qubesmanager/tests/test_qube_manager.py @@ -30,11 +30,16 @@ import datetime import time from PyQt5 import QtTest, QtCore, QtWidgets +from PyQt5.QtCore import (Qt, QSize) +from PyQt5.QtGui import (QIcon) + from qubesadmin import Qubes, events, exc import qubesmanager.qube_manager as qube_manager from qubesmanager.tests import init_qtapp +icon_size = QSize(24, 24) + class QubeManagerTest(unittest.TestCase): def setUp(self): super(QubeManagerTest, self).setUp() @@ -58,14 +63,14 @@ class QubeManagerTest(unittest.TestCase): def test_001_correct_vms_listed(self): vms_in_table = [] - for row in range(self.dialog.table.rowCount()): - vm = self._get_table_item(row, "Name").vm + for row in range(self.dialog.table.model().rowCount()): + vm = self._get_table_vm(row) self.assertIsNotNone(vm) vms_in_table.append(vm.name) # check that name is listed correctly name_item = self._get_table_item(row, "Name") - self.assertEqual(name_item.text(), vm.name, + self.assertEqual(name_item, vm.name, "Incorrect VM name for {}".format(vm.name)) actual_vms = [vm.name for vm in self.qapp.domains] @@ -76,39 +81,42 @@ class QubeManagerTest(unittest.TestCase): "Incorrect VMs loaded") def test_002_correct_template_listed(self): - for row in range(self.dialog.table.rowCount()): - vm = self._get_table_item(row, "Name").vm + for row in range(self.dialog.table.model().rowCount()): + vm = self._get_table_vm(row) # check that template is listed correctly template_item = self._get_table_item(row, "Template") if getattr(vm, "template", None): self.assertEqual(vm.template, - template_item.text(), + template_item, "Incorrect template for {}".format(vm.name)) else: - self.assertEqual(vm.klass, template_item.text(), + self.assertEqual(vm.klass, template_item, "Incorrect class for {}".format(vm.name)) def test_003_correct_netvm_listed(self): - for row in range(self.dialog.table.rowCount()): - vm = self._get_table_item(row, "Name").vm + for row in range(self.dialog.table.model().rowCount()): + vm = self._get_table_vm(row) # check that netvm is listed correctly netvm_item = self._get_table_item(row, "NetVM") netvm_value = getattr(vm, "netvm", None) - netvm_value = "n/a" if not netvm_value else netvm_value + + if not netvm_value: + netvm_value = "n/a" + if netvm_value and hasattr(vm, "netvm") \ and vm.property_is_default("netvm"): netvm_value = "default ({})".format(netvm_value) self.assertEqual(netvm_value, - netvm_item.text(), + netvm_item, "Incorrect netvm for {}".format(vm.name)) def test_004_correct_disk_usage_listed(self): - for row in range(self.dialog.table.rowCount()): - vm = self._get_table_item(row, "Name").vm + for row in range(self.dialog.table.model().rowCount()): + vm = self._get_table_vm(row) - size_item = self._get_table_item(row, "Size") + size_item = self._get_table_item(row, "Disk Usage") if vm.klass == 'AdminVM': size_value = "n/a" else: @@ -116,22 +124,22 @@ class QubeManagerTest(unittest.TestCase): size_value = str(size_value) + " MiB" self.assertEqual(size_value, - size_item.text(), + size_item, "Incorrect size for {}".format(vm.name)) def test_005_correct_internal_listed(self): - for row in range(self.dialog.table.rowCount()): - vm = self._get_table_item(row, "Name").vm + for row in range(self.dialog.table.model().rowCount()): + vm = self._get_table_vm(row) internal_item = self._get_table_item(row, "Internal") internal_value = "Yes" if vm.features.get('internal', False) else "" - self.assertEqual(internal_item.text(), internal_value, + self.assertEqual(internal_item, internal_value, "Incorrect internal value for {}".format(vm.name)) def test_006_correct_ip_listed(self): - for row in range(self.dialog.table.rowCount()): - vm = self._get_table_item(row, "Name").vm + for row in range(self.dialog.table.model().rowCount()): + vm = self._get_table_vm(row) ip_item = self._get_table_item(row, "IP") if hasattr(vm, 'ip'): @@ -140,24 +148,24 @@ class QubeManagerTest(unittest.TestCase): else: ip_value = "n/a" - self.assertEqual(ip_value, ip_item.text(), + self.assertEqual(ip_value, ip_item, "Incorrect ip value for {}".format(vm.name)) def test_007_incl_in_backups_listed(self): - for row in range(self.dialog.table.rowCount()): - vm = self._get_table_item(row, "Name").vm + for row in range(self.dialog.table.model().rowCount()): + vm = self._get_table_vm(row) incl_backups_item = self._get_table_item(row, "Include in backups") incl_backups_value = getattr(vm, 'include_in_backups', False) incl_backups_value = "Yes" if incl_backups_value else "" self.assertEqual( - incl_backups_value, incl_backups_item.text(), + incl_backups_value, incl_backups_item, "Incorrect include in backups value for {}".format(vm.name)) def test_008_last_backup_listed(self): - for row in range(self.dialog.table.rowCount()): - vm = self._get_table_item(row, "Name").vm + for row in range(self.dialog.table.model().rowCount()): + vm = self._get_table_vm(row) last_backup_item = self._get_table_item(row, "Last backup") last_backup_value = getattr(vm, 'backup_timestamp', None) @@ -165,16 +173,14 @@ class QubeManagerTest(unittest.TestCase): if last_backup_value: last_backup_value = str( datetime.datetime.fromtimestamp(last_backup_value)) - else: - last_backup_value = "" self.assertEqual( - last_backup_value, last_backup_item.text(), + last_backup_value, last_backup_item, "Incorrect last backup value for {}".format(vm.name)) def test_009_def_dispvm_listed(self): - for row in range(self.dialog.table.rowCount()): - vm = self._get_table_item(row, "Name").vm + for row in range(self.dialog.table.model().rowCount()): + vm = self._get_table_vm(row) def_dispvm_item = self._get_table_item(row, "Default DispVM") if vm.property_is_default("default_dispvm"): @@ -184,45 +190,40 @@ class QubeManagerTest(unittest.TestCase): def_dispvm_value = getattr(vm, "default_dispvm", None) self.assertEqual( - str(def_dispvm_value), def_dispvm_item.text(), + def_dispvm_value, def_dispvm_item, "Incorrect default dispvm value for {}".format(vm.name)) def test_010_is_dvm_template_listed(self): - for row in range(self.dialog.table.rowCount()): - vm = self._get_table_item(row, "Name").vm + for row in range(self.dialog.table.model().rowCount()): + vm = self._get_table_vm(row) is_dvm_template_item = self._get_table_item(row, "Is DVM Template") is_dvm_template_value = "Yes" if \ getattr(vm, "template_for_dispvms", False) else "" self.assertEqual( - is_dvm_template_value, is_dvm_template_item.text(), + is_dvm_template_value, is_dvm_template_item, "Incorrect is DVM template value for {}".format(vm.name)) def test_011_is_label_correct(self): - for row in range(self.dialog.table.rowCount()): - vm = self._get_table_item(row, "Name").vm + for row in range(self.dialog.table.model().rowCount()): + vm = self._get_table_vm(row) + icon = QIcon.fromTheme(vm.label.icon) + icon = icon.pixmap(icon_size) - label_item = self._get_table_item(row, "Label") - self.assertEqual(label_item.icon_path, vm.label.icon) + label_pixmap = self._get_table_item(row, "Label", Qt.DecorationRole) + + self.assertEqual(label_pixmap.toImage(), icon.toImage()) def test_012_is_state_correct(self): - for row in range(self.dialog.table.rowCount()): - vm = self._get_table_item(row, "Name").vm + for row in range(self.dialog.table.model().rowCount()): + vm = self._get_table_vm(row) - state_item = self._get_table_item(row, "State") - - # this should not be done like that in table_widgets - displayed_power_state = state_item.on_icon.status - - if vm.is_running(): - correct_power_state = 3 - else: - correct_power_state = 0 + displayed_power_state = self._get_table_item(row, "State")['power'] self.assertEqual( - displayed_power_state, correct_power_state, + displayed_power_state, vm.get_power_state(), "Wrong power state displayed for {}".format(vm.name)) def test_013_incorrect_settings_file(self): @@ -245,25 +246,23 @@ class QubeManagerTest(unittest.TestCase): self.assertEqual(mock_warning.call_count, 1) def test_100_sorting(self): - - self.dialog.table.sortByColumn(self.dialog.columns_indices["Template"], - QtCore.Qt.AscendingOrder) + col = self.dialog.qubes_model.columns_indices.index("Template") + self.dialog.table.sortByColumn(col, QtCore.Qt.AscendingOrder) self.__check_sorting("Template") - self.dialog.table.sortByColumn(self.dialog.columns_indices["Name"], - QtCore.Qt.AscendingOrder) + col = self.dialog.qubes_model.columns_indices.index("Name") + self.dialog.table.sortByColumn(col, QtCore.Qt.AscendingOrder) self.__check_sorting("Name") - @unittest.mock.patch('qubesmanager.qube_manager.QtCore.QSettings.setValue') - @unittest.mock.patch('qubesmanager.qube_manager.QtCore.QSettings.sync') - def test_101_hide_column(self, mock_sync, mock_settings): - self.dialog.action_is_dvm_template.trigger() - mock_settings.assert_called_with('columns/Is DVM Template', False) - self.assertEqual(mock_sync.call_count, 1, "Hidden column not synced") - - self.dialog.action_is_dvm_template.trigger() + @unittest.mock.patch('qubesmanager.qube_manager.QSettings.setValue') + def test_101_hide_column(self, mock_settings): + model = self.dialog.qubes_model + action_no = model.columns_indices.index('Is DVM Template') + self.dialog.menu_view.actions()[action_no].trigger() mock_settings.assert_called_with('columns/Is DVM Template', True) - self.assertEqual(mock_sync.call_count, 2, "Hidden column not synced") + + self.dialog.menu_view.actions()[action_no].trigger() + mock_settings.assert_called_with('columns/Is DVM Template', False) @unittest.mock.patch('qubesmanager.settings.VMSettingsWindow') def test_200_vm_open_settings(self, mock_window): @@ -282,9 +281,9 @@ class QubeManagerTest(unittest.TestCase): self.assertFalse(self.dialog.action_settings.isEnabled(), "Settings not disabled for admin VM") self.assertFalse(self.dialog.action_editfwrules.isEnabled(), - "Settings not disabled for admin VM") + "Editfw not disabled for admin VM") self.assertFalse(self.dialog.action_appmenus.isEnabled(), - "Settings not disabled for admin VM") + "Appmenus not disabled for admin VM") @unittest.mock.patch('qubesmanager.settings.VMSettingsWindow') def test_202_vm_open_firewall(self, mock_window): @@ -461,16 +460,17 @@ class QubeManagerTest(unittest.TestCase): self.assertFalse(self.dialog.action_removevm.isEnabled()) - @unittest.mock.patch("PyQt5.QtWidgets.QMessageBox") + @unittest.mock.patch("qubesmanager.qube_manager.QMessageBox") @unittest.mock.patch('qubesadmin.utils.vm_dependencies') def test_218_remove_vm_dependencies(self, mock_dependencies, mock_msgbox): - action = self.dialog.action_removevm - mock_vm = unittest.mock.Mock(spec=['name'], **{'name.return_value': 'test-vm'}) mock_dependencies.return_value = [(mock_vm, "test_prop")] + action = self.dialog.action_removevm + self._select_non_admin_vm() action.trigger() + mock_msgbox().show.assert_called_with() @unittest.mock.patch('PyQt5.QtWidgets.QMessageBox.warning') @@ -486,7 +486,7 @@ class QubeManagerTest(unittest.TestCase): with unittest.mock.patch('qubesmanager.common_threads.RemoveVMThread')\ as mock_thread: - mock_input.return_value = (selected_vm.name, False) + mock_input.return_value = (selected_vm, False) action.trigger() self.assertEqual(mock_thread.call_count, 0, "VM removed despite user clicking 'cancel") @@ -766,7 +766,7 @@ class QubeManagerTest(unittest.TestCase): self.assertEqual(len(self.dialog.threads_list), 3) def test_400_event_domain_added(self): - number_of_vms = self.dialog.table.rowCount() + number_of_vms = self.dialog.table.model().rowCount() self.addCleanup(subprocess.call, ["qvm-remove", "-f", "test-vm"]) @@ -774,7 +774,7 @@ class QubeManagerTest(unittest.TestCase): ["qvm-create", "--label", "red", "test-vm"]) # a single row was added to the table - self.assertEqual(self.dialog.table.rowCount(), number_of_vms + 1) + self.assertEqual(self.dialog.table.model().rowCount(), number_of_vms+1) # table contains the correct vms vms_in_table = self._create_set_of_current_vms() @@ -785,15 +785,14 @@ class QubeManagerTest(unittest.TestCase): "correctly after add") # check if sorting works - self.dialog.table.sortItems(self.dialog.columns_indices["Name"], - QtCore.Qt.AscendingOrder) self.__check_sorting("Name") # try opening settings for the added vm - for row in range(self.dialog.table.rowCount()): + for row in range(self.dialog.table.model().rowCount()): name = self._get_table_item(row, "Name") - if name.text() == "test-vm": - self.dialog.table.setCurrentItem(name) + if name == "test-vm": + index = self.dialog.table.model().index(row, 0) + self.dialog.table.setCurrentIndex(index) break with unittest.mock.patch('qubesmanager.settings.VMSettingsWindow')\ as mock_settings: @@ -816,8 +815,6 @@ class QubeManagerTest(unittest.TestCase): self.assertEqual(initial_vms, current_vms) # check if sorting works - self.dialog.table.sortItems(self.dialog.columns_indices["Name"], - QtCore.Qt.AscendingOrder) self.__check_sorting("Name") def test_403_event_dispvm_added(self): @@ -874,27 +871,28 @@ class QubeManagerTest(unittest.TestCase): target_vm_name = "work" vm_row = self._find_vm_row(target_vm_name) - current_label_path = self._get_table_item(vm_row, "Label").icon_path + current_label = self._get_table_item(vm_row, "Label", Qt.DecorationRole) self.addCleanup( subprocess.call, ["qvm-prefs", target_vm_name, "label", "blue"]) self._run_command_and_process_events( ["qvm-prefs", target_vm_name, "label", "red"]) - new_label_path = self._get_table_item(vm_row, "Label").icon_path + new_label = self._get_table_item(vm_row, "Label", Qt.DecorationRole) - self.assertNotEqual(current_label_path, new_label_path, - "Label path did not change") - self.assertEqual( - new_label_path, - self.qapp.domains[target_vm_name].label.icon, - "Incorrect label") + self.assertNotEqual(current_label.toImage(), new_label.toImage(), + "Label icon did not change") + + icon = QIcon.fromTheme(self.qapp.domains[target_vm_name].label.icon) + icon = icon.pixmap(icon_size) + + self.assertEqual(new_label.toImage(), icon.toImage(), "Incorrect label") def test_406_prop_change_template(self): target_vm_name = "work" vm_row = self._find_vm_row(target_vm_name) - old_template = self._get_table_item(vm_row, "Template").text() + old_template = self._get_table_item(vm_row, "Template") new_template = None for vm in self.qapp.domains: if vm.klass == 'TemplateVM' and vm.name != old_template: @@ -908,10 +906,10 @@ class QubeManagerTest(unittest.TestCase): ["qvm-prefs", target_vm_name, "template", new_template]) self.assertNotEqual(old_template, - self._get_table_item(vm_row, "Template").text(), + self._get_table_item(vm_row, "Template"), "Template did not change") self.assertEqual( - self._get_table_item(vm_row, "Template").text(), + self._get_table_item(vm_row, "Template"), self.qapp.domains[target_vm_name].template.name, "Incorrect template") @@ -919,7 +917,7 @@ class QubeManagerTest(unittest.TestCase): target_vm_name = "work" vm_row = self._find_vm_row(target_vm_name) - old_netvm = self._get_table_item(vm_row, "NetVM").text() + old_netvm = self._get_table_item(vm_row, "NetVM") new_netvm = None for vm in self.qapp.domains: if getattr(vm, "provides_network", False) and vm.name != old_netvm: @@ -932,10 +930,10 @@ class QubeManagerTest(unittest.TestCase): ["qvm-prefs", target_vm_name, "netvm", new_netvm]) self.assertNotEqual(old_netvm, - self._get_table_item(vm_row, "NetVM").text(), + self._get_table_item(vm_row, "NetVM"), "NetVM did not change") self.assertEqual( - self._get_table_item(vm_row, "NetVM").text(), + self._get_table_item(vm_row, "NetVM"), self.qapp.domains[target_vm_name].netvm.name, "Incorrect NetVM") @@ -950,7 +948,7 @@ class QubeManagerTest(unittest.TestCase): ["qvm-features", "work", "interal", "1"]) self.assertEqual( - self._get_table_item(vm_row, "Internal").text(), + self._get_table_item(vm_row, "Internal"), "Yes", "Incorrect value for internal VM") @@ -958,7 +956,7 @@ class QubeManagerTest(unittest.TestCase): ["qvm-features", "--unset", "work", "interal"]) self.assertEqual( - self._get_table_item(vm_row, "Internal").text(), + self._get_table_item(vm_row, "Internal"), "", "Incorrect value for non-internal VM") @@ -966,7 +964,7 @@ class QubeManagerTest(unittest.TestCase): target_vm_name = "work" vm_row = self._find_vm_row(target_vm_name) - old_ip = self._get_table_item(vm_row, "IP").text() + old_ip = self._get_table_item(vm_row, "IP") new_ip = old_ip.replace(".0.", ".5.") self.addCleanup( @@ -975,10 +973,10 @@ class QubeManagerTest(unittest.TestCase): ["qvm-prefs", target_vm_name, "ip", new_ip]) self.assertNotEqual(old_ip, - self._get_table_item(vm_row, "IP").text(), + self._get_table_item(vm_row, "IP"), "IP did not change") self.assertEqual( - self._get_table_item(vm_row, "IP").text(), + self._get_table_item(vm_row, "IP"), self.qapp.domains[target_vm_name].ip, "Incorrect IP") @@ -996,7 +994,7 @@ class QubeManagerTest(unittest.TestCase): ["qvm-prefs", target_vm_name, "include_in_backups", str(new_value)]) self.assertEqual( - self._get_table_item(vm_row, "Internal").text(), + self._get_table_item(vm_row, "Internal"), "Yes" if new_value else "", "Incorrect value for include_in_backups") @@ -1005,7 +1003,7 @@ class QubeManagerTest(unittest.TestCase): target_timestamp = "2015-01-01 17:00:00" vm_row = self._find_vm_row(target_vm_name) - old_value = self._get_table_item(vm_row, "Last backup").text() + old_value = self._get_table_item(vm_row, "Last backup") new_value = datetime.datetime.strptime( target_timestamp, "%Y-%m-%d %H:%M:%S") @@ -1017,10 +1015,10 @@ class QubeManagerTest(unittest.TestCase): str(int(new_value.timestamp()))]) self.assertNotEqual(old_value, - self._get_table_item(vm_row, "Last backup").text(), + self._get_table_item(vm_row, "Last backup"), "Last backup date did not change") self.assertEqual( - self._get_table_item(vm_row, "Last backup").text(), + self._get_table_item(vm_row, "Last backup"), target_timestamp, "Incorrect Last backup date") @@ -1028,8 +1026,9 @@ class QubeManagerTest(unittest.TestCase): target_vm_name = "work" vm_row = self._find_vm_row(target_vm_name) + old_default_dispvm =\ - self._get_table_item(vm_row, "Default DispVM").text() + self._get_table_item(vm_row, "Default DispVM") new_default_dispvm = None for vm in self.qapp.domains: if getattr(vm, "template_for_dispvms", False) and vm.name !=\ @@ -1038,18 +1037,18 @@ class QubeManagerTest(unittest.TestCase): break self.addCleanup( - subprocess.call, - ["qvm-prefs", target_vm_name, "default_dispvm", old_default_dispvm]) + subprocess.call, + ["qvm-prefs", target_vm_name, "default_dispvm", old_default_dispvm]) self._run_command_and_process_events( ["qvm-prefs", target_vm_name, "default_dispvm", new_default_dispvm]) self.assertNotEqual( old_default_dispvm, - self._get_table_item(vm_row, "Default DispVM").text(), + self._get_table_item(vm_row, "Default DispVM"), "Default DispVM did not change") self.assertEqual( - self._get_table_item(vm_row, "Default DispVM").text(), + self._get_table_item(vm_row, "Default DispVM"), self.qapp.domains[target_vm_name].default_dispvm.name, "Incorrect Default DispVM") @@ -1064,7 +1063,7 @@ class QubeManagerTest(unittest.TestCase): ["qvm-prefs", target_vm_name, "template_for_dispvms", "True"]) self.assertEqual( - self._get_table_item(vm_row, "Is DVM Template").text(), + self._get_table_item(vm_row, "Is DVM Template"), "Yes", "Incorrect value for DVM Template") @@ -1072,7 +1071,7 @@ class QubeManagerTest(unittest.TestCase): ["qvm-prefs", "--default", target_vm_name, "template_for_dispvms"]) self.assertEqual( - self._get_table_item(vm_row, "Is DVM Template").text(), + self._get_table_item(vm_row, "Is DVM Template"), "", "Incorrect value for not DVM Template") @@ -1088,19 +1087,17 @@ class QubeManagerTest(unittest.TestCase): self._run_command_and_process_events( ["qvm-start", target_vm_name], timeout=60) - status_item = self._get_table_item(vm_row, "State") + displayed_state = self._get_table_item(vm_row, "State") - displayed_power_state = status_item.on_icon.status - - self.assertEqual(displayed_power_state, 3, + self.assertEqual(displayed_state['power'], 'Running', "Power state failed to update on start") self._run_command_and_process_events( ["qvm-shutdown", "--wait", target_vm_name], timeout=30) - displayed_power_state = status_item.on_icon.status + displayed_state = self._get_table_item(vm_row, "State") - self.assertEqual(displayed_power_state, 0, + self.assertEqual(displayed_state['power'], 'Halted', "Power state failed to update on shutdown") def test_415_template_vm_started(self): @@ -1116,9 +1113,8 @@ class QubeManagerTest(unittest.TestCase): if target_vm_name: break - for i in range(self.dialog.table.rowCount()): - self._get_table_item(i, "State").update_vm_state =\ - unittest.mock.Mock() + for i in range(self.dialog.table.model().rowCount()): + self._get_table_vminfo(i).update = unittest.mock.Mock() self.addCleanup( subprocess.call, @@ -1126,12 +1122,12 @@ class QubeManagerTest(unittest.TestCase): self._run_command_and_process_events( ["qvm-start", target_vm_name], timeout=60) - for i in range(self.dialog.table.rowCount()): - call_count = self._get_table_item( - i, "State").update_vm_state.call_count - if self._get_table_item(i, "Template").text() == target_vm_name: + for i in range(self.dialog.table.model().rowCount()): + call_count = self._get_table_vminfo( + i).update.call_count + if self._get_table_item(i, "Template") == target_vm_name: self.assertGreater(call_count, 0) - elif self._get_table_item(i, "Name").text() == target_vm_name: + elif self._get_table_item(i, "Name") == target_vm_name: self.assertGreater(call_count, 0) else: self.assertEqual(call_count, 0) @@ -1168,15 +1164,15 @@ class QubeManagerTest(unittest.TestCase): "Same logs found for dom0 and non-adminVM") def _find_vm_row(self, vm_name): - for row in range(self.dialog.table.rowCount()): + for row in range(self.dialog.table.model().rowCount()): name = self._get_table_item(row, "Name") - if name.text() == vm_name: + if name == vm_name: return row return None def _count_visible_table_rows(self): result = 0 - for i in range(self.dialog.table.rowCount()): + for i in range(self.dialog.table.model().rowCount()): if not self.dialog.table.isRowHidden(i): result += 1 return result @@ -1210,54 +1206,51 @@ class QubeManagerTest(unittest.TestCase): def _create_set_of_current_vms(self): result = set() - for i in range(self.dialog.table.rowCount()): - result.add(self._get_table_item(i, "Name").vm.name) + for i in range(self.dialog.table.model().rowCount()): + result.add(self._get_table_item(i, "Name")) return result def _select_admin_vm(self): - for row in range(self.dialog.table.rowCount()): - template = self.dialog.table.item( - row, self.dialog.columns_indices["Template"]) - if template.text() == 'AdminVM': - self.dialog.table.setCurrentItem(template) - return template.vm + for row in range(self.dialog.table.model().rowCount()): + template = self._get_table_item(row, "Template") + if template == 'AdminVM': + index = self.dialog.table.model().index(row, 0) + self.dialog.table.setCurrentIndex(index) + return index.data(Qt.UserRole).vm return None def _select_non_admin_vm(self, running=None): - for row in range(self.dialog.table.rowCount()): - template = self.dialog.table.item( - row, self.dialog.columns_indices["Template"]) - status = self.dialog.table.item( - row, self.dialog.columns_indices["State"]) - if template.text() != 'AdminVM' and \ + for row in range(self.dialog.table.model().rowCount()): + template = self._get_table_item(row, "Template") + vm = self._get_table_vm(row) + if template != 'AdminVM' and \ (running is None - or (running and status.on_icon.status == 3) - or (not running and status.on_icon.status != 3)): - self.dialog.table.setCurrentItem(template) - return template.vm + or (running and vm.is_running()) + or (not running and not vm.is_running())): + index = self.dialog.table.model().index(row, 0) + self.dialog.table.setCurrentIndex(index) + return vm return None def _select_templatevm(self, running=None): - for row in range(self.dialog.table.rowCount()): - template = self.dialog.table.item( - row, self.dialog.columns_indices["Template"]) - status = self.dialog.table.item( - row, self.dialog.columns_indices["State"]) - if template.text() == 'TemplateVM' and \ + for row in range(self.dialog.table.model().rowCount()): + template = self._get_table_item(row, "Template") + vm = self._get_table_vm(row) + if template == 'TemplateVM' and \ (running is None - or (running and status.on_icon.status == 3) - or (not running and status.on_icon.status != 3)): - self.dialog.table.setCurrentItem(template) - return template.vm + or (running and vm.is_running()) + or (not running and not vm.is_running())): + index = self.dialog.table.model().index(row, 0) + self.dialog.table.setCurrentIndex(index) + return vm return None def __check_sorting(self, column_name): last_text = None last_vm = None - for row in range(self.dialog.table.rowCount()): - - vm = self._get_table_item(row, "Name").vm.name - text = self._get_table_item(row, column_name).text().lower() + for row in range(self.dialog.table.model().rowCount()): + vm = self._get_table_item(row, "Name") + text = self._get_table_item(row, column_name) if row == 0: self.assertEqual(vm, "dom0", "dom0 is not sorted first") @@ -1267,24 +1260,27 @@ class QubeManagerTest(unittest.TestCase): else: if last_text == text: self.assertGreater( - vm, last_vm, + vm.lower(), last_vm.lower(), "Incorrect sorting for {}".format(column_name)) else: self.assertGreater( - text, last_text, + text.lower(), last_text.lower(), "Incorrect sorting for {}".format(column_name)) last_text = text last_vm = vm - def _get_table_item(self, row, column_name): - value = self.dialog.table.cellWidget( - row, self.dialog.columns_indices[column_name]) - if not value: - value = self.dialog.table.item( - row, self.dialog.columns_indices[column_name]) + def _get_table_vminfo(self, row): + model = self.dialog.table.model() + return model.index(row, 0).data(Qt.UserRole) - return value + def _get_table_vm(self, row): + model = self.dialog.table.model() + return model.index(row, 0).data(Qt.UserRole).vm + def _get_table_item(self, row, column_name, role = Qt.DisplayRole): + model = self.dialog.table.model() + column = self.dialog.qubes_model.columns_indices.index(column_name) + return model.index(row, column).data(role) class QubeManagerThreadTest(unittest.TestCase): def test_01_startvm_thread(self): diff --git a/rpm_spec/qmgr.spec.in b/rpm_spec/qmgr.spec.in index a157aba..25c40d8 100644 --- a/rpm_spec/qmgr.spec.in +++ b/rpm_spec/qmgr.spec.in @@ -71,7 +71,6 @@ rm -rf $RPM_BUILD_ROOT %{python3_sitelib}/qubesmanager/__pycache__ %{python3_sitelib}/qubesmanager/__init__.py %{python3_sitelib}/qubesmanager/clipboard.py -%{python3_sitelib}/qubesmanager/table_widgets.py %{python3_sitelib}/qubesmanager/appmenu_select.py %{python3_sitelib}/qubesmanager/backup.py %{python3_sitelib}/qubesmanager/backup_utils.py diff --git a/test-packages/qubesadmin/exc.py b/test-packages/qubesadmin/exc.py index 4eb7acb..024342d 100644 --- a/test-packages/qubesadmin/exc.py +++ b/test-packages/qubesadmin/exc.py @@ -11,6 +11,9 @@ class QubesVMNotStartedError(BaseException): class QubesPropertyAccessError(BaseException): pass +class QubesNoSuchPropertyError(BaseException): + pass + class QubesDaemonNoResponseError(BaseException): pass diff --git a/ui/qubemanager.ui b/ui/qubemanager.ui index 2926ace..478e1e8 100644 --- a/ui/qubemanager.ui +++ b/ui/qubemanager.ui @@ -52,21 +52,6 @@ QLayout::SetDefaultConstraint - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - @@ -85,7 +70,7 @@ - + 0 @@ -140,21 +125,9 @@ false - - 10 - - - 14 - false - - 150 - - - 150 - false @@ -292,25 +265,6 @@ Template &View - - - - - - - - - - - - - - - - - - -