Skip to content

Commit 9adeb58

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

File tree

5 files changed

+136
-29
lines changed

5 files changed

+136
-29
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 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 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: 100 additions & 17 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,8 @@
3840
"test-standalone",
3941
"test-old",
4042
"test-vm-set",
43+
"default-dvm",
44+
"test-disp",
4145
]
4246

4347
# with a template
@@ -609,7 +613,9 @@ def test_108_disk_space(settings_fixture):
609613
@mock.patch("qubesmanager.settings.RenameVMThread")
610614
@mock.patch("PyQt6.QtWidgets.QMessageBox.warning")
611615
@pytest.mark.parametrize(
612-
"settings_fixture", ["fedora-36", "test-vm-set", "test-blue"], indirect=True
616+
"settings_fixture",
617+
["fedora-36", "test-vm-set", "test-blue", "default-dvm", "test-disp"],
618+
indirect=True,
613619
)
614620
def test_109_renamevm(mock_warning, mock_thread, mock_input, settings_fixture):
615621
settings_window, page, vm_name = settings_fixture
@@ -619,29 +625,68 @@ def test_109_renamevm(mock_warning, mock_thread, mock_input, settings_fixture):
619625
assert not settings_window.rename_vm_button.isEnabled()
620626
assert mock_warning.call_count == 0
621627
return
622-
else:
623-
assert settings_window.rename_vm_button.isEnabled()
628+
assert settings_window.rename_vm_button.isEnabled()
624629

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

628672
if vm.name == "fedora-36":
629673
assert mock_warning.call_count == 1
630674
assert mock_thread.call_count == 0
631675
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
636676

637677
assert mock_warning.call_count == 0
678+
if vm.name in ["test-vm-set", "default-dvm"]:
679+
mock_thread.assert_called_with(vm, "renamed-vm", mock.ANY)
680+
mock_thread().start.assert_called_with()
638681

639682

640683
@mock.patch("PyQt6.QtWidgets.QInputDialog.getText")
641684
@mock.patch("qubesmanager.common_threads.RemoveVMThread")
642685
@mock.patch("PyQt6.QtWidgets.QMessageBox.warning")
643686
@pytest.mark.parametrize(
644-
"settings_fixture", ["fedora-36", "test-vm-set", "test-blue"], indirect=True
687+
"settings_fixture",
688+
["fedora-36", "test-vm-set", "test-blue", "default-dvm", "test-alt-dvm"],
689+
indirect=True,
645690
)
646691
def test_110_deletevm(mock_warning, mock_thread, mock_input, settings_fixture):
647692
settings_window, page, vm_name = settings_fixture
@@ -651,22 +696,60 @@ def test_110_deletevm(mock_warning, mock_thread, mock_input, settings_fixture):
651696
assert not settings_window.delete_vm_button.isEnabled()
652697
assert mock_warning.call_count == 0
653698
return
654-
else:
655-
assert settings_window.delete_vm_button.isEnabled()
699+
assert settings_window.delete_vm_button.isEnabled()
656700

657701
mock_input.return_value = (vm.name, True)
658-
settings_window.delete_vm_button.click()
659702

660-
if vm.name == "fedora-36":
703+
# Qubes with same names from mock_app may be on the system, such as
704+
# sys-net, which affects test.
705+
dependencies = [
706+
(vm, prop)
707+
for (vm, prop) in admin_utils.vm_dependencies(settings_window.qubesapp, vm)
708+
if vm
709+
]
710+
for (qube, prop) in dependencies:
711+
expected_call = (
712+
qube.name,
713+
"admin.vm.property.Get",
714+
"is_preload",
715+
None,
716+
)
717+
assert expected_call not in settings_window.qubesapp.actual_calls
718+
settings_window.qubesapp.expected_calls[expected_call] = (
719+
b"0\x00default=False type=bool False"
720+
)
721+
722+
if vm.name in ["default-dvm", "test-alt-dvm"]:
723+
if vm.name == "default-dvm":
724+
dispvm_name = "test-disp"
725+
else:
726+
dispvm_name = "test-alt-disp"
727+
dispvm = settings_window.qubesapp.domains[dispvm_name]
728+
expected_call = (
729+
dispvm.name,
730+
"admin.vm.property.Get",
731+
"is_preload",
732+
None,
733+
)
734+
settings_window.qubesapp.expected_calls[expected_call] = (
735+
b"0\x00default=False type=bool True"
736+
)
737+
assert expected_call not in settings_window.qubesapp.actual_calls
738+
with mock.patch.object(dispvm, "is_running", return_value=True):
739+
settings_window.delete_vm_button.click()
740+
assert expected_call in settings_window.qubesapp.actual_calls
741+
else:
742+
settings_window.delete_vm_button.click()
743+
744+
if vm.name in ["fedora-36", "default-dvm"]:
661745
assert mock_warning.call_count == 1
662746
assert mock_thread.call_count == 0
663747
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
668748

669749
assert mock_warning.call_count == 0
750+
if vm.name in ["test-vm-set", "test-alt-dvm"]:
751+
mock_thread.assert_called_with(vm)
752+
mock_thread().start.assert_called_with()
670753

671754

672755
@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)