qvm-shutdown: report errors, don't crash on DispVMs

qvm-shutdown with the --wait option checks if the machine
state is 'Halted', but a disposable VM is usually deleted by
the time of the final check, resulting in a non-zero exit code.

This change handles properly disposable VMs, and makes sure
we always output an error message when finishing with a non-zero
exit code.

Fixes QubesOS/qubes-issues#5245.
This commit is contained in:
Pawel Marczewski 2020-01-16 12:31:55 +01:00
parent de1969150d
commit 405464a67e
No known key found for this signature in database
GPG Key ID: DE42EE9B14F96465
3 changed files with 25 additions and 7 deletions

View File

@ -293,6 +293,7 @@ class TC_00_qvm_shutdown(qubesadmin.tests.QubesTestCase):
self.app.expected_calls[
('sys-net', 'admin.vm.List', None, None)] = \
b'0\x00sys-net class=AppVM state=Halted\n'
qubesadmin.tools.qvm_shutdown.main(
['--wait', '--all', '--timeout=1'], app=self.app)
with self.assertRaisesRegexp(SystemExit, '2'):
qubesadmin.tools.qvm_shutdown.main(
['--wait', '--all', '--timeout=1'], app=self.app)
self.assertAllCalled()

View File

@ -408,12 +408,12 @@ class QubesArgumentParser(argparse.ArgumentParser):
return namespace
def error_runtime(self, message):
def error_runtime(self, message, exit_code=1):
'''Runtime error, without showing usage.
:param str message: message to show
'''
self.exit(1, '{}: error: {}\n'.format(self.prog, message))
self.exit(exit_code, '{}: error: {}\n'.format(self.prog, message))
@staticmethod

View File

@ -73,7 +73,12 @@ def main(args=None, app=None): # pylint: disable=missing-docstring
else:
remaining_domains.add(vm)
if not args.wait:
return len(remaining_domains)
if remaining_domains:
parser.error_runtime(
'Failed to shut down: ' +
', '.join(vm.name for vm in remaining_domains),
len(remaining_domains))
return
this_round_domains.difference_update(remaining_domains)
if not this_round_domains:
# no VM shutdown request succeed, no sense to try again
@ -122,8 +127,20 @@ def main(args=None, app=None): # pylint: disable=missing-docstring
if args.wait:
if have_events:
loop.close()
return len([vm for vm in args.domains
if vm.get_power_state() != 'Halted'])
failed = []
for vm in args.domains:
power_state = vm.get_power_state()
# DispVM might have been deleted before we check them,
# so NA is acceptable.
if not (power_state == 'Halted' or
(vm.klass == 'DispVM' and power_state == 'NA')):
failed.append(vm)
if failed:
parser.error_runtime(
'Failed to shut down: ' +
', '.join(vm.name for vm in failed),
len(failed))
if __name__ == '__main__':
sys.exit(main())