Skip to content

Commit 4670c56

Browse files
committed
vmupdate: wait for full VM shutdown before next queued update
VMs with assigned PCI devices require full shutdown before the next queued update can begin, otherwise the update may start while the VM is still shutting down. Add _has_assigned_pci_devices() check in QubeConnection.__exit__ and call shutdown_domains() to block until shutdown completes, instead of the fire-and-forget qube.shutdown(). Add tests verifying that: - shutdown_domains() is called when a PCI-assigned VM was started by the update and must be shut down - qube.shutdown() is used when no PCI devices are assigned - neither is called when the VM was already running before the update Fixes QubesOS/qubes-issues#10617
1 parent 582536c commit 4670c56

2 files changed

Lines changed: 100 additions & 2 deletions

File tree

vmupdate/qube_connection.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from typing import List
2929

3030
import qubesadmin
31+
import qubesadmin.exc
3132
from vmupdate.agent.source.args import AgentArgs
3233
from vmupdate.agent.source.log_config import LOGPATH, LOG_FILE
3334
from vmupdate.agent.source.status import StatusInfo, FinalStatus
@@ -92,11 +93,28 @@ def __exit__(self, exc_type, exc_val, exc_tb):
9293
self.dest_dir, str(err))
9394

9495
if self.qube.is_running() and not self._initially_running:
95-
self.logger.info('Shutdown %s', self.qube.name)
96-
self.qube.shutdown()
96+
if self._has_assigned_pci_devices(self.qube):
97+
# Late import to avoid circular dependency:
98+
# qube_connection <- vmupdate <- update_manager <- qube_connection
99+
from vmupdate.vmupdate import shutdown_domains # pylint: disable=import-outside-toplevel
100+
self.logger.info(
101+
'Waiting for full shutdown %s (PCI devices assigned)',
102+
self.qube.name)
103+
shutdown_domains([self.qube], self.logger)
104+
else:
105+
self.logger.info('Shutdown %s', self.qube.name)
106+
self.qube.shutdown()
97107

98108
self.__connected = False
99109

110+
@staticmethod
111+
def _has_assigned_pci_devices(vm) -> bool:
112+
"""Return True when VM has assigned PCI devices."""
113+
try:
114+
return any(vm.devices['pci'].get_assigned_devices())
115+
except qubesadmin.exc.QubesDaemonAccessError:
116+
return False
117+
100118
def transfer_agent(self, src_dir: str) -> ProcessResult:
101119
"""
102120
Copy a directory content to the workdir in the qube.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# coding=utf-8
2+
#
3+
# The Qubes OS Project, https://www.qubes-os.org
4+
#
5+
# Copyright (C) 2025 Jayant Saxena <jayantmcom@gmail.com>
6+
#
7+
# This program is free software; you can redistribute it and/or
8+
# modify it under the terms of the GNU General Public License
9+
# as published by the Free Software Foundation; either version 2
10+
# of the License, or (at your option) any later version.
11+
#
12+
# This program is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU General Public License for more details.
16+
#
17+
# You should have received a copy of the GNU General Public License
18+
# along with this program; if not, write to the Free Software
19+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
20+
# USA.
21+
from unittest.mock import Mock, patch
22+
23+
from vmupdate.qube_connection import QubeConnection
24+
25+
26+
@patch("vmupdate.qube_connection.shutdown_domains")
27+
def test_wait_for_shutdown_when_vm_started_by_update(shutdown_domains):
28+
vm = Mock()
29+
vm.name = "hvm1"
30+
vm.is_running.side_effect = [False, True]
31+
vm.devices = {'pci': Mock()}
32+
vm.devices['pci'].get_assigned_devices.return_value = ["00_1f.2"]
33+
status_notifier = Mock()
34+
logger = Mock()
35+
36+
with QubeConnection(
37+
vm, "/tmp/qubes-update", cleanup=False, logger=logger,
38+
show_progress=False, status_notifier=status_notifier):
39+
pass
40+
41+
shutdown_domains.assert_called_once_with([vm], logger)
42+
vm.shutdown.assert_not_called()
43+
44+
45+
@patch("vmupdate.qube_connection.shutdown_domains")
46+
def test_do_not_wait_for_shutdown_without_assigned_pci(shutdown_domains):
47+
vm = Mock()
48+
vm.name = "hvm2"
49+
vm.is_running.side_effect = [False, True]
50+
vm.devices = {'pci': Mock()}
51+
vm.devices['pci'].get_assigned_devices.return_value = []
52+
status_notifier = Mock()
53+
logger = Mock()
54+
55+
with QubeConnection(
56+
vm, "/tmp/qubes-update", cleanup=False, logger=logger,
57+
show_progress=False, status_notifier=status_notifier):
58+
pass
59+
60+
vm.shutdown.assert_called_once_with()
61+
shutdown_domains.assert_not_called()
62+
63+
64+
@patch("vmupdate.qube_connection.shutdown_domains")
65+
def test_do_not_shutdown_if_vm_was_already_running(shutdown_domains):
66+
vm = Mock()
67+
vm.name = "hvm3"
68+
vm.is_running.return_value = True
69+
vm.devices = {'pci': Mock()}
70+
vm.devices['pci'].get_assigned_devices.return_value = ["00_1f.2"]
71+
status_notifier = Mock()
72+
logger = Mock()
73+
74+
with QubeConnection(
75+
vm, "/tmp/qubes-update", cleanup=False, logger=logger,
76+
show_progress=False, status_notifier=status_notifier):
77+
pass
78+
79+
vm.shutdown.assert_not_called()
80+
shutdown_domains.assert_not_called()

0 commit comments

Comments
 (0)