Skip to content

Commit 55bb4aa

Browse files
committed
Move qube actions to utils for reusability
1 parent 4fdae15 commit 55bb4aa

7 files changed

Lines changed: 234 additions & 159 deletions

File tree

qubesadmin/tests/tools/qvm_shutdown.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,7 @@ def test_015_wait_all_kill_timeout(self):
145145
b'sys-net class=AppVM state=Running\n' \
146146
b'some-vm class=AppVM state=Running\n' \
147147
b'other-vm class=AppVM state=Running\n'
148-
with self.assertRaisesRegex(SystemExit, '2'):
149-
qubesadmin.tools.qvm_shutdown.main(
150-
['--wait', '--all'], app=self.app)
148+
qubesadmin.tools.qvm_shutdown.main(['--wait', '--all'], app=self.app)
151149
self.assertAllCalled()
152150

153151
def test_016_all_exclude_noforce(self):
@@ -278,10 +276,10 @@ def test_011_wait_retry_once_fail(self):
278276
b'2\x00QubesVMInUseError\x00\x00Denied as qube is in use\x00',
279277
b'2\x00QubesVMInUseError\x00\x00Denied as qube is in use\x00',
280278
]
281-
with self.assertRaises(SystemExit) as cm:
279+
with self.assertRaises(SystemExit) as cmg:
282280
qubesadmin.tools.qvm_shutdown.main(
283281
['--wait', 'some-vm', 'other-vm', 'another-vm'], app=self.app)
284-
self.assertEqual(cm.exception.code, 3)
282+
self.assertEqual(cmg.exception.code, 3)
285283
self.assertAllCalled()
286284

287285
def test_011_wait_retry_as_many_as_in_use(self):
@@ -320,9 +318,10 @@ def test_013_wait_all_shutdown_fail(self):
320318
self.app.expected_calls[
321319
('some-vm', 'admin.vm.Shutdown', 'wait', None)] = \
322320
b'2\x00QubesException\x00\x00Shutdown refused\x00'
323-
with self.assertRaises(SystemExit):
321+
with self.assertRaises(SystemExit) as cmg:
324322
qubesadmin.tools.qvm_shutdown.main(
325323
['--wait', 'some-vm'], app=self.app)
324+
self.assertEqual(cmg.exception.code, 1)
326325
self.assertAllCalled()
327326

328327
def test_014_timeout_deprecated_means_wait(self):
@@ -338,9 +337,10 @@ def test_014_timeout_deprecated_means_wait(self):
338337
self.app.expected_calls[
339338
('some-vm', 'admin.vm.Kill', None, None)] = \
340339
b'2\x00QubesException\x00\x00Kill failed\x00'
341-
with self.assertRaises(SystemExit):
340+
with self.assertRaises(SystemExit) as cmg:
342341
qubesadmin.tools.qvm_shutdown.main(
343342
['--timeout=3', 'some-vm'], app=self.app)
343+
self.assertEqual(cmg.exception.code, 1)
344344
self.assertAllCalled()
345345

346346
def test_016_wait_kill_exception(self):
@@ -357,7 +357,8 @@ def test_016_wait_kill_exception(self):
357357
self.app.expected_calls[
358358
('some-vm', 'admin.vm.Kill', None, None)] = \
359359
b'2\x00QubesException\x00\x00Kill failed\x00'
360-
with self.assertRaises(SystemExit):
360+
with self.assertRaises(SystemExit) as cmg:
361361
qubesadmin.tools.qvm_shutdown.main(
362362
['--wait', 'some-vm'], app=self.app)
363+
self.assertEqual(cmg.exception.code, 1)
363364
self.assertAllCalled()

qubesadmin/tools/qvm_kill.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,12 @@
3737
async def run_async(args=None, app=None):
3838
# pylint: disable=missing-docstring
3939
args = parser.parse_args(args, app=app)
40-
remnants = await qubesadmin.tools.qvm_shutdown.kill(domains=args.domains)
41-
if not remnants:
40+
failed = await qubesadmin.utils.kill(domains=args.domains)
41+
if not failed:
4242
return 0
43-
parser.error_runtime(
44-
"Failed to kill: {}".format(
45-
", ".join(qube.name for qube in remnants)
46-
),
47-
len(remnants)
48-
)
43+
for qube, exc in failed.items():
44+
parser.print_error("Failed to kill: {}: {}".format(qube, exc))
45+
raise SystemExit(len(failed))
4946

