Skip to content

Commit 8aec333

Browse files
committed
qube_manager: support for RemoteVM qubes without crashing
1 parent 488c8a7 commit 8aec333

2 files changed

Lines changed: 123 additions & 4 deletions

File tree

qubesmanager/qube_manager.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,8 @@ def update(self, update_size_on_disk=False, event=None):
371371
if not event or event.endswith(':default_dispvm'):
372372
self.dvm = getattr(self.vm, 'default_dispvm', None)
373373
try:
374-
if self.vm.property_is_default("default_dispvm"):
374+
if hasattr(self.vm, 'default_dispvm') \
375+
and self.vm.property_is_default("default_dispvm"):
375376
self.dvm = "default (" + str(self.dvm) + ")"
376377
elif self.dvm is not None:
377378
self.dvm = str(self.dvm)
@@ -592,7 +593,12 @@ def flags(self, index):
592593

593594
def_flags = QAbstractTableModel.flags(self, index)
594595
if self.columns_indices[index.column()] == "Backup":
595-
return def_flags | Qt.ItemFlag.ItemIsUserCheckable
596+
vm = self.qubes_cache.get_vm(index.row())
597+
if getattr(vm, 'klass', None) == "RemoteVM":
598+
# include_in_backups is absent on a RemoteVM, so the checkbox
599+
# must not be toggleable (the write would raise).
600+
return def_flags
601+
return def_flags | Qt.ItemFlag.ItemIsUserCheckable
596602
return def_flags
597603

598604
vm_restart_check_timeout = 1000 # in msec
@@ -1151,7 +1157,7 @@ def init_network_menu(self):
11511157
action.triggered.connect(partial(self.change_network, 'default'))
11521158

11531159
for vm in self.qubes_app.domains:
1154-
if vm.qid != 0 and vm.provides_network:
1160+
if vm.qid != 0 and getattr(vm, 'provides_network', False):
11551161
action = self.network_menu.addAction(vm.name)
11561162
action.setData(vm.name)
11571163
action.triggered.connect(partial(self.change_network, vm.name))
@@ -1427,6 +1433,24 @@ def table_selection_changed(self):
14271433
elif vm.klass == 'TemplateVM':
14281434
self.template_menu.setEnabled(False)
14291435
self.network_menu.setEnabled(False)
1436+
elif vm.klass == 'RemoteVM':
1437+
# RemoteVM lives on another host: no local lifecycle, console,
1438+
# settings, networking or template. Keep it listed, actions off.
1439+
self.action_settings.setEnabled(False)
1440+
self.action_appmenus.setEnabled(False)
1441+
self.action_clonevm.setEnabled(False)
1442+
self.action_resumevm.setEnabled(False)
1443+
self.action_pausevm.setEnabled(False)
1444+
self.action_shutdownvm.setEnabled(False)
1445+
self.action_restartvm.setEnabled(False)
1446+
self.action_killvm.setEnabled(False)
1447+
self.action_startvm_tools_install.setEnabled(False)
1448+
self.action_open_console.setEnabled(False)
1449+
self.action_run_command_in_vm.setEnabled(False)
1450+
self.action_editfwrules.setEnabled(False)
1451+
self.action_updatevm.setEnabled(False)
1452+
self.template_menu.setEnabled(False)
1453+
self.network_menu.setEnabled(False)
14301454

14311455
if vm.vm.features.get('internal', False):
14321456
self.action_appmenus.setEnabled(False)

qubesmanager/tests/test_qube_manager.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import time
2424
from datetime import datetime
2525

26-
from PyQt6.QtCore import Qt, QSettings, QItemSelectionModel
26+
from PyQt6.QtCore import Qt, QSettings, QItemSelectionModel, QModelIndex
2727
from PyQt6.QtGui import QPixmap, QIcon
2828
from PyQt6.QtWidgets import QMessageBox
2929

@@ -156,6 +156,37 @@ def _is_icon(icon, icon_name: str = 'checked'):
156156
raise ValueError
157157

158158

