Skip to content

Commit dd43ddd

Browse files
committed
Consistent preload template gathering
- Easier copy-paste when code duplication is necessary; - Only fires event if the qube should be really preloaded, which is more difficult to do considering the global feature. The prior behavior didn't error out, just a warning log; - Add tests For: QubesOS/qubes-issues#1512
1 parent 84f1f52 commit dd43ddd

5 files changed

Lines changed: 118 additions & 41 deletions

File tree

linux/aux-tools/preload-dispvm

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,39 +12,43 @@ import concurrent.futures
1212
import qubesadmin
1313

1414

15-
def get_max(qube):
16-
return int(qube.features.get("preload-dispvm-max", 0) or 0)
15+
def get_preload_max(qube) -> int | None:
16+
value = qube.features.get("preload-dispvm-max", None)
17+
return int(value) if value else value
1718

1819

1920
async def main():
2021
app = qubesadmin.Qubes()
2122
domains = app.domains
2223
default_dispvm = getattr(app, "default_dispvm", None)
24+
global_max = get_preload_max(domains["dom0"])
2325
appvms = [
2426
qube
2527
for qube in domains
26-
if get_max(qube) > 0
27-
and (
28-
(
29-
qube.klass == "AppVM"
30-
and getattr(qube, "template_for_dispvms", False)
28+
if (
29+
qube.klass == "AppVM"
30+
and getattr(qube, "template_for_dispvms", False)
31+
and (
32+
(qube != default_dispvm and get_preload_max(qube))
33+
or (
34+
(qube == default_dispvm and global_max)
35+
or (global_max is None and get_preload_max(qube))
36+
)
3137
)
32-
or (qube.name == "dom0" and default_dispvm)
3338
)
3439
]
3540
method = "admin.vm.CreateDisposable"
3641
loop = asyncio.get_running_loop()
3742
tasks = []
38-
if "dom0" in appvms and default_dispvm in appvms:
39-
appvms.remove(default_dispvm)
4043
with concurrent.futures.ThreadPoolExecutor() as executor:
4144
for qube in appvms:
42-
maximum = get_max(qube)
43-
msg = f"{qube}:{maximum}"
44-
if qube.name == "dom0":
45-
qube = default_dispvm
46-
msg = "global:" + msg
47-
print(msg)
45+
if qube == default_dispvm and global_max is not None:
46+
maximum = global_max
47+
msg = f"global:{qube}:{maximum}"
48+
else:
49+
maximum = get_preload_max(qube)
50+
msg = f"{qube}:{maximum}"
51+
print(repr(msg))
4852
exec_args = qube.qubesd_call, qube.name, method, "preload-autostart"
4953
future = loop.run_in_executor(executor, *exec_args)
5054
tasks.append(future)

qubes/tests/integ/dispvm.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,9 @@ def test_017_preload_autostart(self):
607607
'admin.vm.CreateDisposable+preload-autostart' which fires the event
608608
'domain-preload-dispvm-autostart', clearing the current preload list
609609
and filling with new ones."""
610+
# This function doesn't patch qubesadmin.Qubes() of the autostart
611+
# script, therefore, it will also refresh preloaded disposables that
612+
# are not part of the test.
610613
logger.info("start")
611614
self.app.default_dispvm = self.disp_base
612615

@@ -615,7 +618,7 @@ def test_017_preload_autostart(self):
615618
asyncio.create_subprocess_exec("/usr/lib/qubes/preload-dispvm")
616619
)
617620
self.loop.run_until_complete(
618-
asyncio.wait_for(proc.communicate(), timeout=10)
621+
asyncio.wait_for(proc.communicate(), timeout=60)
619622
)
620623
self.assertEqual(self.disp_base.get_feat_preload(), [])
621624

@@ -625,7 +628,7 @@ def test_017_preload_autostart(self):
625628
proc = self.loop.run_until_complete(
626629
asyncio.create_subprocess_exec("/usr/lib/qubes/preload-dispvm")
627630
)
628-
self.loop.run_until_complete(asyncio.wait_for(proc.wait(), timeout=30))
631+
self.loop.run_until_complete(asyncio.wait_for(proc.wait(), timeout=60))
629632
preload_dispvm = self.disp_base.get_feat_preload()
630633
self.assertEqual(len(old_preload), preload_max)
631634
self.assertEqual(len(preload_dispvm), preload_max)
@@ -642,7 +645,7 @@ def test_017_preload_autostart(self):
642645
proc = self.loop.run_until_complete(
643646
asyncio.create_subprocess_exec("/usr/lib/qubes/preload-dispvm")
644647
)
645-
self.loop.run_until_complete(asyncio.wait_for(proc.wait(), timeout=30))
648+
self.loop.run_until_complete(asyncio.wait_for(proc.wait(), timeout=60))
646649
preload_dispvm = self.disp_base.get_feat_preload()
647650
self.assertEqual(len(old_preload), preload_max)
648651
self.assertEqual(len(preload_dispvm), preload_max)

qubes/tests/vm/dispvm.py

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,16 @@
3535
class TestApp(qubes.tests.vm.TestApp):
3636
def __init__(self):
3737
super(TestApp, self).__init__()
38-
self.qid_counter = 1
38+
self.qid_counter = 0
3939

4040
def add_new_vm(self, cls, **kwargs):
4141
qid = self.qid_counter
42-
self.qid_counter += 1
43-
vm = cls(self, None, qid=qid, **kwargs)
42+
if self.qid_counter == 0:
43+
self.qid_counter += 1
44+
vm = cls(self, None, **kwargs)
45+
else:
46+
self.qid_counter += 1
47+
vm = cls(self, None, qid=qid, **kwargs)
4448
self.domains[vm.name] = vm
4549
self.domains[vm] = vm
4650
return vm
@@ -58,6 +62,8 @@ def setUp(self):
5862
name="linux-kernel"
5963
)
6064
self.app.vmm.offline_mode = True
65+
self.app.default_dispvm = None
66+
self.adminvm = self.app.add_new_vm(qubes.vm.adminvm.AdminVM)
6167
self.template = self.app.add_new_vm(
6268
qubes.vm.templatevm.TemplateVM, name="test-template", label="red"
6369
)
@@ -68,8 +74,12 @@ def setUp(self):
6874
template=self.template,
6975
label="red",
7076
)
71-
self.app.domains[self.appvm.name] = self.appvm
72-
self.app.domains[self.appvm] = self.appvm
77+
self.appvm_alt = self.app.add_new_vm(
78+
qubes.vm.appvm.AppVM,
79+
name="test-vm-alt",
80+
template=self.template,
81+
label="red",
82+
)
7383
self.addCleanup(self.cleanup_dispvm)
7484
self.emitter = qubes.tests.TestEmitter()
7585

@@ -83,10 +93,15 @@ def cleanup_dispvm(self):
8393
del self.dispvm
8494
self.template.close()
8595
self.appvm.close()
86-
del self.template
96+
self.appvm_alt.close()
8797
del self.appvm
98+
del self.appvm_alt
99+
del self.template
100+
del self.adminvm
101+
self.app.close()
88102
self.app.domains.clear()
89103
self.app.pools.clear()
104+
del self.app
90105

91106
async def mock_coro(self, *args, **kwargs):
92107
pass
@@ -275,6 +290,49 @@ def test_000_from_appvm_preload_fill_gap(
275290
mock_symlink.assert_not_called()
276291
mock_makedirs.assert_called_once()
277292

293+
def test_000_get_preload_max(self):
294+
self.assertEqual(qubes.vm.dispvm.get_preload_max(self.appvm), None)
295+
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True
296+
self.appvm.features["preload-dispvm-max"] = 1
297+
self.assertEqual(qubes.vm.dispvm.get_preload_max(self.appvm), 1)
298+
self.assertEqual(qubes.vm.dispvm.get_preload_max(self.adminvm), None)
299+
self.adminvm.features["preload-dispvm-max"] = ""
300+
self.assertEqual(qubes.vm.dispvm.get_preload_max(self.adminvm), "")
301+
self.adminvm.features["preload-dispvm-max"] = 2
302+
self.assertEqual(qubes.vm.dispvm.get_preload_max(self.adminvm), 2)
303+
304+
def test_000_get_preload_templates(self):
305+
get_preload_templates = qubes.vm.dispvm.get_preload_templates
306+
self.assertEqual(get_preload_templates(self.app), [])
307+
self.appvm.template_for_dispvms = True
308+
self.appvm_alt.template_for_dispvms = True
309+
self.assertEqual(get_preload_templates(self.app), [])
310+
311+
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True
312+
self.appvm_alt.features["supported-rpc.qubes.WaitForRunningSystem"] = (
313+
True
314+
)
315+
self.appvm.features["preload-dispvm-max"] = 1
316+
self.appvm_alt.features["preload-dispvm-max"] = 0
317+
self.assertEqual(get_preload_templates(self.app), [self.appvm])
318+
319+
self.adminvm.features["preload-dispvm-max"] = ""
320+
# Still not default_dispvm
321+
self.appvm_alt.features["preload-dispvm-max"] = 1
322+
self.assertEqual(
323+
get_preload_templates(self.app), [self.appvm, self.appvm_alt]
324+
)
325+
326+
with mock.patch.object(self.appvm, "fire_event_async"):
327+
self.app.default_dispvm = self.appvm
328+
self.assertEqual(get_preload_templates(self.app), [self.appvm_alt])
329+
330+
self.app.default_dispvm = None
331+
self.adminvm.features["preload-dispvm-max"] = 1
332+
self.assertEqual(
333+
get_preload_templates(self.app), [self.appvm, self.appvm_alt]
334+
)
335+
278336
def test_001_from_appvm_reject_not_allowed(self):
279337
with self.assertRaises(qubes.exc.QubesException):
280338
dispvm = self.loop.run_until_complete(

qubes/tests/vm/mix/dvmtemplate.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,3 +259,9 @@ def test_013_dvm_preload_get_treshold(self):
259259
self.adminvm.features["preload-dispvm-threshold"] = value
260260
threshold = self.appvm.get_feat_preload_threshold()
261261
self.assertEqual(threshold, int(value or 0) * 1024**2)
262+
263+
def test_100_get_preload_templates(self):
264+
print(qubes.vm.dispvm.get_preload_templates(self.app))
265+
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True
266+
self.appvm.features["preload-dispvm-max"] = 1
267+
self.assertEqual(qubes.vm.dispvm.get_preload_max(self.appvm), 1)

qubes/vm/dispvm.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,26 +40,32 @@ def _setter_template(self, prop, value):
4040
return value
4141

4242

43+
# Keep in sync with linux/aux-tools/preload-dispvm
44+
def get_preload_max(qube) -> int | None:
45+
value = qube.features.get("preload-dispvm-max", None)
46+
return int(value) if value else value
47+
48+
49+
# Keep in sync with linux/aux-tools/preload-dispvm
4350
def get_preload_templates(app) -> list:
4451
domains = app.domains
45-
appvms = []
4652
default_dispvm = getattr(app, "default_dispvm", None)
47-
# Only add default_dispvm now if it will not be added by the other clause.
48-
if (
49-
int(domains["dom0"].features.get("preload-dispvm-max", 0) or 0) > 0
50-
and default_dispvm
51-
and int(default_dispvm.features.get("preload-dispvm-max", 0) or 0) == 0
52-
):
53-
appvms.append(default_dispvm)
54-
appvms.extend(
55-
[
56-
qube
57-
for qube in domains
58-
if int(qube.features.get("preload-dispvm-max", 0) or 0) > 0
59-
and qube.klass == "AppVM"
53+
global_max = get_preload_max(domains["dom0"])
54+
appvms = [
55+
qube
56+
for qube in domains
57+
if (
58+
qube.klass == "AppVM"
6059
and getattr(qube, "template_for_dispvms", False)
61-
]
62-
)
60+
and (
61+
(qube != default_dispvm and get_preload_max(qube))
62+
or (
63+
(qube == default_dispvm and global_max)
64+
or (global_max is None and get_preload_max(qube))
65+
)
66+
)
67+
)
68+
]
6369
return appvms
6470

6571

0 commit comments

Comments
 (0)