Skip to content

Commit 57d9023

Browse files
committed
Add --restart to qvm-shutdown
1 parent 2965dd1 commit 57d9023

File tree

4 files changed

+331
-1
lines changed

4 files changed

+331
-1
lines changed

doc/manpages/qvm-restart.rst

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
.. program:: qvm-restart
2+
3+
:program:`qvm-restart` -- Restart selected or currently running qubes
4+
=====================================================================
5+
6+
Synopsis
7+
--------
8+
9+
:command:`qvm-restart` [-h] [--verbose] [--quiet] [--all] [--exclude *EXCLUDE*] [--timeout *TIMEOUT*] [*VMNAME*]
10+
11+
Options
12+
-------
13+
14+
.. option:: --help, -h
15+
16+
show the help message and exit
17+
18+
.. option:: --verbose, -v
19+
20+
increase verbosity
21+
22+
.. option:: --quiet, -q
23+
24+
decrease verbosity
25+
26+
.. option:: --all
27+
28+
perform the action on all running qubes except dom0 & true DispVMs
29+
30+
.. option:: --exclude=EXCLUDE
31+
32+
exclude the qube from :option:`--all`
33+
34+
.. option:: --timeout
35+
36+
timeout after which domains are killed. The default is decided by global
37+
`default_shutdown_timeout` property and qube `shutdown_timeout` property
38+
39+
.. option:: --version
40+
41+
Show program's version number and exit
42+
43+
44+
Authors
45+
-------
46+
47+
| Joanna Rutkowska <joanna at invisiblethingslab dot com>
48+
| Rafal Wojtczuk <rafal at invisiblethingslab dot com>
49+
| Marek Marczykowski <marmarek at invisiblethingslab dot com>
50+
| Wojtek Porczyk <woju at invisiblethingslab dot com>
51+
52+
| For complete author list see: https://github.com/QubesOS/qubes-core-admin-client.git
53+
54+
.. vim: ts=3 sw=3 et tw=80

doc/manpages/qvm-shutdown.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
Synopsis
77
--------
88

9-
:command:`qvm-shutdown` [-h] [--verbose] [--quiet] [--all] [--exclude *EXCLUDE*] [--wait] [--timeout *TIMEOUT*] [*VMNAME*]
9+
:command:`qvm-shutdown` [-h] [--verbose] [--quiet] [--all] [--force] [--exclude *EXCLUDE*] [--wait] [--timeout *TIMEOUT*] [*VMNAME*]
1010