159+
# A RemoteVM is a BaseVM: it has none of the AppVM properties below, so reading
160+
# them must raise (as on a real system) rather than return a mock value.
161+
REMOTE_VM_MISSING_PROPS = [
162+
"template",
163+
"netvm",
164+
"provides_network",
165+
"default_dispvm",
166+
"template_for_dispvms",
167+
"virt_mode",
168+
]
169+
170+
# The qubesd response a property.Get returns for a property the VM doesn't have.
171+
_NO_SUCH_PROPERTY = b"2\x00QubesNoSuchPropertyError\x00\x00No such property\x00"
172+
173+
174+
def _add_remote_vm(qapp, name="test-remote"):
175+
"""Register a RemoteVM in the mock app"""
176+
qube = MockQube(name=name, qapp=qapp, klass="RemoteVM")
177+
qapp._qubes[name] = qube
178+
for prop in REMOTE_VM_MISSING_PROPS:
179+
qube.properties.pop(prop, None)
180+
qapp.update_vm_calls()
181+
for prop in REMOTE_VM_MISSING_PROPS:
182+
qapp.expected_calls[
183+
(name, "admin.vm.property.Get", prop, None)] = _NO_SUCH_PROPERTY
184+
qapp.expected_calls[
185+
(name, "admin.vm.property.GetDefault", prop, None)] = \
186+
_NO_SUCH_PROPERTY
187+
return qube
188+
189+
159190
def test_000_window_loads(qapp, test_qubes_app):
160191
dispatcher = MockDispatcher(test_qubes_app)
161192

@@ -313,6 +344,70 @@ def test_004_hide_column(mock_settings, qubes_manager):
313344
mock_settings.assert_called_with('columns/Is DVM Template', False)
314345

315346

347+
def test_006_remote_vm_listed(qapp, test_qubes_app):
348+
"""
349+
A RemoteVM is listed in the manager
350+
"""
351+
_add_remote_vm(test_qubes_app)
352+
dispatcher = MockAsyncDispatcher(test_qubes_app)
353+
qube_manager_window = qube_manager.VmManagerWindow(
354+
qapp, test_qubes_app, dispatcher)
355+
assert "test-remote" in _get_current_vms(qube_manager_window)
356+
357+
358+
def test_007_remote_vm_added(qubes_manager):
359+
"""
360+
A RemoteVM appearing at runtime is added to the table, not skipped.
361+
"""
362+
assert "test-remote" not in _get_current_vms(qubes_manager)
363+
_add_remote_vm(qubes_manager.qubes_app)
364+
qubes_manager.qubes_app.domains.clear_cache()
365+
qubes_manager.on_domain_added(None, "domain-add", "test-remote")
366+
assert "test-remote" in _get_current_vms(qubes_manager)
367+
368+
369+
def test_008_remote_vm_actions_disabled(qubes_manager):
370+
"""
371+
Selecting a RemoteVM disable the management actions it cannot support.
372+
"""
373+
_add_remote_vm(qubes_manager.qubes_app)
374+
qubes_manager.qubes_app.domains.clear_cache()
375+
qubes_manager.on_domain_added(None, "domain-add", "test-remote")
376+
_select_vm(qubes_manager, "test-remote")
377+
qubes_manager.table_selection_changed()
378+
for action in ("action_settings", "action_appmenus", "action_clonevm",
379+
"action_pausevm", "action_shutdownvm", "action_restartvm",
380+
"action_killvm", "action_open_console",
381+
"action_run_command_in_vm", "action_editfwrules"):
382+
assert not getattr(qubes_manager, action).isEnabled(), action
383+
assert not qubes_manager.network_menu.isEnabled()
384+
assert not qubes_manager.template_menu.isEnabled()
385+
386+
387+
def test_009_remote_vm_backup_not_checkable(qubes_manager):
388+
"""The Backup checkbox is not user-toggleable for a RemoteVM, whose
389+
include_in_backups property does not exist."""
390+
_add_remote_vm(qubes_manager.qubes_app)
391+
qubes_manager.qubes_app.domains.clear_cache()
392+
qubes_manager.on_domain_added(None, "domain-add", "test-remote")
393+
model = qubes_manager.qubes_model
394+
backup_col = model.columns_indices.index("Backup")
395+
name_col = model.columns_indices.index("Name")
396+
checkable = Qt.ItemFlag.ItemIsUserCheckable
397+
seen_remote = seen_normal = False
398+
for row in range(model.rowCount(QModelIndex())):
399+
name = model.data(model.index(row, name_col),
400+
Qt.ItemDataRole.DisplayRole)
401+
flags = model.flags(model.index(row, backup_col))
402+
if name == "test-remote":
403+
seen_remote = True
404+
assert not (flags & checkable)
405+
elif name != "dom0":
406+
seen_normal = True
407+
assert flags & checkable
408+
assert seen_remote and seen_normal
409+
410+
316411
@mock.patch('qubesmanager.settings.VMSettingsWindow')
317412
def test_200_vm_open_settings(mock_window, qubes_manager):
318413
_select_vm(qubes_manager, 'test-blue')

0 commit comments

Comments
 (0)