Skip to content

Commit 38d6003

Browse files
committed
Discard preloaded disposables with outdated props
If a qube property is changed on the disposable template, it is not replicated to the preloaded disposable, no refresh occurs, which means, if a disposable template has the netvm changed, the preloaded disposable would still remain with the old setting. While changing netvm on the fly is possible after unpause, several other settings requires a restart. Fixes: QubesOS/qubes-issues#10525 For: QubesOS/qubes-issues#1512
1 parent a9a692a commit 38d6003

4 files changed

Lines changed: 145 additions & 9 deletions

File tree

qubes/tests/integ/dispvm.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -832,8 +832,29 @@ async def _test_019_preload_refresh(self):
832832
self.log_preload()
833833
logger.info("end")
834834

835+
def test_020_preload_discard_outdated(self):
836+
"""Discard preload if properties differ from the disposable template."""
837+
self.loop.run_until_complete(self._test_020_preload_discard_outdated())
838+
839+
async def _test_020_preload_discard_outdated(self):
840+
logger.info("start")
841+
self.log_preload()
842+
preload_max = 1
843+
self.disp_base.features["preload-dispvm-max"] = str(preload_max)
844+
await self.wait_preload(preload_max)
845+
preload_dispvm = self.disp_base.get_feat_preload()
846+
self.disp_base.netvm = None
847+
try:
848+
dispvm = await asyncio.wait_for(
849+
qubes.vm.dispvm.DispVM.from_appvm(self.disp_base), 30
850+
)
851+
self.assertNotIn(dispvm.name, preload_dispvm)
852+
finally:
853+
await dispvm.cleanup()
854+
logger.info("end")
855+
835856
@unittest.skipUnless(which("xdotool"), "xdotool not installed")
836-
def test_020_gui_app(self):
857+
def test_080_gui_app(self):
837858
dispvm = self.loop.run_until_complete(
838859
qubes.vm.dispvm.DispVM.from_appvm(self.disp_base)
839860
)
@@ -1057,7 +1078,7 @@ def _whonix_ws_dispvm_confirm(self, action_str):
10571078
return (True, "")
10581079

