Skip to content

Commit 703dd60

Browse files
committed
Merge remote-tracking branch 'origin/pr/673'
* origin/pr/673: Assure paused domains prior to suspending remain paused
2 parents fc8d672 + 2d553d4 commit 703dd60

2 files changed

Lines changed: 70 additions & 4 deletions

File tree

qubes/api/internal.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import asyncio
2424
import json
25+
import os
2526
import subprocess
2627

2728
import qubes.api
@@ -30,6 +31,9 @@
3031
import qubes.vm.dispvm
3132

3233

34+
PREVIOUSLY_PAUSED = "/run/qubes/previously-paused.list"
35+
36+
3337
class SystemInfoCache:
3438
cache = None
3539
cache_for_app = None
@@ -258,13 +262,24 @@ async def suspend_pre(self):
258262
:return:
259263
"""
260264

261-
# first notify all VMs
265+
# first keep track of VMs which were paused before suspending
266+
previously_paused = [
267+
vm.name
268+
for vm in self.app.domains
269+
if vm.get_power_state() in ["Paused", "Suspended"]
270+
]
271+
with open(PREVIOUSLY_PAUSED, "w", encoding="ascii") as file:
272+
file.write("\n".join(previously_paused))
273+
274+
# then notify all VMs (except paused ones)
262275
processes = []
263276
for vm in self.app.domains:
264277
if isinstance(vm, qubes.vm.adminvm.AdminVM):
265278
continue
266279
if not vm.is_running():
267280
continue
281+
if vm.name in previously_paused:
282+
continue
268283
if not vm.features.check_with_template("qrexec", False):
269284
continue
270285
try:
@@ -303,6 +318,8 @@ async def suspend_pre(self):
303318
for vm in self.app.domains:
304319
if isinstance(vm, qubes.vm.adminvm.AdminVM):
305320
continue
321+
if vm.name in previously_paused:
322+
continue
306323
if vm.is_running():
307324
coros.append(asyncio.create_task(vm.suspend()))
308325
if coros:
@@ -326,23 +343,37 @@ async def suspend_post(self):
326343
:return:
327344
"""
328345

346+
# Reload list of previously paused qubes before suspending
347+
previously_paused = []
348+
try:
349+
if os.path.isfile(PREVIOUSLY_PAUSED):
350+
with open(PREVIOUSLY_PAUSED, encoding="ascii") as file:
351+
previously_paused = file.read().split("\n")
352+
os.unlink(PREVIOUSLY_PAUSED)
353+
except OSError:
354+
previously_paused = []
355+
329356
coros = []
330357
# first resume/unpause VMs
331358
for vm in self.app.domains:
332359
if isinstance(vm, qubes.vm.adminvm.AdminVM):
333360
continue
361+
if vm.name in previously_paused:
362+
continue
334363
if vm.get_power_state() in ["Paused", "Suspended"]:
335364
coros.append(asyncio.create_task(vm.resume()))
336365
if coros:
337366
await asyncio.wait(coros)
338367

339-
# then notify all VMs
368+
# then notify all VMs (except previously paused ones)
340369
processes = []
341370
for vm in self.app.domains:
342371
if isinstance(vm, qubes.vm.adminvm.AdminVM):
343372
continue
344373
if not vm.is_running():
345374
continue
375+
if vm.name in previously_paused:
376+
continue
346377
if not vm.features.check_with_template("qrexec", False):
347378
continue
348379
try:

qubes/tests/api_internal.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,26 @@ def test_000_suspend_pre(self):
9393
no_qrexec_vm = self.create_mockvm()
9494
no_qrexec_vm.is_running.return_value = True
9595

96+
paused_vm = self.create_mockvm(features={"qrexec": True})
97+
paused_vm.is_running.return_value = True
98+
paused_vm.get_power_state.return_value = "Paused"
99+
paused_vm.name = "SleepingBeauty"
100+
96101
self.domains.update(
97102
{
98103
"running": running_vm,
99104
"not-running": not_running_vm,
100105
"no-qrexec": no_qrexec_vm,
106+
"paused": paused_vm,
101107
}
102108
)
103109

104-
ret = self.call_mgmt_func(b"internal.SuspendPre")
110+
with mock.patch.object(
111+
qubes.api.internal,
112+
"PREVIOUSLY_PAUSED",
113+
"/tmp/qubes-previously-paused.tmp",
114+
):
115+
ret = self.call_mgmt_func(b"internal.SuspendPre")
105116
self.assertIsNone(ret)
106117
self.assertFalse(self.dom0.called)
107118

@@ -121,6 +132,13 @@ def test_000_suspend_pre(self):
121132
("run_service", ("qubes.SuspendPreAll",), mock.ANY),
122133
no_qrexec_vm.mock_calls,
123134
)
135+
136+
self.assertNotIn(
137+
("run_service", ("qubes.SuspendPreAll",), mock.ANY),
138+
paused_vm.mock_calls,
139+
)
140+
self.assertIn(("suspend", (), {}), running_vm.mock_calls)
141+
124142
self.assertIn(("suspend", (), {}), no_qrexec_vm.mock_calls)
125143

126144
def test_001_suspend_post(self):
@@ -136,15 +154,26 @@ def test_001_suspend_post(self):
136154
no_qrexec_vm.is_running.return_value = True
137155
no_qrexec_vm.get_power_state.return_value = "Suspended"
138156

157+
paused_vm = self.create_mockvm(features={"qrexec": True})
158+
paused_vm.is_running.return_value = True
159+
paused_vm.get_power_state.return_value = "Paused"
160+
paused_vm.name = "SleepingBeauty"
161+
139162
self.domains.update(
140163
{
141164
"running": running_vm,
142165
"not-running": not_running_vm,
143166
"no-qrexec": no_qrexec_vm,
167+
"paused": paused_vm,
144168
}
145169
)
146170

147-
ret = self.call_mgmt_func(b"internal.SuspendPost")
171+
with mock.patch.object(
172+
qubes.api.internal,
173+
"PREVIOUSLY_PAUSED",
174+
"/tmp/qubes-previously-paused.tmp",
175+
):
176+
ret = self.call_mgmt_func(b"internal.SuspendPost")
148177
self.assertIsNone(ret)
149178
self.assertFalse(self.dom0.called)
150179

@@ -166,6 +195,12 @@ def test_001_suspend_post(self):
166195
)
167196
self.assertIn(("resume", (), {}), no_qrexec_vm.mock_calls)
168197

198+
self.assertNotIn(
199+
("run_service", ("qubes.SuspendPostAll",), mock.ANY),
200+
paused_vm.mock_calls,
201+
)
202+
self.assertNotIn(("resume", (), {}), paused_vm.mock_calls)
203+
169204
def test_010_get_system_info(self):
170205
self.dom0.name = "dom0"
171206
self.dom0.tags = ["tag1", "tag2"]

0 commit comments

Comments
 (0)