Merge tag 'pull-nvme-20260707' of https://gitlab.com/birkelund/qemu into staging

nvme queue

# -----BEGIN PGP SIGNATURE-----
#
# iQEzBAABCgAdFiEEUigzqnXi3OaiR2bATeGvMW1PDekFAmpML5AACgkQTeGvMW1P
# DekLeAf+JsOqQZ8rowk6ysUG4bp7Yy72vyIs05mOKrXsIEb1N3KpYAL10qs3Psl/
# /P37sBw51mbkgovpUgEV+J3kamCF/+8li1lgpsMV8HwbiO0QqQUMXPMGHWwA4CmF
# 3dkZWYP9xKjU/o9tdzSJ5F8hX372leu+z8FHslhK/XIlopRWeRaH/HxrEjZvKuIr
# ETPpVwffuAgM2hpi7/ekQhIxWz9nBvP/+fz57AHnj70O2Cl01JQm2PZrVX976C54
# o8j+Uz0DQup/lXunk83t1snJcYNNB/vbVPlUlF0nX/oQCiZ85SN8GqXblZPvELB9
# AzeYXmhxMWYrgf2eiqrqPl8xw1QvGg==
# =wZBE
# -----END PGP SIGNATURE-----
# gpg: Signature made Tue 07 Jul 2026 00:43:28 CEST
# gpg:                using RSA key 522833AA75E2DCE6A24766C04DE1AF316D4F0DE9
# gpg: Good signature from "Klaus Jensen <its@irrelevant.dk>" [unknown]
# gpg:                 aka "Klaus Jensen <k.jensen@samsung.com>" [unknown]
# gpg: WARNING: This key is not certified with a trusted signature!
# gpg:          There is no indication that the signature belongs to the owner.
# Primary key fingerprint: DDCA 4D9C 9EF9 31CC 3468  4272 63D5 6FC5 E55D A838
#      Subkey fingerprint: 5228 33AA 75E2 DCE6 A247  66C0 4DE1 AF31 6D4F 0DE9

* tag 'pull-nvme-20260707' of https://gitlab.com/birkelund/qemu:
  hw/nvme: add namespace hotplug support
  tests/qtest/nvme-test: add migration test with full CQ
  tests/functional/x86_64: add migration test for NVMe device
  hw/nvme: add basic live migration support
  hw/nvme: unmap req->sg earlier in nvme_enqueue_req_completion
  hw/nvme: set CQE.sq_id earlier in nvme_process_sq
  hw/nvme: split nvme_init_sq/nvme_init_cq into helpers
  hw/nvme: add migration blockers for non-supported cases
  tests/functional/migration: add VM launch/configure hooks
  hw/nvme: ensure sgl forward progress
  hw/nvme: fix FDP set FDP events

Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
This commit is contained in:
Stefan Hajnoczi
2026-07-07 07:09:37 +02:00
11 changed files with 1948 additions and 43 deletions

View File

