Skip to content

Commit e427a69

Browse files
committed
Easy dependency switch if only preload is in chain
Fixes: QubesOS/qubes-issues#10227 For: QubesOS/qubes-issues#1512
1 parent 7e72d38 commit e427a69

10 files changed

Lines changed: 313 additions & 34 deletions

File tree

qubes/api/internal.py

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

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

272272
# first keep track of VMs which were paused before suspending
273273
previously_paused = [

qubes/app.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1632,6 +1632,7 @@ def _domain_event_callback(self, _conn, domain, event, _detail, _opaque):
16321632
@qubes.events.handler("domain-pre-delete")
16331633
def on_domain_pre_deleted(self, event, vm):
16341634
# pylint: disable=unused-argument
1635+
preloads = []
16351636
for obj in itertools.chain(self.domains, (self,)):
16361637
if obj is vm:
16371638
# allow removed VM to reference itself
@@ -1642,6 +1643,11 @@ def on_domain_pre_deleted(self, event, vm):
16421643
isinstance(prop, qubes.vm.VMProperty)
16431644
and getattr(obj, prop.__name__) == vm
16441645
):
1646+
if getattr(obj, "is_preload") and (
1647+
prop.__name__ in ["default_dispvm", "template"]
1648+
):
1649+
preloads.append(obj)
1650+
continue
16451651
self.log.error(
16461652
"Cannot remove %s, used by %s.%s",
16471653
vm,
@@ -1664,6 +1670,9 @@ def on_domain_pre_deleted(self, event, vm):
16641670
vm, "VM has devices assigned to other VMs: " + desc
16651671
)
16661672

