Skip to content

Commit 04d170e

Browse files
committed
Easy dependency switch if only preload is in chain
Fixes: QubesOS/qubes-issues#10227 For: QubesOS/qubes-issues#1512
1 parent 335c908 commit 04d170e

File tree

11 files changed

+434
-55
lines changed

11 files changed

+434
-55
lines changed

qubes/api/internal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ async def suspend_pre(self):
265265

266266
preload_templates = qubes.vm.dispvm.get_preload_templates(self.app)
267267
for qube in preload_templates:
268-
qube.remove_preload_excess(0)
268+
qube.remove_preload_excess(0, reason="system wants to suspend")
269269

270270
# first keep track of VMs which were paused before suspending
271271
previously_paused = [

qubes/app.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1643,6 +1643,8 @@ def on_domain_pre_deleted(self, event, vm):
16431643
:param qubes.vm.QubesVM name: Qube name.
16441644
"""
16451645
# pylint: disable=unused-argument
1646+
preloads = set()
1647+
dependencies = []
16461648
for obj in itertools.chain(self.domains, (self,)):
16471649
if obj is vm:
16481650
# allow removed VM to reference itself
@@ -1653,18 +1655,32 @@ def on_domain_pre_deleted(self, event, vm):
16531655
isinstance(prop, qubes.vm.VMProperty)
16541656
and getattr(obj, prop.__name__) == vm
16551657
):
1656-
self.log.error(
1657-
"Cannot remove %s, used by %s.%s",
1658-
vm,
1659-
obj,
1660-
prop.__name__,
1661-
)
1662-
raise qubes.exc.QubesVMInUseError(
1663-
vm,
1664-
"Domain is in use: {!r};"
1665-
"see 'journalctl -u qubesd -e' in dom0 for "
1666-
"details".format(vm.name),
1667-
)
1658+
if getattr(obj, "is_preload", False) and (
1659+
prop.__name__ == "template"
1660+
or (
1661+
prop.__name__ == "default_dispvm"
1662+
and getattr(obj, "template", None) == vm
1663+
)
1664+
):
1665+
preloads.add(obj.name)
1666+
continue
1667+
if isinstance(obj, qubes.app.Qubes):
1668+
dependencies.insert(0, ('"GLOBAL"', prop.__name__))
1669+
elif not obj.property_is_default(prop):
1670+
dependencies.append((obj.name, prop.__name__))
1671+
if dependencies:
1672+
self.log.error(
1673+
"Cannot remove %s as it is used by %s",
1674+
vm,
1675+
", ".join(
1676+
":".join(str(i) for i in tup) for tup in dependencies
1677+
),
1678+
)
1679+
raise qubes.exc.QubesVMInUseError(
1680+
vm,
1681+
"Domain is in use: {!r}; see 'journalctl -u qubesd -e' in dom0"
1682+
" for details".format(vm.name),
1683+
)
16681684
if isinstance(vm, qubes.vm.qubesvm.QubesVM):
16691685
assignments = vm.get_provided_assignments()
16701686
else:
@@ -1675,6 +1691,9 @@ def on_domain_pre_deleted(self, event, vm):
16751691
vm, "VM has devices assigned to other VMs: " + desc
16761692
)
16771693

1694+
if preloads:
1695+
vm.remove_preload_excess(0, reason="domain will be deleted")
1696+
16781697
@qubes.events.handler("domain-delete")
16791698
def on_domain_deleted(self, event, vm):
16801699
# pylint: disable=unused-argument

qubes/ext/audio.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,12 @@ def set_tag_and_qubesdb_entry(self, subject, event, newvalue=None):
6969
@qubes.ext.handler("domain-pre-shutdown")
7070
def on_domain_pre_shutdown(self, vm, event, **kwargs):
7171
attached_vms = [
72-
domain for domain in self.attached_vms(vm) if domain.is_running()
72+
domain
73+
for domain in self.attached_vms(vm)
74+
if domain.is_running() and not getattr(domain, "is_preload", False)
7375
]
7476
if attached_vms and not kwargs.get("force", False):
75-
raise qubes.exc.QubesVMError(
77+
raise qubes.exc.QubesVMInUseError(
7678
self,
7779
"There are running VMs using this VM as AudioVM: "
7880
"{}".format(", ".join(vm.name for vm in attached_vms)),

qubes/tests/api_internal.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def setUp(self):
4444
self.app = mock.NonCallableMock()
4545
self.dom0 = mock.NonCallableMock(spec=qubes.vm.adminvm.AdminVM)
4646
self.dom0.name = "dom0"
47+
self.dom0.klass = "AdminVM"
4748
self.dom0.features = {}
4849
self.domains = {
4950
"dom0": self.dom0,
@@ -88,17 +89,24 @@ def call_mgmt_func(self, method, arg=b"", payload=b""):
8889
def test_000_suspend_pre(self):
8990
running_vm = self.create_mockvm(features={"qrexec": True})
9091
running_vm.is_running.return_value = True
92+
running_vm.klass = "AppVM"
9193

92-
not_running_vm = self.create_mockvm(features={"qrexec": True})
94+
not_running_vm = self.create_mockvm(
95+
features={"qrexec": True, "preload-dispvm-max": "1"}
96+
)
9397
not_running_vm.is_running.return_value = False
98+
not_running_vm.template_for_dispvms = True
99+
not_running_vm.klass = "AppVM"
94100

95101
no_qrexec_vm = self.create_mockvm()
96102
no_qrexec_vm.is_running.return_value = True
103+
no_qrexec_vm.klass = "AppVM"
97104

98105
paused_vm = self.create_mockvm(features={"qrexec": True})
99106
paused_vm.is_running.return_value = True
100107
paused_vm.get_power_state.return_value = "Paused"
101108
paused_vm.name = "SleepingBeauty"
109+
paused_vm.klass = "AppVM"
102110

103111
self.domains.update(
104112
{
@@ -113,8 +121,11 @@ def test_000_suspend_pre(self):
113121
qubes.api.internal,
114122
"PREVIOUSLY_PAUSED",
115123
"/tmp/qubes-previously-paused.tmp",
116-
):
124+
), mock.patch.object(
125+
not_running_vm, "remove_preload_excess"
126+
) as mock_remove:
117127
ret = self.call_mgmt_func(b"internal.SuspendPre")
128+
mock_remove.assert_called_once_with(0, reason=mock.ANY)
118129
self.assertIsNone(ret)
119130
self.assertFalse(self.dom0.called)
120131

qubes/tests/app.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,6 +1096,46 @@ def test_205_remove_appvm_dispvm(self):
10961096
with self.assertRaises(qubes.exc.QubesVMInUseError):
10971097
del self.app.domains[appvm]
10981098

1099+
def test_205_remove_appvm_dispvm_preload(self):
1100+
appvm = self.app.add_new_vm(
1101+
"AppVM",
1102+
name="test-appvm",
1103+
template=self.template,
1104+
template_for_dispvms=True,
1105+
label="red",
1106+
)
1107+
dispvm = self.app.add_new_vm(
1108+
"DispVM", name="test-dispvm", template=appvm, label="red"
1109+
)
1110+
dispvm_alt = self.app.add_new_vm(
1111+
"DispVM", name="test-dispvm-alt", template=appvm, label="red"
1112+
)
1113+
with mock.patch.object(self.app, "vmm"):
1114+
with self.assertRaises(qubes.exc.QubesVMInUseError):
1115+
del self.app.domains[appvm]
1116+
1117+
with mock.patch.object(self.app, "vmm"):
1118+
with mock.patch.object(appvm, "fire_event_async"):
1119+
appvm.features["preload-dispvm-max"] = "1"
1120+
appvm.features["preload-dispvm"] = str(dispvm.name)
1121+
with self.assertRaises(qubes.exc.QubesVMInUseError):
1122+
del self.app.domains[appvm]
1123+
1124+
with mock.patch.object(self.app, "vmm"):
1125+
with (
1126+
mock.patch.object(appvm, "fire_event_async"),
1127+
mock.patch.object(
1128+
appvm, "remove_preload_excess"
1129+
) as mock_remove,
1130+
):
1131+
appvm.features["preload-dispvm-max"] = "2"
1132+
appvm.features["preload-dispvm"] = dispvm.name
1133+
appvm.features["preload-dispvm"] = (
1134+
dispvm.name + " " + dispvm_alt.name
1135+
)
1136+
del self.app.domains[appvm]
1137+
mock_remove.assert_called_with(0, reason=mock.ANY)
1138+
10991139
def test_206_remove_attached(self):
11001140
# See also qubes.tests.api_admin.
11011141
vm = self.app.add_new_vm(

qubes/tests/ext.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import unittest.mock
2323

2424
import qubes.ext.admin
25+
import qubes.ext.audio
2526
import qubes.ext.core_features
2627
import qubes.ext.custom_persist
2728
import qubes.ext.services
@@ -2610,3 +2611,58 @@ def test_000_tags_permission(self):
26102611
"admin-permission:admin.vm.tag.Set",
26112612
tag,
26122613
)
2614+
2615+
2616+
class TC_60_Audio(qubes.tests.QubesTestCase):
2617+
def setUp(self):
2618+
super().setUp()
2619+
self.ext = qubes.ext.audio.AUDIO()
2620+
self.audiovm = mock.MagicMock()
2621+
self.audiovm.name = "sys-audio"
2622+
self.client = mock.MagicMock()
2623+
self.client.name = "client"
2624+
self.client_alt = mock.MagicMock()
2625+
self.client_alt.name = "client"
2626+
2627+
def test_000_shutdown(self):
2628+
self.ext.on_domain_pre_shutdown(
2629+
self.audiovm,
2630+
"domain-pre-shutdown",
2631+
)
2632+
2633+
def test_000_shutdown_used(self):
2634+
with unittest.mock.patch.object(
2635+
self.ext, "attached_vms", return_value=[self.client]
2636+
), unittest.mock.patch.object(
2637+
self.client, "is_running", return_value=True
2638+
):
2639+
self.client.is_preload = False
2640+
with self.assertRaises(qubes.exc.QubesVMInUseError):
2641+
self.ext.on_domain_pre_shutdown(
2642+
self.audiovm,
2643+
"domain-pre-shutdown",
2644+
)
2645+
2646+
self.client.is_preload = True
2647+
self.ext.on_domain_pre_shutdown(
2648+
self.audiovm,
2649+
"domain-pre-shutdown",
2650+
)
2651+
2652+
def test_000_shutdown_used_by_some(self):
2653+
with unittest.mock.patch.object(
2654+
self.ext,
2655+
"attached_vms",
2656+
return_value=[self.client, self.client_alt],
2657+
), unittest.mock.patch.object(
2658+
self.client, "is_running", return_value=True
2659+
), unittest.mock.patch.object(
2660+
self.client_alt, "is_running", return_value=True
2661+
):
2662+
self.client.is_preload = False
2663+
self.client_alt.is_preload = True
2664+
with self.assertRaises(qubes.exc.QubesVMInUseError):
2665+
self.ext.on_domain_pre_shutdown(
2666+
self.audiovm,
2667+
"domain-pre-shutdown",
2668+
)

0 commit comments

Comments
 (0)