Skip to content

Commit c2093bf

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 c2093bf

File tree

11 files changed

+432
-57
lines changed

11 files changed

+432
-57
lines changed

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: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1632,6 +1632,8 @@ 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 = set()
1636+
dependencies = []
16351637
for obj in itertools.chain(self.domains, (self,)):
16361638
if obj is vm:
16371639
# allow removed VM to reference itself
@@ -1642,18 +1644,32 @@ def on_domain_pre_deleted(self, event, vm):
16421644
isinstance(prop, qubes.vm.VMProperty)
16431645
and getattr(obj, prop.__name__) == vm
16441646
):
1645-
self.log.error(
1646-
"Cannot remove %s, used by %s.%s",
1647-
vm,
1648-
obj,
1649-
prop.__name__,
1650-
)
1651-
raise qubes.exc.QubesVMInUseError(
1652-
vm,
1653-
"Domain is in use: {!r};"
1654-
"see 'journalctl -u qubesd -e' in dom0 for "
1655-
"details".format(vm.name),
1656-
)
1647+
if getattr(obj, "is_preload", False) and (
1648+
prop.__name__ == "template"
1649+
or (
1650+
prop.__name__ == "default_dispvm"
1651+
and getattr(obj, "template", None) == vm
1652+
)
1653+
):
1654+
preloads.add(obj.name)
1655+
continue
1656+
if isinstance(obj, qubes.app.Qubes):
1657+
dependencies.append(('"GLOBAL"', prop.__name__))
1658+
elif not obj.property_is_default(prop):
1659+
dependencies.append((obj.name, prop.__name__))
1660+
if dependencies:
1661+
self.log.error(
1662+
"Cannot remove %s as it is used by %s",
1663+
vm,
1664+
", ".join(
1665+
":".join(str(i) for i in tup) for tup in dependencies
1666+
),
1667+
)
1668+
raise qubes.exc.QubesVMInUseError(
1669+
vm,
1670+
"Domain is in use: {!r}; see 'journalctl -u qubesd -e' in dom0"
1671+
" for details".format(vm.name),
1672+
)
16571673
if isinstance(vm, qubes.vm.qubesvm.QubesVM):
16581674
assignments = vm.get_provided_assignments()
16591675
else:
@@ -1664,6 +1680,9 @@ def on_domain_pre_deleted(self, event, vm):
16641680
vm, "VM has devices assigned to other VMs: " + desc
16651681
)
16661682

1683+
if preloads:
1684+
vm.remove_preload_excess(0, reason="domain will be deleted")
1685+
16671686
@qubes.events.handler("domain-delete")
16681687
def on_domain_deleted(self, event, vm):
16691688
# 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
@@ -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+
)

0 commit comments

Comments
 (0)