1111
Options
1212
-------
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# -*- encoding: utf-8 -*-
2+
#
3+
# The Qubes OS Project, http://www.qubes-os.org
4+
#
5+
# Copyright (C) 2025 Marek Marczykowski-Górecki
6+
7+
#
8+
# This program is free software; you can redistribute it and/or modify
9+
# it under the terms of the GNU Lesser General Public License as published by
10+
# the Free Software Foundation; either version 2.1 of the License, or
11+
# (at your option) any later version.
12+
#
13+
# This program is distributed in the hope that it will be useful,
14+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
# GNU Lesser General Public License for more details.
17+
#
18+
# You should have received a copy of the GNU Lesser General Public License along
19+
# with this program; if not, see <http://www.gnu.org/licenses/>.
20+
21+
# pylint: disable=missing-docstring
22+
23+
import asyncio
24+
import unittest.mock
25+
26+
import qubesadmin.tests
27+
import qubesadmin.tests.tools
28+
import qubesadmin.tools.qvm_restart
29+
30+
31+
class TC_00_qvm_restart(qubesadmin.tests.QubesTestCase):
32+
@unittest.skipUnless(
33+
qubesadmin.tools.qvm_restart.have_events, "Events not present"
34+
)
35+
def test_000_restart_running(self):
36+
"""Restarting just one already running qube"""
37+
loop = asyncio.new_event_loop()
38+
asyncio.set_event_loop(loop)
39+
40+
mock_events = unittest.mock.AsyncMock()
41+
patch = unittest.mock.patch(
42+
"qubesadmin.events.EventsDispatcher._get_events_reader", mock_events
43+
)
44+
patch.start()
45+
self.addCleanup(patch.stop)
46+
mock_events.side_effect = qubesadmin.tests.tools.MockEventsReader(
47+
[
48+
b"1\0\0connection-established\0\0",
49+
b"1\0some-vm\0domain-shutdown\0\0",
50+
]
51+
)
52+
53+
self.app.expected_calls[("dom0", "admin.vm.List", None, None)] = (
54+
b"0\x00some-vm class=AppVM state=Running\n"
55+
)
56+
self.app.expected_calls[
57+
("some-vm", "admin.vm.Shutdown", "force", None)
58+
] = b"0\x00"
59+
self.app.expected_calls[
60+
("some-vm", "admin.vm.CurrentState", None, None)
61+
] = (
62+
[b"0\x00power_state=Running"]
63+
+ [b"0\x00power_state=Halted"]
64+
+ [b"0\x00power_state=Halted"]
65+
)
66+
self.app.expected_calls[("some-vm", "admin.vm.Start", None, None)] = (
67+
b"0\x00"
68+
)
69+
qubesadmin.tools.qvm_restart.main(["some-vm"], app=self.app)
70+
self.assertAllCalled()
71+
72+
@unittest.skipUnless(
73+
qubesadmin.tools.qvm_restart.have_events, "Events not present"
74+
)
75+
def test_001_restart_halted(self):
76+
"""Trying restart on a already halted qube"""
77+
loop = asyncio.new_event_loop()
78+
asyncio.set_event_loop(loop)
79+
80+
mock_events = unittest.mock.AsyncMock()
81+
patch = unittest.mock.patch(
82+
"qubesadmin.events.EventsDispatcher._get_events_reader", mock_events
83+
)
84+
patch.start()
85+
self.addCleanup(patch.stop)
86+
mock_events.side_effect = qubesadmin.tests.tools.MockEventsReader(
87+
[
88+
b"1\0\0connection-established\0\0",
89+
b"1\0some-vm\0domain-shutdown\0\0",
90+
]
91+
)
92+
93+
self.app.expected_calls[("dom0", "admin.vm.List", None, None)] = (
94+
b"0\x00some-vm class=AppVM state=Halted\n"
95+
)
96+
self.app.expected_calls[
97+
("some-vm", "admin.vm.Shutdown", "force", None)
98+
] = b"0\x00"
99+
self.app.expected_calls[
100+
("some-vm", "admin.vm.CurrentState", None, None)
101+
] = (
102+
[b"0\x00power_state=Halted"]
103+
+ [b"0\x00power_state=Halted"]
104+
+ [b"0\x00power_state=Halted"]
105+
)
106+
self.app.expected_calls[("some-vm", "admin.vm.Start", None, None)] = (
107+
b"0\x00"
108+
)
109+
qubesadmin.tools.qvm_restart.main(["some-vm"], app=self.app)
110+
self.assertAllCalled()
111+
112+
@unittest.skipUnless(
113+
qubesadmin.tools.qvm_restart.have_events, "Events not present"
114+
)
115+
def test_002_restart_all(self):
116+
"""Restarting all running qubes (and skipping true DispVMs)"""
117+
loop = asyncio.new_event_loop()
118+
asyncio.set_event_loop(loop)
119+
120+
mock_events = unittest.mock.AsyncMock()
121+
patch = unittest.mock.patch(
122+
"qubesadmin.events.EventsDispatcher._get_events_reader", mock_events
123+
)
124+
patch.start()
125+
self.addCleanup(patch.stop)
126+
mock_events.side_effect = qubesadmin.tests.tools.MockEventsReader(
127+
[
128+
b"1\0\0connection-established\0\0",
129+
b"1\0some-vm\0domain-shutdown\0\0",
130+
b"1\0sys-usb\0domain-shutdown\0\0",
131+
]
132+
)
133+
134+
self.app.expected_calls[("dom0", "admin.vm.List", None, None)] = (
135+
b"0\x00some-vm class=AppVM state=Running\n"
136+
b"dom0 class=AdminVM state=Running\n"
137+
b"sys-usb class=DispVM state=Running\n"
138+
b"disp007 class=DispVM state=Running\n"
139+
b"dormant-vm class=DispVM state=Halted\n"
140+
)
141+
self.app.expected_calls[
142+
("sys-usb", "admin.vm.CurrentState", None, None)
143+
] = [b"0\x00power_state=Running"]
144+
self.app.expected_calls[
145+
("sys-usb", "admin.vm.property.Get", "auto_cleanup", None)
146+
] = b"0\x00default=True type=bool False"
147+
self.app.expected_calls[
148+
("disp007", "admin.vm.CurrentState", None, None)
149+
] = [b"0\x00power_state=Running"]
150+
self.app.expected_calls[
151+
("disp007", "admin.vm.property.Get", "auto_cleanup", None)
152+
] = b"0\x00default=True type=bool True"
153+
self.app.expected_calls[
154+
("dormant-vm", "admin.vm.CurrentState", None, None)
155+
] = [b"0\x00power_state=Halted"]
156+
for vm in ["some-vm", "sys-usb"]:
157+
self.app.expected_calls[
158+
(vm, "admin.vm.Shutdown", "force", None)
159+
] = b"0\x00"
160+
self.app.expected_calls[
161+
(vm, "admin.vm.CurrentState", None, None)
162+
] = (
163+
[b"0\x00power_state=Running"]
164+
+ [b"0\x00power_state=Running"]
165+
+ [b"0\x00power_state=Halted"]
166+
+ [b"0\x00power_state=Halted"]
167+
)
168+
self.app.expected_calls[(vm, "admin.vm.Start", None, None)] = (
169+
b"0\x00"
170+
)
171+
qubesadmin.tools.qvm_restart.main(["--all"], app=self.app)
172+
self.assertAllCalled()
173+
174+
def test_003_restart_dispvm(self):
175+
"""Trying to restart a true DispVM"""
176+
self.app.expected_calls[("dom0", "admin.vm.List", None, None)] = (
177+
b"0\x00some-vm class=AppVM state=Running\n"
178+
b"dom0 class=AdminVM state=Running\n"
179+
b"sys-usb class=DispVM state=Running\n"
180+
b"disp007 class=DispVM state=Running\n"
181+
b"dormant-vm class=DispVM state=Halted\n"
182+
)
183+
self.app.expected_calls[
184+
("disp007", "admin.vm.property.Get", "auto_cleanup", None)
185+
] = b"0\x00default=True type=bool True"
186+
with self.assertRaises(SystemExit):
187+
qubesadmin.tools.qvm_restart.main(["disp007"], app=self.app)
188+
self.assertAllCalled()