@@ -2680,6 +2680,7 @@ S: Supported
F: hw/nvme/*
F: include/block/nvme.h
F: tests/qtest/nvme-test.c
F: tests/functional/x86_64/test_nvme_migration.py
F: docs/system/devices/nvme.rst
T: git git://git.infradead.org/qemu-nvme.git nvme-next

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,7 @@
#include "qemu/bitops.h"
#include "system/system.h"
#include "system/block-backend.h"
#include "migration/vmstate.h"
#include "nvme.h"
#include "trace.h"
@@ -719,10 +720,17 @@ void nvme_ns_cleanup(NvmeNamespace *ns)
static void nvme_ns_unrealize(DeviceState *dev)
{
NvmeNamespace *ns = NVME_NS(dev);
NvmeSubsystem *subsys = ns->subsys;
uint32_t nsid = ns->params.nsid;
nvme_ns_drain(ns);
nvme_ns_shutdown(ns);
nvme_ns_cleanup(ns);
/* Symmetric with nvme_ns_realize() which sets subsys->namespaces[nsid]. */
if (subsys && nsid && subsys->namespaces[nsid] == ns) {
subsys->namespaces[nsid] = NULL;
}
}
void nvme_ns_atomic_configure_boundary(bool dn, uint16_t nabsn,
@@ -886,6 +894,168 @@ static void nvme_ns_realize(DeviceState *dev, Error **errp)
}
}
static const VMStateDescription nvme_vmstate_lbaf = {
.name = "nvme_lbaf",
.version_id = 1,
.minimum_version_id = 1,
.fields = (const VMStateField[]) {
VMSTATE_UINT16(ms, NvmeLBAF),
VMSTATE_UINT8(ds, NvmeLBAF),
VMSTATE_UINT8(rp, NvmeLBAF),
VMSTATE_END_OF_LIST()
}
};
static const VMStateDescription nvme_vmstate_id_ns = {
.name = "nvme_id_ns",
.version_id = 1,
.minimum_version_id = 1,
.fields = (const VMStateField[]) {
VMSTATE_UINT64(nsze, NvmeIdNs),
VMSTATE_UINT64(ncap, NvmeIdNs),
VMSTATE_UINT64(nuse, NvmeIdNs),
VMSTATE_UINT8(nsfeat, NvmeIdNs),
VMSTATE_UINT8(nlbaf, NvmeIdNs),
VMSTATE_UINT8(flbas, NvmeIdNs),
VMSTATE_UINT8(mc, NvmeIdNs),
VMSTATE_UINT8(dpc, NvmeIdNs),
VMSTATE_UINT8(dps, NvmeIdNs),
VMSTATE_UINT8(nmic, NvmeIdNs),
VMSTATE_UINT8(rescap, NvmeIdNs),
VMSTATE_UINT8(fpi, NvmeIdNs),
VMSTATE_UINT8(dlfeat, NvmeIdNs),
VMSTATE_UINT16(nawun, NvmeIdNs),
VMSTATE_UINT16(nawupf, NvmeIdNs),
VMSTATE_UINT16(nacwu, NvmeIdNs),
VMSTATE_UINT16(nabsn, NvmeIdNs),
VMSTATE_UINT16(nabo, NvmeIdNs),
VMSTATE_UINT16(nabspf, NvmeIdNs),
VMSTATE_UINT16(noiob, NvmeIdNs),
VMSTATE_UINT8_ARRAY(nvmcap, NvmeIdNs, 16),
VMSTATE_UINT16(npwg, NvmeIdNs),
VMSTATE_UINT16(npwa, NvmeIdNs),
VMSTATE_UINT16(npdg, NvmeIdNs),
VMSTATE_UINT16(npda, NvmeIdNs),
VMSTATE_UINT16(nows, NvmeIdNs),
VMSTATE_UINT16(mssrl, NvmeIdNs),
VMSTATE_UINT32(mcl, NvmeIdNs),
VMSTATE_UINT8(msrc, NvmeIdNs),
VMSTATE_UINT8_ARRAY(rsvd81, NvmeIdNs, 18),
VMSTATE_UINT8(nsattr, NvmeIdNs),
VMSTATE_UINT16(nvmsetid, NvmeIdNs),
VMSTATE_UINT16(endgid, NvmeIdNs),
VMSTATE_UINT8_ARRAY(nguid, NvmeIdNs, 16),
VMSTATE_UINT64(eui64, NvmeIdNs),
VMSTATE_STRUCT_ARRAY(lbaf, NvmeIdNs, NVME_MAX_NLBAF, 1,
nvme_vmstate_lbaf, NvmeLBAF),
VMSTATE_UINT8_ARRAY(vs, NvmeIdNs, 3712),
VMSTATE_END_OF_LIST()
}
};
static const VMStateDescription nvme_vmstate_id_ns_nvm = {
.name = "nvme_id_ns_nvm",
.version_id = 1,
.minimum_version_id = 1,
.fields = (const VMStateField[]) {
VMSTATE_UINT64(lbstm, NvmeIdNsNvm),
VMSTATE_UINT8(pic, NvmeIdNsNvm),
VMSTATE_UINT8_ARRAY(rsvd9, NvmeIdNsNvm, 3),
VMSTATE_UINT32_ARRAY(elbaf, NvmeIdNsNvm, NVME_MAX_NLBAF),
VMSTATE_UINT32(npdgl, NvmeIdNsNvm),
VMSTATE_UINT32(nprg, NvmeIdNsNvm),
VMSTATE_UINT32(npra, NvmeIdNsNvm),
VMSTATE_UINT32(nors, NvmeIdNsNvm),
VMSTATE_UINT32(npdal, NvmeIdNsNvm),
VMSTATE_UINT8_ARRAY(rsvd288, NvmeIdNsNvm, 3808),
VMSTATE_END_OF_LIST()
}
};
static const VMStateDescription nvme_vmstate_id_ns_ind = {
.name = "nvme_id_ns_ind",
.version_id = 1,
.minimum_version_id = 1,
.fields = (const VMStateField[]) {
VMSTATE_UINT8(nsfeat, NvmeIdNsInd),
VMSTATE_UINT8(nmic, NvmeIdNsInd),
VMSTATE_UINT8(rescap, NvmeIdNsInd),
VMSTATE_UINT8(fpi, NvmeIdNsInd),
VMSTATE_UINT32(anagrpid, NvmeIdNsInd),
VMSTATE_UINT8(nsattr, NvmeIdNsInd),
VMSTATE_UINT8(rsvd9, NvmeIdNsInd),
VMSTATE_UINT16(nvmsetid, NvmeIdNsInd),
VMSTATE_UINT16(endgrpid, NvmeIdNsInd),
VMSTATE_UINT8(nstat, NvmeIdNsInd),
VMSTATE_UINT8_ARRAY(rsvd15, NvmeIdNsInd, 4081),
VMSTATE_END_OF_LIST()
}
};
typedef struct TmpNvmeNamespace {
NvmeNamespace *parent;
bool enable_write_cache;
} TmpNvmeNamespace;
static bool nvme_ns_tmp_pre_save(void *opaque, Error **errp)
{
struct TmpNvmeNamespace *tns = opaque;
tns->enable_write_cache = blk_enable_write_cache(tns->parent->blkconf.blk);
return true;
}
static bool nvme_ns_tmp_post_load(void *opaque, int version_id, Error **errp)
{
struct TmpNvmeNamespace *tns = opaque;
blk_set_enable_write_cache(tns->parent->blkconf.blk,
tns->enable_write_cache);
return true;
}
static const VMStateDescription nvme_vmstate_ns_tmp = {
.name = "nvme_ns_tmp",
.pre_save_errp = nvme_ns_tmp_pre_save,
.post_load_errp = nvme_ns_tmp_post_load,
.fields = (const VMStateField[]) {
VMSTATE_BOOL(enable_write_cache, TmpNvmeNamespace),
VMSTATE_END_OF_LIST()
}
};
const VMStateDescription nvme_vmstate_ns = {
.name = "nvme_ns",
.version_id = 1,
.minimum_version_id = 1,
.fields = (const VMStateField[]) {
VMSTATE_WITH_TMP(NvmeNamespace, TmpNvmeNamespace, nvme_vmstate_ns_tmp),
VMSTATE_STRUCT(id_ns, NvmeNamespace, 0, nvme_vmstate_id_ns, NvmeIdNs),
VMSTATE_STRUCT(id_ns_nvm, NvmeNamespace, 0,
nvme_vmstate_id_ns_nvm, NvmeIdNsNvm),
VMSTATE_STRUCT(id_ns_ind, NvmeNamespace, 0,
nvme_vmstate_id_ns_ind, NvmeIdNsInd),
VMSTATE_STRUCT(lbaf, NvmeNamespace, 0, nvme_vmstate_lbaf, NvmeLBAF),
VMSTATE_UINT32(nlbaf, NvmeNamespace),
VMSTATE_UINT8(csi, NvmeNamespace),
VMSTATE_UINT16(status, NvmeNamespace),
VMSTATE_UINT8(pif, NvmeNamespace),
VMSTATE_UINT16(zns.zrwas, NvmeNamespace),
VMSTATE_UINT16(zns.zrwafg, NvmeNamespace),
VMSTATE_UINT32(zns.numzrwa, NvmeNamespace),
VMSTATE_UINT32(features.err_rec, NvmeNamespace),
VMSTATE_STRUCT(atomic, NvmeNamespace, 0,
nvme_vmstate_atomic, NvmeAtomic),
VMSTATE_END_OF_LIST()
}
};
static const Property nvme_ns_props[] = {
DEFINE_BLOCK_PROPERTIES(NvmeNamespace, blkconf),
DEFINE_PROP_BOOL("detached", NvmeNamespace, params.detached, false),
@@ -937,6 +1107,8 @@ static void nvme_ns_class_init(ObjectClass *oc, const void *data)
dc->bus_type = TYPE_NVME_BUS;
dc->realize = nvme_ns_realize;
dc->unrealize = nvme_ns_unrealize;
dc->hotpluggable = true;
dc->vmsd = &nvme_vmstate_ns;
device_class_set_props(dc, nvme_ns_props);
dc->desc = "Virtual NVMe namespace";
}

