qemu-kvm/kvm-scsi-save-load-SCSI-reservation-state.patch
2026-07-21 09:48:32 -04:00

349 lines
12 KiB
Diff

From 91e9a0fbb3d6ee8726ea0ef17ff17404f823577a Mon Sep 17 00:00:00 2001
From: Stefan Hajnoczi <stefanha@redhat.com>
Date: Thu, 29 Jan 2026 16:20:34 -0500
Subject: [PATCH 5/7] scsi: save/load SCSI reservation state
RH-Author: Stefan Hajnoczi <stefanha@redhat.com>
RH-MergeRequest: 464: scsi: persistent reservation live migration
RH-Jira: RHEL-132749
RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com>
RH-Acked-by: Kevin Wolf <kwolf@redhat.com>
RH-Commit: [4/5] 2ebcfedf1a9e54433ed1ba3593d4fbcbadcd644b (stefanha/centos-stream-qemu-kvm)
Add a vmstate subsection to SCSIDiskState so that scsi-block devices can
transfer their reservation state during live migration. Upon loading the
subsection, the destination QEMU invokes the PERSISTENT RESERVE OUT
command's PREEMPT service action to atomically move the reservation from
the source I_T nexus to the destination I_T nexus. This results in
transparent live migration of SCSI reservations.
This approach is incomplete since SCSI reservations are cooperative and
other hosts could interfere. Neither the source QEMU nor the destination
QEMU are aware of changes made by other hosts. The assumption is that
reservation is not taken over by a third host without cooperation from
the source host.
I considered adding the vmstate subsection to SCSIDevice instead of
SCSIDiskState, since reservations are part of the SCSI Primary Commands
that other devices apart from disks could support. However, due to
fragility of migrating reservations, we will probably limit support to
scsi-block and maybe scsi-disk in the future. In the end, I think it
makes sense to place this within scsi-disk.c.
Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
Reviewed-by: Paolo Bonzini <pbonzini@redhat.com>
Message-id: 20260129212035.219676-5-stefanha@redhat.com
Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
(cherry picked from commit ab57b51f1375b6a6f098a74c6f79207a9630948d)
Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
Conflicts:
- hw/core/machine.c
Downstream does not have hw_compat_10_2. Make sure that machine types
prior to RHEL 10.2 default to migrate-pr=off.
- hw/scsi/scsi-disk.c
Downstream is missing commit 40de712a89d8f ("migration: Add
error-parameterized function variants in VMSD struct"), so add a
.post_load() wrapper function that reports the Error and returns
-EINVAL.
---
hw/core/machine.c | 1 +
hw/scsi/scsi-disk.c | 99 +++++++++++++++++++++++++++++++++++++++++-
hw/scsi/scsi-generic.c | 83 +++++++++++++++++++++++++++++++++++
hw/scsi/trace-events | 1 +
include/hw/scsi/scsi.h | 1 +
5 files changed, 184 insertions(+), 1 deletion(-)
diff --git a/hw/core/machine.c b/hw/core/machine.c
index 2a1a42cebc..2b339f6a13 100644
--- a/hw/core/machine.c
+++ b/hw/core/machine.c
@@ -295,6 +295,7 @@ const char *rhel_old_machine_deprecation =
"machine types for previous major releases are deprecated";
GlobalProperty hw_compat_rhel_10_2[] = {
+ { "scsi-block", "migrate-pr", "off" },
/* hw_compat_rhel_10_2 from hw_compat_10_0 */
{ "scsi-hd", "dpofua", "off" },
/* hw_compat_rhel_10_2 from hw_compat_10_0 */
diff --git a/hw/scsi/scsi-disk.c b/hw/scsi/scsi-disk.c
index c736988ef6..e7b821ad19 100644
--- a/hw/scsi/scsi-disk.c
+++ b/hw/scsi/scsi-disk.c
@@ -28,6 +28,7 @@
#include "qemu/hw-version.h"
#include "qemu/memalign.h"
#include "hw/scsi/scsi.h"
+#include "migration/misc.h"
#include "migration/qemu-file-types.h"
#include "migration/vmstate.h"
#include "hw/scsi/emulation.h"
@@ -122,6 +123,7 @@ struct SCSIDiskState {
*/
uint16_t rotation_rate;
bool migrate_emulated_scsi_request;
+ NotifierWithReturn migration_notifier;
};
static void scsi_free_request(SCSIRequest *req)
@@ -2737,6 +2739,29 @@ static SCSIRequest *scsi_new_request(SCSIDevice *d, uint32_t tag, uint32_t lun,
}
#ifdef __linux__
+/*
+ * Preempt on the SCSI Persistent Reservation on the source when migration
+ * fails because the destination may have already preempted and we need to get
+ * the reservation back.
+ */
+static int scsi_block_migration_notifier(NotifierWithReturn *notifier,
+ MigrationEvent *e, Error **errp)
+{
+ if (e->type == MIG_EVENT_PRECOPY_FAILED) {
+ SCSIDiskState *s =
+ container_of(notifier, SCSIDiskState, migration_notifier);
+ SCSIDevice *d = &s->qdev;
+ Error *local_err = NULL;
+
+ if (!scsi_generic_pr_state_preempt(d, &local_err)) {
+ /* MIG_EVENT_PRECOPY_FAILED cannot fail, so just warn */
+ error_prepend(&local_err, "scsi-block migration rollback: ");
+ warn_report_err(local_err);
+ }
+ }
+ return 0;
+}
+
static int get_device_type(SCSIDiskState *s)
{
uint8_t cmd[16];
@@ -2815,6 +2840,16 @@ static void scsi_block_realize(SCSIDevice *dev, Error **errp)
scsi_realize(&s->qdev, errp);
scsi_generic_read_device_inquiry(&s->qdev);
+
+ migration_add_notifier(&s->migration_notifier,
+ scsi_block_migration_notifier);
+}
+
+static void scsi_block_unrealize(SCSIDevice *dev)
+{
+ SCSIDiskState *s = DO_UPCAST(SCSIDiskState, qdev, dev);
+
+ migration_remove_notifier(&s->migration_notifier);
}
typedef struct SCSIBlockReq {
@@ -3209,6 +3244,60 @@ static const Property scsi_hd_properties[] = {
DEFINE_BLOCK_CHS_PROPERTIES(SCSIDiskState, qdev.conf),
};
+#ifdef __linux__
+static bool scsi_disk_pr_state_post_load_errp(void *opaque, int version_id,
+ Error **errp)
+{
+ SCSIDiskState *s = opaque;
+ SCSIDevice *dev = &s->qdev;
+
+ return scsi_generic_pr_state_preempt(dev, errp);
+}
+
+static int scsi_disk_pr_state_post_load(void *opaque, int version_id)
+{
+ SCSIDiskState *s = opaque;
+ Error *errp = NULL;
+
+ if (scsi_disk_pr_state_post_load_errp(s, version_id, &errp)) {
+ return 0;
+ } else {
+ error_report_err(errp);
+ return -EINVAL;
+ }
+}
+
+static bool scsi_disk_pr_state_needed(void *opaque)
+{
+ SCSIDiskState *s = opaque;
+ SCSIPRState *pr_state = &s->qdev.pr_state;
+ bool ret;
+
+ if (!s->qdev.migrate_pr) {
+ return false;
+ }
+
+ /* A reservation requires a key, so checking this field is enough */
+ WITH_QEMU_LOCK_GUARD(&pr_state->mutex) {
+ ret = pr_state->key;
+ }
+ return ret;
+}
+
+static const VMStateDescription vmstate_scsi_disk_pr_state = {
+ .name = "scsi-disk/pr",
+ .version_id = 1,
+ .minimum_version_id = 1,
+ .post_load = scsi_disk_pr_state_post_load,
+ .needed = scsi_disk_pr_state_needed,
+ .fields = (const VMStateField[]) {
+ VMSTATE_UINT64(qdev.pr_state.key, SCSIDiskState),
+ VMSTATE_UINT8(qdev.pr_state.resv_type, SCSIDiskState),
+ VMSTATE_END_OF_LIST()
+ }
+};
+#endif /* __linux__ */
+
static const VMStateDescription vmstate_scsi_disk_state = {
.name = "scsi-disk",
.version_id = 1,
@@ -3221,7 +3310,13 @@ static const VMStateDescription vmstate_scsi_disk_state = {
VMSTATE_BOOL(tray_open, SCSIDiskState),
VMSTATE_BOOL(tray_locked, SCSIDiskState),
VMSTATE_END_OF_LIST()
- }
+ },
+ .subsections = (const VMStateDescription * const []) {
+#ifdef __linux__
+ &vmstate_scsi_disk_pr_state,
+#endif
+ NULL
+ },
};
static void scsi_hd_class_initfn(ObjectClass *klass, const void *data)
@@ -3301,6 +3396,7 @@ static const Property scsi_block_properties[] = {
-1),
DEFINE_PROP_UINT32("io_timeout", SCSIDiskState, qdev.io_timeout,
DEFAULT_IO_TIMEOUT),
+ DEFINE_PROP_BOOL("migrate-pr", SCSIDiskState, qdev.migrate_pr, true),
};
static void scsi_block_class_initfn(ObjectClass *klass, const void *data)
@@ -3310,6 +3406,7 @@ static void scsi_block_class_initfn(ObjectClass *klass, const void *data)
SCSIDiskClass *sdc = SCSI_DISK_BASE_CLASS(klass);
sc->realize = scsi_block_realize;
+ sc->unrealize = scsi_block_unrealize;
sc->alloc_req = scsi_block_new_request;
sc->parse_cdb = scsi_block_parse_cdb;
sdc->dma_readv = scsi_block_dma_readv;
diff --git a/hw/scsi/scsi-generic.c b/hw/scsi/scsi-generic.c
index a6280eaa87..b8b3f399f0 100644
--- a/hw/scsi/scsi-generic.c
+++ b/hw/scsi/scsi-generic.c
@@ -423,6 +423,89 @@ static void scsi_handle_persistent_reserve_out_reply(
}
}
+static bool scsi_generic_pr_register(SCSIDevice *s, uint64_t key, Error **errp)
+{
+ uint8_t cmd[10] = {};
+ uint8_t buf[24] = {};
+ uint64_t key_be = cpu_to_be64(key);
+ int ret;
+
+ cmd[0] = PERSISTENT_RESERVE_OUT;
+ cmd[1] = PRO_REGISTER;
+ cmd[8] = sizeof(buf);
+ memcpy(&buf[8], &key_be, sizeof(key_be));
+
+ ret = scsi_SG_IO(s->conf.blk, SG_DXFER_TO_DEV, cmd, sizeof(cmd),
+ buf, sizeof(buf), s->io_timeout, errp);
+ if (ret < 0) {
+ error_prepend(errp, "PERSISTENT RESERVE OUT with REGISTER");
+ return false;
+ }
+ return true;
+}
+
+static bool scsi_generic_pr_preempt(SCSIDevice *s, uint64_t key,
+ uint8_t resv_type, Error **errp)
+{
+ uint8_t cmd[10] = {};
+ uint8_t buf[24] = {};
+ uint64_t key_be = cpu_to_be64(key);
+ int ret;
+
+ cmd[0] = PERSISTENT_RESERVE_OUT;
+ cmd[1] = PRO_PREEMPT;
+ cmd[2] = resv_type & 0xf;
+ cmd[8] = sizeof(buf);
+ memcpy(&buf[0], &key_be, sizeof(key_be));
+ memcpy(&buf[8], &key_be, sizeof(key_be));
+
+ ret = scsi_SG_IO(s->conf.blk, SG_DXFER_TO_DEV, cmd, sizeof(cmd),
+ buf, sizeof(buf), s->io_timeout, errp);
+ if (ret < 0) {
+ error_prepend(errp, "PERSISTENT RESERVE OUT with PREEMPT");
+ return false;
+ }
+ return true;
+}
+
+/* Register keys and preempt reservations after live migration */
+bool scsi_generic_pr_state_preempt(SCSIDevice *s, Error **errp)
+{
+ SCSIPRState *pr_state = &s->pr_state;
+ uint64_t key;
+ uint8_t resv_type;
+
+ WITH_QEMU_LOCK_GUARD(&pr_state->mutex) {
+ key = pr_state->key;
+ resv_type = pr_state->resv_type;
+ }
+
+ trace_scsi_generic_pr_state_preempt(key, resv_type);
+
+ if (key) {
+ if (!scsi_generic_pr_register(s, key, errp)) {
+ return false;
+ }
+
+ /*
+ * Two cases:
+ *
+ * 1. There is no reservation (resv_type is 0) and the other I_T nexus
+ * will be unregistered. This is important so the source host does
+ * not leak registered keys across live migration.
+ *
+ * 2. There is a reservation (resv_type is not 0) and the other I_T
+ * nexus will be unregistered and its reservation is atomically
+ * taken over by us. This is the scenario where a reservation is
+ * migrated along with the guest.
+ */
+ if (!scsi_generic_pr_preempt(s, key, resv_type, errp)) {
+ return false;
+ }
+ }
+ return true;
+}
+
static void scsi_read_complete(void * opaque, int ret)
{
SCSIGenericReq *r = (SCSIGenericReq *)opaque;
diff --git a/hw/scsi/trace-events b/hw/scsi/trace-events
index fdb87a237f..ab7a6f4cea 100644
--- a/hw/scsi/trace-events
+++ b/hw/scsi/trace-events
@@ -362,3 +362,4 @@ scsi_generic_aio_sgio_command(uint32_t tag, uint8_t cmd, uint32_t timeout) "gene
scsi_generic_ioctl_sgio_command(uint8_t cmd, uint32_t timeout) "generic ioctl sgio: cmd=0x%x timeout=%u"
scsi_generic_ioctl_sgio_done(uint8_t cmd, int ret, uint8_t status, uint8_t host_status) "generic ioctl sgio: cmd=0x%x ret=%d status=0x%x host_status=0x%x"
scsi_generic_persistent_reserve_out_reply(uint8_t service_action, uint8_t resv_type, uint64_t old_key, uint64_t new_key) "persistent reserve out reply service_action=%u resv_type=%u old_key=0x%" PRIx64 " new_key=0x%" PRIx64
+scsi_generic_pr_state_preempt(uint64_t key, uint8_t resv_type) "key=0x%" PRIx64 " resv_type=%u"
diff --git a/include/hw/scsi/scsi.h b/include/hw/scsi/scsi.h
index 7e120fdd6a..f61c63c5ea 100644
--- a/include/hw/scsi/scsi.h
+++ b/include/hw/scsi/scsi.h
@@ -253,6 +253,7 @@ SCSIDevice *scsi_device_get(SCSIBus *bus, int channel, int target, int lun);
/* scsi-generic.c. */
extern const SCSIReqOps scsi_generic_req_ops;
+bool scsi_generic_pr_state_preempt(SCSIDevice *s, Error **errp);
/* scsi-disk.c */
#define SCSI_DISK_QUIRK_MODE_PAGE_APPLE_VENDOR 0
--
2.47.3