From: Christian Brauner Date: Wed, 22 Apr 2026 08:19:09 +0000 (+0200) Subject: vmspawn-qmp: add vmspawn_qmp_remove_block_device X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=93c04d4aba520afb4bf57e27fb0faac8268ed685;p=thirdparty%2Fsystemd.git vmspawn-qmp: add vmspawn_qmp_remove_block_device Hot-remove counterpart to vmspawn_qmp_add_block_device. Looks the drive up in the bridge's block_devices registry by caller-supplied id and dispatches device_del using the internal qmp_device_id; the varlink link gets the immediate ack/error reply once QEMU completes the request. Concurrency: a second remove for the same id while the first is in flight (between device_del dispatch and DEVICE_DELETED) would otherwise reach QEMU and earn a confusing 'already in the process of unplug' reply. Track the in-flight state with a new BLOCK_DEVICE_REMOVE_PENDING bit on the existing rollback_mask, and short-circuit duplicate calls with -EBUSY. The bit is cleared on device_del failure (the drive is still attached, so retries make sense) and naturally vanishes on success when the registry entry is dropped. DEVICE_DELETED handling: the actual blockdev-del + registry removal + pcie-port release is deferred to vmspawn_qmp_dispatch_device_deleted, which fires from on_qmp_event in vmspawn-varlink.c when the guest acks the eject. Hooking it from the existing QMP event dispatcher keeps the cleanup local to vmspawn-qmp.{c,h}. The function has no varlink callers in this PR — the io.systemd.VirtualMachineInstance method handler that forwards into it lands with the rest of the hotplug PR. Signed-off-by: Christian Brauner --- diff --git a/src/vmspawn/vmspawn-qmp.c b/src/vmspawn/vmspawn-qmp.c index 9158d5b10a9..621c5e781a7 100644 --- a/src/vmspawn/vmspawn-qmp.c +++ b/src/vmspawn/vmspawn-qmp.c @@ -991,6 +991,89 @@ static int qmp_setup_regular_drive(VmspawnQmpBridge *bridge, DriveInfo *drive) { return vmspawn_qmp_add_block_device(bridge, drive); } +/* device_del completion is just QEMU acking the request; teardown happens + * in vmspawn_qmp_dispatch_device_deleted() once the guest acks the eject. */ +static int on_remove_device_del_complete( + QmpClient *client, + sd_json_variant *result, + const char *error_desc, + int error, + void *userdata) { + + _cleanup_(drive_info_unrefp) DriveInfo *drive = ASSERT_PTR(userdata); + _cleanup_(sd_varlink_unrefp) sd_varlink *link = TAKE_PTR(drive->link); + + assert(client); + assert(link); + + if (error < 0) { + /* device_del rejected: clear the pending bit so the caller can retry. */ + drive->state &= ~BLOCK_DEVICE_STATE_REMOVE_PENDING; + + return reply_qmp_error(link, error_desc, error); + } + + return sd_varlink_reply(link, NULL); +} + +int vmspawn_qmp_remove_block_device(VmspawnQmpBridge *bridge, sd_varlink *link, const char *id) { + int r; + + assert(bridge); + assert(link); + assert(id); + + DriveInfo *drive = hashmap_get(bridge->block_devices, id); + if (!drive) + return reply_qmp_error(link, "Unknown block device id", -ENOENT); + if (!FLAGS_SET(drive->state, BLOCK_DEVICE_STATE_BLOCKDEV_ADDED)) + return reply_qmp_error(link, "Block device add pending", -EBUSY); + if (FLAGS_SET(drive->state, BLOCK_DEVICE_STATE_REMOVE_PENDING)) + return reply_qmp_error(link, "Block device removal pending", -EBUSY); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL; + r = sd_json_buildo(&args, SD_JSON_BUILD_PAIR_STRING("id", drive->qmp_device_id)); + if (r < 0) + return sd_varlink_error_errno(link, r); + + assert(!drive->link); + drive->link = sd_varlink_ref(link); + drive->state |= BLOCK_DEVICE_STATE_REMOVE_PENDING; + + r = qmp_client_invoke(bridge->qmp, /* ret_slot= */ NULL, "device_del", QMP_CLIENT_ARGS(args), + on_remove_device_del_complete, drive_info_ref(drive)); + if (r < 0) { + drive->link = sd_varlink_unref(drive->link); + drive->state &= ~BLOCK_DEVICE_STATE_REMOVE_PENDING; + drive_info_unref(drive); + return sd_varlink_error_errno(link, r); + } + return 0; +} + +/* DEVICE_DELETED arrives once the guest has acked the eject; only then is it + * safe to drop the blockdev node and release the registry slot (and PCIe port). */ +int vmspawn_qmp_dispatch_device_deleted(VmspawnQmpBridge *bridge, sd_json_variant *data) { + assert(bridge); + + if (!data) + return 0; + + const char *qmp_device_id = sd_json_variant_string(sd_json_variant_by_key(data, "device")); + if (!qmp_device_id) + return 0; + + DriveInfo *drive = hashmap_get(bridge->block_devices_by_qmp_id, qmp_device_id); + if (!drive) + return 0; + + vmspawn_qmp_block_device_teardown(bridge->qmp, drive->qmp_node_name, drive->state); + + assert_se(bridge_unregister_drive(bridge, drive) == drive); + drive_info_unref(drive); + return 0; +} + int vmspawn_qmp_setup_network(VmspawnQmpBridge *bridge, NetworkInfo *network) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *netdev_args = NULL, *device_args = NULL; bool tap_by_fd; diff --git a/src/vmspawn/vmspawn-qmp.h b/src/vmspawn/vmspawn-qmp.h index f1407c6f2f5..d8403520c9a 100644 --- a/src/vmspawn/vmspawn-qmp.h +++ b/src/vmspawn/vmspawn-qmp.h @@ -77,6 +77,7 @@ typedef enum QmpDriveFlags { typedef enum BlockDeviceStateFlags { BLOCK_DEVICE_STATE_BLOCKDEV_ADDED = 1u << 0, BLOCK_DEVICE_STATE_ADD_FAILED = 1u << 1, /* first error fired; suppress cascades */ + BLOCK_DEVICE_STATE_REMOVE_PENDING = 1u << 2, /* device_del in flight; reject concurrent removes */ } BlockDeviceStateFlags; /* Ref-counted; each of the four add-stage QMP slots holds one ref. @@ -176,3 +177,5 @@ int vmspawn_qmp_setup_drives(VmspawnQmpBridge *bridge, DriveInfos *drives); int vmspawn_qmp_setup_network(VmspawnQmpBridge *bridge, NetworkInfo *network); int vmspawn_qmp_setup_virtiofs(VmspawnQmpBridge *bridge, const VirtiofsInfos *virtiofs); int vmspawn_qmp_setup_vsock(VmspawnQmpBridge *bridge, VsockInfo *vsock); +int vmspawn_qmp_remove_block_device(VmspawnQmpBridge *bridge, sd_varlink *link, const char *id); +int vmspawn_qmp_dispatch_device_deleted(VmspawnQmpBridge *bridge, sd_json_variant *data); diff --git a/src/vmspawn/vmspawn-varlink.c b/src/vmspawn/vmspawn-varlink.c index 8df234a9bba..2e0daa6039f 100644 --- a/src/vmspawn/vmspawn-varlink.c +++ b/src/vmspawn/vmspawn-varlink.c @@ -12,6 +12,7 @@ #include "varlink-io.systemd.QemuMachineInstance.h" #include "varlink-io.systemd.VirtualMachineInstance.h" #include "varlink-util.h" +#include "vmspawn-qmp.h" #include "vmspawn-varlink.h" DEFINE_PRIVATE_HASH_OPS_FULL( @@ -315,6 +316,10 @@ static int on_qmp_event( if (streq(event, "JOB_STATUS_CHANGE")) return dispatch_pending_job(ctx->bridge, data); + /* Notification still fans out below. */ + if (streq(event, "DEVICE_DELETED")) + (void) vmspawn_qmp_dispatch_device_deleted(ctx->bridge, data); + return notify_event_subscribers(ctx, event, data); }