Skip to content

Commit e6cffff

Browse files
committed
Improve preloaded dispvm tests
1 parent a76ed3e commit e6cffff

15 files changed

Lines changed: 308 additions & 154 deletions

File tree

ci/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ pylint
88
sphinx
99
PyYAML
1010
pyinotify
11+
psutil

linux/aux-tools/Makefile

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

44
install:
55
mkdir -p $(DESTDIR)/usr/lib/qubes
6-
cp cleanup-dispvms $(DESTDIR)/usr/lib/qubes
7-
cp startup-misc.sh $(DESTDIR)/usr/lib/qubes
6+
cp preload-dispvm $(DESTDIR)/usr/lib/qubes/
7+
cp cleanup-dispvms $(DESTDIR)/usr/lib/qubes/
8+
cp startup-misc.sh $(DESTDIR)/usr/lib/qubes/
89
cp fix-dir-perms.sh $(DESTDIR)/usr/lib/qubes/

linux/aux-tools/preload-dispvm

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,21 @@
11
#!/usr/bin/env python3
22

3-
#import asyncio
43
import qubesadmin
54

6-
def get_apps():
7-
domains = qubesadmin.Qubes().domains
8-
return [qube for qube in domains
9-
if int(qube.features.get("dispvm-preload-max", 0)) > 0
10-
and qube.klass == "AppVM"
11-
and getattr(qube, "template_for_dispvms", False)
12-
]
13-
14-
#async def main():
15-
# appvms = get_apps()
16-
# ## TODO: How to fire events outside of qubesd? How to load DispVM by hand.
17-
# #o = MyClass()
18-
# #o.events_enabled = True
19-
# #effect = o.fire_event('event1')
20-
# event = "domain-preloaded-dispvm-autostart"
21-
# tasks = [qube.fire_event_async(event) for qube in appvms]
22-
# await asyncio.gather(*tasks)
235

246
def main():
25-
appvms = get_apps()
7+
domains = qubesadmin.Qubes().domains
8+
appvms = [
9+
qube
10+
for qube in domains
11+
if int(qube.features.get("dispvm-preload-max", 0)) > 0
12+
and qube.klass == "AppVM"
13+
and getattr(qube, "template_for_dispvms", False)
14+
]
2615
method = "admin.vm.CreateDisposable"
2716
for qube in appvms:
2817
qube.qubesd_call("dom0", method, "preload-autostart")
2918

19+
3020
if __name__ == "__main__":
31-
#asyncio.run(main())
3221
main()

linux/systemd/qubes-preload-dispvm.service

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
[Unit]
22
Description=Preload Qubes DispVMs
3-
After=qubes-vm@.service
4-
# TODO: or 'Requires='? Should a failure to autostart sys-(usb|net) make this
5-
# unit be skipped?
6-
Wants=qubes-vm@.service
73
ConditionKernelCommandLine=!qubes.skip_autostart
84

95
[Service]
106
Type=oneshot
11-
Environment=DISPLAY=:0
127
ExecStart=/usr/lib/qubes/preload-dispvm
138
Group=qubes
149
RemainAfterExit=yes

linux/systemd/qubes-vm@.service

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
[Unit]
22
Description=Start Qubes VM %i
33
After=qubesd.service qubes-meminfo-writer-dom0.service
4+
Before=qubes-vm@.service
45
ConditionKernelCommandLine=!qubes.skip_autostart
56

67
[Service]
78
Type=oneshot
8-
Environment=DISPLAY=:0
9-
ExecStart=/usr/bin/qvm-start --skip-if-running %i
9+
ExecStart=/usr/bin/qvm-start --skip-if-running -- %i
1010
Group=qubes
1111
RemainAfterExit=yes
1212

qubes/api/admin.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,9 @@ async def vm_property_set(self, untrusted_payload):
298298
async def property_set(self, untrusted_payload):
299299
"""Set property value"""
300300
self.enforce(self.dest.name == "dom0")
301-
return self._property_set(self.app, untrusted_payload=untrusted_payload)
301+
return self._property_set(
302+
self.app, untrusted_payload=untrusted_payload
303+
)
302304

