Skip to content

Commit 0860aba

Browse files
committed
Merge remote-tracking branch 'origin/pr/807'
* origin/pr/807: Avoid hanging qubesd on unresponsive domU shutdown Raise appropriate exception on unresponsive guest Cleanup libvirt state query Helper to detect hypervisor via libvirt Pull request description: For: QubesOS/qubes-issues#10835
2 parents 06e8660 + bc93a9b commit 0860aba

4 files changed

Lines changed: 168 additions & 49 deletions

File tree

qubes/app.py

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ def __init__(self, offline_mode=None, libvirt_reconnect_cb=None):
181181
self._libvirt_conn = None
182182
self._xs = None
183183
self._xc = None
184+
self._libvirt_conn_type = None
185+
self._libvirt_conn_uri = None
186+
self._is_xen = False
184187

185188
@property
186189
def offline_mode(self):
@@ -203,14 +206,18 @@ def init_vmm_connection(self):
203206
"VMM operations disabled in offline mode"
204207
)
205208

206-
if "xen.lowlevel.xs" in sys.modules:
207-
self._xs = xen.lowlevel.xs.xs()
208-
if "xen.lowlevel.xc" in sys.modules:
209-
self._xc = xen.lowlevel.xc.xc()
210209
self._libvirt_conn = VirConnectWrapper(
211210
qubes.config.defaults["libvirt_uri"],
212211
reconnect_cb=self._libvirt_reconnect_cb,
213212
)
213+
self._libvirt_conn_type = self._libvirt_conn.getType()
214+
self._libvirt_conn_uri = self._libvirt_conn.getURI()
215+
if self._libvirt_conn_type == "Xen":
216+
self._is_xen = True
217+
if "xen.lowlevel.xs" in sys.modules:
218+
self._xs = xen.lowlevel.xs.xs()
219+
if "xen.lowlevel.xc" in sys.modules:
220+
self._xc = xen.lowlevel.xc.xc()
214221
libvirt.registerErrorHandler(self._libvirt_error_handler, None)
215222

216223
@property
@@ -219,21 +226,33 @@ def libvirt_conn(self):
219226
self.init_vmm_connection()
220227
return self._libvirt_conn
221228

229+
@property
230+
def libvirt_conn_type(self):
231+
"""Connection type to libvirt"""
232+
self.init_vmm_connection()
233+
return self._libvirt_conn_type
234+
235+
@property
236+
def libvirt_conn_uri(self):
237+
"""Connection URI to libvirt"""
238+
self.init_vmm_connection()
239+
return self._libvirt_conn_uri
240+
241+
@property
242+
def is_xen(self):
243+
self.init_vmm_connection()
244+
return self._is_xen
245+
222246
@property
223247
def xs(self):
224248
"""Connection to Xen Store
225249
226250
This property is available only when running on Xen.
227251
"""
228-
229-
# XXX what about the case when we run under KVM,
230-
# but xen modules are importable?
231-
if "xen.lowlevel.xs" not in sys.modules:
252+
if not self.is_xen:
232253
raise AttributeError(
233254
"xs object is available under Xen hypervisor only"
234255
)
235-
236-
self.init_vmm_connection()
237256
return self._xs
238257

239258
@property
@@ -242,15 +261,10 @@ def xc(self):
242261
243262
This property is available only when running on Xen.
244263
"""
245-
246-
# XXX what about the case when we run under KVM,
247-
# but xen modules are importable?
248-
if "xen.lowlevel.xc" not in sys.modules:
264+
if not self.is_xen:
249265
raise AttributeError(
250266
"xc object is available under Xen hypervisor only"
251267
)
252-
253-
self.init_vmm_connection()
254268
return self._xc
255269

256270
def close(self):
@@ -347,21 +361,19 @@ def get_free_xen_memory(self):
347361
348362
:raises NotImplementedError: when not under Xen
349363
"""
350-
try:
351-
self._physinfo = self.app.vmm.xc.physinfo()
352-
except AttributeError:
364+
if not self.app.vmm.is_xen:
353365
raise NotImplementedError("This function requires Xen hypervisor")
366+
self._physinfo = self.app.vmm.xc.physinfo()
354367
return int(self._physinfo["free_memory"])
355368

356369
def is_iommu_supported(self):
357370
"""Check if IOMMU is supported on this platform"""
358371
if self._physinfo is None:
359-
try:
360-
self._physinfo = self.app.vmm.xc.physinfo()
361-
except AttributeError:
372+
if not self.app.vmm.is_xen:
362373
raise NotImplementedError(
363374
"This function requires Xen hypervisor"
364375
)
376+
self._physinfo = self.app.vmm.xc.physinfo()
365377
return "hvm_directio" in self._physinfo["virt_caps"]
366378