1673+
if preloads:
1674+
vm.remove_preload_excess(0, reason="domain will be deleted")
1675+
16671676
@qubes.events.handler("domain-delete")
16681677
def on_domain_deleted(self, event, vm):
16691678
# 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")
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/app.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,6 +1096,44 @@ 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(appvm, "remove_preload_excess") as mock_remove
1128+
):
1129+
appvm.features["preload-dispvm-max"] = "2"
1130+
appvm.features["preload-dispvm"] = dispvm.name
1131+
appvm.features["preload-dispvm"] = (
1132+
dispvm.name + " " + dispvm_alt.name
1133+
)
1134+
del self.app.domains[appvm]
1135+
mock_remove.assert_called_with(0, reason=mock.ANY)
1136+
10991137
def test_206_remove_attached(self):
11001138
# See also qubes.tests.api_admin.
11011139
vm = self.app.add_new_vm(

qubes/tests/ext.py

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

24+
import qubes.ext.audio
2425
import qubes.ext.core_features
2526
import qubes.ext.custom_persist
2627
import qubes.ext.services
@@ -2579,3 +2580,58 @@ def test_015_feature_set_path_with_colon_without_options(self):
25792580
self.vm.untrusted_qdb.write.assert_called_with(
25802581
"/persist/test", "/var/test:dir:with:colon"
25812582
)
2583+
2584+
2585+
class TC_60_Audio(qubes.tests.QubesTestCase):
2586+
def setUp(self):
2587+
super().setUp()
2588+
self.ext = qubes.ext.audio.AUDIO()
2589+
self.audiovm = mock.MagicMock()
2590+
self.audiovm.name = "sys-audio"
2591+
self.client = mock.MagicMock()
2592+
self.client.name = "client"
2593+
self.client_alt = mock.MagicMock()
2594+
self.client_alt.name = "client"
2595+
2596+
def test_000_shutdown(self):
2597+
self.ext.on_domain_pre_shutdown(
2598+
self.audiovm,
2599+
"domain-pre-shutdown",
2600+
)
2601+
2602+
def test_000_shutdown_used(self):
2603+
with unittest.mock.patch.object(
2604+
self.ext, "attached_vms", return_value=[self.client]
2605+
), unittest.mock.patch.object(
2606+
self.client, "is_running", return_value=True
2607+
):
2608+
self.client.is_preload = False
2609+
with self.assertRaises(qubes.exc.QubesVMInUseError):
2610+
self.ext.on_domain_pre_shutdown(
2611+
self.audiovm,
2612+
"domain-pre-shutdown",
2613+
)
2614+
2615+
self.client.is_preload = True
2616+
self.ext.on_domain_pre_shutdown(
2617+
self.audiovm,
2618+
"domain-pre-shutdown",
2619+
)
2620+
2621+
def test_000_shutdown_used_by_some(self):
2622+
with unittest.mock.patch.object(
2623+
self.ext,
2624+
"attached_vms",
2625+
return_value=[self.client, self.client_alt],
2626+
), unittest.mock.patch.object(
2627+
self.client, "is_running", return_value=True
2628+
), unittest.mock.patch.object(
2629+
self.client_alt, "is_running", return_value=True
2630+
):
2631+
self.client.is_preload = False
2632+
self.client_alt.is_preload = True
2633+
with self.assertRaises(qubes.exc.QubesVMInUseError):
2634+
self.ext.on_domain_pre_shutdown(
2635+
self.audiovm,
2636+
"domain-pre-shutdown",
2637+
)

qubes/tests/vm/mix/dvmtemplate.py

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ def setUp(self):
6969
self.template = self.app.add_new_vm(
7070
qubes.vm.templatevm.TemplateVM, name="test-template", label="red"
7171
)
72+
self.template_alt = self.app.add_new_vm(
73+
qubes.vm.templatevm.TemplateVM,
74+
name="test-template-alt",
75+
label="red",
76+
)
7277
self.appvm = self.app.add_new_vm(
7378
qubes.vm.appvm.AppVM,
7479
name="test-vm",
@@ -98,9 +103,14 @@ def cleanup_dispvm(self):
98103
if hasattr(self, "dispvm"):
99104
self.dispvm.close()
100105
del self.dispvm
106+
if hasattr(self, "dispvm_alt"):
107+
self.dispvm_alt.close()
108+
del self.dispvm_alt
101109
self.template.close()
110+
self.template_alt.close()
102111
self.appvm.close()
103112
del self.template
113+
del self.template_alt
104114
del self.appvm
105115
self.app.domains.clear()
106116
self.app.pools.clear()
@@ -241,7 +251,7 @@ def test_011_dvm_preload_del_max(self, mock_remove_preload_excess):
241251
del self.adminvm.features["preload-dispvm-max"]
242252
self.appvm.features["preload-dispvm-max"] = ""
243253
del self.appvm.features["preload-dispvm-max"]
244-
mock_remove_preload_excess.assert_called_once_with(0)
254+
mock_remove_preload_excess.assert_called_once_with(0, reason=mock.ANY)
245255

246256
@mock.patch("qubes.events.Emitter.fire_event_async")
247257
def test_012_dvm_preload_set_max(self, mock_events):
@@ -251,6 +261,11 @@ def test_012_dvm_preload_set_max(self, mock_events):
251261
"domain-preload-dispvm-start", reason=mock.ANY
252262
)
253263

264+
mock_events.reset_mock()
265+
self.appvm.template_for_dispvms = False
266+
self.appvm.features["preload-dispvm-max"] = "2"
267+
mock_events.assert_not_called()
268+
254269
def test_013_dvm_preload_get_treshold(self):
255270
cases = [None, False, "0", "2", "1000"]
256271
self.assertEqual(self.appvm.get_feat_preload_threshold(), 0)
@@ -260,6 +275,101 @@ def test_013_dvm_preload_get_treshold(self):
260275
threshold = self.appvm.get_feat_preload_threshold()
261276
self.assertEqual(threshold, int(value or 0) * 1024**2)
262277

278+
@mock.patch("qubes.events.Emitter.fire_event_async")
279+
@mock.patch(
280+
"qubes.vm.mix.dvmtemplate.DVMTemplateMixin.remove_preload_excess"
281+
)
282+
def test_030_dvm_preload_set_template(self, mock_remove, mock_events):
283+
# Don't try to preload if max is not set.
284+
mock_events.side_effect = self.mock_coro
285+
self.appvm.template = self.template_alt
286+
mock_events.assert_not_called()
287+
mock_remove.assert_called_once_with(0, reason=mock.ANY)
288+
289+
# Try to remove and preload if max is set and template has changed.
290+
mock_remove.reset_mock()
291+
self.appvm.features["preload-dispvm-max"] = "1"
292+
mock_events.reset_mock()
293+
self.appvm.template = self.template
294+
mock_remove.assert_called_once_with(0, reason=mock.ANY)
295+
mock_events.assert_called_once_with(
296+
"domain-preload-dispvm-start", reason=mock.ANY
297+
)
298+
299+
# Don't change anything if template hasn't changed.
300+
mock_remove.reset_mock()
301+
mock_events.reset_mock()
302+
self.appvm.template = self.template
303+
mock_remove.assert_not_called()
304+
mock_events.assert_not_called()
305+
306+
@mock.patch("qubes.events.Emitter.fire_event_async")
307+
@mock.patch(
308+
"qubes.vm.mix.dvmtemplate.DVMTemplateMixin.remove_preload_excess"
309+
)
310+
def test_040_dvm_preload_set_template_for_dispvms(
311+
self, mock_remove, mock_events
312+
):
313+
# Remove preloads when disabling property.
314+
mock_events.side_effect = self.mock_coro
315+
self.appvm.template_for_dispvms = False
316+
mock_events.assert_not_called()
317+
mock_remove.assert_called_once_with(0, reason=mock.ANY)
318+
319+
# Preload when enabling property.
320+
self.appvm.features["preload-dispvm-max"] = "1"
321+
mock_events.reset_mock()
322+
mock_remove.reset_mock()
323+
self.appvm.template_for_dispvms = True
324+
mock_remove.assert_not_called()
325+
mock_events.assert_called_once_with(
326+
"domain-preload-dispvm-start", reason=mock.ANY
327+
)
328+
329+
# Try to disable property if it has dependents.
330+
mock_events.reset_mock()
331+
self.dispvm = self.app.add_new_vm(
332+
qubes.vm.dispvm.DispVM,
333+
name="test-dispvm",
334+
template=self.appvm,
335+
label="red",
336+
dispid=42,
337+
)
338+
self.dispvm_alt = self.app.add_new_vm(
339+
qubes.vm.dispvm.DispVM,
340+
name="test-dispvm-alt",
341+
template=self.appvm,
342+
label="red",
343+
dispid=43,
344+
)
345+
with self.assertRaises(qubes.exc.QubesVMInUseError):
346+
self.appvm.template_for_dispvms = False
347+
mock_remove.assert_not_called()
348+
mock_events.assert_not_called()
349+
350+
# Disabling property when not all dependents are preloads
351+
self.appvm.features["preload-dispvm-max"] = 1
352+
self.appvm.features["preload-dispvm"] = self.dispvm.name
353+
mock_events.reset_mock()
354+
mock_remove.reset_mock()
355+
with self.assertRaises(qubes.exc.QubesVMInUseError):
356+
self.appvm.template_for_dispvms = False
357+
mock_remove.assert_not_called()
358+
mock_events.assert_not_called()
359+
360+
# Disabling property when all dependents are preloads
361+
self.appvm.features["preload-dispvm-max"] = 2
362+
mock_events.reset_mock()
363+
mock_remove.reset_mock()
364+
self.appvm.features["preload-dispvm"] = (
365+
self.dispvm.name + " " + self.dispvm_alt.name
366+
)
367+
mock_events.reset_mock()
368+
mock_remove.reset_mock()
369+
self.appvm.template_for_dispvms = False
370+
mock_remove.assert_called_once_with(0, reason=mock.ANY)
371+
mock_events.assert_not_called()
372+
263373
def test_100_get_preload_templates(self):
264374
print(qubes.vm.dispvm.get_preload_templates(self.app))
265375
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True

qubes/tests/vm/mix/net.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,19 @@ def test_170_provides_network_netvm(self):
213213
self.assertPropertyValue(vm2, "netvm", "", None, "")
214214
self.assertPropertyValue(vm, "provides_network", False, False, "False")
215215

216+
@patch("qubes.vm.qubesvm.QubesVM.libvirt_domain")
217+
@patch("qubes.vm.qubesvm.QubesVM.is_halted", return_value=False)
218+
def test_180_shutdown(self, mock_halted, mock_shutdown):
219+
# pylint: disable=unused-argument
220+
vm = self.get_vm()
221+
self.setup_netvms(vm)
222+
with patch.object(vm, "is_running", return_value=True):
223+
vm.is_preload = False
224+
with self.assertRaises(qubes.exc.QubesVMInUseError):
225+
self.loop.run_until_complete(vm.netvm.shutdown())
226+
vm.is_preload = True
227+
self.loop.run_until_complete(vm.netvm.shutdown())
228+
216229
def test_200_vmid_to_ipv4(self):
217230
testcases = (
218231
(1, "0.1"),

qubes/vm/dispvm.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,9 @@ async def from_appvm(cls, appvm, preload=False, **kwargs):
511511
# The gap is filled after the delay set by the
512512
# 'domain-shutdown' of its ancestors. Not refilling now to
513513
# deliver a disposable faster.
514-
appvm.remove_preload_from_list([qube.name])
514+
appvm.remove_preload_from_list(
515+
[qube.name], reason="of outdated volume(s)"
516+
)
515517
# Delay to not affect this run.
516518
asyncio.ensure_future(
517519
qube.delay(delay=2, coros=[qube.cleanup()])
@@ -527,7 +529,9 @@ async def from_appvm(cls, appvm, preload=False, **kwargs):
527529
# - Another request to this function will not return the same
528530
# qube.
529531
dispvm.features["preload-dispvm-in-progress"] = True
530-
appvm.remove_preload_from_list([dispvm.name])
532+
appvm.remove_preload_from_list(
533+
[dispvm.name], reason="qube was requested"
534+
)
531535
dispvm.preload_requested = True
532536
app.save()
533537
timeout = int(dispvm.qrexec_timeout * 1.2)
@@ -603,7 +607,9 @@ def use_preload(self):
603607
self.log.warning("Using a preloaded qube before requesting it")
604608
if not appvm.features.get("internal", None):
605609
del self.features["internal"]
606-
appvm.remove_preload_from_list([self.name])
610+
appvm.remove_preload_from_list(
611+
[self.name], reason="qube was used without being requested"
612+
)
607613
self.features["preload-dispvm-in-progress"] = False
608614
self.app.save()
609615
asyncio.ensure_future(
@@ -620,8 +626,9 @@ async def _bare_cleanup(self):
620626
def _preload_cleanup(self):
621627
"""Cleanup preload from list"""
622628
if self.name in self.template.get_feat_preload():
623-
self.log.info("Automatic cleanup removes qube from preload list")
624-
self.template.remove_preload_from_list([self.name])
629+
self.template.remove_preload_from_list(
630+
[self.name], reason="automatic cleanup was called"
631+
)
625632

626633
async def cleanup(self):
627634
"""Clean up after the DispVM

0 commit comments

Comments
 (0)