303305
def _property_set(self, dest, untrusted_payload):
304306
if self.arg not in dest.property_list():
@@ -484,7 +486,9 @@ async def vm_volume_clone_to(self, untrusted_payload):
484486

485487
# make sure the volume still exists, but invalidate token anyway
486488
self.enforce(str(src_volume.pool) in self.app.pools)
487-
self.enforce(src_volume in self.app.pools[str(src_volume.pool)].volumes)
489+
self.enforce(
490+
src_volume in self.app.pools[str(src_volume.pool)].volumes
491+
)
488492

489493
dst_volume = self.dest.volumes[self.arg]
490494

@@ -766,7 +770,9 @@ async def pool_add(self, untrusted_payload):
766770

767771
driver_parameters = qubes.storage.driver_parameters(self.arg)
768772
unexpected_parameters = [
769-
key for key in untrusted_pool_config if key not in driver_parameters
773+
key
774+
for key in untrusted_pool_config
775+
if key not in driver_parameters
770776
]
771777
if unexpected_parameters:
772778
raise qubes.exc.QubesException(
@@ -1114,7 +1120,9 @@ async def vm_feature_checkwithtpladminvm(self):
11141120

11151121
self.fire_event_for_permission()
11161122
try:
1117-
value = self.dest.features.check_with_template_and_adminvm(self.arg)
1123+
value = self.dest.features.check_with_template_and_adminvm(
1124+
self.arg
1125+
)
11181126
except KeyError:
11191127
raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
11201128
return value
@@ -1286,20 +1294,18 @@ async def create_disposable(self, untrusted_payload):
12861294
)
12871295

12881296
if self.dest.name == "dom0":
1289-
dispvm_template = self.src.default_dispvm
1297+
appvm = self.src.default_dispvm
12901298
else:
1291-
dispvm_template = self.dest
1299+
appvm = self.dest
12921300

1293-
self.fire_event_for_permission(dispvm_template=dispvm_template)
1301+
self.fire_event_for_permission(dispvm_template=appvm)
12941302

12951303
if preload_autostart:
1296-
await (
1297-
self.dest.fire_event_async("domain-preloaded-dispvm-autostart")
1298-
)
1304+
await appvm.fire_event_async("domain-preloaded-dispvm-autostart")
12991305
return
13001306

13011307
dispvm = await qubes.vm.dispvm.DispVM.from_appvm(
1302-
dispvm_template, preload=preload
1308+
appvm, preload=preload
13031309
)
13041310
# TODO: move this to extension (in race-free fashion, better than here)
13051311
dispvm.tags.add("created-by-" + str(self.src))
@@ -1646,7 +1652,10 @@ async def vm_device_set_required(self, endpoint, untrusted_payload):
16461652
self.app.save()
16471653

