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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-