qubesadmin/tools/qvm_restart.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# encoding=utf-8
2+
#
3+
# The Qubes OS Project, http://www.qubes-os.org
4+
#
5+
# Copyright (C) 2010-2016 Joanna Rutkowska <[email protected]>
6+
# Copyright (C) 2011-2025 Marek Marczykowski-Górecki
7+
8+
# Copyright (C) 2016 Wojtek Porczyk <[email protected]>
9+
#
10+
# This program is free software; you can redistribute it and/or modify
11+
# it under the terms of the GNU Lesser General Public License as published by
12+
# the Free Software Foundation; either version 2.1 of the License, or
13+
# (at your option) any later version.
14+
#
15+
# This program is distributed in the hope that it will be useful,
16+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
# GNU Lesser General Public License for more details.
19+
#
20+
# You should have received a copy of the GNU Lesser General Public License along
21+
# with this program; if not, see <http://www.gnu.org/licenses/>.
22+
23+
"""Restart a qube / qubes"""
24+
25+
import sys
26+
27+
try:
28+
import qubesadmin.events.utils
29+
30+
have_events = True
31+
except ImportError:
32+
have_events = False
33+
import qubesadmin.tools
34+
from qubesadmin.tools import qvm_start, qvm_shutdown
35+
36+
parser = qubesadmin.tools.QubesArgumentParser(
37+
description=__doc__, vmname_nargs="+"
38+
)
39+
40+
parser.add_argument(
41+
"--timeout",
42+
action="store",
43+
type=float,
44+
help="timeout after which domains are killed before being restarted",
45+
)
46+
47+
48+
def main(args=None, app=None): # pylint: disable=missing-docstring
49+
args = parser.parse_args(args, app=app)
50+
51+
if not args.all_domains:
52+
# Check if user explicitly specified dom0 or true DispVMs
53+
invalid_domains = [
54+
vm
55+
for vm in args.domains
56+
if vm.klass == "AdminVM"
57+
or (vm.klass == "DispVM" and vm.auto_cleanup)
58+
]
59+
if invalid_domains:
60+
parser.error_runtime(
61+
"Can not restart: "
62+
+ ", ".join(vm.name for vm in invalid_domains),
63+
"dom0 or true DispVMs could not be restarted",
64+
)
65+
target_domains = args.domains
66+
else:
67+
# Only restart running, non-DispVM and not dom0 with --all option
68+
target_domains = [
69+
vm
70+
for vm in args.domains
71+
if vm.get_power_state() == "Running"
72+
and vm.klass != "AdminVM"
73+
and not (vm.klass == "DispVM" and vm.auto_cleanup)
74+
]
75+
76+
# Forcing shutdown to allow graceful restart of ServiceVMs
77+
shutdown_cmd = [vm.name for vm in target_domains] + ["--wait", "--force"]
78+
shutdown_cmd += ["--timeout", str(args.timeout)] if args.timeout else []
79+
shutdown_cmd += ["--quiet"] if args.quiet else []
80+
qvm_shutdown.main(shutdown_cmd, app=args.app)
81+
82+
start_cmd = [vm.name for vm in target_domains]
83+
start_cmd += ["--quiet"] if args.quiet else []
84+
qvm_start.main(start_cmd, app=args.app)
85+
86+
87+
if __name__ == "__main__":
88+
sys.exit(main())

0 commit comments

Comments
 (0)