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 <brauner@kernel.org>
This commit is contained in:
Christian Brauner
2026-04-24 14:39:25 +02:00
parent 1d0a8e5dbd
commit 93c04d4aba
3 changed files with 91 additions and 0 deletions
+83
View 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;
+3
View 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);
+5
View 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);
}