Skip to content

Commit 868b1f8

Browse files
committed
vmupdate: wait for PCI-assigned VM shutdown in update flow
1 parent 582536c commit 868b1f8

2 files changed

Lines changed: 89 additions & 0 deletions

File tree

vmupdate/qube_connection.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@
2222
import shutil
2323
import signal
2424
import tempfile
25+
import asyncio
2526
import concurrent.futures
2627
from os.path import join
2728
from subprocess import CalledProcessError
2829
from typing import List
2930

3031
import qubesadmin
32+
from qubesadmin.events.utils import wait_for_domain_shutdown
3133
from vmupdate.agent.source.args import AgentArgs
3234
from vmupdate.agent.source.log_config import LOGPATH, LOG_FILE
3335
from vmupdate.agent.source.status import StatusInfo, FinalStatus
@@ -94,9 +96,19 @@ def __exit__(self, exc_type, exc_val, exc_tb):
9496
if self.qube.is_running() and not self._initially_running:
9597
self.logger.info('Shutdown %s', self.qube.name)
9698
self.qube.shutdown()
99+
if self._has_assigned_pci_devices(self.qube):
100+
asyncio.run(wait_for_domain_shutdown([self.qube]))
97101

98102
self.__connected = False
99103

104+
@staticmethod
105+
def _has_assigned_pci_devices(vm) -> bool:
106+
"""Return True when VM has assigned PCI devices."""
107+
try:
108+
return any(vm.devices['pci'].get_assigned_devices())
109+
except Exception: # best effort check only
110+
return False
111+
100112
def transfer_agent(self, src_dir: str) -> ProcessResult:
101113
"""
102114
Copy a directory content to the workdir in the qube.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# coding=utf-8
2+
#
3+
# The Qubes OS Project, https://www.qubes-os.org
4+
#
5+
# Copyright (C) 2026
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+
from unittest.mock import Mock, patch
13+
14+
from vmupdate.qube_connection import QubeConnection
15+
16+
17+
@patch("vmupdate.qube_connection.wait_for_domain_shutdown")
18+
@patch("vmupdate.qube_connection.asyncio.run")
19+
def test_wait_for_shutdown_when_vm_started_by_update(_arun, wait_shutdown):
20+
vm = Mock()
21+
vm.name = "hvm1"
22+
vm.is_running.side_effect = [False, True]
23+
vm.devices = {'pci': Mock()}
24+
vm.devices['pci'].get_assigned_devices.return_value = ["00_1f.2"]
25+
status_notifier = Mock()
26+
logger = Mock()
27+
28+
with QubeConnection(
29+
vm, "/tmp/qubes-update", cleanup=False, logger=logger,
30+
show_progress=False, status_notifier=status_notifier):
31+
pass
32+
33+
vm.shutdown.assert_called_once_with()
34+
wait_shutdown.assert_called_once_with([vm])
35+
_arun.assert_called_once()
36+
37+
38+
@patch("vmupdate.qube_connection.wait_for_domain_shutdown")
39+
@patch("vmupdate.qube_connection.asyncio.run")
40+
def test_do_not_wait_for_shutdown_without_assigned_pci(_arun, wait_shutdown):
41+
vm = Mock()
42+
vm.name = "hvm2"
43+
vm.is_running.side_effect = [False, True]
44+
vm.devices = {'pci': Mock()}
45+
vm.devices['pci'].get_assigned_devices.return_value = []
46+
status_notifier = Mock()
47+
logger = Mock()
48+
49+
with QubeConnection(
50+
vm, "/tmp/qubes-update", cleanup=False, logger=logger,
51+
show_progress=False, status_notifier=status_notifier):
52+
pass
53+
54+
vm.shutdown.assert_called_once_with()
55+
wait_shutdown.assert_not_called()
56+
_arun.assert_not_called()
57+
58+
59+
@patch("vmupdate.qube_connection.wait_for_domain_shutdown")
60+
@patch("vmupdate.qube_connection.asyncio.run")
61+
def test_do_not_shutdown_if_vm_was_already_running(_arun, wait_shutdown):
62+
vm = Mock()
63+
vm.name = "hvm3"
64+
vm.is_running.return_value = True
65+
vm.devices = {'pci': Mock()}
66+
vm.devices['pci'].get_assigned_devices.return_value = ["00_1f.2"]
67+
status_notifier = Mock()
68+
logger = Mock()
69+
70+
with QubeConnection(
71+
vm, "/tmp/qubes-update", cleanup=False, logger=logger,
72+
show_progress=False, status_notifier=status_notifier):
73+
pass
74+
75+
vm.shutdown.assert_not_called()
76+
wait_shutdown.assert_not_called()
77+
_arun.assert_not_called()

0 commit comments

Comments
 (0)