10591080
@unittest.skipUnless(which("xdotool"), "xdotool not installed")
1060-
def test_030_edit_file(self):
1081+
def test_090_edit_file(self):
10611082
self.testvm1 = self.app.add_new_vm(
10621083
qubes.vm.appvm.AppVM,
10631084
name=self.make_vm_name("vm1"),

qubes/tests/vm/dispvm.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ def test_000_from_appvm_preload_use(
181181
mock_symlink,
182182
mock_start,
183183
):
184+
# pylint: disable=unused-argument
184185
mock_storage.return_value.create.side_effect = self.mock_coro
185186
mock_start.side_effect = self.mock_coro
186187
self.appvm.template_for_dispvms = True
@@ -222,6 +223,7 @@ def test_000_from_appvm_preload_use(
222223
mock_qube.features = dispvm.features
223224
mock_qube.unpause = self.mock_coro
224225
mock_qube.request_preload.return_value = dispvm
226+
mock_qube.is_preload_outdated = dispvm.is_preload_outdated
225227
mock_qube.get_preload = mock.AsyncMock()
226228
mock_qube.volumes = {}
227229
fresh_dispvm = self.loop.run_until_complete(
@@ -656,3 +658,61 @@ def test_023_inherit_ephemeral(self, _mock_makedirs, _mock_symlink):
656658
self.loop.run_until_complete(dispvm.create_on_disk())
657659
self.assertIs(dispvm.template, self.appvm)
658660
self.assertTrue(dispvm.volumes["volatile"].ephemeral)
661+
662+
@mock.patch("qubes.vm.qubesvm.QubesVM.start")
663+
@mock.patch("os.symlink")
664+
@mock.patch("os.makedirs")
665+
@mock.patch("qubes.storage.Storage")
666+
def test_024_is_preload_outdated(
667+
self,
668+
mock_storage,
669+
mock_makedirs,
670+
mock_symlink,
671+
mock_start,
672+
):
673+
mock_storage.return_value.create.side_effect = self.mock_coro
674+
mock_makedirs.return_value = self.mock_coro
675+
mock_symlink.return_value = self.mock_coro
676+
mock_start.side_effect = self.mock_coro
677+
self.appvm.template_for_dispvms = True
678+
679+
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True
680+
self.appvm.features["preload-dispvm-max"] = "1"
681+
orig_getitem = self.app.domains.__getitem__
682+
with mock.patch.object(
683+
self.app, "domains", wraps=self.app.domains
684+
) as mock_domains:
685+
mock_qube = mock.Mock()
686+
mock_qube.template = self.appvm
687+
mock_qube.qrexec_timeout = self.appvm.qrexec_timeout
688+
mock_qube.preload_complete = mock.Mock(spec=asyncio.Event)
689+
mock_qube.preload_complete.is_set.return_value = True
690+
mock_qube.preload_complete.set = self.mock_coro
691+
mock_qube.preload_complete.clear = self.mock_coro
692+
mock_qube.preload_complete.wait = self.mock_coro
693+
mock_domains.configure_mock(
694+
**{
695+
"get_new_unused_dispid": mock.Mock(return_value=42),
696+
"__contains__.return_value": True,
697+
"__getitem__.side_effect": lambda key: (
698+
mock_qube if key == "disp42" else orig_getitem(key)
699+
),
700+
}
701+
)
702+
dispvm = self.loop.run_until_complete(
703+
qubes.vm.dispvm.DispVM.from_appvm(self.appvm, preload=True)
704+
)
705+
706+
self.assertFalse(dispvm.is_preload_outdated())
707+
self.appvm.debug = not self.appvm.debug
708+
self.assertEqual(list(dispvm.is_preload_outdated().keys()), ["debug"])
709+
self.appvm.debug = not self.appvm.debug
710+
711+
self.assertFalse(dispvm.is_preload_outdated())
712+
self.appvm_alt.provides_network = True
713+
self.assertFalse(dispvm.is_preload_outdated())
714+
self.appvm.netvm = self.appvm_alt
715+
self.assertEqual(
716+
sorted(list(dispvm.is_preload_outdated().keys())),
717+
sorted(["netvm", "dns", "visible_netmask"]),
718+
)

qubes/vm/dispvm.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,52 @@ def is_preload(self) -> bool:
417417
return True
418418
return False
419419

420+
def is_preload_outdated(self) -> dict:
421+
"""
422+
Show properties that differ on disposable compared to its template.
423+
424+
:rtype: dict
425+
"""
426+
if not self.is_preload:
427+
return {}
428+
appvm = self.template
429+
appvm_props = appvm.property_dict()
430+
props = self.property_dict()
431+
exclude_props = [
432+
"backup_timestamp",
433+
"default_dispvm",
434+
"dispid",
435+
"gateway",
436+
"gateway6",
437+
"icon",
438+
"include_in_backups",
439+
"installed_by_rpm",
440+
"ip",
441+
"ip6",
442+
"klass",
443+
"name",
444+
"qid",
445+
"start_time",
446+
"stubdom_uuid",
447+
"stubdom_xid",
448+
"template",
449+
"template_for_dispvms",
450+
"updateable",
451+
"uuid",
452+
"visible_gateway",
453+
"visible_gateway6",
454+
"visible_ip",
455+
"visible_ip6",
456+
"xid",
457+
]
458+
differed_props = {
459+
k: v
460+
for k, v in props.items() & appvm_props.items()
461+
if k not in exclude_props
462+
and getattr(self, k, None) != getattr(appvm, k, None)
463+
}
464+
return differed_props
465+
420466
@qubes.events.handler("domain-load")
421467
def on_domain_loaded(self, event) -> None:
422468
"""

qubes/vm/mix/dvmtemplate.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -682,16 +682,24 @@ def request_preload(self) -> Optional["qubes.vm.dispvm.DispVM"]:
682682
dispvm = None
683683
for item in preload_dispvm:
684684
qube = self.app.domains[item]
685-
if any(vol.is_outdated() for vol in qube.volumes.values()):
685+
outdated_settings = None
686+
if (outdated_settings := qube.is_preload_outdated()) or any(
687+
vol.is_outdated() for vol in qube.volumes.values()
688+
):
689+
if outdated_settings:
690+
discard_reason = "settings(s): " + ", ".join(
691+
map(str, outdated_settings.keys())
692+
)
693+
else:
694+
discard_reason = "volume(s)"
686695
qube.log.warning(
687-
"Requested preloaded qube but it is outdated, trying "
688-
"another one if available"
696+
"Requested preloaded qube but it has outdated %s. Trying "
697+
"another one if available",
698+
discard_reason,
689699
)
690-
# The gap is filled after the delay set by the
691-
# 'domain-shutdown' of its ancestors. Not refilling now to
692-
# deliver a disposable faster.
700+
# Not refilling now to deliver a disposable faster.
693701
self.remove_preload_from_list(
694-
[qube.name], reason="of outdated volume(s)"
702+
[qube.name], reason="of outdated " + discard_reason
695703
)
696704
# Delay to not affect this run.
697705
asyncio.ensure_future(
@@ -706,6 +714,7 @@ def request_preload(self) -> Optional["qubes.vm.dispvm.DispVM"]:
706714
"Found only outdated preloaded qube(s), falling back to "
707715
"normal disposable"
708716
)
717+
self.fill_preload_gap()
709718
return None
710719
dispvm.mark_preload_requested()
711720
return dispvm

0 commit comments

Comments
 (0)