Skip to content

Commit 396e84f

Browse files
committed
Make qvm_shutdown reusable for other tools
It was necessary to make alternative main async functions, as Python is picky about running async code when mixing async and sync functions.
1 parent 0c56c3a commit 396e84f

6 files changed

Lines changed: 119 additions & 119 deletions

File tree

qubesadmin/tests/tools/qvm_kill.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
# pylint: disable=missing-docstring
2222

23+
import asyncio
24+
2325
import qubesadmin.tests
2426
import qubesadmin.tests.tools
2527
import qubesadmin.tools.qvm_kill
@@ -35,6 +37,20 @@ def test_000_with_vm(self):
3537
qubesadmin.tools.qvm_kill.main(['some-vm'], app=self.app)
3638
self.assertAllCalled()
3739

40+
def test_000_with_vm_async(self):
41+
loop = asyncio.new_event_loop()
42+
asyncio.set_event_loop(loop)
43+
44+
self.app.expected_calls[
45+
('dom0', 'admin.vm.List', None, None)] = \
46+
b'0\x00some-vm class=AppVM state=Running\n'
47+
self.app.expected_calls[
48+
('some-vm', 'admin.vm.Kill', None, None)] = b'0\x00'
49+
loop.run_until_complete(
50+
qubesadmin.tools.qvm_kill.main_async(['some-vm'], app=self.app)
51+
)
52+
self.assertAllCalled()
53+
3854
def test_001_missing_vm(self):
3955
with self.assertRaises(SystemExit):
4056
with qubesadmin.tests.tools.StderrBuffer() as stderr:
@@ -74,9 +90,9 @@ def test_004_other_error(self):
7490
('dom0', 'admin.vm.List', None, None)] = \
7591
b'0\x00some-vm class=AppVM state=Running\n'
7692
with qubesadmin.tests.tools.StderrBuffer() as stderr:
77-
self.assertEqual(
78-
qubesadmin.tools.qvm_kill.main(['some-vm'], app=self.app),
79-
1)
93+
with self.assertRaises(SystemExit):
94+
self.assertEqual(
95+
qubesadmin.tools.qvm_kill.main(['some-vm'], app=self.app),
96+
1)
8097
self.assertAllCalled()
81-
self.assertIn("Failed to kill 'some-vm': Error message",
82-
stderr.getvalue())
98+
self.assertIn("Failed to kill: some-vm", stderr.getvalue())

qubesadmin/tests/tools/qvm_shutdown.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,24 @@ def test_010_wait(self):
9696
qubesadmin.tools.qvm_shutdown.main(['--wait', 'some-vm'], app=self.app)
9797
self.assertAllCalled()
9898

99+
def test_011_wait_async(self):
100+
'''test --wait option with async main'''
101+
loop = asyncio.new_event_loop()
102+
asyncio.set_event_loop(loop)
103+
104+
self.app.expected_calls[
105+
('some-vm', 'admin.vm.Shutdown', 'wait', None)] = \
106+
b'0\x00'
107+
self.app.expected_calls[
108+
('dom0', 'admin.vm.List', None, None)] = \
109+
b'0\x00some-vm class=AppVM state=Running\n'
110+
loop.run_until_complete(
111+
qubesadmin.tools.qvm_shutdown.main_async(
112+
['--wait', 'some-vm'], app=self.app
113+
)
114+
)
115+
self.assertAllCalled()
116+
99117
def test_012_wait_all(self):
100118
'''test --wait option, with multiple VMs'''
101119
loop = asyncio.new_event_loop()
@@ -198,6 +216,21 @@ def test_006_dry_run(self):
198216
['--dry-run', 'some-vm'], app=self.app)
199217
self.assertAllCalled()
200218