5047

5148
def main(args=None, app=None):

qubesadmin/tools/qvm_pause.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
'''qvm-pause - Pause a domain'''
2222

23+
import asyncio
2324
import sys
2425
import qubesadmin
2526

@@ -45,15 +46,16 @@ def main(args=None, app=None):
4546

4647
args = parser.parse_args(args, app=app)
4748
exit_code = 0
48-
for domain in args.domains:
49-
try:
50-
if args.suspend:
51-
domain.suspend()
52-
else:
53-
domain.pause()
54-
except (IOError, OSError, qubesadmin.exc.QubesException) as e:
55-
exit_code = 1
56-
parser.print_error(str(e))
49+
if args.suspend:
50+
action = "suspend"
51+
failed = asyncio.run(qubesadmin.utils.suspend(domains=args.domains))
52+
else:
53+
action = "pause"
54+
failed = asyncio.run(qubesadmin.utils.pause(domains=args.domains))
55+
if failed:
56+
exit_code = 1
57+
for qube, exc in failed.items():
58+
parser.print_error("Failed to {}: {}: {}".format(action, qube, exc))
5759

5860
return exit_code
5961

qubesadmin/tools/qvm_shutdown.py

Lines changed: 44 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -67,67 +67,27 @@
6767
)
6868

6969

70-
async def shutdown(args, domains: list[qubesadmin.vm.QubesVM]):
71-
"""
72-
Asynchronously shutdown qubes and return qubes that failed to shutdown
73-
because and the client can't handle, as well as qubes that were in use
74-
while --force was not provided, as well as timed out.
75-
"""
76-
# pylint: disable=missing-docstring
77-
unhandled, used, timedout = [], [], []
78-
tasks = [
79-
asyncio.to_thread(qube.shutdown, force=args.force, wait=args.wait)
80-
for qube in domains
81-
]
82-
results = await asyncio.gather(*tasks, return_exceptions=True)
83-
for qube, res in zip(domains, results):
84-
if not isinstance(res, BaseException):
85-
qube.log.info("Shutdown succeeded")
86-
continue
87-
try:
88-
raise res
89-
except qubesadmin.exc.QubesVMNotStartedError:
90-
pass
91-
except qubesadmin.exc.QubesVMInUseError as e:
92-
if args.wait:
93-
qube.log.error("Shutdown error: {}".format(e))
94-
else:
95-
qube.log.error("Shutdown error: (try --force): {}".format(e))
96-
used.append(qube)
97-
except qubesadmin.exc.QubesVMShutdownTimeoutError as e:
98-
if args.wait:
99-
qube.log.error("Shutdown error: {}".format(e))
100-
else:
101-
qube.log.error("Shutdown error: (try qvm-kill): {}".format(e))
102-
timedout.append(qube)
103-
except qubesadmin.exc.QubesException as e:
104-
qube.log.error("Shutdown error: {}".format(e))
105-
unhandled.append(qube)
70+
async def shutdown(domains, **shutdown_kwargs):
71+
# pylint: disable=missing-function-docstring
72+
failed = await qubesadmin.utils.shutdown(domains=domains, **shutdown_kwargs)
73+
used = {
74+
qube: exc
75+
for qube, exc in failed.items()
76+
if isinstance(exc, qubesadmin.exc.QubesVMInUseError)
77+
}
78+
timedout = {
79+
qube: exc
80+
for qube, exc in failed.items()
81+
if isinstance(exc, qubesadmin.exc.QubesVMShutdownTimeoutError)
82+
}
83+
unhandled = {
84+
qube: exc
85+
for qube, exc in failed.items()
86+
if qube not in used and qube not in timedout
87+
}
10688
return unhandled, used, timedout
10789

10890