367379
def get_vm_stats(self, previous_time=None, previous=None, only_vm=None):
@@ -401,18 +413,17 @@ def get_vm_stats(self, previous_time=None, previous=None, only_vm=None):
401413

402414
current_time = time.time()
403415
current = {}
404-
try:
405-
if only_vm:
406-
xid = only_vm.xid
407-
if xid < 0:
408-
raise qubes.exc.QubesVMNotRunningError(only_vm)
409-
info = self.app.vmm.xc.domain_getinfo(xid, 1)
410-
if info[0]["domid"] != xid:
411-
raise qubes.exc.QubesVMNotRunningError(only_vm)
412-
else:
413-
info = self.app.vmm.xc.domain_getinfo(0, 1024)
414-
except AttributeError:
416+
if not self.app.vmm.is_xen:
415417
raise NotImplementedError("This function requires Xen hypervisor")
418+
if only_vm:
419+
xid = only_vm.xid
420+
if xid < 0:
421+
raise qubes.exc.QubesVMNotRunningError(only_vm)
422+
info = self.app.vmm.xc.domain_getinfo(xid, 1)
423+
if info[0]["domid"] != xid:
424+
raise qubes.exc.QubesVMNotRunningError(only_vm)
425+
else:
426+
info = self.app.vmm.xc.domain_getinfo(0, 1024)
416427
# TODO: add stubdomain stats to actual VMs
417428
for vm in info:
418429
domid = vm["domid"]

qubes/tests/vm/mix/net.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
2121
#
2222
import ipaddress
23+
from unittest import mock
2324
from unittest.mock import patch
2425

2526
import qubes
@@ -395,7 +396,16 @@ def test_180_shutdown(self, mock_halted, mock_shutdown):
395396
with self.assertRaises(qubes.exc.QubesVMInUseError):
396397
self.loop.run_until_complete(vm.netvm.shutdown())
397398
vm.is_preload = True
398-
self.loop.run_until_complete(vm.netvm.shutdown())
399+
mock_proc = mock.AsyncMock()
400+
mock_proc.communicate.return_value = (b"", None)
401+
mock_proc.wait.return_value = 0
402+
mock_proc.returncode = 0
403+
with (
404+
patch.object(vm.app, "vmm") as mock_vmm,
405+
patch("asyncio.create_subprocess_exec", return_value=mock_proc),
406+
):
407+
mock_vmm.is_xen = True
408+
self.loop.run_until_complete(vm.netvm.shutdown())
399409

