]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
vmspawn-qmp: add vmspawn_qmp_remove_block_device
authorChristian Brauner <brauner@kernel.org>
Wed, 22 Apr 2026 08:19:09 +0000 (10:19 +0200)
committerChristian Brauner <brauner@kernel.org>
Fri, 24 Apr 2026 12:39:25 +0000 (14:39 +0200)
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 <brauner@kernel.org>
src/vmspawn/vmspawn-qmp.c
src/vmspawn/vmspawn-qmp.h
src/vmspawn/vmspawn-varlink.c

index 9158d5b10a99b20be3eb0c4ac58cd93f93cf6c2b..621c5e781a7a653579830bc33684fc5c97f18c3a 100644 (file)
@@ -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;
index f1407c6f2f5b156d24df00ba7f68ca2656923038..d8403520c9afebee4654a51e9656d719c0cf899e 100644 (file)
@@ -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);
index 8df234a9bba1e75d920e5213e65fb362ca62dc00..2e0daa6039f1527db4afc44dd43c8513c9d03239 100644 (file)
@@ -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);
 }