@samitouri / QOSamiQemu / commits / d77f38a2b5

tests/functional/x86_64: add migration test for NVMe device

Introduce a very simple test to ensure that NVMe device migration works fine. Test plan is simple: 1. prepare VM with NVMe device 2. run workload that produces relatively heavy IO on the device 3. migrate VM 4. ensure that workload is alive and finishes without errors Test can be run as simple as: $ meson test 'func-x86_64-nvme_migration' --setup thorough -C build In the future we can extend this approach, and introduce some fio-based tests. And probably, it makes sense to make this test to apply not only to NVMe device, but also virtio-{blk,scsi}, ide, sata and other migratable devices. Acked-by: Stefan Hajnoczi <stefanha@redhat.com> Signed-off-by: Alexander Mikhalitsyn <aleksandr.mikhalitsyn@futurfusion.io> Acked-by: Klaus Jensen <k.jensen@samsung.com> Acked-by: Fabiano Rosas <farosas@suse.de> Signed-off-by: Klaus Jensen <k.jensen@samsung.com>

Alexander Mikhalitsyn committed Jun 11, 2026 at 20:08 UTC d77f38a2b596845c0afa509380f490237bbf75d3
3 files changed +174
MAINTAINERS
+1
@@ -2680,6 +2680,7 @@ S: Supported
2680 F: hw/nvme/*
2681 F: include/block/nvme.h
2682 F: tests/qtest/nvme-test.c
2683 +F: tests/functional/x86_64/test_nvme_migration.py
2684 F: docs/system/devices/nvme.rst
2685 T: git git://git.infradead.org/qemu-nvme.git nvme-next
2686
tests/functional/x86_64/meson.build
+1
@@ -37,6 +37,7 @@ tests_x86_64_system_thorough = [
37 'linux_initrd',
38 'multiprocess',
39 'netdev_ethtool',
40 + 'nvme_migration',
41 'replay',
42 'reverse_debug',
43 'tuxrun',
tests/functional/x86_64/test_nvme_migration.py new
+172
@@ -0,0 +1,172 @@
1 +#!/usr/bin/env python3
2 +#
3 +# SPDX-License-Identifier: GPL-2.0-or-later
4 +#
5 +# x86_64 NVMe migration test
6 +
7 +from migration import MigrationTest
8 +from qemu_test import QemuSystemTest, Asset
9 +from qemu_test import wait_for_console_pattern
10 +from qemu_test import exec_command, exec_command_and_wait_for_pattern
11 +
12 +
13 +class X8664NVMeMigrationTest(MigrationTest):
14 + ASSET_KERNEL = Asset(
15 + ('https://archives.fedoraproject.org/pub/archive/fedora/linux/releases'
16 + '/31/Server/x86_64/os/images/pxeboot/vmlinuz'),
17 + 'd4738d03dbbe083ca610d0821d0a8f1488bebbdccef54ce33e3adb35fda00129')
18 +
19 + ASSET_INITRD = Asset(
20 + ('https://archives.fedoraproject.org/pub/archive/fedora/linux/releases'
21 + '/31/Server/x86_64/os/images/pxeboot/initrd.img'),
22 + '277cd6c7adf77c7e63d73bbb2cded8ef9e2d3a2f100000e92ff1f8396513cd8b')
23 +
24 + ASSET_DISKIMAGE = Asset(
25 + ('https://archives.fedoraproject.org/pub/archive/fedora/linux/releases'
26 + '/31/Cloud/x86_64/images/Fedora-Cloud-Base-31-1.9.x86_64.qcow2'),
27 + 'e3c1b309d9203604922d6e255c2c5d098a309c2d46215d8fc026954f3c5c27a0')
28 +
29 + DEFAULT_KERNEL_PARAMS = ('root=/dev/nvme0n1p1 console=ttyS0 net.ifnames=0 '
30 + 'rd.rescue quiet')
31 +
32 + def wait_for_console_pattern(self, success_message, vm):
33 + wait_for_console_pattern(
34 + self,
35 + success_message,
36 + failure_message="Kernel panic - not syncing",
37 + vm=vm,
38 + )
39 +
40 + def exec_command_and_check(self, command, vm):
41 + prompt = '# '
42 + exec_command_and_wait_for_pattern(self,
43 + f"{command} && echo OK || echo FAIL",
44 + 'FAIL', vm=vm)
45 + # Note, that commands we send to the console are echo-ed back,
46 + # so if we have a word "FAIL" in the command itself, we should
47 + # expect to see it once.
48 + wait_for_console_pattern(self, 'OK', failure_message="FAIL", vm=vm)
49 + self.wait_for_console_pattern(prompt, vm)
50 +
51 + def configure_machine(self, vm):
52 + kernel_path = self.ASSET_KERNEL.fetch()
53 + initrd_path = self.ASSET_INITRD.fetch()
54 + diskimage_path = self.ASSET_DISKIMAGE.fetch()
55 +
56 + vm.set_console()
57 + vm.add_args("-cpu", "max")
58 + vm.add_args("-m", "2G")
59 + vm.add_args("-accel", "kvm")
60 +
61 + vm.add_args('-drive',
62 + f'file={diskimage_path},if=none,id=drv0,snapshot=on')
63 + vm.add_args('-device', 'nvme,bus=pcie.0,' +
64 + 'drive=drv0,id=nvme-disk0,serial=nvmemigtest,bootindex=1')
65 +
66 + vm.add_args(
67 + "-kernel",
68 + kernel_path,
69 + "-initrd",
70 + initrd_path,
71 + "-append",
72 + self.DEFAULT_KERNEL_PARAMS
73 + )
74 +
75 + def launch_source_vm(self, vm):
76 + vm.launch()
77 +
78 + self.wait_for_console_pattern('Entering emergency mode.', vm)
79 + prompt = '# '
80 + self.wait_for_console_pattern(prompt, vm)
81 +
82 + # Synchronize on NVMe driver creating the root device
83 + exec_command_and_wait_for_pattern(self,
84 + "while ! (dmesg -c | grep nvme0n1:) ; do sleep 1 ; done",
85 + "nvme0n1", vm=vm)
86 + self.wait_for_console_pattern(prompt, vm)
87 +
88 + # prepare system
89 + exec_command_and_wait_for_pattern(self, 'mount /dev/nvme0n1p1 /sysroot',
90 + prompt, vm=vm)
91 + exec_command_and_wait_for_pattern(self, 'chroot /sysroot',
92 + prompt, vm=vm)
93 + exec_command_and_wait_for_pattern(self, 'mount -t proc proc /proc',
94 + prompt, vm=vm)
95 + exec_command_and_wait_for_pattern(self, 'mount -t sysfs sysfs /sys',
96 + prompt, vm=vm)
97 +
98 + # Run workload before migration to check if it continues
99 + # to run properly after migration.
100 + #
101 + # Workload is simple: it continuously calculates checksums of
102 + # all files in /usr/bin to generate some I/O load on
103 + # the NVMe disk and at the same time it drops caches to
104 + # make sure that we have some read I/O on the disk as well.
105 + # If there are any issues with the migration of the NVMe device,
106 + # we should see errors in dmesg and consequently in the workload log.
107 + exec_command_and_wait_for_pattern(self,
108 + "(while [ ! -f /tmp/test_nvme_mig_workload.stop ]; do \
109 + rm -f /tmp/test_nvme_mig_workload.iter_finished; \
110 + echo 3 > /proc/sys/vm/drop_caches; \
111 + find /usr/bin -type f -exec cksum {} \\;; \
112 + touch /tmp/test_nvme_mig_workload.iter_finished; \
113 + done) > /dev/null 2> /tmp/test_nvme_mig_workload.errors &",
114 + prompt, vm=vm)
115 + exec_command_and_wait_for_pattern(self,
116 + 'echo $! > /tmp/test_nvme_mig_workload.pid',
117 + prompt, vm=vm)
118 +
119 + # check if process is alive and running
120 + self.exec_command_and_check(
121 + "kill -0 $(cat /tmp/test_nvme_mig_workload.pid)", vm)
122 +
123 + def assert_dest_vm(self, vm):
124 + prompt = '# '
125 +
126 + # check if process is alive and running after migration,
127 + # if not - fail the test
128 + self.exec_command_and_check(
129 + "kill -0 $(cat /tmp/test_nvme_mig_workload.pid)", vm)
130 +
131 + # signal workload to stop
132 + exec_command_and_wait_for_pattern(self,
133 + 'touch /tmp/test_nvme_mig_workload.stop',
134 + prompt, vm=vm)
135 +
136 + # wait workload to finish, because we want to examine log
137 + # to see if there are any errors
138 + exec_command_and_wait_for_pattern(self,
139 + "while [ ! -f /tmp/test_nvme_mig_workload.iter_finished ]; do \
140 + sleep 1; \
141 + done;",
142 + prompt, vm=vm)
143 +
144 + exec_command_and_wait_for_pattern(self,
145 + 'cat /tmp/test_nvme_mig_workload.errors',
146 + prompt, vm=vm)
147 +
148 + # fail the test if non-empty
149 + self.exec_command_and_check(
150 + "[ ! -s /tmp/test_nvme_mig_workload.errors ]", vm)
151 +
152 + def test_migration_with_tcp_localhost(self):
153 + self.set_machine('q35')
154 + self.require_accelerator("kvm")
155 +
156 + self.migration_with_tcp_localhost()
157 +
158 + def test_migration_with_unix(self):
159 + self.set_machine('q35')
160 + self.require_accelerator("kvm")
161 +
162 + self.migration_with_unix()
163 +
164 + def test_migration_with_exec(self):
165 + self.set_machine('q35')
166 + self.require_accelerator("kvm")
167 +
168 + self.migration_with_exec()
169 +
170 +
171 +if __name__ == '__main__':
172 + MigrationTest.main()