219+
def test_006_dry_run_async(self):
220+
'''test --dry-run skips shutdown calls'''
221+
loop = asyncio.new_event_loop()
222+
asyncio.set_event_loop(loop)
223+
224+
self.app.expected_calls[
225+
('dom0', 'admin.vm.List', None, None)] = \
226+
b'0\x00some-vm class=AppVM state=Running\n'
227+
loop.run_until_complete(
228+
qubesadmin.tools.qvm_shutdown.main_async(
229+
['--dry-run', 'some-vm'], app=self.app
230+
)
231+
)
232+
self.assertAllCalled()
233+
201234
def test_011_wait_retry(self):
202235
'''test --wait retries VMs whose shutdown request failed'''
203236
loop = asyncio.new_event_loop()

qubesadmin/tests/tools/qvm_template_postprocess.py

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -402,17 +402,6 @@ def test_020_post_install(self, mock_import_root_img,
402402
self.app.expected_calls[
403403
('test-vm', 'admin.vm.Shutdown', None, None)] = b'0\0'
404404

405-
if qubesadmin.tools.qvm_template_postprocess.have_events:
406-
patch_domain_shutdown = mock.patch(
407-
'qubesadmin.events.utils.wait_for_domain_shutdown')
408-
self.addCleanup(patch_domain_shutdown.stop)
409-
mock_domain_shutdown = patch_domain_shutdown.start()
410-
mock_domain_shutdown.side_effect = self.wait_for_shutdown
411-
else:
412-
self.app.expected_calls[
413-
('test-vm', 'admin.vm.List', None, None)] = \
414-
b'0\0test-vm class=TemplateVM state=Halted\n'
415-
416405
asyncio.set_event_loop(asyncio.new_event_loop())
417406
ret = qubesadmin.tools.qvm_template_postprocess.main([
418407
'--really', 'post-install', 'test-vm', self.source_dir.name],
@@ -424,9 +413,6 @@ def test_020_post_install(self, mock_import_root_img,
424413
'test-vm'], self.source_dir.name)
425414
mock_import_appmenus.assert_called_once_with(self.app.domains[
426415
'test-vm'], self.source_dir.name, skip_generate=True)
427-
if qubesadmin.tools.qvm_template_postprocess.have_events:
428-
mock_domain_shutdown.assert_called_once_with([self.app.domains[
429-
'test-vm']])
430416
self.assertEqual(self.app.service_calls, [
431417
('test-vm', 'qubes.PostInstall', {
432418
'stdin': subprocess.PIPE,
@@ -460,17 +446,6 @@ def test_021_post_install_reinstall(self, mock_reset_private_img,
460446
self.app.expected_calls[
461447
('test-vm', 'admin.vm.Shutdown', None, None)] = b'0\0'
462448

463-
if qubesadmin.tools.qvm_template_postprocess.have_events:
464-
patch_domain_shutdown = mock.patch(
465-
'qubesadmin.events.utils.wait_for_domain_shutdown')
466-
self.addCleanup(patch_domain_shutdown.stop)
467-
mock_domain_shutdown = patch_domain_shutdown.start()
468-
mock_domain_shutdown.side_effect = self.wait_for_shutdown
469-
else:
470-
self.app.expected_calls[
471-
('test-vm', 'admin.vm.List', None, None)] = \
472-
b'0\0test-vm class=TemplateVM state=Halted\n'
473-
474449
asyncio.set_event_loop(asyncio.new_event_loop())
475450
ret = qubesadmin.tools.qvm_template_postprocess.main([
476451
'--really', 'post-install', 'test-vm', self.source_dir.name],
@@ -483,9 +458,6 @@ def test_021_post_install_reinstall(self, mock_reset_private_img,
483458
'test-vm'])
484459
mock_import_appmenus.assert_called_once_with(self.app.domains[
485460
'test-vm'], self.source_dir.name, skip_generate=True)
486-
if qubesadmin.tools.qvm_template_postprocess.have_events:
487-
mock_domain_shutdown.assert_called_once_with([self.app.domains[
488-
'test-vm']])
489461
self.assertEqual(self.app.service_calls, [
490462
('test-vm', 'qubes.PostInstall', {
491463
'stdin': subprocess.PIPE,
@@ -508,13 +480,6 @@ def test_022_post_install_skip_start(self, mock_reset_private_img,
508480
= b'0\0'
509481
self.app.add_new_vm = mock.Mock()
510482

511-
if qubesadmin.tools.qvm_template_postprocess.have_events:
512-
patch_domain_shutdown = mock.patch(
513-
'qubesadmin.events.utils.wait_for_domain_shutdown')
514-
self.addCleanup(patch_domain_shutdown.stop)
515-
mock_domain_shutdown = patch_domain_shutdown.start()
516-
mock_domain_shutdown.side_effect = self.wait_for_shutdown
517-
518483
asyncio.set_event_loop(asyncio.new_event_loop())
519484
ret = qubesadmin.tools.qvm_template_postprocess.main([
520485
'--really', '--skip-start', 'post-install', 'test-vm',
@@ -528,8 +493,6 @@ def test_022_post_install_skip_start(self, mock_reset_private_img,
528493
'test-vm'])
529494
mock_import_appmenus.assert_called_once_with(self.app.domains[
530495
'test-vm'], self.source_dir.name, skip_generate=False)
531-
if qubesadmin.tools.qvm_template_postprocess.have_events:
532-
self.assertFalse(mock_domain_shutdown.called)
533496
self.assertEqual(self.app.service_calls, [])
534497
self.assertAllCalled()
535498

qubesadmin/tools/qvm_kill.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,36 +21,44 @@
2121
'''Immediately terminate a qube without a graceful shutdown sequence.'''
2222

2323

24+
import asyncio
2425
import sys
26+
2527
import qubesadmin.exc
2628
import qubesadmin.tools
29+
import qubesadmin.tools.qvm_shutdown
2730

2831
parser = qubesadmin.tools.QubesArgumentParser(
2932
description='immediately terminate a qube without a graceful shutdown'
3033
' sequence',
3134
vmname_nargs='+')
3235

33-
def main(args=None, app=None):
34-
'''Main routine of :program:`qvm-kill`.
3536

36-
:param list args: Optional arguments to override those delivered from \
37-
command line.
38-
'''
37+
async def run_async(domains=None):
38+
# pylint: disable=missing-docstring
39+
remnants = await qubesadmin.tools.qvm_shutdown.kill(domains=domains)
40+
if not remnants:
41+
return 0
42+
parser.error_runtime(
43+
"Failed to kill: {}".format(
44+
", ".join(qube.name for qube in remnants)
45+
),
46+
len(remnants)
47+
)
48+
3949

50+
async def main_async(args=None, app=None):
51+
# pylint: disable=missing-docstring
4052
args = parser.parse_args(args, app=app)
53+
return await run_async(domains=args.domains)
4154

42-
exit_code = 0
43-
for domain in args.domains:
44-
try:
45-
domain.kill()
46-
except qubesadmin.exc.QubesVMNotStartedError:
47-
pass
48-
except (IOError, OSError, qubesadmin.exc.QubesException) as e:
49-
exit_code = 1
50-
parser.print_error("Failed to kill '{}': {}".format(
51-
domain.name, e))
52-
53-
return exit_code
55+
56+
def main(args=None, app=None):
57+
# pylint: disable=missing-docstring
58+
args = parser.parse_args(args, app=app)
59+
loop = asyncio.new_event_loop()
60+
asyncio.set_event_loop(loop)
61+
return loop.run_until_complete(run_async(domains=args.domains))
5462

5563

5664
if __name__ == '__main__':

qubesadmin/tools/qvm_shutdown.py

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,7 @@
7070
)
7171

7272

73-
def shutdown(
74-
args,
75-
loop: asyncio.AbstractEventLoop,
76-
domains: list[qubesadmin.vm.QubesVM],
77-
force: bool,
78-
):
73+
async def shutdown(args, domains: list[qubesadmin.vm.QubesVM]):
7974
"""
8075
Asynchronously shutdown qubes and return qubes that failed to shutdown as
8176
well as failed with a timeout.
@@ -84,14 +79,12 @@ def shutdown(
8479
remnants, timedout = [], []
8580
tasks = [
8681
asyncio.wait_for(
87-
async_thread(qube.shutdown, force=force, wait=args.wait),
82+
async_thread(qube.shutdown, force=args.force, wait=args.wait),
8883
timeout=args.timeout,
8984
)
9085
for qube in domains
9186
]
92-
results = loop.run_until_complete(
93-
asyncio.gather(*tasks, return_exceptions=True)
94-
)
87+
results = await asyncio.gather(*tasks, return_exceptions=True)
9588
for qube, res in zip(domains, results):
9689
if not isinstance(res, BaseException):
9790
qube.log.info("Shutdown succeeded")
@@ -120,16 +113,14 @@ def shutdown(
120113
return remnants, timedout
121114

122115

123-
def kill(loop: asyncio.AbstractEventLoop, domains: list[qubesadmin.vm.QubesVM]):
116+
async def kill(domains: list[qubesadmin.vm.QubesVM]):
124117
"""
125118
Asynchronously kill qubes and return qubes that failed to shutdown.
126119
"""
127120
# pylint: disable=missing-docstring
128121
remnants = domains.copy()
129122
tasks = [async_thread(qube.kill) for qube in domains]
130-
results = loop.run_until_complete(
131-
asyncio.gather(*tasks, return_exceptions=True)
132-
)
123+
results = await asyncio.gather(*tasks, return_exceptions=True)
133124
for qube, res in zip(domains, results):
134125
if not isinstance(res, BaseException):
135126
qube.log.info("Killing succeeded")
@@ -144,36 +135,24 @@ def kill(loop: asyncio.AbstractEventLoop, domains: list[qubesadmin.vm.QubesVM]):
144135
return remnants
145136

146137

147-
def main(args=None, app=None):
138+
async def run_async(args=None, domains=None):
148139
# pylint: disable=missing-docstring
149-
args = parser.parse_args(args, app=app)
150-
force = args.force or (args.all_domains and not args.exclude)
151-
if args.dry_run:
152-
return
153-
domains = set(args.domains)
154-
155-
loop = asyncio.new_event_loop()
156-
asyncio.set_event_loop(loop)
157-
remnants, timedout = shutdown(
158-
args=args, force=force, loop=loop, domains=domains
159-
)
140+
remnants, timedout = await shutdown(args=args, domains=domains)
160141
if timedout:
161142
args.app.log.info(
162143
"Retrying shutdown of qubes that timed out: {}".format(
163144
", ".join(qube.name for qube in timedout)
164145
)
165146
)
166-
remnants, timedout = shutdown(
167-
args=args, force=force, loop=loop, domains=timedout
168-
)
147+
remnants, timedout = await shutdown(args=args, domains=timedout)
169148

170149
if timedout:
171150
args.app.log.info(
172151
"Killing timed out qubes: {}".format(
173152
", ".join(qube.name for qube in timedout)
174153
)
175154
)
176-
remnants = kill(loop=loop, domains=timedout)
155+
remnants = await kill(domains=timedout)
177156

178157
if not remnants:
179158
return
@@ -185,5 +164,31 @@ def main(args=None, app=None):
185164
)
186165

187166

167+
def parse_common(args=None, app=None):
168+
# pylint: disable=missing-docstring
169+
args = parser.parse_args(args, app=app)
170+
args.force = args.force or (args.all_domains and not args.exclude)
171+
domains = set(args.domains)
172+
return args, domains
173+
174+
175+
async def main_async(args=None, app=None):
176+
# pylint: disable=missing-docstring
177+
args, domains = parse_common(args=args, app=app)
178+
if args.dry_run:
179+
return
180+
await run_async(args=args, domains=domains)
181+
182+
183+
def main(args=None, app=None):
184+
# pylint: disable=missing-docstring
185+
args, domains = parse_common(args=args, app=app)
186+
if args.dry_run:
187+
return
188+
loop = asyncio.new_event_loop()
189+
asyncio.set_event_loop(loop)
190+
loop.run_until_complete(run_async(args=args, domains=domains))
191+
192+
188193
if __name__ == "__main__":
189194
sys.exit(main())

0 commit comments

Comments
 (0)