Skip to content

Commit b474909

Browse files
committed
Switch autostart event in favor of only start
The autostart made a refresh, which cleaned all preloaded disposables, served to: - Invalidate previous incomplete remnants - Get fresh ones after a change was made to a template in chain Both of these issues were handled, "domain-load" of the disposable template invalidates incomplete ones while "domain-shutdown" of a template in chain triggers a refresh. For: QubesOS/qubes-issues#1512
1 parent 1186794 commit b474909

6 files changed

Lines changed: 98 additions & 61 deletions

File tree

linux/aux-tools/preload-dispvm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ async def main():
5454
maximum = get_preload_max(qube)
5555
msg = f"{qube}:{maximum}"
5656
print(repr(msg))
57-
exec_args = qube.qubesd_call, qube.name, method, "preload-autostart"
57+
exec_args = qube.qubesd_call, qube.name, method, "preload"
5858
future = loop.run_in_executor(executor, *exec_args)
5959
tasks.append(future)
6060
await asyncio.gather(*tasks)

qubes/api/admin.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,13 +1308,13 @@ async def _vm_create(
13081308
@qubes.api.method("admin.vm.CreateDisposable", scope="global", write=True)
13091309
async def create_disposable(self, untrusted_payload):
13101310
"""
1311-
Create a disposable. If the RPC argument is ``preload-autostart``,
1311+
Create a disposable. If the RPC argument is ``preload``,
13121312
cleanse the preload list and start preloading fresh disposables.
13131313
"""
1314-
self.enforce(self.arg in [None, "", "preload-autostart"])
1315-
preload_autostart = False
1316-
if self.arg == "preload-autostart":
1317-
preload_autostart = True
1314+
self.enforce(self.arg in [None, "", "preload"])
1315+
preload = False
1316+
if self.arg == "preload":
1317+
preload = True
13181318
if untrusted_payload not in (b"", b"uuid"):
13191319
raise qubes.exc.QubesValueError(
13201320
"Invalid payload for admin.vm.CreateDisposable: "
@@ -1327,8 +1327,10 @@ async def create_disposable(self, untrusted_payload):
13271327
appvm = self.dest
13281328

13291329
self.fire_event_for_permission(dispvm_template=appvm)
1330-
if preload_autostart:
1331-
await appvm.fire_event_async("domain-preload-dispvm-autostart")
1330+
if preload:
1331+
await appvm.fire_event_async(
1332+
"domain-preload-dispvm-start", reason="autostart was requested"
1333+
)
13321334
return
13331335
dispvm = await qubes.vm.dispvm.DispVM.from_appvm(appvm)
13341336
# TODO: move this to extension (in race-free fashion, better than here)

qubes/api/internal.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,5 +413,7 @@ async def suspend_post(self):
413413
preload_templates = qubes.vm.dispvm.get_preload_templates(self.app)
414414
for qube in preload_templates:
415415
asyncio.ensure_future(
416-
qube.fire_event_async("domain-preload-dispvm-autostart")
416+
qube.fire_event_async(
417+
"domain-preload-dispvm-start", reason="system resumed"
418+
)
417419
)

qubes/tests/api_admin.py

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3992,7 +3992,7 @@ def test_643_vm_create_disposable_preload_autostart(
39923992
self.vm.template_for_dispvms = True
39933993
self.app.default_dispvm = self.vm
39943994
self.vm.add_handler(
3995-
"domain-preload-dispvm-autostart", self._test_event_handler
3995+
"domain-preload-dispvm-start", self._test_event_handler
39963996
)
39973997
self.vm.features["qrexec"] = "1"
39983998
self.vm.features["supported-rpc.qubes.WaitForRunningSystem"] = "1"
@@ -4005,27 +4005,23 @@ def test_643_vm_create_disposable_preload_autostart(
40054005
self.fail("didn't preload in time")
40064006
old_preload = self.vm.get_feat_preload()
40074007
retval = self.call_mgmt_func(
4008-
b"admin.vm.CreateDisposable", b"dom0", arg=b"preload-autostart"
4008+
b"admin.vm.CreateDisposable", b"dom0", arg=b"preload"
40094009
)
40104010
self.assertTrue(
40114011
self._test_event_was_handled(
4012-
self.vm.name, "domain-preload-dispvm-autostart"
4012+
self.vm.name, "domain-preload-dispvm-start"
40134013
)
40144014
)
4015-
for _ in range(10):
4016-
if (
4017-
old_preload != self.vm.get_feat_preload()
4018-
and self.vm.get_feat_preload() != []
4019-
):
4020-
break
4021-
self.loop.run_until_complete(asyncio.sleep(1))
4022-
else:
4023-
self.fail("didn't preload again in time")
4024-
dispvm_preload = self.vm.get_feat_preload()
4025-
self.assertEqual(len(dispvm_preload), 1)
4015+
self.call_mgmt_func(
4016+
b"admin.vm.CreateDisposable", b"dom0", arg=b"preload"
4017+
)
4018+
self.assertEqual(1, mock_storage_create.call_count)
4019+
new_preload = self.vm.get_feat_preload()
4020+
self.assertEqual(len(old_preload), 1)
4021+
self.assertEqual(len(new_preload), 1)
4022+
self.assertEqual(old_preload, new_preload)
40264023
self.assertIsNone(retval)
4027-
self.assertEqual(2, mock_storage_create.call_count)
4028-
self.assertEqual(2, mock_dispvm_start.call_count)
4024+
self.assertEqual(1, mock_dispvm_start.call_count)
40294025
self.assertTrue(self.app.save.called)
40304026

40314027
def test_650_vm_device_set_mode_required(self):

qubes/tests/integ/dispvm.py

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,20 @@ def tearDown(self): # pylint: disable=invalid-name
262262
super(TC_20_DispVMMixin, self).tearDown()
263263
logger.info("end")
264264

265+
def _run_cmd_and_log_output(self, qube, cmd, user="root", timeout=30):
266+
try:
267+
stdout, _ = self.loop.run_until_complete(
268+
asyncio.wait_for(
269+
qube.run_for_stdio(
270+
cmd, user=user, stderr=subprocess.STDOUT
271+
),
272+
timeout=timeout,
273+
)
274+
)
275+
except subprocess.CalledProcessError as e:
276+
stdout = getattr(e, "stdout", str(e))
277+
logger.critical("{}: {}: {}".format(qube.name, cmd, stdout))
278+
265279
def _test_event_handler(
266280
self, vm, event, *args, **kwargs
267281
): # pylint: disable=unused-argument
@@ -285,7 +299,6 @@ def _test_event_was_handled(self, vm, event):
285299
def _register_handlers(self, vm): # pylint: disable=unused-argument
286300
events = [
287301
# appvm
288-
"domain-preload-dispvm-autostart",
289302
"domain-preload-dispvm-start",
290303
"domain-preload-dispvm-used",
291304
# dispvm
@@ -304,11 +317,12 @@ def _register_handlers(self, vm): # pylint: disable=unused-argument
304317
def _on_domain_add(self, app, event, vm): # pylint: disable=unused-argument
305318
self._register_handlers(vm)
306319

307-
async def cleanup_preload_run(self, qube):
320+
async def cleanup_preload_run(self, qube, down_to=0):
308321
old_preload = qube.features.get("preload-dispvm", "")
309322
old_preload = old_preload.split(" ") if old_preload else []
310323
if not old_preload:
311324
return
325+
old_preload = old_preload[:down_to]
312326
logger.info(
313327
"cleaning up preloaded disposables: %s:%s", qube.name, old_preload
314328
)
@@ -333,7 +347,9 @@ def cleanup_preload(self):
333347
self.disp_base_alt,
334348
]:
335349
continue
336-
logger.info("removing preloaded disposables: '%s'", qube.name)
350+
logger.info(
351+
"removing preloaded disposables configured in: '%s'", qube.name
352+
)
337353
target = qube
338354
if qube.klass == "AdminVM" and default_dispvm:
339355
target = default_dispvm
@@ -453,7 +469,6 @@ async def run_preload(self):
453469
stdout = await self.run_preload_proc()
454470
self.assertEqual(stdout, dispvm_name)
455471
test_cases = [
456-
(False, appvm.name, "domain-preload-dispvm-autostart", True),
457472
(False, appvm.name, "domain-preload-dispvm-start", True),
458473
(True, appvm.name, "domain-preload-dispvm-used", True),
459474
(
@@ -637,15 +652,12 @@ async def _test_016_preload_race_less(self):
637652
logger.info("end")
638653

639654
def test_017_preload_autostart(self):
640-
"""The script triggers the API call
641-
'admin.vm.CreateDisposable+preload-autostart' which fires the event
642-
'domain-preload-dispvm-autostart', clearing the current preload list
643-
and filling with new ones."""
655+
"""The script triggers the API call 'admin.vm.CreateDisposable+preload'
656+
which is responsible for bootstrapping."""
644657
logger.info("start")
645658
self.app.default_dispvm = self.disp_base
646659

647-
preload_max = 1
648-
logger.info("no refresh to be made")
660+
logger.info("must not change as max is 0")
649661
proc = self.loop.run_until_complete(
650662
asyncio.create_subprocess_exec("/usr/lib/qubes/preload-dispvm")
651663
)
@@ -654,39 +666,70 @@ def test_017_preload_autostart(self):
654666
)
655667
self.assertEqual(self.disp_base.get_feat_preload(), [])
656668

657-
logger.info("refresh to be made")
669+
preload_max = 1
670+
logger.info("must not change existing preloaded disposables")
658671
self.disp_base.features["preload-dispvm-max"] = str(preload_max)
659672
self.loop.run_until_complete(self.wait_preload(preload_max))
660673
old_preload = self.disp_base.get_feat_preload()
661674
proc = self.loop.run_until_complete(
662675
asyncio.create_subprocess_exec("/usr/lib/qubes/preload-dispvm")
663676
)
664-
self.loop.run_until_complete(asyncio.wait_for(proc.wait(), timeout=40))
677+
self.loop.run_until_complete(asyncio.wait_for(proc.wait(), timeout=10))
665678
preload_dispvm = self.disp_base.get_feat_preload()
666-
self.assertEqual(len(old_preload), preload_max)
667-
self.assertEqual(len(preload_dispvm), preload_max)
668-
self.assertTrue(
669-
set(old_preload).isdisjoint(preload_dispvm),
670-
f"old_preload={old_preload} preload_dispvm={preload_dispvm}",
679+
self.assertEqual(
680+
old_preload,
681+
preload_dispvm,
682+
msg=f"old_preload={old_preload} preload_dispvm={preload_dispvm}",
671683
)
672684

673-
logger.info("global refresh to be made")
674685
preload_max += 1
686+
logger.info("global refill must work")
675687
self.adminvm.features["preload-dispvm-max"] = str(preload_max)
676688
self.loop.run_until_complete(self.wait_preload(preload_max))
677-
del self.disp_base.features["preload-dispvm-max"]
689+
old_old_preload = self.disp_base.get_feat_preload()
690+
self.loop.run_until_complete(
691+
self.cleanup_preload_run(self.disp_base, down_to=preload_max - 1)
692+
)
678693
old_preload = self.disp_base.get_feat_preload()
694+
self.assertEqual(len(old_preload), preload_max - 1)
695+
self.assertIn(old_preload[0], old_old_preload)
679696
proc = self.loop.run_until_complete(
680697
asyncio.create_subprocess_exec("/usr/lib/qubes/preload-dispvm")
681698
)
682-
self.loop.run_until_complete(asyncio.wait_for(proc.wait(), timeout=40))
699+
try:
700+
self.loop.run_until_complete(
701+
asyncio.wait_for(proc.wait(), timeout=40)
702+
)
703+
except asyncio.TimeoutError:
704+
debug_preload = self.disp_base.get_feat_preload()
705+
for qube in debug_preload:
706+
qube = self.app.domains[qube]
707+
if qube.is_paused():
708+
self.loop.run_until_complete(
709+
asyncio.wait_for(qube.unpause(), timeout=5)
710+
)
711+
self._run_cmd_and_log_output(
712+
qube, "systemtl --user is-system-running", user="user"
713+
)
714+
self._run_cmd_and_log_output(
715+
qube, "systemtl is-system-running", user="root"
716+
)
717+
self._run_cmd_and_log_output(
718+
qube, "systemd-analyze --no-pager --user blame", user="user"
719+
)
720+
self._run_cmd_and_log_output(
721+
qube, "systemd-analyze --no-pager blame", user="root"
722+
)
723+
self._run_cmd_and_log_output(
724+
qube, "journalctl --no-pager --user", user="user"
725+
)
726+
self._run_cmd_and_log_output(
727+
qube, "journalctl --no-pager", user="root"
728+
)
729+
raise
683730
preload_dispvm = self.disp_base.get_feat_preload()
684-
self.assertEqual(len(old_preload), preload_max)
731+
self.assertIn(old_preload[0], preload_dispvm)
685732
self.assertEqual(len(preload_dispvm), preload_max)
686-
self.assertTrue(
687-
set(old_preload).isdisjoint(preload_dispvm),
688-
f"old_preload={old_preload} preload_dispvm={preload_dispvm}",
689-
)
690733

691734
self.app.default_dispvm = None
692735
logger.info("end")

qubes/vm/mix/dvmtemplate.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def on_domain_loaded(self, event) -> None:
6363
# pylint: disable=unused-argument
6464
assert isinstance(self, qubes.vm.BaseVM)
6565
changes = False
66-
# Preloading began and host rebooted and autostart event didn't run yet.
66+
# Began preloading, host rebooted, autostart script didn't run yet.
6767
old_preload = self.get_feat_preload()
6868
clean_preload = old_preload.copy()
6969
for unwanted_disp in old_preload:
@@ -411,7 +411,6 @@ def __on_property_set_template(
411411

412412
@qubes.events.handler(
413413
"domain-preload-dispvm-used",
414-
"domain-preload-dispvm-autostart",
415414
"domain-preload-dispvm-start",
416415
)
417416
async def on_domain_preload_dispvm_used(
@@ -423,14 +422,11 @@ async def on_domain_preload_dispvm_used(
423422
**kwargs, # pylint: disable=unused-argument
424423
) -> None:
425424
"""
426-
Offloads on excess and preload on vacancy. On ``autostart``, the
427-
preloaded list is emptied before preloading.
425+
Offloads on excess and preload on vacancy.
428426
429427
:param str event: Event which was fired. Events have the prefix \
430-
``domain-preload-dispvm-``. If the suffix is ``autostart``, the \
431-
preload list is emptied before attempting to preload. If the \
432-
suffix is ``used`` or ``start``, tries to preload until it fills \
433-
gaps.
428+
``domain-preload-dispvm-``. It always tries to preload until it \
429+
fills the gaps if there is enough memory.
434430
:param qubes.vm.dispvm.DispVM dispvm: Disposable that was used
435431
:param str reason: Why the event was fired
436432
:param float delay: Proceed only after sleeping that many seconds
@@ -454,9 +450,7 @@ async def on_domain_preload_dispvm_used(
454450
if delay:
455451
await asyncio.sleep(delay)
456452

457-
if event == "autostart":
458-
self.remove_preload_excess(0, reason="event autostart was called")
459-
elif not self.can_preload():
453+
if not self.can_preload():
460454
self.remove_preload_excess(reason="there may be absent qubes")
461455
# Absent qubes might be removed above.
462456
if not self.can_preload():

0 commit comments

Comments
 (0)