Skip to content

Commit ce23ab7

Browse files
committed
Rename disp template if only preloads are running
Fixes: QubesOS/qubes-issues#10227 For: QubesOS/qubes-issues#1512
1 parent 064e343 commit ce23ab7

File tree

5 files changed

+141
-30
lines changed

5 files changed

+141
-30
lines changed

qubesmanager/qube_manager.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1472,7 +1472,11 @@ def action_removevm_triggered(self):
14721472
for vm_info in self.get_selected_vms():
14731473
vm = vm_info.vm
14741474

1475-
dependencies = utils.vm_dependencies(self.qubes_app, vm)
1475+
dependencies = [
1476+
(vm, prop)
1477+
for (vm, prop) in utils.vm_dependencies(self.qubes_app, vm)
1478+
if not vm or (vm and not manager_utils.is_preload(vm))
1479+
]
14761480

14771481
if dependencies:
14781482
list_deps = manager_utils.format_dependencies_list(dependencies)

qubesmanager/settings.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ def run(self):
8787
if holder is None:
8888
setattr(self.vm.app, prop, new_vm)
8989
else:
90+
if utils.is_preload(holder):
91+
continue
9092
setattr(holder, prop, new_vm)
9193
except qubesadmin.exc.QubesException:
9294
failed_props += [(holder, prop)]
@@ -872,7 +874,10 @@ def rename_vm(self):
872874
running_dependencies = [
873875
vm.name
874876
for (vm, prop) in dependencies
875-
if vm and prop == "template" and utils.is_running(vm, False)
877+
if vm
878+
and prop == "template"
879+
and utils.is_running(vm, False)
880+
and not utils.is_preload(vm)
876881
]
877882

878883
if running_dependencies:
@@ -911,7 +916,11 @@ def rename_vm(self):
911916

912917
def remove_vm(self):
913918

914-
dependencies = admin_utils.vm_dependencies(self.vm.app, self.vm)
919+
dependencies = [
920+
(vm, prop)
921+
for (vm, prop) in admin_utils.vm_dependencies(self.vm.app, self.vm)
922+
if not vm or (vm and not utils.is_preload(vm))
923+
]
915924

916925
if dependencies:
917926
list_text = utils.format_dependencies_list(dependencies)
@@ -1239,8 +1248,8 @@ def __apply_advanced_tab__(self):
12391248
self.dvm_template_checkbox.isChecked()
12401249
and self.preload_dispvm.isEnabled()
12411250
):
1242-
curr_preload_dispvm = (
1243-
int(self.vm.features.get("preload-dispvm-max") or 0)
1251+
curr_preload_dispvm = int(
1252+
self.vm.features.get("preload-dispvm-max") or 0
12441253
)
12451254
preload_dispvm = self.preload_dispvm.value()
12461255
if preload_dispvm != curr_preload_dispvm:

qubesmanager/tests/test_qube_manager.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -489,12 +489,14 @@ def test_212_remove_vm_dependencies(mock_msgbox, qubes_manager):
489489
@mock.patch("PyQt6.QtWidgets.QInputDialog.getText")
490490
def test_213_remove_vm_no_dependencies(mock_input, mock_warning, qubes_manager):
491491
# get a non-running vm
492-
_select_vm(qubes_manager, 'test-red')
492+
qube_name = 'default-dvm'
493+
_select_vm(qubes_manager, qube_name)
494+
assert qubes_manager.qubes_app.domains['test-disp'].template.name == qube_name
493495

494-
with (mock.patch('qubesmanager.common_threads.RemoveVMThread') as
495-
mock_thread):
496+
with mock.patch('qubesmanager.common_threads.RemoveVMThread') as mock_thread, \
497+
mock.patch('qubesmanager.utils.is_preload', return_value=True):
496498
# user cancels
497-
mock_input.return_value = ('test-red', False)
499+
mock_input.return_value = (qube_name, False)
498500
qubes_manager.action_removevm.trigger()
499501
assert mock_thread.call_count == 0
500502
assert mock_warning.call_count == 0
@@ -504,11 +506,11 @@ def test_213_remove_vm_no_dependencies(mock_input, mock_warning, qubes_manager):
504506
assert mock_warning.call_count == 1
505507
assert mock_thread.call_count == 0
506508

507-
mock_input.return_value = ('test-red', True)
509+
mock_input.return_value = (qube_name, True)
508510
qubes_manager.action_removevm.trigger()
509511
assert mock_warning.call_count == 1
510512
mock_thread.assert_called_once_with(
511-
qubes_manager.qubes_app.domains['test-red'])
513+
qubes_manager.qubes_app.domains[qube_name])
512514
mock_thread().finished.connect.assert_called_once_with(
513515
qubes_manager.clear_threads)
514516
mock_thread().start.assert_called_once_with()