16481654
@qubes.api.method(
1649-
"admin.vm.device.denied.List", no_payload=True, scope="local", read=True
1655+
"admin.vm.device.denied.List",
1656+
no_payload=True,
1657+
scope="local",
1658+
read=True,
16501659
)
16511660
async def vm_device_denied_list(self):
16521661
"""
@@ -1754,7 +1763,10 @@ async def vm_firewall_set(self, untrusted_payload):
17541763
self.dest.firewall.save()
17551764

17561765
@qubes.api.method(
1757-
"admin.vm.firewall.Reload", no_payload=True, scope="local", execute=True
1766+
"admin.vm.firewall.Reload",
1767+
no_payload=True,
1768+
scope="local",
1769+
execute=True,
17581770
)
17591771
async def vm_firewall_reload(self):
17601772
self.enforce(not self.arg)
@@ -1955,7 +1967,9 @@ async def backup_info(self):
19551967
"Backup profile {} does not exist".format(self.arg)
19561968
)
19571969

1958-
backup = await self._load_backup_profile(self.arg, skip_passphrase=True)
1970+
backup = await self._load_backup_profile(
1971+
self.arg, skip_passphrase=True
1972+
)
19591973
return backup.get_backup_summary()
19601974

19611975
def _send_stats_single(

qubes/tests/api_admin.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3720,31 +3720,41 @@ def test_642_vm_create_disposable_not_allowed(self, storage_mock):
37203720
def test_643_vm_create_disposable_preload(self, mock_storage):
37213721
mock_storage.side_effect = self.dummy_coro
37223722
self.vm.template_for_dispvms = True
3723-
self.vm.features["preload-dispvm-max"] = 1
3723+
self.vm.features["preload-dispvm-max"] = "1"
37243724
self.app.default_dispvm = self.vm
37253725
retval = self.call_mgmt_func(
3726-
b"admin.vm.CreateDisposable", b"dom0", arg="preload"
3726+
b"admin.vm.CreateDisposable", b"dom0", arg=b"preload"
37273727
)
3728-
dispvm_preload = self.vm.features.get("dispvm-preload", "").split(" ")
3729-
self.assertIn(retval, dispvm_preload)
3728+
dispvm_preload = self.vm.get_feat_preload()
3729+
self.assertEqual(dispvm_preload, [retval])
3730+
dispvm = self.app.domains["".join(dispvm_preload)]
3731+
self.assertIn("created-by-dom0", dispvm.tags)
3732+
self.assertIn("disp-created-by-dom0", dispvm.tags)
37303733
mock_storage.assert_called_once_with()
37313734
self.assertTrue(self.app.save.called)
37323735

37333736
@unittest.mock.patch("qubes.storage.Storage.create")
37343737
def test_643_vm_create_disposable_preload_autostart(self, mock_storage):
37353738
mock_storage.side_effect = self.dummy_coro
37363739
self.vm.template_for_dispvms = True
3737-
self.vm.features["preload-dispvm-max"] = 1
3740+
self.app.domains[self.vm].fire_event = self.emitter.fire_event
3741+
self.vm.features["preload-dispvm-max"] = "1"
37383742
self.app.default_dispvm = self.vm
3743+
# TODO: how to mock start of a qube that we don't have its object?
3744+
# Looked as test_220_start(), but 'self.vm.start = corountine_mock'
3745+
# won't help if we don't have the dispvm object.
37393746
retval = self.call_mgmt_func(
3740-
b"admin.vm.CreateDisposable", b"dom0", arg="preload-autostart"
3747+
b"admin.vm.CreateDisposable", b"dom0", arg=b"preload-autostart"
37413748
)
3742-
# TODO: doesn't return any value, so how to check if it was preloaded?
3743-
#dispvm_preload = self.vm.features.get("preload-dispvm", "").split(" ")
3749+
self.assertEventFired(self.emitter, "domain-preloaded-dispvm-autostart")
3750+
dispvm_preload = self.vm.get_feat_preload()
3751+
self.assertEqual(len(dispvm_preload), 1)
37443752
self.assertIsNone(retval)
37453753
mock_storage.assert_called_once_with()
37463754
self.assertTrue(self.app.save.called)
37473755

3756+
## TODO: test return of the preloaded dispvm
3757+
37483758
def test_650_vm_device_set_mode_required(self):
37493759
assignment = DeviceAssignment(
37503760
VirtualDevice(Port(self.vm, "1234", "testclass"), device_id="bee"),

qubes/tests/integ/dispvm.py

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737

3838

3939
class TC_04_DispVM(qubes.tests.SystemTestCase):
40-
4140
def setUp(self):
4241
super(TC_04_DispVM, self).setUp()
4342
self.init_default_template()
@@ -151,7 +150,9 @@ def test_010_failed_start(self):
151150
)
152151
)
153152
timeout = 120
154-
self.loop.run_until_complete(asyncio.wait_for(p.communicate(), timeout))
153+
self.loop.run_until_complete(
154+
asyncio.wait_for(p.communicate(), timeout)
155+
)
155156
self.assertEqual(p.returncode, 126)
156157
self.assertEqual(self.startup_counter, 1)
157158

@@ -178,13 +179,14 @@ def test_011_failed_start_timeout(self):
178179
)
179180
)
180181
timeout = 120
181-
self.loop.run_until_complete(asyncio.wait_for(p.communicate(), timeout))
182+
self.loop.run_until_complete(
183+
asyncio.wait_for(p.communicate(), timeout)
184+
)
182185
self.assertEqual(p.returncode, 126)
183186
self.assertEqual(self.startup_counter, 1)
184187

185188

186189
class TC_20_DispVMMixin(object):
187-
188190
def setUp(self):
189191
super(TC_20_DispVMMixin, self).setUp()
190192
if "whonix-g" in self.template:
@@ -206,8 +208,7 @@ def tearDown(self):
206208
self.app.default_dispvm = None
207209
super(TC_20_DispVMMixin, self).tearDown()
208210

209-
# TODO: Test if run_service() marks the prelaoded DispVM as used.
210-
def test_010_simple_dvm_run(self):
211+
def test_010_dvm_run_simple(self):
211212
dispvm = self.loop.run_until_complete(
212213
qubes.vm.dispvm.DispVM.from_appvm(self.disp_base)
213214
)
@@ -222,6 +223,45 @@ def test_010_simple_dvm_run(self):
222223
finally:
223224
self.loop.run_until_complete(dispvm.cleanup())
224225

226+
def test_011_dvm_run_preload(self):
227+
dispvm = self.loop.run_until_complete(
228+
qubes.vm.dispvm.DispVM.from_appvm(self.disp_base, preload=True)
229+
)
230+
try:
231+
self.assertEqual(dispvm.get_feat_preload(), [dispvm.name])
232+
self.assertTrue(dispvm.features.get("internal", False))
233+
self.assertTrue(dispvm.is_paused)
234+
self.loop.run_until_complete(
235+
dispvm.run_service_for_stdio(
236+
"qubes.VMShell", input=b"echo test"
237+
)
238+
)
239+
self.assertFalse(self.disp_base.get_feat_preload())
240+
self.assertFalse(dispvm.features.get("internal", False))
241+
self.assertFalse(dispvm.is_paused)
242+
finally:
243+
self.loop.run_until_complete(dispvm.cleanup())
244+
245+
def test_011_dvm_run_preload_nogui(self):
246+
self.disp_base.features["gui"] = False
247+
dispvm = self.loop.run_until_complete(
248+
qubes.vm.dispvm.DispVM.from_appvm(self.disp_base, preload=True)
249+
)
250+
try:
251+
self.assertEqual(dispvm.get_feat_preload(), [dispvm.name])
252+
self.assertTrue(dispvm.features.get("internal", False))
253+
self.assertTrue(dispvm.is_paused)
254+
self.loop.run_until_complete(
255+
dispvm.run_service(
256+
"qubes.VMShell", input=b"echo test", wait=False
257+
)
258+
)
259+
self.assertFalse(self.disp_base.get_feat_preload())
260+
self.assertFalse(dispvm.features.get("internal", False))
261+
self.assertFalse(dispvm.is_paused)
262+
finally:
263+
self.loop.run_until_complete(dispvm.cleanup())
264+
225265
@unittest.skipUnless(
226266
spawn.find_executable("xdotool"), "xdotool not installed"
227267
)

qubes/tests/run.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#!/usr/bin/env python3
12
#
23
# The Qubes OS Project, https://www.qubes-os.org/
34
#
@@ -305,8 +306,8 @@ def test_6_unexpected_success(self):
305306

306307
parser = argparse.ArgumentParser(
307308
epilog="""When running only specific tests, write their names like in log,
308-
in format: MODULE+"/"+CLASS+"/"+FUNCTION. MODULE should omit initial
309-
"qubes.tests.". Example: basic/TC_00_Basic/test_000_create"""
309+
in format: MODULE+"/"+CLASS+"/"+FUNCTION.
310+
Example: qubes.tests.basic/TC_00_Basic/test_000_create"""
310311
)
311312

312313
parser.add_argument(

0 commit comments

Comments
 (0)