400410
def test_200_vmid_to_ipv4(self):
401411
testcases = (

qubes/tests/vm/qubesvm.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2950,6 +2950,74 @@ def test_626_qdb_keyboard_layout_change(
29502950
async def coroutine_mock(self, mock, *args, **kwargs):
29512951
return mock(*args, **kwargs)
29522952

2953+
@unittest.mock.patch("asyncio.create_subprocess_exec")
2954+
@unittest.mock.patch(
2955+
"qubes.vm.qubesvm.QubesVM.is_running", return_value=True
2956+
)
2957+
@unittest.mock.patch("qubes.vm.qubesvm.QubesVM.libvirt_domain")
2958+
@unittest.mock.patch(
2959+
"qubes.vm.qubesvm.QubesVM.is_halted", return_value=False
2960+
)
2961+
def test_650_shutdown(
2962+
self, mock_halted, mock_shutdown, mock_running, mock_async_sub
2963+
):
2964+
# pylint: disable=unused-argument
2965+
vm = self.get_vm()
2966+
mock_proc = unittest.mock.AsyncMock()
2967+
mock_proc.communicate.return_value = (b"", None)
2968+
mock_proc.returncode = 0
2969+
mock_proc.wait.return_value = mock_proc.returncode
2970+
mock_async_sub.return_value = mock_proc
2971+
with unittest.mock.patch.object(vm.app, "vmm") as mock_vmm:
2972+
with self.subTest("xen"):
2973+
mock_vmm.is_xen = True
2974+
self.loop.run_until_complete(vm.shutdown())
2975+
mock_async_sub.assert_called_once_with(
2976+
"xl",
2977+
"shutdown",
2978+
"-F",
2979+
vm.name,
2980+
stdin=unittest.mock.ANY,
2981+
stdout=unittest.mock.ANY,
2982+
stderr=unittest.mock.ANY,
2983+
)
2984+
2985+
mock_async_sub.reset_mock()
2986+
with self.subTest("xen-fail"):
2987+
mock_proc.communicate.return_value = (b"Oh no (xen)", None)
2988+
mock_proc.returncode = 1
2989+
mock_vmm.is_xen = True
2990+
with self.assertRaises(qubes.exc.QubesVMShutdownTimeoutError):
2991+
self.loop.run_until_complete(vm.shutdown())
2992+
2993+
mock_async_sub.reset_mock()
2994+
with self.subTest("not-xen"):
2995+
mock_proc.returncode = 0
2996+
uri = "kvm:///"
2997+
mock_vmm.is_xen = False
2998+
mock_vmm.libvirt_conn_uri = uri
2999+
self.loop.run_until_complete(vm.shutdown())
3000+
mock_async_sub.assert_called_once_with(
3001+
"virsh",
3002+
"-c",
3003+
uri,
3004+
"shutdown",
3005+
vm.name,
3006+
stdin=unittest.mock.ANY,
3007+
stdout=unittest.mock.ANY,
3008+
stderr=unittest.mock.ANY,
3009+
)
3010+
3011+
mock_async_sub.reset_mock()
3012+
with self.subTest("not-xen-fail"):
3013+
mock_proc.communicate.return_value = (b"Oh no (not-xen)", None)
3014+
mock_proc.returncode = 1
3015+
uri = "kvm:///"
3016+
mock_vmm.is_xen = False
3017+
mock_vmm.libvirt_conn_uri = uri
3018+
with self.assertRaises(qubes.exc.QubesVMShutdownTimeoutError):
3019+
self.loop.run_until_complete(vm.shutdown())
3020+
29533021
@unittest.mock.patch("asyncio.create_subprocess_exec")
29543022
def test_700_run_service(self, mock_subprocess):
29553023
start_mock = unittest.mock.AsyncMock()

qubes/vm/qubesvm.py

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# pylint: disable=too-many-lines
12
#
23
# The Qubes OS Project, https://www.qubes-os.org/
34
#
@@ -1661,7 +1662,39 @@ async def shutdown(self, force=False, wait=False, timeout=None):
16611662
if self.is_paused():
16621663
self.libvirt_domain.destroy()
16631664
else:
1664-
self.libvirt_domain.shutdown()
1665+
# Some libvirt actions have a global lock on a domain, blocking
1666+
# a lot of libvirt operations and even qubesd. When possible to
1667+
# act without it, do so to avoid the whole qubesd hanging.
1668+
if self.app.vmm.is_xen:
1669+
command = ["xl", "shutdown", "-F", self.name]
1670+
else:
1671+
uri = self.app.vmm.libvirt_conn_uri
1672+
command = ["virsh", "-c", uri, "shutdown", self.name]
1673+
try:
1674+
proc = await asyncio.create_subprocess_exec(
1675+
*command,
1676+
stdin=asyncio.subprocess.DEVNULL,
1677+
stdout=asyncio.subprocess.PIPE,
1678+
stderr=asyncio.subprocess.STDOUT,
1679+
)
1680+
stdout, _ = await proc.communicate()
1681+
await proc.wait()
1682+
if proc.returncode:
1683+
raise subprocess.CalledProcessError(
1684+
proc.returncode,
1685+
command,
1686+
output=stdout,
1687+
)
1688+
except subprocess.CalledProcessError as e:
1689+
self.log.error(
1690+
"Attempted {!s} with subprocess but exited with "
1691+
"error code: {!s}: {!r}".format(
1692+
"shutdown",
1693+
e.returncode,
1694+
qubes.utils.sanitize_stderr_for_log(e.output),
1695+
)
1696+
)
1697+
raise qubes.exc.QubesVMShutdownTimeoutError(self)
16651698

16661699
if wait:
16671700
if timeout is None:
@@ -2453,22 +2486,19 @@ def get_power_state(self):
24532486

24542487
try:
24552488
if libvirt_domain.isActive():
2456-
# pylint: disable=line-too-long
2457-
if libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_PAUSED:
2458-
return "Paused"
2459-
if libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_CRASHED:
2460-
return "Crashed"
2461-
if libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_SHUTDOWN:
2462-
return "Halting"
2463-
if libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_SHUTOFF:
2464-
return "Dying"
2465-
if (
2466-
libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_PMSUSPENDED
2467-
): # nopep8
2468-
return "Suspended"
2489+
state_dict = {
2490+
libvirt.VIR_DOMAIN_PAUSED: "Paused", # 0x3
2491+
libvirt.VIR_DOMAIN_SHUTDOWN: "Halting", # 0x4
2492+
libvirt.VIR_DOMAIN_SHUTOFF: "Dying", # 0x5
2493+
libvirt.VIR_DOMAIN_CRASHED: "Crashed", # 0x6
2494+
libvirt.VIR_DOMAIN_PMSUSPENDED: "Suspended", # 0x7
2495+
}
2496+
state = libvirt_domain.state()[0]
2497+
if state in state_dict:
2498+
return state_dict[state]
24692499
if not self.is_fully_usable():
24702500
return "Transient"
2471-
return "Running"
2501+
return "Running" # 0x1
24722502

24732503
return "Halted"
24742504
except libvirt.libvirtError as e:

0 commit comments

Comments
 (0)