qubesmanager/tests/test_vm_settings.py

Lines changed: 105 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626

2727
from PyQt6 import QtCore, QtWidgets
2828
import qubesmanager.settings as vm_settings
29+
import qubesmanager.utils as utils
30+
import qubesadmin.utils as admin_utils
2931
from qubesadmin.tests.mock_app import MockQube, MockDevice
3032

3133
PAGES = ["basic", "advanced", "firewall", "devices", "applications", "services"]
@@ -38,6 +40,10 @@
3840
"test-standalone",
3941
"test-old",
4042
"test-vm-set",
43+
"default-dvm",
44+
"test-disp",
45+
"test-alt-dvm",
46+
"test-alt-disp",
4147
]
4248

4349
# with a template
@@ -372,7 +378,9 @@ def test_101_change_template(settings_fixture):
372378
settings_window, page, vm_name = settings_fixture
373379
vm = settings_window.qubesapp.domains[vm_name]
374380

375-
if vm.is_running() or not hasattr(vm, "template"):
381+
if vm.klass == "DispVM":
382+
return
383+
if vm.is_running() or not hasattr(vm, "template") or vm.klass == "StandaloneVM":
376384
assert not settings_window.template_name.isEnabled()
377385
return
378386

@@ -609,7 +617,9 @@ def test_108_disk_space(settings_fixture):
609617
@mock.patch("qubesmanager.settings.RenameVMThread")
610618
@mock.patch("PyQt6.QtWidgets.QMessageBox.warning")
611619
@pytest.mark.parametrize(
612-
"settings_fixture", ["fedora-36", "test-vm-set", "test-blue"], indirect=True
620+
"settings_fixture",
621+
["fedora-36", "test-vm-set", "test-blue", "default-dvm", "test-disp"],
622+
indirect=True,
613623
)
614624
def test_109_renamevm(mock_warning, mock_thread, mock_input, settings_fixture):
615625
settings_window, page, vm_name = settings_fixture
@@ -619,29 +629,68 @@ def test_109_renamevm(mock_warning, mock_thread, mock_input, settings_fixture):
619629
assert not settings_window.rename_vm_button.isEnabled()
620630
assert mock_warning.call_count == 0
621631
return
622-
else:
623-
assert settings_window.rename_vm_button.isEnabled()
632+
assert settings_window.rename_vm_button.isEnabled()
624633

625634
mock_input.return_value = ("renamed-vm", True)
626-
settings_window.rename_vm_button.click()
635+
# Qubes with same names from mock_app may be on the system, such as
636+
# sys-net, which affects test.
637+
dependencies = admin_utils.vm_dependencies(settings_window.qubesapp, vm)
638+
running_dependencies = []
639+
running_dependencies.extend(
640+
[
641+
vm
642+
for (vm, prop) in dependencies
643+
if vm and prop == "template" and utils.is_running(vm, False)
644+
]
645+
)
646+
for qube in running_dependencies:
647+
expected_call = (
648+
qube.name,
649+
"admin.vm.property.Get",
650+
"is_preload",
651+
None,
652+
)
653+
assert expected_call not in settings_window.qubesapp.actual_calls
654+
settings_window.qubesapp.expected_calls[expected_call] = (
655+
b"0\x00default=False type=bool False"
656+
)
657+
658+
if vm.name == "default-dvm":
659+
dispvm = settings_window.qubesapp.domains["test-disp"]
660+
expected_call = (
661+
dispvm.name,
662+
"admin.vm.property.Get",
663+
"is_preload",
664+
None,
665+
)
666+
settings_window.qubesapp.expected_calls[expected_call] = (
667+
b"0\x00default=False type=bool True"
668+
)
669+
assert expected_call not in settings_window.qubesapp.actual_calls
670+
with mock.patch.object(dispvm, "is_running", return_value=True):
671+
settings_window.rename_vm_button.click()
672+
assert expected_call in settings_window.qubesapp.actual_calls
673+
else:
674+
settings_window.rename_vm_button.click()
627675

628676
if vm.name == "fedora-36":
629677
assert mock_warning.call_count == 1
630678
assert mock_thread.call_count == 0
631679
return
632-
elif vm.name == "test-vm-set":
633-
mock_thread.assert_called_with(vm, "renamed-vm", mock.ANY)
634-
mock_thread().start.assert_called_with()
635-
assert mock_warning.call_count == 0
636680