109-
async def kill(domains: list[qubesadmin.vm.QubesVM]):
110-
"""
111-
Asynchronously kill qubes and return qubes that failed to shutdown.
112-
"""
113-
# pylint: disable=missing-docstring
114-
unhandled = domains.copy()
115-
tasks = [asyncio.to_thread(qube.kill) for qube in domains]
116-
results = await asyncio.gather(*tasks, return_exceptions=True)
117-
for qube, res in zip(domains, results):
118-
if not isinstance(res, BaseException):
119-
qube.log.info("Killing succeeded")
120-
unhandled.remove(qube)
121-
continue
122-
try:
123-
raise res
124-
except qubesadmin.exc.QubesVMNotStartedError:
125-
unhandled.remove(qube)
126-
except qubesadmin.exc.QubesException as e:
127-
qube.log.error("Kill error: {}".format(e))
128-
return unhandled
129-
130-
13191
async def run_async(args=None, app=None):
13292
# pylint: disable=missing-docstring
13393
args = parser.parse_args(args, app=app)
@@ -140,10 +100,16 @@ async def run_async(args=None, app=None):
140100
)
141101
args.wait = True
142102
args.force = args.force or (args.all_domains and not args.exclude)
143-
144-
unhandled, used, timedout = await shutdown(args=args, domains=args.domains)
145-
unhandled_retry = []
146-
timedout_retry = []
103+
shutdown_kwargs = {
104+
"force": args.force,
105+
"wait": args.wait,
106+
}
107+
108+
unhandled, used, timedout = await shutdown(
109+
domains=args.domains, **shutdown_kwargs
110+
)
111+
unhandled_retry = {}
112+
timedout_retry = {}
147113
if used:
148114
old_failed = unhandled, used, timedout
149115
parser.print_error(
@@ -157,7 +123,7 @@ async def run_async(args=None, app=None):
157123
", ".join(qube.name for qube in used)
158124
)
159125
)
160-
failed = await shutdown(args=args, domains=used)
126+
failed = await shutdown(domains=used, **shutdown_kwargs)
161127
unhandled_retry, used, timedout_retry = failed
162128
if not failed:
163129
break
@@ -168,8 +134,8 @@ async def run_async(args=None, app=None):
168134
break
169135
old_failed = failed
170136

171-
unhandled.extend(qube for qube in unhandled_retry if qube not in unhandled)
172-
timedout.extend(qube for qube in timedout_retry if qube not in timedout)
137+
unhandled.update(unhandled_retry)
138+
timedout.update(timedout_retry)
173139

174140
# Retry timed out only once, as it can take a long time, 60s by default.
175141
if timedout:
@@ -178,38 +144,28 @@ async def run_async(args=None, app=None):
178144
", ".join(qube.name for qube in timedout)
179145
)
180146
)
181-
unhandled, used, timedout = await shutdown(args=args, domains=timedout)
147+
unhandled, used, timedout = await shutdown(
148+
domains=timedout, **shutdown_kwargs
149+
)
182150

183151
if timedout:
184152
parser.print_error(
185153
"Killing timed out qubes: {}".format(
186154
", ".join(qube.name for qube in timedout)
187155
)
188156
)
189-
unhandled = await kill(domains=timedout)
157+
timedout = await qubesadmin.utils.kill(domains=timedout)
190158

191-
if not unhandled and not used and not timedout:
192-
return
193-
194-
if unhandled:
195-
parser.print_error(
196-
"Failed to shut down for unknown reason: {}".format(
197-
", ".join(qube.name for qube in unhandled)
198-
)
199-
)
200-
if used:
201-
parser.print_error(
202-
"Failed to shut down because it's in use: {}".format(
203-
", ".join(qube.name for qube in used)
159+
for item in [unhandled, used, timedout]:
160+
for qube, exc in item.items():
161+
parser.print_error(
162+
"Failed to shut down: {}: {}".format(qube.name, str(exc))
204163
)
205-
)
206-
if timedout:
207-
parser.print_error(
208-
"Failed to shut down because of time out: {}".format(
209-
", ".join(qube.name for qube in timedout)
210-
)
211-
)
212-
raise SystemExit(len(unhandled + used + timedout))
164+
165+
exit_code = len(unhandled) + len(used) + len(timedout)
166+
if exit_code == 0:
167+
return
168+
raise SystemExit(exit_code)
213169

214170

215171
def main(args=None, app=None):

0 commit comments

Comments
 (0)