View File

@@ -160,7 +160,14 @@ typedef struct NvmeZone {
#define NVME_FDP_MAX_NS_RUHS 32u
#define FDPVSS 0
static const uint8_t nvme_fdp_evf_shifts[FDP_EVT_MAX] = {
/*
* NOTE: Apart from event type 0, any event type with a shift value of 0 is
* considered unsupported and thus skipped in get/set features calls.
*
* NOTE: NvmeRuHandle uses a 64bit event mask - refactor to support event types
* of 63 or greater.
*/
static const uint8_t nvme_fdp_evf_shifts[FDP_EVT_MAX + 1] = {
/* Host events */
[FDP_EVT_RU_NOT_FULLY_WRITTEN] = 0,
[FDP_EVT_RU_ATL_EXCEEDED] = 1,
@@ -444,6 +451,11 @@ typedef struct NvmeRequest {
NvmeSg sg;
bool atomic_write;
QTAILQ_ENTRY(NvmeRequest)entry;
/*
* If you add a new field here, please make sure to update
* nvme_vmstate_request, pre_save_validate_aer_req() and
* pre_save_validate_cq_req().
*/
} NvmeRequest;
typedef struct NvmeBounceContext {
@@ -640,6 +652,7 @@ typedef struct NvmeCtrl {
NvmeNamespace namespace;
NvmeNamespace *namespaces[NVME_MAX_NAMESPACES + 1];
uint32_t num_queues;
NvmeSQueue **sq;
NvmeCQueue **cq;
NvmeSQueue admin_sq;
@@ -668,6 +681,9 @@ typedef struct NvmeCtrl {
/* Socket mapping to SPDM over NVMe Security In/Out commands */
int spdm_socket;
/* Migration-related stuff */
Error *migration_blocker;
} NvmeCtrl;
typedef enum NvmeResetType {
@@ -748,4 +764,7 @@ void nvme_atomic_configure_max_write_size(bool dn, uint16_t awun,
void nvme_ns_atomic_configure_boundary(bool dn, uint16_t nabsn,
uint16_t nabspf, NvmeAtomic *atomic);
extern const VMStateDescription nvme_vmstate_atomic;
extern const VMStateDescription nvme_vmstate_ns;
#endif /* HW_NVME_NVME_H */

View File

@@ -9,6 +9,7 @@
#include "qemu/osdep.h"
#include "qemu/units.h"
#include "qapi/error.h"
#include "hw/core/qdev.h"
#include "nvme.h"
@@ -205,6 +206,7 @@ static void nvme_subsys_realize(DeviceState *dev, Error **errp)
NvmeSubsystem *subsys = NVME_SUBSYS(dev);
qbus_init(&subsys->bus, sizeof(NvmeBus), TYPE_NVME_BUS, dev, dev->id);
qbus_set_bus_hotplug_handler(BUS(&subsys->bus));
nvme_subsys_setup(subsys, errp);
}

View File

@@ -7,6 +7,16 @@ pci_nvme_dbbuf_config(uint64_t dbs_addr, uint64_t eis_addr) "dbs_addr=0x%"PRIx64
pci_nvme_map_addr(uint64_t addr, uint64_t len) "addr 0x%"PRIx64" len %"PRIu64""
pci_nvme_map_addr_cmb(uint64_t addr, uint64_t len) "addr 0x%"PRIx64" len %"PRIu64""
pci_nvme_map_prp(uint64_t trans_len, uint32_t len, uint64_t prp1, uint64_t prp2, int num_prps) "trans_len %"PRIu64" len %"PRIu32" prp1 0x%"PRIx64" prp2 0x%"PRIx64" num_prps %d"
pci_nvme_pre_save_enter(void *n) "n=%p"
pci_nvme_pre_save_ns_drain(void *n, int i) "n=%p i=%d"
pci_nvme_pre_save_sq_out_req_check(void *n, int i, uint32_t head, uint32_t tail, uint32_t size) "n=%p i=%d head=0x%"PRIx32" tail=0x%"PRIx32" size=0x%"PRIx32""
pci_nvme_pre_save_cq_req_check(void *n, int i, uint32_t head, uint32_t tail, uint32_t size) "n=%p i=%d head=0x%"PRIx32" tail=0x%"PRIx32" size=0x%"PRIx32""
pci_nvme_pre_save_cq_unposted_cqe(void *n, int i, uint16_t cid, uint32_t nsid, uint32_t dw0, uint32_t dw1, uint16_t status, uint8_t opc) "n=%p i=%d cid %"PRIu16" nsid %"PRIu32" dw0 0x%"PRIx32" dw1 0x%"PRIx32" status 0x%"PRIx16" opc 0x%"PRIx8""
pci_nvme_pre_save_aer(uint8_t typ, uint8_t info, uint8_t log_page) "type 0x%"PRIx8" info 0x%"PRIx8" lid 0x%"PRIx8""
pci_nvme_post_load_enter(void *n) "n=%p"
pci_nvme_post_load_restore_cq(void *n, int i, uint32_t head, uint32_t tail, uint32_t size) "n=%p i=%d head=0x%"PRIx32" tail=0x%"PRIx32" size=0x%"PRIx32""
pci_nvme_post_load_restore_sq(void *n, int i, uint32_t head, uint32_t tail, uint32_t size) "n=%p i=%d head=0x%"PRIx32" tail=0x%"PRIx32" size=0x%"PRIx32""
pci_nvme_post_load_aer(uint8_t typ, uint8_t info, uint8_t log_page) "type 0x%"PRIx8" info 0x%"PRIx8" lid 0x%"PRIx8""
pci_nvme_map_sgl(uint8_t typ, uint64_t len) "type 0x%"PRIx8" len %"PRIu64""
pci_nvme_io_cmd(uint16_t cid, uint32_t nsid, uint16_t sqid, uint8_t opcode, const char *opname) "cid %"PRIu16" nsid 0x%"PRIx32" sqid %"PRIu16" opc 0x%"PRIx8" opname '%s'"
pci_nvme_admin_cmd(uint16_t cid, uint16_t sqid, uint8_t opcode, const char *opname) "cid %"PRIu16" sqid %"PRIu16" opc 0x%"PRIx8" opname '%s'"

View File

@@ -141,6 +141,18 @@ enum NvmeCapMask {
#define NVME_CAP_SET_CMBS(cap, val) \
((cap) |= (uint64_t)((val) & CAP_CMBS_MASK) << CAP_CMBS_SHIFT)
#define NVME_MIGRATION_SUPPORTED_CAP_BITS ( \
((uint64_t)CAP_MQES_MASK << CAP_MQES_SHIFT) \
| ((uint64_t)CAP_CQR_MASK << CAP_CQR_SHIFT) \
| ((uint64_t)CAP_AMS_MASK << CAP_AMS_SHIFT) \
| ((uint64_t)CAP_TO_MASK << CAP_TO_SHIFT) \
| ((uint64_t)CAP_DSTRD_MASK << CAP_DSTRD_SHIFT) \
| ((uint64_t)CAP_NSSRS_MASK << CAP_NSSRS_SHIFT) \
| ((uint64_t)CAP_CSS_MASK << CAP_CSS_SHIFT) \
| ((uint64_t)CAP_MPSMIN_MASK << CAP_MPSMIN_SHIFT) \
| ((uint64_t)CAP_MPSMAX_MASK << CAP_MPSMAX_SHIFT) \
)
enum NvmeCapCss {
NVME_CAP_CSS_NCSS = 1 << 0,
NVME_CAP_CSS_IOCSS = 1 << 6,

View File

@@ -40,19 +40,36 @@ class MigrationTest(QemuSystemTest):
self.assertEqual(dst_vm.cmd('query-status')['status'], 'running')
self.assertEqual(src_vm.cmd('query-status')['status'],'postmigrate')
# Can be overridden by subclasses to configure both source/dest VMs.
def configure_machine(self, vm):
vm.add_args('-nodefaults')
# Can be overridden by subclasses to prepare the source VM before
# migration, e.g. by running some workload inside the source VM
# to see if it continues to run properly after migration.
def launch_source_vm(self, vm):
vm.launch()
# Can be overridden by subclasses to check the destination VM after
# migration, e.g. by checking if the workload is still running after
# migration.
def assert_dest_vm(self, vm):
pass
def migrate_vms(self, dst_uri, src_uri, dst_vm, src_vm):
dst_vm.qmp('migrate-incoming', uri=dst_uri)
src_vm.qmp('migrate', uri=src_uri)
self.assert_migration(src_vm, dst_vm)
self.assert_dest_vm(dst_vm)
def migrate(self, dst_uri, src_uri=None):
dst_vm = self.get_vm('-incoming', 'defer', name="dst-qemu")
dst_vm.add_args('-nodefaults')
self.configure_machine(dst_vm)
dst_vm.launch()
src_vm = self.get_vm(name="src-qemu")
src_vm.add_args('-nodefaults')
src_vm.launch()
self.configure_machine(src_vm)
self.launch_source_vm(src_vm)
if src_uri is None:
src_uri = dst_uri

View File

@@ -37,6 +37,7 @@ tests_x86_64_system_thorough = [
'linux_initrd',
'multiprocess',
'netdev_ethtool',
'nvme_migration',
'replay',
'reverse_debug',
'tuxrun',

View File

@@ -0,0 +1,172 @@
#!/usr/bin/env python3
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
# x86_64 NVMe migration test
from migration import MigrationTest
from qemu_test import QemuSystemTest, Asset
from qemu_test import wait_for_console_pattern
from qemu_test import exec_command, exec_command_and_wait_for_pattern
class X8664NVMeMigrationTest(MigrationTest):
ASSET_KERNEL = Asset(
('https://archives.fedoraproject.org/pub/archive/fedora/linux/releases'
'/31/Server/x86_64/os/images/pxeboot/vmlinuz'),
'd4738d03dbbe083ca610d0821d0a8f1488bebbdccef54ce33e3adb35fda00129')
ASSET_INITRD = Asset(
('https://archives.fedoraproject.org/pub/archive/fedora/linux/releases'
'/31/Server/x86_64/os/images/pxeboot/initrd.img'),
'277cd6c7adf77c7e63d73bbb2cded8ef9e2d3a2f100000e92ff1f8396513cd8b')
ASSET_DISKIMAGE = Asset(
('https://archives.fedoraproject.org/pub/archive/fedora/linux/releases'
'/31/Cloud/x86_64/images/Fedora-Cloud-Base-31-1.9.x86_64.qcow2'),
'e3c1b309d9203604922d6e255c2c5d098a309c2d46215d8fc026954f3c5c27a0')
DEFAULT_KERNEL_PARAMS = ('root=/dev/nvme0n1p1 console=ttyS0 net.ifnames=0 '
'rd.rescue quiet')
def wait_for_console_pattern(self, success_message, vm):
wait_for_console_pattern(
self,
success_message,
failure_message="Kernel panic - not syncing",
vm=vm,
)
def exec_command_and_check(self, command, vm):
prompt = '# '
exec_command_and_wait_for_pattern(self,
f"{command} && echo OK || echo FAIL",
'FAIL', vm=vm)
# Note, that commands we send to the console are echo-ed back,
# so if we have a word "FAIL" in the command itself, we should
# expect to see it once.
wait_for_console_pattern(self, 'OK', failure_message="FAIL", vm=vm)
self.wait_for_console_pattern(prompt, vm)
def configure_machine(self, vm):
kernel_path = self.ASSET_KERNEL.fetch()
initrd_path = self.ASSET_INITRD.fetch()
diskimage_path = self.ASSET_DISKIMAGE.fetch()
vm.set_console()
vm.add_args("-cpu", "max")
vm.add_args("-m", "2G")
vm.add_args("-accel", "kvm")
vm.add_args('-drive',
f'file={diskimage_path},if=none,id=drv0,snapshot=on')
vm.add_args('-device', 'nvme,bus=pcie.0,' +
'drive=drv0,id=nvme-disk0,serial=nvmemigtest,bootindex=1')
vm.add_args(
"-kernel",
kernel_path,
"-initrd",
initrd_path,
"-append",
self.DEFAULT_KERNEL_PARAMS
)
def launch_source_vm(self, vm):
vm.launch()
self.wait_for_console_pattern('Entering emergency mode.', vm)
prompt = '# '
self.wait_for_console_pattern(prompt, vm)
# Synchronize on NVMe driver creating the root device
exec_command_and_wait_for_pattern(self,
"while ! (dmesg -c | grep nvme0n1:) ; do sleep 1 ; done",
"nvme0n1", vm=vm)
self.wait_for_console_pattern(prompt, vm)
# prepare system
exec_command_and_wait_for_pattern(self, 'mount /dev/nvme0n1p1 /sysroot',
prompt, vm=vm)
exec_command_and_wait_for_pattern(self, 'chroot /sysroot',
prompt, vm=vm)
exec_command_and_wait_for_pattern(self, 'mount -t proc proc /proc',
prompt, vm=vm)
exec_command_and_wait_for_pattern(self, 'mount -t sysfs sysfs /sys',
prompt, vm=vm)
# Run workload before migration to check if it continues
# to run properly after migration.
#
# Workload is simple: it continuously calculates checksums of
# all files in /usr/bin to generate some I/O load on
# the NVMe disk and at the same time it drops caches to
# make sure that we have some read I/O on the disk as well.
# If there are any issues with the migration of the NVMe device,
# we should see errors in dmesg and consequently in the workload log.
exec_command_and_wait_for_pattern(self,
"(while [ ! -f /tmp/test_nvme_mig_workload.stop ]; do \
rm -f /tmp/test_nvme_mig_workload.iter_finished; \
echo 3 > /proc/sys/vm/drop_caches; \
find /usr/bin -type f -exec cksum {} \\;; \
touch /tmp/test_nvme_mig_workload.iter_finished; \
done) > /dev/null 2> /tmp/test_nvme_mig_workload.errors &",
prompt, vm=vm)
exec_command_and_wait_for_pattern(self,
'echo $! > /tmp/test_nvme_mig_workload.pid',
prompt, vm=vm)
# check if process is alive and running
self.exec_command_and_check(
"kill -0 $(cat /tmp/test_nvme_mig_workload.pid)", vm)
def assert_dest_vm(self, vm):
prompt = '# '
# check if process is alive and running after migration,
# if not - fail the test
self.exec_command_and_check(
"kill -0 $(cat /tmp/test_nvme_mig_workload.pid)", vm)
# signal workload to stop
exec_command_and_wait_for_pattern(self,
'touch /tmp/test_nvme_mig_workload.stop',
prompt, vm=vm)
# wait workload to finish, because we want to examine log
# to see if there are any errors
exec_command_and_wait_for_pattern(self,
"while [ ! -f /tmp/test_nvme_mig_workload.iter_finished ]; do \
sleep 1; \
done;",
prompt, vm=vm)
exec_command_and_wait_for_pattern(self,
'cat /tmp/test_nvme_mig_workload.errors',
prompt, vm=vm)
# fail the test if non-empty
self.exec_command_and_check(
"[ ! -s /tmp/test_nvme_mig_workload.errors ]", vm)
def test_migration_with_tcp_localhost(self):
self.set_machine('q35')
self.require_accelerator("kvm")
self.migration_with_tcp_localhost()
def test_migration_with_unix(self):
self.set_machine('q35')
self.require_accelerator("kvm")
self.migration_with_unix()
def test_migration_with_exec(self):
self.set_machine('q35')
self.require_accelerator("kvm")
self.migration_with_exec()
if __name__ == '__main__':
MigrationTest.main()

View File

@@ -8,9 +8,12 @@
*/
#include "qemu/osdep.h"
#include <glib/gstdio.h>
#include "qemu/bswap.h"
#include "qemu/module.h"
#include "qemu/units.h"
#include "libqtest.h"
#include "libqtest-single.h"
#include "libqos/qgraph.h"
#include "libqos/pci.h"
#include "block/nvme.h"
@@ -142,6 +145,420 @@ static void nvmetest_pmr_reg_test(void *obj, void *data, QGuestAllocator *alloc)
qpci_iounmap(pdev, pmr_bar);
}
typedef struct nvme_ctrl nvme_ctrl;
typedef struct nvme_queue {
nvme_ctrl *ctrl;
uint64_t doorbell;
uint32_t size;
} nvme_queue;
typedef struct nvme_cq {
nvme_queue common;
uint64_t phys_cqe; /* NvmeCqe* */
uint16_t head;
uint8_t phase;
} nvme_cq;
typedef struct nvme_sq {
nvme_queue common;
uint64_t phys_sqe; /* NvmeCmd* */
nvme_cq *cq;
uint16_t head;
uint16_t tail;
} nvme_sq;
struct nvme_ctrl {
QGuestAllocator *alloc;
QPCIDevice *pdev;
QPCIBar bar;
uint32_t db_stride;
nvme_sq admin_sq;
nvme_cq admin_cq;
};
#define PHYS_ADDR_OF_FIELD(T, base_phys_addr, field) \
((uint64_t)&((T *)(base_phys_addr))->field)
#define PHYS_ADDR_OF(T, base_phys_addr, accessor) \
((uint64_t)&((T *)(base_phys_addr))accessor)
static void nvme_init_queue_common(nvme_ctrl *ctrl, nvme_queue *q,
uint16_t db_idx, uint32_t size)
{
q->ctrl = ctrl;
q->doorbell = (sizeof(NvmeBar) + db_idx * ctrl->db_stride);
g_test_message(" q %p db_idx %u doorbell 0x%" PRIx64, q, db_idx, q->doorbell);
q->size = size;
}
static void nvme_init_sq(nvme_ctrl *ctrl, nvme_sq *sq, uint16_t db_idx,
uint32_t size, nvme_cq *cq)
{
nvme_init_queue_common(ctrl, &sq->common, db_idx, size);
sq->phys_sqe = guest_alloc(ctrl->alloc, sizeof(NvmeCmd) * size);
g_assert(sq->phys_sqe);
g_test_message("sq %p db_idx %u sqe 0x%" PRIx64, sq, db_idx, sq->phys_sqe);
sq->cq = cq;
sq->head = 0;
sq->tail = 0;
}
static void nvme_init_cq(nvme_ctrl *ctrl, nvme_cq *cq, uint16_t db_idx,
uint32_t size)
{
nvme_init_queue_common(ctrl, &cq->common, db_idx, size);
cq->phys_cqe = guest_alloc(ctrl->alloc, sizeof(NvmeCqe) * size);
g_assert(cq->phys_cqe);
g_test_message("cq %p db_idx %u cqe 0x%" PRIx64, cq, db_idx, cq->phys_cqe);
cq->head = 0;
cq->phase = 1;
}
static int nvme_cqe_pending(nvme_cq *cq)
{
uint16_t status = qtest_readw(
cq->common.ctrl->pdev->bus->qts,
PHYS_ADDR_OF(NvmeCqe, cq->phys_cqe, [cq->head].status));
return (status & 1) == cq->phase;
}
static int nvme_is_cqe_success(NvmeCqe *cqe)
{
return (le16_to_cpu(cqe->status) >> 1) == NVME_SUCCESS;
}
static NvmeCqe nvme_handle_cqe(nvme_sq *sq)
{
nvme_cq *cq = sq->cq;
uint64_t phys_cqe = PHYS_ADDR_OF(
NvmeCqe, cq->phys_cqe, [cq->head]); /* NvmeCqe* */
NvmeCqe cqe;
uint16_t cq_next_head;
g_assert(nvme_cqe_pending(cq));
qtest_memread(sq->common.ctrl->pdev->bus->qts, phys_cqe, &cqe, sizeof(cqe));
cq_next_head = (cq->head + 1) % cq->common.size;
g_test_message("cq %p head %u -> %u", cq, cq->head, cq_next_head);
if (cq_next_head < cq->head) {
cq->phase ^= 1;
}
cq->head = cq_next_head;
if (cqe.sq_head != sq->head) {
sq->head = cqe.sq_head;
g_test_message("sq %p head = %u", sq, sq->head);
}
qpci_io_writel(cq->common.ctrl->pdev, cq->common.ctrl->bar,
cq->common.doorbell, cq->head);
return cqe;
}
static NvmeCqe nvme_wait(nvme_sq *sq)
{
int i;
bool ready = false;
for (i = 0; i < 10; i++) {
if (nvme_cqe_pending(sq->cq)) {
ready = true;
break;
}
g_usleep(1000);
}
g_assert(ready);
return nvme_handle_cqe(sq);
}
static uint64_t nvme_get_next_sqe(nvme_sq *sq, uint8_t opcode,
uint16_t cid, uint64_t prp1)
{
uint64_t phys_sqe = PHYS_ADDR_OF(NvmeCmd, sq->phys_sqe, [sq->tail]);
if (((sq->tail + 1) % sq->common.size) == sq->head) {
/* no space in SQ */
g_test_message("%s head %d tail %d", __func__, sq->head, sq->tail);
g_assert_not_reached();
return 0;
}
qtest_memset(sq->common.ctrl->pdev->bus->qts,
phys_sqe, 0, sizeof(NvmeCmd));
#define GUEST_MEM_WRITE(fn, phys_addr, val) \
fn(sq->common.ctrl->pdev->bus->qts, phys_addr, (val))
GUEST_MEM_WRITE(qtest_writeb,
PHYS_ADDR_OF_FIELD(NvmeCmd, phys_sqe, opcode), opcode);
GUEST_MEM_WRITE(qtest_writew,
PHYS_ADDR_OF_FIELD(NvmeCmd, phys_sqe, cid), cid);
GUEST_MEM_WRITE(qtest_writeq,
PHYS_ADDR_OF_FIELD(NvmeCmd, phys_sqe, dptr.prp1), prp1);
#undef GUEST_MEM_WRITE
g_test_message("sq %p next_sqe %u sqe 0x%" PRIx64, sq, sq->tail, phys_sqe);
return phys_sqe;
}
static void nvme_commit_sqe(nvme_sq *sq)
{
g_test_message("sq %p commit sqe tail %u", sq, sq->tail);
sq->tail = (sq->tail + 1) % sq->common.size;
qpci_io_writel(sq->common.ctrl->pdev, sq->common.ctrl->bar,
sq->common.doorbell, sq->tail);
}
static uint64_t nvme_admin_identify_ctrl(nvme_ctrl *ctrl,
uint16_t cid, bool no_wait)
{
uint64_t phys_cmd_identify; /* NvmeCmd* */
uint64_t phys_identify; /* NvmeIdCtrl* */
NvmeCqe cqe;
g_test_message("sending req cid %u no_wait %d", cid, no_wait);
phys_identify = guest_alloc(ctrl->alloc, sizeof(NvmeIdCtrl));
g_assert(phys_identify);
phys_cmd_identify = nvme_get_next_sqe(&ctrl->admin_sq,
NVME_ADM_CMD_IDENTIFY, cid,
phys_identify);
g_assert(phys_cmd_identify);
#define GUEST_MEM_WRITE(fn, phys_addr, val) \
fn(ctrl->pdev->bus->qts, phys_addr, (val))
GUEST_MEM_WRITE(qtest_writel,
PHYS_ADDR_OF_FIELD(NvmeCmd, phys_cmd_identify, nsid), 0);
GUEST_MEM_WRITE(qtest_writel,
PHYS_ADDR_OF_FIELD(NvmeIdentify, phys_cmd_identify, cns),
NVME_ID_CNS_CTRL);
#undef GUEST_MEM_WRITE
nvme_commit_sqe(&ctrl->admin_sq);
if (no_wait) {
return phys_identify;
}
cqe = nvme_wait(&ctrl->admin_sq);
g_assert(nvme_is_cqe_success(&cqe));
g_assert(le16_to_cpu(cqe.cid) == cid);
return phys_identify;
}
static void nvme_wait_ready(nvme_ctrl *ctrl, int val)
{
int i;
for (i = 0; i < 10; i++) {
uint32_t csts = qpci_io_readl(ctrl->pdev, ctrl->bar, NVME_REG_CSTS);
g_test_message("%s: csts %x", __func__, csts);
if (NVME_CSTS_RDY(csts) == val) {
return;
}
g_usleep(1000);
}
g_assert_not_reached();
}
static void test_migrate_setup_nvme_ctrl(nvme_ctrl *ctrl)
{
uint64_t cap;
/* disable controller */
qpci_io_writel(ctrl->pdev, ctrl->bar, NVME_REG_CC, 0);
nvme_wait_ready(ctrl, 0);
cap = qpci_io_readq(ctrl->pdev, ctrl->bar, NVME_REG_CAP);
ctrl->db_stride = 4 << NVME_CAP_DSTRD(cap);
nvme_init_cq(ctrl, &ctrl->admin_cq, 1, 2 /* CQEs num */);
nvme_init_sq(ctrl, &ctrl->admin_sq, 0, 4 /* SQEs num */, &ctrl->admin_cq);
qpci_io_writel(ctrl->pdev, ctrl->bar, NVME_REG_AQA,
((ctrl->admin_cq.common.size - 1) << AQA_ACQS_SHIFT) |
((ctrl->admin_sq.common.size - 1) << AQA_ASQS_SHIFT)
);
qpci_io_writeq(ctrl->pdev, ctrl->bar,
NVME_REG_ASQ, (uint64_t)ctrl->admin_sq.phys_sqe);
qpci_io_writeq(ctrl->pdev, ctrl->bar,
NVME_REG_ACQ, (uint64_t)ctrl->admin_cq.phys_cqe);
/* enable controller */
{
uint32_t cc = 0;
NVME_SET_CC_EN(cc, 1);
qpci_io_writel(ctrl->pdev, ctrl->bar, NVME_REG_CC, cc);
}
nvme_wait_ready(ctrl, 1);
}
typedef struct test_migrate_req {
uint16_t cid;
bool handle_cqe;
uint64_t phys_identify; /* NvmeIdCtrl* */
} test_migrate_req;
static void test_migrate_send_nvme_reqs(nvme_ctrl *ctrl, test_migrate_req *reqs,
int num)
{
int i;
for (i = 0; i < num; i++) {
reqs[i].phys_identify = nvme_admin_identify_ctrl(ctrl, reqs[i].cid,
!reqs[i].handle_cqe);
g_assert(reqs[i].phys_identify);
if (reqs[i].handle_cqe) {
guest_free(ctrl->alloc, reqs[i].phys_identify);
}
}
}
static void test_migrate_check_nvme(nvme_ctrl *ctrl,
test_migrate_req *reqs, int num)
{
int i;
for (i = 0; i < num; i++) {
NvmeCqe cqe;
if (reqs[i].handle_cqe) {
continue;
}
cqe = nvme_wait(&ctrl->admin_sq);
g_assert(nvme_is_cqe_success(&cqe));
g_assert_cmpint(le16_to_cpu(cqe.cid), ==, reqs[i].cid);
#define GUEST_MEM_READB(phys_addr) \
qtest_readb(ctrl->pdev->bus->qts, (phys_addr))
g_assert_cmpint(GUEST_MEM_READB(
PHYS_ADDR_OF_FIELD(NvmeIdCtrl, reqs[i].phys_identify, ieee[0])),
==, 0x0);
g_assert_cmpint(GUEST_MEM_READB(
PHYS_ADDR_OF_FIELD(NvmeIdCtrl, reqs[i].phys_identify, ieee[1])),
==, 0x54);
g_assert_cmpint(GUEST_MEM_READB(
PHYS_ADDR_OF_FIELD(NvmeIdCtrl, reqs[i].phys_identify, ieee[2])),
==, 0x52);
#undef GUEST_MEM_READB
guest_free(ctrl->alloc, reqs[i].phys_identify);
}
}
static void test_migrate(void *obj, void *data, QGuestAllocator *alloc)
{
g_autofree gchar *tmpfs = NULL;
GError *err = NULL;
g_autofree gchar *mig_path = NULL;
g_autofree gchar *uri = NULL;
GString *dest_cmdline;
QTestState *to;
QDict *rsp;
QNvme *nvme = obj;
QPCIDevice *pdev = &nvme->dev;
g_autofree nvme_ctrl *ctrl = NULL;
test_migrate_req test_reqs[] = {
{ 123, true },
{ 456, false },
{ 300, false },
{ 333, false }
};
if (qpci_check_buggy_msi(pdev)) {
return;
}
/* create temporary dir and prepare unix socket path for migration */
tmpfs = g_dir_make_tmp("nvme-test-XXXXXX", &err);
if (!tmpfs) {
g_test_message("Can't create temporary directory in %s: %s",
g_get_tmp_dir(), err->message);
g_error_free(err);
}
g_assert(tmpfs);
mig_path = g_strdup_printf("%s/socket.mig", tmpfs);
uri = g_strdup_printf("unix:%s", mig_path);
/* enable NVMe PCI device */
qpci_device_enable(pdev);
ctrl = g_malloc0(sizeof(*ctrl));
ctrl->alloc = alloc;
ctrl->pdev = pdev;
ctrl->bar = qpci_iomap(ctrl->pdev, 0, NULL);
g_assert(pdev->bus->qts == global_qtest);
test_migrate_setup_nvme_ctrl(ctrl);
test_migrate_send_nvme_reqs(ctrl, test_reqs, ARRAY_SIZE(test_reqs));
qpci_iounmap(ctrl->pdev, ctrl->bar);
dest_cmdline = g_string_new(qos_get_current_command_line());
g_string_append_printf(dest_cmdline, " -incoming %s", uri);
/* Create destination VM */
to = qtest_init(dest_cmdline->str);
/* Get access to PCI device from destination VM */
nvme = qos_allocate_objects(to, &ctrl->alloc);
pdev = &nvme->dev;
ctrl->pdev = pdev;
ctrl->bar = qpci_iomap(ctrl->pdev, 0, NULL);
g_assert(pdev->bus->qts == to);
/* Migrate VM */
rsp = qmp("{ 'execute': 'migrate', 'arguments': { 'uri': %s } }", uri);
g_assert(qdict_haskey(rsp, "return"));
qobject_unref(rsp);
/* Wait when source VM is stopped */
qmp_eventwait("STOP");
/* Copy guest physical memory allocator state */
migrate_allocator(alloc, ctrl->alloc);
/* Wait for destination VM to become alive */
qtest_qmp_eventwait(to, "RESUME");
test_migrate_check_nvme(ctrl, test_reqs, ARRAY_SIZE(test_reqs));
qpci_iounmap(ctrl->pdev, ctrl->bar);
qtest_quit(to);
g_unlink(mig_path);
g_rmdir(tmpfs);
g_string_free(dest_cmdline, true);
}
static void nvme_register_nodes(void)
{
QOSGraphEdgeOptions opts = {
@@ -168,6 +585,8 @@ static void nvme_register_nodes(void)
});
qos_add_test("reg-read", "nvme", nvmetest_reg_read_test, NULL);
qos_add_test("migrate", "nvme", test_migrate, NULL);
}
libqos_init(nvme_register_nodes);