637681
assert mock_warning.call_count == 0
682+
if vm.name in ["test-vm-set", "default-dvm"]:
683+
mock_thread.assert_called_with(vm, "renamed-vm", mock.ANY)
684+
mock_thread().start.assert_called_with()
638685

639686

640687
@mock.patch("PyQt6.QtWidgets.QInputDialog.getText")
641688
@mock.patch("qubesmanager.common_threads.RemoveVMThread")
642689
@mock.patch("PyQt6.QtWidgets.QMessageBox.warning")
643690
@pytest.mark.parametrize(
644-
"settings_fixture", ["fedora-36", "test-vm-set", "test-blue"], indirect=True
691+
"settings_fixture",
692+
["fedora-36", "test-vm-set", "test-blue", "default-dvm", "test-alt-dvm"],
693+
indirect=True,
645694
)
646695
def test_110_deletevm(mock_warning, mock_thread, mock_input, settings_fixture):
647696
settings_window, page, vm_name = settings_fixture
@@ -651,22 +700,60 @@ def test_110_deletevm(mock_warning, mock_thread, mock_input, settings_fixture):
651700
assert not settings_window.delete_vm_button.isEnabled()
652701
assert mock_warning.call_count == 0
653702
return
654-
else:
655-
assert settings_window.delete_vm_button.isEnabled()
703+
assert settings_window.delete_vm_button.isEnabled()
656704

657705
mock_input.return_value = (vm.name, True)
658-
settings_window.delete_vm_button.click()
659706

660-
if vm.name == "fedora-36":
707+
# Qubes with same names from mock_app may be on the system, such as
708+
# sys-net, which affects test.
709+
dependencies = [
710+
(vm, prop)
711+
for (vm, prop) in admin_utils.vm_dependencies(settings_window.qubesapp, vm)
712+
if vm
713+
]
714+
for qube, prop in dependencies:
715+
expected_call = (
716+
qube.name,
717+
"admin.vm.property.Get",
718+
"is_preload",
719+
None,
720+
)
721+
assert expected_call not in settings_window.qubesapp.actual_calls
722+
settings_window.qubesapp.expected_calls[expected_call] = (
723+
b"0\x00default=False type=bool False"
724+
)
725+
726+
if vm.name in ["default-dvm", "test-alt-dvm"]:
727+
if vm.name == "default-dvm":
728+
dispvm_name = "test-disp"
729+
else:
730+
dispvm_name = "test-alt-disp"
731+
dispvm = settings_window.qubesapp.domains[dispvm_name]
732+
expected_call = (
733+
dispvm.name,
734+
"admin.vm.property.Get",
735+
"is_preload",
736+
None,
737+
)
738+
settings_window.qubesapp.expected_calls[expected_call] = (
739+
b"0\x00default=False type=bool True"
740+
)
741+
assert expected_call not in settings_window.qubesapp.actual_calls
742+
with mock.patch.object(dispvm, "is_running", return_value=True):
743+
settings_window.delete_vm_button.click()
744+
assert expected_call in settings_window.qubesapp.actual_calls
745+
else:
746+
settings_window.delete_vm_button.click()
747+
748+
if vm.name in ["fedora-36", "default-dvm"]:
661749
assert mock_warning.call_count == 1
662750
assert mock_thread.call_count == 0
663751
return
664-
elif vm.name == "test-vm-set":
665-
mock_thread.assert_called_with(vm)
666-
mock_thread().start.assert_called_with()
667-
assert mock_warning.call_count == 0
668752

669753
assert mock_warning.call_count == 0
754+
if vm.name in ["test-vm-set", "test-alt-dvm"]:
755+
mock_thread.assert_called_with(vm)
756+
mock_thread().start.assert_called_with()
670757

671758

672759
@mock.patch("PyQt6.QtWidgets.QInputDialog.getText")

qubesmanager/utils.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,22 @@ def is_internal(vm):
8585

8686
def is_running(vm, default_state):
8787
"""Checks if the VM is running, returns default_state if we have
88-
insufficient permissions to deteremine that."""
88+
insufficient permissions to determine that."""
8989
try:
9090
return vm.is_running()
9191
except exc.QubesDaemonAccessError:
9292
return default_state
9393

9494

95+
def is_preload(vm):
96+
"""Checks if the VM is a preloaded disposable, returns False if we have
97+
insufficient permissions to determine that."""
98+
try:
99+
return getattr(vm, "is_preload", False)
100+
except exc.QubesDaemonAccessError:
101+
return False
102+
103+
95104
def translate(string):
96105
"""helper function for translations"""
97106
return QtCore.QCoreApplication.translate(

0 commit comments

Comments
 (0)