diff --git a/src/shared/machine-util.c b/src/shared/machine-util.c new file mode 100644 index 0000000000..fa5e46ace1 --- /dev/null +++ b/src/shared/machine-util.c @@ -0,0 +1,102 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "extract-word.h" +#include "machine-util.h" +#include "parse-argument.h" +#include "string-table.h" + +static const char *const image_format_table[_IMAGE_FORMAT_MAX] = { + [IMAGE_FORMAT_RAW] = "raw", + [IMAGE_FORMAT_QCOW2] = "qcow2", +}; + +DEFINE_STRING_TABLE_LOOKUP(image_format, ImageFormat); + +static const char *const disk_type_table[_DISK_TYPE_MAX] = { + [DISK_TYPE_VIRTIO_BLK] = "virtio-blk", + [DISK_TYPE_VIRTIO_SCSI] = "virtio-scsi", + [DISK_TYPE_NVME] = "nvme", + [DISK_TYPE_VIRTIO_SCSI_CDROM] = "scsi-cd", +}; + +DEFINE_STRING_TABLE_LOOKUP(disk_type, DiskType); + +/* Wire value for the io.systemd.VirtualMachineInstance.BlockDriver IDL enum. */ +static const char *const block_driver_table[_DISK_TYPE_MAX] = { + [DISK_TYPE_VIRTIO_BLK] = "virtio_blk", + [DISK_TYPE_VIRTIO_SCSI] = "scsi_hd", + [DISK_TYPE_NVME] = "nvme", + [DISK_TYPE_VIRTIO_SCSI_CDROM] = "scsi_cd", +}; + +DEFINE_STRING_TABLE_LOOKUP(block_driver, DiskType); + +/* QEMU -device driver name (e.g. "virtio-blk-pci"). */ +static const char *const qemu_device_driver_table[_DISK_TYPE_MAX] = { + [DISK_TYPE_VIRTIO_BLK] = "virtio-blk-pci", + [DISK_TYPE_VIRTIO_SCSI] = "scsi-hd", + [DISK_TYPE_NVME] = "nvme", + [DISK_TYPE_VIRTIO_SCSI_CDROM] = "scsi-cd", +}; + +DEFINE_STRING_TABLE_LOOKUP(qemu_device_driver, DiskType); + +int parse_disk_spec( + const char *arg, + ImageFormat *format, + DiskType *disk_type, + char **ret_path) { + + int r; + + assert(arg); + assert(format); + assert(disk_type); + assert(ret_path); + + ImageFormat parsed_format = *format; + DiskType parsed_disk_type = *disk_type; + const char *dp = arg; + + /* Format and disk-type vocabularies don't overlap, so prefixes may appear in any order. */ + for (;;) { + _cleanup_free_ char *word = NULL; + const char *save = dp; + + r = extract_first_word(&dp, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r < 0) + return r; + if (r == 0 || !dp) { + /* No ':' remained after this word — rest is the path. */ + dp = save; + break; + } + + ImageFormat f = image_format_from_string(word); + if (f >= 0) { + parsed_format = f; + continue; + } + + DiskType dt = disk_type_from_string(word); + if (dt >= 0) { + parsed_disk_type = dt; + continue; + } + + /* Unknown prefix — rewind, remainder is the path. */ + dp = save; + break; + } + + _cleanup_free_ char *path = NULL; + r = parse_path_argument(dp, /* suppress_root= */ false, &path); + if (r < 0) + return r; + + *format = parsed_format; + *disk_type = parsed_disk_type; + *ret_path = TAKE_PTR(path); + return 0; +} diff --git a/src/shared/machine-util.h b/src/shared/machine-util.h new file mode 100644 index 0000000000..3937ce1703 --- /dev/null +++ b/src/shared/machine-util.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "shared-forward.h" + +typedef enum ImageFormat { + IMAGE_FORMAT_RAW, + IMAGE_FORMAT_QCOW2, + _IMAGE_FORMAT_MAX, + _IMAGE_FORMAT_INVALID = -EINVAL, +} ImageFormat; + +typedef enum DiskType { + DISK_TYPE_VIRTIO_BLK, + DISK_TYPE_VIRTIO_SCSI, + DISK_TYPE_NVME, + DISK_TYPE_VIRTIO_SCSI_CDROM, + _DISK_TYPE_MAX, + _DISK_TYPE_INVALID = -EINVAL, +} DiskType; + +DECLARE_STRING_TABLE_LOOKUP(image_format, ImageFormat); +DECLARE_STRING_TABLE_LOOKUP(disk_type, DiskType); +DECLARE_STRING_TABLE_LOOKUP(block_driver, DiskType); +DECLARE_STRING_TABLE_LOOKUP(qemu_device_driver, DiskType); + +/* Parse "[FORMAT:][DISKTYPE:]PATH"; *format and *disk_type are in-out. */ +int parse_disk_spec( + const char *arg, + ImageFormat *format, + DiskType *disk_type, + char **ret_path); diff --git a/src/shared/meson.build b/src/shared/meson.build index 741dbb60a4..a94b78ba94 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -133,6 +133,7 @@ shared_sources = files( 'machine-credential.c', 'machine-id-setup.c', 'machine-register.c', + 'machine-util.c', 'macvlan-util.c', 'main-func.c', 'metrics.c', @@ -237,7 +238,6 @@ shared_sources = files( 'varlink-io.systemd.Network.Link.c', 'varlink-io.systemd.PCRExtend.c', 'varlink-io.systemd.PCRLock.c', - 'varlink-io.systemd.QemuMachineInstance.c', 'varlink-io.systemd.Repart.c', 'varlink-io.systemd.Resolve.c', 'varlink-io.systemd.Resolve.Hook.c', diff --git a/src/shared/qmp-client.c b/src/shared/qmp-client.c index cc51259cd1..41b0c6dd57 100644 --- a/src/shared/qmp-client.c +++ b/src/shared/qmp-client.c @@ -45,10 +45,12 @@ struct QmpClient { qmp_disconnect_callback_t disconnect_callback; void *disconnect_userdata; - unsigned next_fdset_id; /* monotonic fdset-id allocator for add-fd */ + uint64_t next_fdset_id; /* monotonic fdset-id allocator for add-fd */ QmpClientState state; sd_json_variant *current; /* most recently parsed message, pending dispatch */ + + void *userdata; }; static void qmp_slot_hash_func(const QmpSlot *p, struct siphash *state) { @@ -508,6 +510,21 @@ bool qmp_client_is_disconnected(QmpClient *c) { return c->state == QMP_CLIENT_DISCONNECTED; } +void* qmp_client_set_userdata(QmpClient *c, void *userdata) { + void *old; + + assert(c); + + old = c->userdata; + c->userdata = userdata; + return old; +} + +void* qmp_client_get_userdata(QmpClient *c) { + assert(c); + return c->userdata; +} + /* Map our state to the transport phase used for POLLIN / salvage / timeout decisions. */ static JsonStreamPhase qmp_client_phase(void *userdata) { QmpClient *c = ASSERT_PTR(userdata); @@ -876,7 +893,7 @@ sd_event* qmp_client_get_event(QmpClient *c) { return json_stream_get_event(&c->stream); } -unsigned qmp_client_next_fdset_id(QmpClient *c) { +uint64_t qmp_client_next_fdset_id(QmpClient *c) { assert(c); return c->next_fdset_id++; } diff --git a/src/shared/qmp-client.h b/src/shared/qmp-client.h index 7a477f7e4f..7dcd53355d 100644 --- a/src/shared/qmp-client.h +++ b/src/shared/qmp-client.h @@ -54,6 +54,9 @@ bool qmp_client_is_idle(QmpClient *c); /* True iff the connection is dead. Stable terminal state — once set, it stays set. */ bool qmp_client_is_disconnected(QmpClient *c); +void* qmp_client_set_userdata(QmpClient *c, void *userdata); +void* qmp_client_get_userdata(QmpClient *c); + /* Async send. Returns 0 on send (callback will fire later), negative errno on failure. If * ret_slot is non-NULL, returns a reference to a QmpSlot which can be used to cancel the call * (by unreffing it before the reply arrives). */ @@ -79,7 +82,7 @@ void qmp_client_bind_event(QmpClient *c, qmp_event_callback_t callback, void *us void qmp_client_bind_disconnect(QmpClient *c, qmp_disconnect_callback_t callback, void *userdata); int qmp_client_set_description(QmpClient *c, const char *description); sd_event* qmp_client_get_event(QmpClient *c); -unsigned qmp_client_next_fdset_id(QmpClient *client); +uint64_t qmp_client_next_fdset_id(QmpClient *client); DECLARE_TRIVIAL_REF_UNREF_FUNC(QmpClient, qmp_client); DEFINE_TRIVIAL_CLEANUP_FUNC(QmpClient *, qmp_client_unref); diff --git a/src/shared/varlink-io.systemd.Machine.c b/src/shared/varlink-io.systemd.Machine.c index da373a3c20..cb1b0665d6 100644 --- a/src/shared/varlink-io.systemd.Machine.c +++ b/src/shared/varlink-io.systemd.Machine.c @@ -57,7 +57,7 @@ static SD_VARLINK_DEFINE_METHOD( SD_VARLINK_DEFINE_INPUT(vSockCid, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_INPUT(sshAddress, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_INPUT(sshPrivateKeyPath, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), - SD_VARLINK_FIELD_COMMENT("Varlink socket address for direct machine control. The server at this address is expected to implement io.systemd.MachineInstance and optionally io.systemd.VirtualMachineInstance and io.systemd.QemuMachineInstance."), + SD_VARLINK_FIELD_COMMENT("Varlink socket address for direct machine control. The server at this address is expected to implement io.systemd.MachineInstance and optionally io.systemd.VirtualMachineInstance."), SD_VARLINK_DEFINE_INPUT(controlAddress, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("Controls whether to allocate a scope unit for the machine to register. If false, the client already took care of that and registered a service/scope specific to the machine."), SD_VARLINK_DEFINE_INPUT(allocateUnit, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), diff --git a/src/shared/varlink-io.systemd.QemuMachineInstance.c b/src/shared/varlink-io.systemd.QemuMachineInstance.c deleted file mode 100644 index b03fa2199c..0000000000 --- a/src/shared/varlink-io.systemd.QemuMachineInstance.c +++ /dev/null @@ -1,17 +0,0 @@ -/* SPDX-License-Identifier: LGPL-2.1-or-later */ - -#include "varlink-io.systemd.QemuMachineInstance.h" - -static SD_VARLINK_DEFINE_METHOD_FULL( - AcquireQMP, - SD_VARLINK_REQUIRES_UPGRADE); - -static SD_VARLINK_DEFINE_ERROR(AlreadyAcquired); - -SD_VARLINK_DEFINE_INTERFACE( - io_systemd_QemuMachineInstance, - "io.systemd.QemuMachineInstance", - SD_VARLINK_SYMBOL_COMMENT("Acquire a direct QMP connection to the QEMU instance via protocol upgrade"), - &vl_method_AcquireQMP, - SD_VARLINK_SYMBOL_COMMENT("A QMP connection has already been acquired by another client"), - &vl_error_AlreadyAcquired); diff --git a/src/shared/varlink-io.systemd.QemuMachineInstance.h b/src/shared/varlink-io.systemd.QemuMachineInstance.h deleted file mode 100644 index 203dacb40c..0000000000 --- a/src/shared/varlink-io.systemd.QemuMachineInstance.h +++ /dev/null @@ -1,6 +0,0 @@ -/* SPDX-License-Identifier: LGPL-2.1-or-later */ -#pragma once - -#include "sd-varlink-idl.h" - -extern const sd_varlink_interface vl_interface_io_systemd_QemuMachineInstance; diff --git a/src/vmspawn/vmspawn-qmp.c b/src/vmspawn/vmspawn-qmp.c index f69d2a5e7c..621c5e781a 100644 --- a/src/vmspawn/vmspawn-qmp.c +++ b/src/vmspawn/vmspawn-qmp.c @@ -6,9 +6,11 @@ #include "sd-event.h" #include "sd-json.h" +#include "sd-varlink.h" #include "alloc-util.h" #include "blockdev-util.h" +#include "errno-util.h" #include "ether-addr-util.h" #include "fd-util.h" #include "hashmap.h" @@ -19,28 +21,102 @@ #include "string-util.h" #include "strv.h" #include "vmspawn-qmp.h" +#include "vmspawn-util.h" DEFINE_PRIVATE_HASH_OPS_FULL( pending_job_hash_ops, char, string_hash_func, string_compare_func, free, PendingJob, pending_job_free); -void drive_info_done(DriveInfo *info) { - assert(info); - info->serial = mfree(info->serial); - info->node_name = mfree(info->node_name); - info->pcie_port = mfree(info->pcie_port); - info->fd = safe_close(info->fd); - info->overlay_fd = safe_close(info->overlay_fd); +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR( + block_devices_hash_ops, + char, string_hash_func, string_compare_func, + DriveInfo, drive_info_unref); + +DriveInfo* drive_info_new(void) { + DriveInfo *d = new(DriveInfo, 1); + if (!d) + return NULL; + + *d = (DriveInfo) { + .n_ref = 1, + .fd = -EBADF, + .overlay_fd = -EBADF, + .pcie_port_idx = -1, + }; + return d; } +static int vmspawn_qmp_bridge_allocate_pcie_port( + VmspawnQmpBridge *bridge, + const char *owner_id, + char **ret_name, + int *ret_idx) { + + assert(bridge); + assert(owner_id); + assert(ret_name); + assert(ret_idx); + + for (int i = 0; i < VMSPAWN_PCIE_HOTPLUG_SPARES; i++) { + if (bridge->hotplug_port_owner[i]) + continue; + + _cleanup_free_ char *owner = strdup(owner_id), *name = NULL; + if (!owner || asprintf(&name, "vmspawn-hotplug-pci-root-port-%d", i) < 0) + return -ENOMEM; + + bridge->hotplug_port_owner[i] = TAKE_PTR(owner); + *ret_name = TAKE_PTR(name); + *ret_idx = i; + return 0; + } + + return log_debug_errno(SYNTHETIC_ERRNO(EBUSY), + "No free PCIe hotplug port available for owner '%s'.", + owner_id); +} + +static void vmspawn_qmp_bridge_release_pcie_port_by_idx(VmspawnQmpBridge *bridge, int idx) { + assert(bridge); + + if (idx < 0) + return; + + assert(idx < VMSPAWN_PCIE_HOTPLUG_SPARES); + + bridge->hotplug_port_owner[idx] = mfree(bridge->hotplug_port_owner[idx]); +} + +static DriveInfo* drive_info_free(DriveInfo *d) { + assert(d); + + if (d->bridge) + vmspawn_qmp_bridge_release_pcie_port_by_idx(d->bridge, d->pcie_port_idx); + + free(d->path); + free(d->format); + free(d->disk_driver); + free(d->serial); + free(d->pcie_port); + free(d->id); + free(d->qmp_node_name); + free(d->qmp_device_id); + free(d->fdset_path); + sd_varlink_unref(d->link); + safe_close(d->fd); + safe_close(d->overlay_fd); + return mfree(d); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(DriveInfo, drive_info, drive_info_free); + void drive_infos_done(DriveInfos *infos) { assert(infos); FOREACH_ARRAY(d, infos->drives, infos->n_drives) - drive_info_done(d); + drive_info_unref(*d); infos->drives = mfree(infos->drives); infos->n_drives = 0; - infos->scsi_pcie_port = mfree(infos->scsi_pcie_port); } void network_info_done(NetworkInfo *info) { @@ -82,39 +158,49 @@ void machine_config_done(MachineConfig *c) { vsock_info_done(&c->vsock); } -/* Generic async QMP setup-completion callback. The userdata argument carries the - * command name (as a string literal) for logging. On failure, request a clean - * event loop exit so vmspawn shuts down instead of running a VM with missing devices. */ -static int on_qmp_setup_complete( +/* Generic completion callback; userdata is a string literal label. Exits the event loop on boot-time failures. */ +static int on_qmp_complete( QmpClient *client, sd_json_variant *result, const char *error_desc, int error, void *userdata) { - const char *label = ASSERT_PTR(userdata); - assert(client); + VmspawnQmpBridge *bridge = ASSERT_PTR(qmp_client_get_userdata(client)); + const char *label = ASSERT_PTR(userdata); + if (error < 0) { log_error_errno(error, "%s failed: %s", label, strna(error_desc)); + + if (bridge->setup_done) + return 0; + return sd_event_exit(qmp_client_get_event(client), error); } return 0; } -/* Send add-fd via SCM_RIGHTS; return /dev/fdset/N. Allocations run before invoke so a late - * OOM cannot orphan an fdset on QEMU's side; *ret_path is only written on full success. */ -static int qmp_fdset_add(QmpClient *qmp, int fd_consume, char **ret_path) { +/* Send add-fd via SCM_RIGHTS; return /dev/fdset/N and the numeric fdset id. */ +static int qmp_fdset_add( + QmpClient *qmp, + int fd_consume, + qmp_command_callback_t callback, + void *userdata, + char **ret_path, + uint64_t *ret_fdset_id) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL; _cleanup_close_ int fd = fd_consume; _cleanup_free_ char *path = NULL; - unsigned id; + uint64_t id; int r; assert(qmp); assert(fd_consume >= 0); + assert(callback); assert(ret_path); id = qmp_client_next_fdset_id(qmp); @@ -123,18 +209,43 @@ static int qmp_fdset_add(QmpClient *qmp, int fd_consume, char **ret_path) { if (r < 0) return r; - if (asprintf(&path, "/dev/fdset/%u", id) < 0) + if (asprintf(&path, "/dev/fdset/%" PRIu64, id) < 0) return -ENOMEM; r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "add-fd", QMP_CLIENT_ARGS_FD(args, TAKE_FD(fd)), - on_qmp_setup_complete, (void*) "add-fd"); + callback, userdata); if (r < 0) return r; *ret_path = TAKE_PTR(path); + if (ret_fdset_id) + *ret_fdset_id = id; return 0; } +/* Issue remove-fd for an fdset whose dup is now held by a blockdev. The fdset + * persists until the dup is closed (in raw_close at blockdev-del time) — see + * QEMU's monitor/fds.c:177-181 on the fds/dup_fds split. */ +static int qmp_fdset_remove( + QmpClient *qmp, + uint64_t fdset_id, + qmp_command_callback_t callback, + void *userdata) { + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL; + int r; + + assert(qmp); + assert(callback); + + r = sd_json_buildo(&args, SD_JSON_BUILD_PAIR_UNSIGNED("fdset-id", fdset_id)); + if (r < 0) + return r; + + return qmp_client_invoke(qmp, /* ret_slot= */ NULL, "remove-fd", QMP_CLIENT_ARGS(args), + callback, userdata); +} + typedef struct QmpFileNodeParams { const char *node_name; const char *filename; @@ -193,16 +304,17 @@ static int qmp_build_blockdev_add_format(const QmpFormatNodeParams *p, sd_json_v SD_JSON_BUILD_PAIR_CONDITION(!!p->backing, "backing", SD_JSON_BUILD_STRING(p->backing))); } -/* Build device_add JSON arguments for a drive */ static int qmp_build_device_add(const DriveInfo *drive, sd_json_variant **ret) { assert(drive); + assert(drive->qmp_node_name); + assert(drive->qmp_device_id); assert(ret); return sd_json_buildo( ret, SD_JSON_BUILD_PAIR_STRING("driver", drive->disk_driver), - SD_JSON_BUILD_PAIR_STRING("drive", drive->node_name), - SD_JSON_BUILD_PAIR_STRING("id", drive->node_name), + SD_JSON_BUILD_PAIR_STRING("drive", drive->qmp_node_name), + SD_JSON_BUILD_PAIR_STRING("id", drive->qmp_device_id), SD_JSON_BUILD_PAIR_CONDITION(FLAGS_SET(drive->flags, QMP_DRIVE_BOOT), "bootindex", SD_JSON_BUILD_INTEGER(1)), SD_JSON_BUILD_PAIR_CONDITION(!!drive->serial, "serial", SD_JSON_BUILD_STRING(drive->serial)), SD_JSON_BUILD_PAIR_CONDITION(STR_IN_SET(drive->disk_driver, "scsi-hd", "scsi-cd"), @@ -212,6 +324,43 @@ static int qmp_build_device_add(const DriveInfo *drive, sd_json_variant **ret) { "bus", SD_JSON_BUILD_STRING(drive->pcie_port))); } +/* Inline form: one blockdev-add creates format+file; one blockdev-del tears + * down the whole tree. Used for regular boot drives and hotplug. */ +static int qmp_build_blockdev_add_inline( + const char *node_name, + const char *format, + const char *filename, + const char *file_driver, + QmpDriveFlags flags, + VmspawnQmpFeatureFlags features, + sd_json_variant **ret) { + + bool use_io_uring = FLAGS_SET(features, VMSPAWN_QMP_FEATURE_IO_URING); + bool use_discard_no_unref = FLAGS_SET(flags, QMP_DRIVE_DISCARD_NO_UNREF); + + assert(node_name); + assert(format); + assert(filename); + assert(file_driver); + assert(ret); + + return sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_STRING("node-name", node_name), + SD_JSON_BUILD_PAIR_STRING("driver", format), + SD_JSON_BUILD_PAIR_CONDITION(FLAGS_SET(flags, QMP_DRIVE_READ_ONLY), "read-only", SD_JSON_BUILD_BOOLEAN(true)), + SD_JSON_BUILD_PAIR_CONDITION(FLAGS_SET(flags, QMP_DRIVE_DISCARD), "discard", JSON_BUILD_CONST_STRING("unmap")), + SD_JSON_BUILD_PAIR_CONDITION(use_discard_no_unref, "discard-no-unref", SD_JSON_BUILD_BOOLEAN(true)), + SD_JSON_BUILD_PAIR("file", SD_JSON_BUILD_OBJECT( + SD_JSON_BUILD_PAIR_STRING("driver", file_driver), + SD_JSON_BUILD_PAIR_STRING("filename", filename), + SD_JSON_BUILD_PAIR_CONDITION(FLAGS_SET(flags, QMP_DRIVE_READ_ONLY), "read-only", SD_JSON_BUILD_BOOLEAN(true)), + SD_JSON_BUILD_PAIR_CONDITION(use_io_uring, "aio", JSON_BUILD_CONST_STRING("io_uring")), + SD_JSON_BUILD_PAIR("cache", SD_JSON_BUILD_OBJECT( + SD_JSON_BUILD_PAIR_BOOLEAN("direct", false), + SD_JSON_BUILD_PAIR_BOOLEAN("no-flush", FLAGS_SET(flags, QMP_DRIVE_NO_FLUSH))))))); +} + /* Issue blockdev-add for a file node. */ static int qmp_add_file_node(QmpClient *qmp, const QmpFileNodeParams *p) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL; @@ -221,7 +370,7 @@ static int qmp_add_file_node(QmpClient *qmp, const QmpFileNodeParams *p) { if (r < 0) return r; - return qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(args), on_qmp_setup_complete, (void*) "blockdev-add"); + return qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(args), on_qmp_complete, (void*) "blockdev-add"); } /* Get the virtual size of an image from the fd directly. For raw images the virtual size @@ -272,28 +421,27 @@ static int get_image_virtual_size(int fd, const char *format, bool is_block_devi return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unsupported image format '%s'", format); } -/* Ephemeral drive continuation — fired when the blockdev-create job concludes. - * Completes the drive setup by adding the overlay format node and the device. */ +/* Forward declarations — on_ephemeral_create_concluded routes failures through + * the shared block-device add callbacks defined further below. */ +static int drive_info_add_fail(DriveInfo *d, int error, const char *error_desc); +static int on_add_blockdev_stage(QmpClient *client, sd_json_variant *result, + const char *error_desc, int error, void *userdata); +static int on_add_device_add_complete(QmpClient *client, sd_json_variant *result, + const char *error_desc, int error, void *userdata); + +/* Continuation state for on_ephemeral_create_concluded: overlay format + device_add. */ typedef struct EphemeralDriveCtx { - char *node_name; /* overlay format node name (= drive node name) */ + DriveInfo *drive; /* ref */ char *overlay_file_node; char *base_fmt_node; - /* Fields for device_add */ - char *disk_driver; - char *serial; /* NULL if unset */ - char *pcie_port; /* pcie-root-port bus for device_add (NULL on non-PCIe) */ - QmpDriveFlags flags; /* subset: QMP_DRIVE_DISCARD, QMP_DRIVE_DISCARD_NO_UNREF, QMP_DRIVE_BOOT */ } EphemeralDriveCtx; static EphemeralDriveCtx* ephemeral_drive_ctx_free(EphemeralDriveCtx *ctx) { if (!ctx) return NULL; - free(ctx->node_name); + drive_info_unref(ctx->drive); free(ctx->overlay_file_node); free(ctx->base_fmt_node); - free(ctx->disk_driver); - free(ctx->serial); - free(ctx->pcie_port); return mfree(ctx); } @@ -306,50 +454,48 @@ static void ephemeral_drive_ctx_free_void(void *p) { static int on_ephemeral_create_concluded(QmpClient *qmp, void *userdata) { _cleanup_(ephemeral_drive_ctx_freep) EphemeralDriveCtx *ctx = ASSERT_PTR(userdata); _cleanup_(sd_json_variant_unrefp) sd_json_variant *fmt_args = NULL, *device_args = NULL; + _cleanup_(drive_info_unrefp) DriveInfo *slot_ref = NULL; + DriveInfo *drive = ctx->drive; int r; + assert(qmp); + /* Open formatted overlay as qcow2 with backing reference */ QmpFormatNodeParams overlay_fmt_params = { - .node_name = ctx->node_name, + .node_name = drive->qmp_node_name, .format = "qcow2", .file_node_name = ctx->overlay_file_node, .backing = ctx->base_fmt_node, - .flags = ctx->flags & (QMP_DRIVE_DISCARD|QMP_DRIVE_DISCARD_NO_UNREF), + .flags = drive->flags & (QMP_DRIVE_DISCARD|QMP_DRIVE_DISCARD_NO_UNREF), }; r = qmp_build_blockdev_add_format(&overlay_fmt_params, &fmt_args); if (r < 0) - return log_error_errno(r, "Failed to build overlay format JSON for '%s': %m", ctx->node_name); + return drive_info_add_fail(drive, r, NULL); - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(fmt_args), on_qmp_setup_complete, (void*) "blockdev-add"); + slot_ref = drive_info_ref(drive); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(fmt_args), + on_add_blockdev_stage, slot_ref); if (r < 0) - return r; + return drive_info_add_fail(drive, r, NULL); + TAKE_PTR(slot_ref); - /* device_add: attach to virtual hardware. Build a temporary DriveInfo as a - * read-only view into the continuation context to reuse qmp_build_device_add(). */ - const DriveInfo tmp = { - .disk_driver = ctx->disk_driver, - .node_name = ctx->node_name, - .serial = ctx->serial, - .pcie_port = ctx->pcie_port, - .flags = ctx->flags & QMP_DRIVE_BOOT, - .fd = -EBADF, - .overlay_fd = -EBADF, - }; - r = qmp_build_device_add(&tmp, &device_args); + r = qmp_build_device_add(drive, &device_args); if (r < 0) - return log_error_errno(r, "Failed to build device_add JSON for '%s': %m", ctx->node_name); + return drive_info_add_fail(drive, r, NULL); - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_setup_complete, (void*) "device_add"); + slot_ref = drive_info_ref(drive); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), + on_add_device_add_complete, slot_ref); if (r < 0) - return r; + return drive_info_add_fail(drive, r, NULL); + TAKE_PTR(slot_ref); - log_debug("Queued ephemeral drive completion for '%s'", ctx->node_name); + log_debug("Queued ephemeral drive completion for '%s'", drive->qmp_device_id); return 0; } -/* Set up an ephemeral drive: base image (read-only) + anonymous qcow2 overlay (read-write). - * The final steps (overlay format + device_add) are deferred to a job continuation that - * fires when the blockdev-create job concludes. */ +/* Base image (read-only) + anonymous qcow2 overlay (read-write). Overlay format + * and device_add run from the blockdev-create continuation. */ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, DriveInfo *drive) { int r; @@ -359,13 +505,24 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D assert(drive->fd >= 0); assert(drive->overlay_fd >= 0); - /* Node names: -base-file, -base-fmt, -overlay-file, */ - _cleanup_free_ char *base_file_node = strjoin(drive->node_name, "-base-file"); - _cleanup_free_ char *base_fmt_node = strjoin(drive->node_name, "-base-fmt"); - _cleanup_free_ char *overlay_file_node = strjoin(drive->node_name, "-overlay-file"); - if (!base_file_node || !base_fmt_node || !overlay_file_node) + drive->bridge = bridge; + drive->counter = bridge->next_block_counter++; + + _cleanup_free_ char *base_file_node = NULL, *base_fmt_node = NULL, *overlay_file_node = NULL; + if (asprintf(&drive->qmp_node_name, "vmspawn-%" PRIu64 "-storage", drive->counter) < 0 || + asprintf(&drive->qmp_device_id, "vmspawn-%" PRIu64 "-disk", drive->counter) < 0 || + asprintf(&base_file_node, "vmspawn-%" PRIu64 "-base-file", drive->counter) < 0 || + asprintf(&base_fmt_node, "vmspawn-%" PRIu64 "-base-fmt", drive->counter) < 0 || + asprintf(&overlay_file_node, "vmspawn-%" PRIu64 "-overlay-file", drive->counter) < 0) return log_oom(); + /* Auto-assigned user id reuses qmp_device_id (matching vmspawn_qmp_add_block_device). */ + if (!drive->id) { + drive->id = strdup(drive->qmp_device_id); + if (!drive->id) + return log_oom(); + } + /* Read virtual size before passing the fd to QEMU (TAKE_FD consumes it) */ uint64_t virtual_size; r = get_image_virtual_size(drive->fd, drive->format, FLAGS_SET(drive->flags, QMP_DRIVE_BLOCK_DEVICE), &virtual_size); @@ -374,12 +531,16 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D /* Step 1-2: Pass both fds to QEMU */ _cleanup_free_ char *base_path = NULL; - r = qmp_fdset_add(qmp, TAKE_FD(drive->fd), &base_path); + uint64_t base_fdset_id; + r = qmp_fdset_add(qmp, TAKE_FD(drive->fd), + on_qmp_complete, (void*) "add-fd", &base_path, &base_fdset_id); if (r < 0) return log_error_errno(r, "Failed to send add-fd for base image '%s': %m", drive->path); _cleanup_free_ char *overlay_path = NULL; - r = qmp_fdset_add(qmp, TAKE_FD(drive->overlay_fd), &overlay_path); + uint64_t overlay_fdset_id; + r = qmp_fdset_add(qmp, TAKE_FD(drive->overlay_fd), + on_qmp_complete, (void*) "add-fd", &overlay_path, &overlay_fdset_id); if (r < 0) return log_error_errno(r, "Failed to send add-fd for overlay of '%s': %m", drive->path); @@ -396,6 +557,12 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D if (r < 0) return log_error_errno(r, "Failed to send blockdev-add for base file '%s': %m", drive->path); + /* The base file node now holds a dup of the fd; release the monitor's + * original so the fdset auto-frees when raw_close runs at teardown. */ + r = qmp_fdset_remove(qmp, base_fdset_id, on_qmp_complete, (void*) "remove-fd"); + if (r < 0) + return log_error_errno(r, "Failed to send remove-fd for base image '%s': %m", drive->path); + /* Step 4: Base image format node (read-only) */ QmpFormatNodeParams base_fmt_params = { .node_name = base_fmt_node, @@ -408,7 +575,7 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D if (r < 0) return r; - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(base_fmt_args), on_qmp_setup_complete, (void*) "blockdev-add"); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(base_fmt_args), on_qmp_complete, (void*) "blockdev-add"); if (r < 0) return log_error_errno(r, "Failed to send blockdev-add for base format '%s': %m", drive->path); @@ -424,10 +591,15 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D if (r < 0) return r; - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(overlay_file_args), on_qmp_setup_complete, (void*) "blockdev-add"); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(overlay_file_args), on_qmp_complete, (void*) "blockdev-add"); if (r < 0) return log_error_errno(r, "Failed to send blockdev-add for overlay file '%s': %m", drive->path); + /* Same as for base: the overlay file node has the dup. */ + r = qmp_fdset_remove(qmp, overlay_fdset_id, on_qmp_complete, (void*) "remove-fd"); + if (r < 0) + return log_error_errno(r, "Failed to send remove-fd for overlay of '%s': %m", drive->path); + /* Step 6: Fire blockdev-create to format the overlay */ _cleanup_(sd_json_variant_unrefp) sd_json_variant *create_options = NULL; r = sd_json_buildo(&create_options, @@ -439,8 +611,8 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D if (r < 0) return log_error_errno(r, "Failed to build blockdev-create options: %m"); - _cleanup_free_ char *job_id = strjoin("create-", drive->node_name); - if (!job_id) + _cleanup_free_ char *job_id = NULL; + if (asprintf(&job_id, "vmspawn-%" PRIu64 "-overlay-create", drive->counter) < 0) return log_oom(); _cleanup_(sd_json_variant_unrefp) sd_json_variant *cmd_args = NULL; @@ -450,28 +622,23 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D if (r < 0) return log_error_errno(r, "Failed to build blockdev-create JSON: %m"); + /* Fold DISCARD_NO_UNREF into drive->flags so the continuation's overlay format blockdev-add + * picks it up via drive->flags. */ + if (FLAGS_SET(drive->flags, QMP_DRIVE_DISCARD) && + FLAGS_SET(bridge->features, VMSPAWN_QMP_FEATURE_DISCARD_NO_UNREF)) + drive->flags |= QMP_DRIVE_DISCARD_NO_UNREF; + /* Register continuation: when the job concludes, fire overlay format + device_add */ _cleanup_(ephemeral_drive_ctx_freep) EphemeralDriveCtx *ectx = new(EphemeralDriveCtx, 1); if (!ectx) return log_oom(); - QmpDriveFlags ectx_flags = drive->flags & (QMP_DRIVE_DISCARD|QMP_DRIVE_BOOT); - if (FLAGS_SET(drive->flags, QMP_DRIVE_DISCARD) && - FLAGS_SET(bridge->features, VMSPAWN_QMP_FEATURE_DISCARD_NO_UNREF)) - ectx_flags |= QMP_DRIVE_DISCARD_NO_UNREF; - *ectx = (EphemeralDriveCtx) { - .node_name = strdup(drive->node_name), + .drive = drive_info_ref(drive), .overlay_file_node = strdup(overlay_file_node), .base_fmt_node = strdup(base_fmt_node), - .disk_driver = strdup(drive->disk_driver), - .serial = drive->serial ? strdup(drive->serial) : NULL, - .pcie_port = drive->pcie_port ? strdup(drive->pcie_port) : NULL, - .flags = ectx_flags, }; - if (!ectx->node_name || !ectx->overlay_file_node || !ectx->base_fmt_node || - !ectx->disk_driver || (drive->serial && !ectx->serial) || - (drive->pcie_port && !ectx->pcie_port)) + if (!ectx->overlay_file_node || !ectx->base_fmt_node) return log_oom(); r = vmspawn_qmp_bridge_register_job(bridge, job_id, @@ -482,82 +649,429 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D TAKE_PTR(ectx); - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-create", QMP_CLIENT_ARGS(cmd_args), on_qmp_setup_complete, (void*) "blockdev-create"); - if (r < 0) + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-create", QMP_CLIENT_ARGS(cmd_args), on_qmp_complete, (void*) "blockdev-create"); + if (r < 0) { + _unused_ _cleanup_(pending_job_freep) PendingJob *dead = hashmap_remove(bridge->pending_jobs, job_id); return log_error_errno(r, "Failed to send blockdev-create for '%s': %m", drive->path); + } log_debug("Queued ephemeral drive setup for '%s' (job %s)", drive->path, job_id); return 0; } -/* Set up a regular (non-ephemeral) drive: single file node + format node + device_add. */ -static int qmp_setup_regular_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, DriveInfo *drive) { +static int reply_qmp_error(sd_varlink *link, const char *error_desc, int error) { + assert(link); + + if (ERRNO_IS_DISCONNECT(error)) + return sd_varlink_error(link, "io.systemd.MachineInstance.NotConnected", NULL); + if (error_desc) + log_warning("QMP error: %s", error_desc); + return sd_varlink_error_errno(link, error < 0 ? error : -EIO); +} + +/* After the pipelined remove-fd at add time, QEMU auto-frees the fdset when + * raw_close (during blockdev-del) releases the last dup. So teardown only + * needs to fire blockdev-del. */ +static void vmspawn_qmp_block_device_teardown(QmpClient *client, const char *qmp_node_name, BlockDeviceStateFlags stages) { + assert(client); + assert(qmp_node_name); + + if (!FLAGS_SET(stages, BLOCK_DEVICE_STATE_BLOCKDEV_ADDED)) + return; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL; + if (sd_json_buildo(&args, SD_JSON_BUILD_PAIR_STRING("node-name", qmp_node_name)) >= 0) + (void) qmp_client_invoke(client, /* ret_slot= */ NULL, "blockdev-del", QMP_CLIENT_ARGS(args), + on_qmp_complete, (void*) "teardown blockdev-del"); +} + +/* Insert into the owning primary map and the non-owning qmp_device_id view. On + * secondary-put failure, roll back the primary so neither map carries a stale entry. */ +static int bridge_register_drive(VmspawnQmpBridge *b, DriveInfo *d) { + int r; + + assert(b); + assert(d); + assert(d->id); + assert(d->qmp_device_id); + + r = hashmap_ensure_put(&b->block_devices, &block_devices_hash_ops, + d->id, drive_info_ref(d)); + if (r < 0) { + drive_info_unref(d); + return r; + } + + r = hashmap_ensure_put(&b->block_devices_by_qmp_id, &string_hash_ops, + d->qmp_device_id, d); + if (r < 0) { + drive_info_unref(hashmap_remove(b->block_devices, d->id)); + return r; + } + + return 0; +} + +/* Drop the drive from both maps; returns the pointer removed from the primary + * (NULL if it wasn't there) so the caller can decide whether to unref. */ +static DriveInfo* bridge_unregister_drive(VmspawnQmpBridge *b, DriveInfo *d) { + assert(b); + assert(d); + + hashmap_remove_value(b->block_devices_by_qmp_id, d->qmp_device_id, d); + return hashmap_remove_value(b->block_devices, d->id, d); +} + +/* First-error entry point: marks FAILED so cascading callbacks no-op, drops + * the registry slot, then replies on the link or exits the loop. */ +static int drive_info_add_fail(DriveInfo *d, int error, const char *error_desc) { + assert(d); + + if (FLAGS_SET(d->state, BLOCK_DEVICE_STATE_ADD_FAILED)) + return 0; + + vmspawn_qmp_block_device_teardown(d->bridge->qmp, d->qmp_node_name, d->state); + d->state = BLOCK_DEVICE_STATE_ADD_FAILED; + + if (bridge_unregister_drive(d->bridge, d)) + drive_info_unref(d); + + if (d->link) { + (void) reply_qmp_error(d->link, error_desc, error); + d->link = sd_varlink_unref(d->link); + return 0; + } + + log_error_errno(error, "Block device '%s' setup failed: %s", + strna(d->id), strna(error_desc)); + + /* Boot-time (link == NULL) is always fatal — even for late-arriving ephemeral replies. */ + return sd_event_exit(qmp_client_get_event(d->bridge->qmp), error); +} + +/* Rolls back the up-front registry insert on a sync error path. */ +static void drive_info_unregister_on_failurep(DriveInfo **dp) { + assert(dp); + + DriveInfo *d = *dp; + if (!d) + return; + d->state |= BLOCK_DEVICE_STATE_ADD_FAILED; + if (bridge_unregister_drive(d->bridge, d)) + drive_info_unref(d); +} + +/* Shared by the intermediate stages that don't need to record a rollback bit + * (add-fd, remove-fd). Just forwards errors to drive_info_add_fail so cascades + * from earlier stage failures get suppressed via the FAILED sentinel. */ +static int on_add_observe_stage( + QmpClient *client, + sd_json_variant *result, + const char *error_desc, + int error, + void *userdata) { + + _cleanup_(drive_info_unrefp) DriveInfo *d = ASSERT_PTR(userdata); + assert(client); + + if (error < 0) + return drive_info_add_fail(d, error, error_desc); + return 0; +} + +static int on_add_blockdev_stage( + QmpClient *client, + sd_json_variant *result, + const char *error_desc, + int error, + void *userdata) { + + _cleanup_(drive_info_unrefp) DriveInfo *d = ASSERT_PTR(userdata); + assert(client); + + if (error < 0) + return drive_info_add_fail(d, error, error_desc); + + /* A sync error after blockdev-add was queued may have marked the chain FAILED. + * The blockdev node we just created is orphaned — tear it down retroactively + * and don't claim BLOCKDEV_ADDED on a drive that's already been unregistered. */ + if (FLAGS_SET(d->state, BLOCK_DEVICE_STATE_ADD_FAILED)) { + vmspawn_qmp_block_device_teardown(d->bridge->qmp, d->qmp_node_name, + BLOCK_DEVICE_STATE_BLOCKDEV_ADDED); + return 0; + } + + d->state |= BLOCK_DEVICE_STATE_BLOCKDEV_ADDED; + return 0; +} + +static int on_add_device_add_complete( + QmpClient *client, + sd_json_variant *result, + const char *error_desc, + int error, + void *userdata) { + + _cleanup_(drive_info_unrefp) DriveInfo *d = ASSERT_PTR(userdata); + + assert(client); + + if (error < 0) + return drive_info_add_fail(d, error, error_desc); + + if (FLAGS_SET(d->state, BLOCK_DEVICE_STATE_ADD_FAILED)) + return 0; + + if (d->link) { + (void) sd_varlink_replybo(d->link, SD_JSON_BUILD_PAIR_STRING("id", d->id)); + d->link = sd_varlink_unref(d->link); + } + + log_info("Block device '%s' attached", d->id); + return 0; +} + +static int on_scsi_controller_complete( + QmpClient *client, + sd_json_variant *result, + const char *error_desc, + int error, + void *userdata) { + + assert(client); + + VmspawnQmpBridge *bridge = ASSERT_PTR(qmp_client_get_userdata(client)); + + if (error < 0) { + /* QEMU's device_add is transactional — on error it calls object_unparent() + * before replying, so the "vmspawn_scsi" id is free for the next retry. */ + vmspawn_qmp_bridge_release_pcie_port_by_idx(bridge, bridge->scsi_controller_port_idx); + bridge->scsi_controller_port_idx = -1; + bridge->scsi_controller_created = false; + log_warning("virtio-scsi-pci controller setup failed: %s", strna(error_desc)); + if (!bridge->setup_done) + return sd_event_exit(qmp_client_get_event(client), error); + } + + return 0; +} + +static int qmp_setup_scsi_controller(VmspawnQmpBridge *bridge, const char *pcie_port) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL; + int r; + + assert(bridge); + + r = sd_json_buildo( + &args, + SD_JSON_BUILD_PAIR_STRING("driver", "virtio-scsi-pci"), + SD_JSON_BUILD_PAIR_STRING("id", "vmspawn_scsi"), + SD_JSON_BUILD_PAIR_CONDITION(!!pcie_port, "bus", SD_JSON_BUILD_STRING(pcie_port))); + if (r < 0) + return log_error_errno(r, "Failed to build SCSI controller JSON: %m"); + + r = qmp_client_invoke(bridge->qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(args), + on_scsi_controller_complete, NULL); + if (r < 0) + return log_error_errno(r, "Failed to send SCSI controller device_add: %m"); + + log_debug("Queued virtio-scsi-pci controller setup"); + return 0; +} + +static int vmspawn_qmp_add_block_device(VmspawnQmpBridge *bridge, DriveInfo *drive) { int r; assert(bridge); - assert(qmp); assert(drive); + assert(drive->format); + assert(drive->disk_driver); assert(drive->fd >= 0); - /* Node names: -file, */ - _cleanup_free_ char *file_node_name = strjoin(drive->node_name, "-file"); - if (!file_node_name) + _unused_ _cleanup_(drive_info_unrefp) DriveInfo *owned = drive; + _cleanup_(drive_info_unrefp) DriveInfo *slot_ref = NULL; + _cleanup_(drive_info_unregister_on_failurep) DriveInfo *registered = NULL; + + drive->bridge = bridge; + drive->counter = bridge->next_block_counter++; + if (asprintf(&drive->qmp_node_name, "vmspawn-%" PRIu64 "-storage", drive->counter) < 0) return log_oom(); + if (asprintf(&drive->qmp_device_id, "vmspawn-%" PRIu64 "-disk", drive->counter) < 0) + return log_oom(); + /* Auto-assigned user ids reuse qmp_device_id. */ + if (!drive->id) { + drive->id = strdup(drive->qmp_device_id); + if (!drive->id) + return log_oom(); + } - _cleanup_free_ char *fdset_path = NULL; - r = qmp_fdset_add(qmp, TAKE_FD(drive->fd), &fdset_path); + /* Reserve the registry slot up-front so the device_add callback's commit can't fail. */ + r = bridge_register_drive(bridge, drive); if (r < 0) - return log_error_errno(r, "Failed to send add-fd for '%s': %m", drive->path); + return r; + registered = drive; - QmpFileNodeParams file_params = { - .node_name = file_node_name, - .filename = fdset_path, - .driver = FLAGS_SET(drive->flags, QMP_DRIVE_BLOCK_DEVICE) ? "host_device" : "file", - .flags = drive->flags & (QMP_DRIVE_READ_ONLY|QMP_DRIVE_NO_FLUSH), - }; - if (FLAGS_SET(bridge->features, VMSPAWN_QMP_FEATURE_IO_URING)) - file_params.flags |= QMP_DRIVE_IO_URING; - r = qmp_add_file_node(qmp, &file_params); + /* First SCSI hotplug needs a virtio-scsi-pci controller to attach to. */ + if (STR_IN_SET(drive->disk_driver, "scsi-hd", "scsi-cd") && !bridge->scsi_controller_created) { + _cleanup_free_ char *controller_port = NULL; + int controller_port_idx = -1; + if (ARCHITECTURE_NEEDS_PCIE_ROOT_PORTS) { + r = vmspawn_qmp_bridge_allocate_pcie_port(bridge, "vmspawn_scsi", + &controller_port, &controller_port_idx); + if (r == -EBUSY) + return log_error_errno(r, "No PCIe hotplug ports left for SCSI controller"); + if (r < 0) + return log_error_errno(r, "Failed to allocate PCIe hotplug port for SCSI controller: %m"); + } + + r = qmp_setup_scsi_controller(bridge, controller_port); + if (r < 0) { + vmspawn_qmp_bridge_release_pcie_port_by_idx(bridge, controller_port_idx); + return r; + } + + /* Set before the reply so a second SCSI hotplug queued in the meantime + * doesn't re-create the controller; reset in on_scsi_controller_complete on error. */ + bridge->scsi_controller_port_idx = controller_port_idx; + bridge->scsi_controller_created = true; + } + + uint64_t fdset_id; + slot_ref = drive_info_ref(drive); + r = qmp_fdset_add(bridge->qmp, TAKE_FD(drive->fd), + on_add_observe_stage, slot_ref, &drive->fdset_path, &fdset_id); if (r < 0) - return log_error_errno(r, "Failed to send blockdev-add for '%s': %m", drive->path); + return r; + TAKE_PTR(slot_ref); - QmpFormatNodeParams fmt_params = { - .node_name = drive->node_name, - .format = drive->format, - .file_node_name = file_node_name, - .flags = drive->flags & (QMP_DRIVE_READ_ONLY|QMP_DRIVE_DISCARD), - }; - _cleanup_(sd_json_variant_unrefp) sd_json_variant *fmt_args = NULL; - r = qmp_build_blockdev_add_format(&fmt_params, &fmt_args); + _cleanup_(sd_json_variant_unrefp) sd_json_variant *blockdev_args = NULL; + r = qmp_build_blockdev_add_inline( + drive->qmp_node_name, drive->format, drive->fdset_path, + FLAGS_SET(drive->flags, QMP_DRIVE_BLOCK_DEVICE) ? "host_device" : "file", + drive->flags, bridge->features, &blockdev_args); if (r < 0) return r; - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(fmt_args), on_qmp_setup_complete, (void*) "blockdev-add"); + slot_ref = drive_info_ref(drive); + r = qmp_client_invoke(bridge->qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(blockdev_args), + on_add_blockdev_stage, slot_ref); if (r < 0) - return log_error_errno(r, "Failed to send blockdev-add format for '%s': %m", drive->path); + return r; + TAKE_PTR(slot_ref); + + /* Release the monitor's original fd; the blockdev-add above took a dup. */ + slot_ref = drive_info_ref(drive); + r = qmp_fdset_remove(bridge->qmp, fdset_id, on_add_observe_stage, slot_ref); + if (r < 0) + return r; + TAKE_PTR(slot_ref); - /* device_add: attach to virtual hardware */ _cleanup_(sd_json_variant_unrefp) sd_json_variant *device_args = NULL; r = qmp_build_device_add(drive, &device_args); if (r < 0) return r; - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_setup_complete, (void*) "device_add"); + slot_ref = drive_info_ref(drive); + r = qmp_client_invoke(bridge->qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), + on_add_device_add_complete, slot_ref); if (r < 0) - return log_error_errno(r, "Failed to send device_add for '%s': %m", drive->path); + return r; + TAKE_PTR(slot_ref); - log_debug("Queued drive setup for '%s'", drive->path); + TAKE_PTR(registered); return 0; } -/* Configure a single drive via QMP. Dispatches to ephemeral or regular setup. */ -static int qmp_setup_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, DriveInfo *drive) { +static int qmp_setup_regular_drive(VmspawnQmpBridge *bridge, DriveInfo *drive) { + assert(bridge); assert(drive); + assert(drive->fd >= 0); + assert(!drive->id); - if (drive->overlay_fd >= 0) - return qmp_setup_ephemeral_drive(bridge, qmp, drive); + return vmspawn_qmp_add_block_device(bridge, drive); +} - return qmp_setup_regular_drive(bridge, qmp, 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) { @@ -585,7 +1099,7 @@ int vmspawn_qmp_setup_network(VmspawnQmpBridge *bridge, NetworkInfo *network) { return log_error_errno(r, "Failed to build getfd JSON: %m"); r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "getfd", QMP_CLIENT_ARGS_FD(getfd_args, TAKE_FD(network->fd)), - on_qmp_setup_complete, (void*) "getfd"); + on_qmp_complete, (void*) "getfd"); if (r < 0) return log_error_errno(r, "Failed to send getfd for TAP fd: %m"); } @@ -606,7 +1120,7 @@ int vmspawn_qmp_setup_network(VmspawnQmpBridge *bridge, NetworkInfo *network) { if (r < 0) return log_error_errno(r, "Failed to build netdev_add JSON: %m"); - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "netdev_add", QMP_CLIENT_ARGS(netdev_args), on_qmp_setup_complete, (void*) "netdev_add"); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "netdev_add", QMP_CLIENT_ARGS(netdev_args), on_qmp_complete, (void*) "netdev_add"); if (r < 0) return log_error_errno(r, "Failed to send netdev_add: %m"); @@ -623,7 +1137,7 @@ int vmspawn_qmp_setup_network(VmspawnQmpBridge *bridge, NetworkInfo *network) { if (r < 0) return log_error_errno(r, "Failed to build NIC device_add JSON: %m"); - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_setup_complete, (void*) "device_add"); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_complete, (void*) "device_add"); if (r < 0) return log_error_errno(r, "Failed to send NIC device_add: %m"); @@ -658,7 +1172,7 @@ static int vmspawn_qmp_setup_one_virtiofs(QmpClient *qmp, const VirtiofsInfo *vf if (r < 0) return log_error_errno(r, "Failed to build chardev-add JSON for '%s': %m", vfs->id); - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "chardev-add", QMP_CLIENT_ARGS(chardev_args), on_qmp_setup_complete, (void*) "chardev-add"); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "chardev-add", QMP_CLIENT_ARGS(chardev_args), on_qmp_complete, (void*) "chardev-add"); if (r < 0) return log_error_errno(r, "Failed to send chardev-add '%s': %m", vfs->id); @@ -675,7 +1189,7 @@ static int vmspawn_qmp_setup_one_virtiofs(QmpClient *qmp, const VirtiofsInfo *vf if (r < 0) return log_error_errno(r, "Failed to build virtiofs device_add JSON for '%s': %m", vfs->id); - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_setup_complete, (void*) "device_add"); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_complete, (void*) "device_add"); if (r < 0) return log_error_errno(r, "Failed to send virtiofs device_add '%s': %m", vfs->id); @@ -720,7 +1234,7 @@ int vmspawn_qmp_setup_vsock(VmspawnQmpBridge *bridge, VsockInfo *vsock) { return log_error_errno(r, "Failed to build getfd JSON for VSOCK: %m"); r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "getfd", QMP_CLIENT_ARGS_FD(getfd_args, TAKE_FD(vsock->fd)), - on_qmp_setup_complete, (void*) "getfd"); + on_qmp_complete, (void*) "getfd"); if (r < 0) return log_error_errno(r, "Failed to send getfd for VSOCK fd: %m"); @@ -736,7 +1250,7 @@ int vmspawn_qmp_setup_vsock(VmspawnQmpBridge *bridge, VsockInfo *vsock) { if (r < 0) return log_error_errno(r, "Failed to build VSOCK device_add JSON: %m"); - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_setup_complete, (void*) "device_add"); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_complete, (void*) "device_add"); if (r < 0) return log_error_errno(r, "Failed to send VSOCK device_add: %m"); @@ -744,36 +1258,6 @@ int vmspawn_qmp_setup_vsock(VmspawnQmpBridge *bridge, VsockInfo *vsock) { return 0; } -static bool drives_need_scsi_controller(DriveInfos *drives) { - assert(drives); - - FOREACH_ARRAY(d, drives->drives, drives->n_drives) - if (STR_IN_SET(d->disk_driver, "scsi-hd", "scsi-cd")) - return true; - - return false; -} - -static int qmp_setup_scsi_controller(QmpClient *qmp, const char *pcie_port) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL; - int r; - - r = sd_json_buildo( - &args, - SD_JSON_BUILD_PAIR_STRING("driver", "virtio-scsi-pci"), - SD_JSON_BUILD_PAIR_STRING("id", "vmspawn_scsi"), - SD_JSON_BUILD_PAIR_CONDITION(!!pcie_port, "bus", SD_JSON_BUILD_STRING(pcie_port))); - if (r < 0) - return log_error_errno(r, "Failed to build SCSI controller JSON: %m"); - - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(args), on_qmp_setup_complete, (void*) "device_add"); - if (r < 0) - return log_error_errno(r, "Failed to send SCSI controller device_add: %m"); - - log_debug("Queued virtio-scsi-pci controller setup"); - return 0; -} - int vmspawn_qmp_setup_drives(VmspawnQmpBridge *bridge, DriveInfos *drives) { int r; @@ -783,16 +1267,15 @@ int vmspawn_qmp_setup_drives(VmspawnQmpBridge *bridge, DriveInfos *drives) { QmpClient *qmp = vmspawn_qmp_bridge_get_qmp(bridge); /* io_uring support was probed during vmspawn_qmp_init(). The cached result in - * bridge->features is passed to each file node setup call. */ - - if (drives_need_scsi_controller(drives)) { - r = qmp_setup_scsi_controller(qmp, drives->scsi_pcie_port); - if (r < 0) - return r; - } + * bridge->features is passed to each file node setup call. SCSI controller + * creation is handled on-demand by vmspawn_qmp_add_block_device() for the first + * SCSI drive, using the hotplug-spares pool. */ FOREACH_ARRAY(d, drives->drives, drives->n_drives) { - r = qmp_setup_drive(bridge, qmp, d); + if ((*d)->overlay_fd >= 0) + r = qmp_setup_ephemeral_drive(bridge, qmp, *d); + else + r = qmp_setup_regular_drive(bridge, TAKE_PTR(*d)); if (r < 0) return r; } @@ -812,9 +1295,16 @@ VmspawnQmpBridge* vmspawn_qmp_bridge_free(VmspawnQmpBridge *b) { if (!b) return NULL; + /* Unref first: pending QMP callbacks may release hotplug ports through the bridge. */ + qmp_client_unref(b->qmp); + + hashmap_free(b->block_devices_by_qmp_id); + hashmap_free(b->block_devices); hashmap_free(b->pending_jobs); - qmp_client_unref(b->qmp); + FOREACH_ELEMENT(owner, b->hotplug_port_owner) + free(*owner); + return mfree(b); } @@ -910,7 +1400,7 @@ static int on_io_uring_probe_add_reply( return r; return qmp_client_invoke(c, /* ret_slot= */ NULL, "blockdev-del", QMP_CLIENT_ARGS(del_args), - on_io_uring_probe_del_reply, bridge); + on_io_uring_probe_del_reply, bridge); } static int probe_io_uring(QmpClient *c, VmspawnQmpBridge *bridge) { @@ -931,7 +1421,7 @@ static int probe_io_uring(QmpClient *c, VmspawnQmpBridge *bridge) { return r; return qmp_client_invoke(c, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(args), - on_io_uring_probe_add_reply, bridge); + on_io_uring_probe_add_reply, bridge); } static int on_probe_schema_reply( @@ -966,7 +1456,7 @@ static int probe_schema(QmpClient *c, VmspawnQmpBridge *bridge) { assert(bridge); return qmp_client_invoke(c, /* ret_slot= */ NULL, "query-qmp-schema", QMP_CLIENT_ARGS(NULL), - on_probe_schema_reply, bridge); + on_probe_schema_reply, bridge); } int vmspawn_qmp_init(VmspawnQmpBridge **ret, int fd, sd_event *event) { @@ -981,6 +1471,8 @@ int vmspawn_qmp_init(VmspawnQmpBridge **ret, int fd, sd_event *event) { if (!bridge) return log_oom(); + bridge->scsi_controller_port_idx = -1; + r = qmp_client_connect_fd(&bridge->qmp, fd); if (r < 0) return log_error_errno(r, "Failed to create QMP client: %m"); @@ -1044,16 +1536,20 @@ static int on_cont_complete( assert(client); + VmspawnQmpBridge *bridge = ASSERT_PTR(userdata); + if (error < 0) { log_error_errno(error, "Failed to resume QEMU execution: %s", strna(error_desc)); return sd_event_exit(qmp_client_get_event(client), error); } + /* VM is running — all boot-time device setup has completed. */ + bridge->setup_done = true; return 0; } int vmspawn_qmp_start(VmspawnQmpBridge *bridge) { assert(bridge); - return qmp_client_invoke(bridge->qmp, /* ret_slot= */ NULL, "cont", /* args= */ NULL, on_cont_complete, /* userdata= */ NULL); + return qmp_client_invoke(bridge->qmp, /* ret_slot= */ NULL, "cont", /* args= */ NULL, on_cont_complete, bridge); } diff --git a/src/vmspawn/vmspawn-qmp.h b/src/vmspawn/vmspawn-qmp.h index 8f8c26fb03..d8403520c9 100644 --- a/src/vmspawn/vmspawn-qmp.h +++ b/src/vmspawn/vmspawn-qmp.h @@ -3,8 +3,11 @@ #include +#include "machine-util.h" #include "shared-forward.h" +#define VMSPAWN_PCIE_HOTPLUG_SPARES 10 + /* Pending job continuation — called when a QMP background job reaches "concluded" state. * Used by blockdev-create to chain remaining drive setup after the job completes. */ typedef int (*pending_job_callback_t)(QmpClient *qmp, void *userdata); @@ -26,8 +29,15 @@ typedef enum VmspawnQmpFeatureFlags { typedef struct VmspawnQmpBridge { QmpClient *qmp; - Hashmap *pending_jobs; /* job_id (string, owned) -> PendingJob* */ + Hashmap *pending_jobs; /* blockdev-create continuations */ + Hashmap *block_devices; /* user_id (char*) → DriveInfo* (owned ref) */ + Hashmap *block_devices_by_qmp_id; /* qmp_device_id (char*) → DriveInfo* (non-owning view) */ + char *hotplug_port_owner[VMSPAWN_PCIE_HOTPLUG_SPARES]; /* owner id per port; NULL = free */ + int scsi_controller_port_idx; /* hotplug port idx taken by virtio-scsi-pci, -1 if none */ + uint64_t next_block_counter; /* monotonic counter feeding internal QMP names (vmspawn--*) */ VmspawnQmpFeatureFlags features; + bool setup_done; + bool scsi_controller_created; /* virtio-scsi-pci has been device_add'd */ } VmspawnQmpBridge; VmspawnQmpBridge* vmspawn_qmp_bridge_free(VmspawnQmpBridge *b); @@ -64,25 +74,49 @@ typedef enum QmpDriveFlags { QMP_DRIVE_DISCARD_NO_UNREF = 1u << 6, /* qcow2 only */ } QmpDriveFlags; -/* Drive info for QMP-based drive setup */ +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. + * + * link == NULL → boot-time: failure calls sd_event_exit (if !setup_done). + * link != NULL → hotplug: failure replies via the varlink link. */ typedef struct DriveInfo { - const char *path; /* kept for logging only — not passed to QEMU */ - const char *format; /* "raw" or "qcow2" */ - const char *disk_driver; /* "virtio-blk-pci", "scsi-hd", "scsi-cd", "nvme" */ - char *serial; /* owned */ - char *node_name; /* owned */ - char *pcie_port; /* owned: pcie-root-port id for device_add bus (NULL on non-PCIe) */ - int fd; /* pre-opened image fd (owned, -EBADF if unused) */ - int overlay_fd; /* pre-opened anonymous overlay fd for ephemeral (owned, -EBADF if unused) */ + unsigned n_ref; + + /* Config */ + char *path; /* original path (for logging; not passed to QEMU) */ + char *format; /* "raw" or "qcow2" */ + char *disk_driver; /* "virtio-blk-pci", "scsi-hd", "scsi-cd", "nvme" */ + char *serial; + char *pcie_port; /* pcie-root-port id for device_add bus (NULL on non-PCIe) */ + int fd; /* pre-opened image fd (-EBADF if unused) */ + int overlay_fd; /* pre-opened anonymous overlay fd for ephemeral (-EBADF if unused) */ QmpDriveFlags flags; + + /* Per-add-op state (populated by the add flow; zeroed at CLI-parse time) */ + VmspawnQmpBridge *bridge; /* weak */ + char *id; /* varlink-visible id (caller-supplied, or falls back to qmp_device_id) */ + DiskType disk_type; /* for the ListBlockDevices `driver` field */ + uint64_t counter; /* internal N used in qmp_node_name / qmp_device_id */ + char *qmp_node_name; /* "vmspawn--storage" */ + char *qmp_device_id; /* "vmspawn--disk" */ + char *fdset_path; /* "/dev/fdset/N" */ + int pcie_port_idx; /* hotplug port idx held by this drive; -1 once committed or unused */ + BlockDeviceStateFlags state; + sd_varlink *link; /* ref'd iff hotplug */ } DriveInfo; -void drive_info_done(DriveInfo *info); +DriveInfo* drive_info_new(void); +DECLARE_TRIVIAL_REF_UNREF_FUNC(DriveInfo, drive_info); +DEFINE_TRIVIAL_CLEANUP_FUNC(DriveInfo *, drive_info_unref); typedef struct DriveInfos { - DriveInfo *drives; + DriveInfo **drives; /* array of individually heap-allocated entries */ size_t n_drives; - char *scsi_pcie_port; /* owned: pcie-root-port id for SCSI controller (NULL if no SCSI or non-PCIe) */ } DriveInfos; void drive_infos_done(DriveInfos *infos); @@ -143,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-settings.c b/src/vmspawn/vmspawn-settings.c index 9382172e2c..502189e7be 100644 --- a/src/vmspawn/vmspawn-settings.c +++ b/src/vmspawn/vmspawn-settings.c @@ -3,22 +3,6 @@ #include "string-table.h" #include "vmspawn-settings.h" -static const char *const image_format_table[_IMAGE_FORMAT_MAX] = { - [IMAGE_FORMAT_RAW] = "raw", - [IMAGE_FORMAT_QCOW2] = "qcow2", -}; - -DEFINE_STRING_TABLE_LOOKUP(image_format, ImageFormat); - -static const char *const disk_type_table[_DISK_TYPE_MAX] = { - [DISK_TYPE_VIRTIO_BLK] = "virtio-blk", - [DISK_TYPE_VIRTIO_SCSI] = "virtio-scsi", - [DISK_TYPE_NVME] = "nvme", - [DISK_TYPE_VIRTIO_SCSI_CDROM] = "scsi-cd", -}; - -DEFINE_STRING_TABLE_LOOKUP(disk_type, DiskType); - void extra_drive_context_done(ExtraDriveContext *ctx) { assert(ctx); diff --git a/src/vmspawn/vmspawn-settings.h b/src/vmspawn/vmspawn-settings.h index f02b499201..596a66cecd 100644 --- a/src/vmspawn/vmspawn-settings.h +++ b/src/vmspawn/vmspawn-settings.h @@ -1,24 +1,9 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "machine-util.h" #include "shared-forward.h" -typedef enum ImageFormat { - IMAGE_FORMAT_RAW, - IMAGE_FORMAT_QCOW2, - _IMAGE_FORMAT_MAX, - _IMAGE_FORMAT_INVALID = -EINVAL, -} ImageFormat; - -typedef enum DiskType { - DISK_TYPE_VIRTIO_BLK, - DISK_TYPE_VIRTIO_SCSI, - DISK_TYPE_NVME, - DISK_TYPE_VIRTIO_SCSI_CDROM, - _DISK_TYPE_MAX, - _DISK_TYPE_INVALID = -EINVAL, -} DiskType; - typedef struct ExtraDrive { char *path; ImageFormat format; @@ -69,6 +54,4 @@ typedef enum SettingsMask { DECLARE_STRING_TABLE_LOOKUP(console_mode, ConsoleMode); DECLARE_STRING_TABLE_LOOKUP(console_transport, ConsoleTransport); -DECLARE_STRING_TABLE_LOOKUP(disk_type, DiskType); DECLARE_STRING_TABLE_LOOKUP(firmware, Firmware); -DECLARE_STRING_TABLE_LOOKUP(image_format, ImageFormat); diff --git a/src/vmspawn/vmspawn-varlink.c b/src/vmspawn/vmspawn-varlink.c index 4a11b6fd4e..51a1091e40 100644 --- a/src/vmspawn/vmspawn-varlink.c +++ b/src/vmspawn/vmspawn-varlink.c @@ -9,9 +9,9 @@ #include "string-util.h" #include "strv.h" #include "varlink-io.systemd.MachineInstance.h" -#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( @@ -51,10 +51,10 @@ static int on_qmp_simple_complete( assert(client); - if (error == 0) - (void) sd_varlink_reply(link, NULL); - else + if (error < 0) (void) qmp_error_to_varlink(link, error_desc, error); + else + (void) sd_varlink_reply(link, NULL); sd_varlink_unref(link); return 0; @@ -118,18 +118,23 @@ static int on_qmp_describe_complete( assert(client); - if (error != 0) { + if (error < 0) { (void) qmp_error_to_varlink(link, error_desc, error); return 0; } - sd_json_variant *running = sd_json_variant_by_key(result, "running"); - sd_json_variant *status = sd_json_variant_by_key(result, "status"); + sd_json_variant *running_v = sd_json_variant_by_key(result, "running"); + sd_json_variant *status_v = sd_json_variant_by_key(result, "status"); + + bool running = running_v ? sd_json_variant_boolean(running_v) : false; + + const char *status = status_v && sd_json_variant_is_string(status_v) ? + sd_json_variant_string(status_v) : "unknown"; (void) sd_varlink_replybo( link, - SD_JSON_BUILD_PAIR_BOOLEAN("running", running ? sd_json_variant_boolean(running) : false), - SD_JSON_BUILD_PAIR_STRING("status", status && sd_json_variant_is_string(status) ? sd_json_variant_string(status) : "unknown")); + SD_JSON_BUILD_PAIR_BOOLEAN("running", running), + SD_JSON_BUILD_PAIR_STRING("status", status)); return 0; } @@ -154,6 +159,10 @@ static int vl_method_subscribe_events(sd_varlink *link, sd_json_variant *paramet if (r != 0) return r; + /* Treat [] identically to null: deliver all events. */ + if (strv_isempty(filter)) + filter = strv_free(filter); + sd_varlink_ref(link); r = hashmap_ensure_put(&ctx->subscribed, &varlink_subscriber_hash_ops, link, filter); @@ -174,10 +183,6 @@ static int vl_method_subscribe_events(sd_varlink *link, sd_json_variant *paramet return 0; } -static int vl_method_acquire_qmp(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { - return sd_varlink_error_errno(link, -EOPNOTSUPP); -} - static void vl_disconnect(sd_varlink_server *server, sd_varlink *link, void *userdata) { VmspawnVarlinkContext *ctx = ASSERT_PTR(userdata); @@ -258,31 +263,21 @@ static int dispatch_pending_job(VmspawnQmpBridge *bridge, sd_json_variant *data) return 1; } -static int on_qmp_event( - QmpClient *client, - const char *event, - sd_json_variant *data, - void *userdata) { - - VmspawnVarlinkContext *ctx = ASSERT_PTR(userdata); +static int notify_event_subscribers(VmspawnVarlinkContext *ctx, const char *event_name, sd_json_variant *data) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *notification = NULL; sd_varlink *link; char **filter; int r; - assert(client); - assert(event); - - /* Dispatch job status changes to pending continuations (e.g. blockdev-create) */ - if (streq(event, "JOB_STATUS_CHANGE")) - return dispatch_pending_job(ctx->bridge, data); + assert(ctx); + assert(event_name); if (hashmap_isempty(ctx->subscribed)) return 0; r = sd_json_buildo( ¬ification, - SD_JSON_BUILD_PAIR_STRING("event", event), + SD_JSON_BUILD_PAIR_STRING("event", event_name), SD_JSON_BUILD_PAIR_CONDITION(!!data, "data", SD_JSON_BUILD_VARIANT(data))); if (r < 0) { log_warning_errno(r, "Failed to build event notification, ignoring: %m"); @@ -290,7 +285,7 @@ static int on_qmp_event( } HASHMAP_FOREACH_KEY(filter, link, ctx->subscribed) { - if (filter && !strv_contains(filter, event)) + if (filter && !strv_contains(filter, event_name)) continue; r = sd_varlink_notify(link, notification); @@ -301,6 +296,28 @@ static int on_qmp_event( return 0; } +static int on_qmp_event( + QmpClient *client, + const char *event, + sd_json_variant *data, + void *userdata) { + + VmspawnVarlinkContext *ctx = ASSERT_PTR(userdata); + + assert(client); + assert(event); + + /* Dispatch job status changes to pending continuations (e.g. blockdev-create) */ + 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); +} + /* Free all subscriber entries — varlink_subscriber_hash_ops handles * close + unref for each key and strv_free for each value. */ static void drain_event_subscribers(Hashmap **subscribed) { @@ -350,8 +367,7 @@ int vmspawn_varlink_setup( r = sd_varlink_server_add_interface_many( ctx->varlink_server, &vl_interface_io_systemd_MachineInstance, - &vl_interface_io_systemd_VirtualMachineInstance, - &vl_interface_io_systemd_QemuMachineInstance); + &vl_interface_io_systemd_VirtualMachineInstance); if (r < 0) return log_error_errno(r, "Failed to add varlink interfaces: %m"); @@ -363,8 +379,7 @@ int vmspawn_varlink_setup( "io.systemd.MachineInstance.Resume", vl_method_resume, "io.systemd.MachineInstance.Reboot", vl_method_reboot, "io.systemd.MachineInstance.Describe", vl_method_describe, - "io.systemd.MachineInstance.SubscribeEvents", vl_method_subscribe_events, - "io.systemd.QemuMachineInstance.AcquireQMP", vl_method_acquire_qmp); + "io.systemd.MachineInstance.SubscribeEvents", vl_method_subscribe_events); if (r < 0) return log_error_errno(r, "Failed to bind varlink methods: %m"); @@ -387,6 +402,7 @@ int vmspawn_varlink_setup( ctx->bridge = bridge; qmp_client_bind_event(ctx->bridge->qmp, on_qmp_event, ctx); qmp_client_bind_disconnect(ctx->bridge->qmp, on_qmp_disconnect, ctx); + qmp_client_set_userdata(ctx->bridge->qmp, ctx->bridge); log_debug("Varlink control server listening on %s", listen_address); diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index e0ddcee9b1..1a349a1d63 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -102,9 +102,6 @@ #define DISK_SERIAL_MAX_LEN_NVME 20 #define DISK_SERIAL_MAX_LEN_VIRTIO_BLK 20 -/* Spare pcie-root-ports reserved for future runtime hotplug beyond the pre-wired devices. */ -#define VMSPAWN_PCIE_HOTPLUG_SPARES 10u - /* An enum controlling how auxiliary state for the VM are maintained, i.e. the TPM state and the EFI variable * NVRAM. */ typedef enum StateMode { @@ -752,39 +749,9 @@ static int parse_argv(int argc, char *argv[]) { OPTION_LONG("extra-drive", "[FORMAT:][DISKTYPE:]PATH", "Adds an additional disk to the VM"): { ImageFormat format = IMAGE_FORMAT_RAW; DiskType extra_disk_type = _DISK_TYPE_INVALID; - const char *dp = arg; - - /* Parse optional colon-separated prefixes. The format and disk type - * value sets don't overlap, so they can appear in any order. */ - for (;;) { - const char *colon = strchr(dp, ':'); - if (!colon) - break; - - _cleanup_free_ char *prefix = strndup(dp, colon - dp); - if (!prefix) - return log_oom(); - - ImageFormat f = image_format_from_string(prefix); - if (f >= 0) { - format = f; - dp = colon + 1; - continue; - } - - DiskType dt = disk_type_from_string(prefix); - if (dt >= 0) { - extra_disk_type = dt; - dp = colon + 1; - continue; - } - - /* Not a recognized prefix, treat the rest as the path */ - break; - } - _cleanup_free_ char *drive_path = NULL; - r = parse_path_argument(dp, /* suppress_root= */ false, &drive_path); + + r = parse_disk_spec(arg, &format, &extra_disk_type, &drive_path); if (r < 0) return r; @@ -2303,6 +2270,7 @@ static int qemu_config_add_qmp_monitor(FILE *config_file, int bridge_fds[2], int } static int resolve_disk_driver(DiskType dt, const char *filename, DriveInfo *info) { + size_t serial_max; int r; assert(filename); @@ -2310,34 +2278,30 @@ static int resolve_disk_driver(DiskType dt, const char *filename, DriveInfo *inf switch (dt) { case DISK_TYPE_VIRTIO_BLK: - info->disk_driver = "virtio-blk-pci"; - r = disk_serial(filename, DISK_SERIAL_MAX_LEN_VIRTIO_BLK, &info->serial); - if (r < 0) - return r; + serial_max = DISK_SERIAL_MAX_LEN_VIRTIO_BLK; break; case DISK_TYPE_VIRTIO_SCSI: - info->disk_driver = "scsi-hd"; - r = disk_serial(filename, DISK_SERIAL_MAX_LEN_SCSI, &info->serial); - if (r < 0) - return r; + serial_max = DISK_SERIAL_MAX_LEN_SCSI; break; case DISK_TYPE_NVME: - info->disk_driver = "nvme"; - r = disk_serial(filename, DISK_SERIAL_MAX_LEN_NVME, &info->serial); - if (r < 0) - return r; + serial_max = DISK_SERIAL_MAX_LEN_NVME; break; case DISK_TYPE_VIRTIO_SCSI_CDROM: - info->disk_driver = "scsi-cd"; + serial_max = DISK_SERIAL_MAX_LEN_SCSI; info->flags |= QMP_DRIVE_READ_ONLY; - r = disk_serial(filename, DISK_SERIAL_MAX_LEN_SCSI, &info->serial); - if (r < 0) - return r; break; default: assert_not_reached(); } + info->disk_driver = strdup(ASSERT_PTR(qemu_device_driver_to_string(dt))); + if (!info->disk_driver) + return log_oom(); + + r = disk_serial(filename, serial_max, &info->serial); + if (r < 0) + return r; + return 0; } @@ -2355,8 +2319,9 @@ static int prepare_primary_drive(const char *runtime_dir, DriveInfos *drives) { if (r < 0) return log_error_errno(r, "Failed to extract filename from path '%s': %m", arg_image); - DriveInfo *d = &drives->drives[drives->n_drives++]; - *d = (DriveInfo) { .fd = -EBADF, .overlay_fd = -EBADF }; + _cleanup_(drive_info_unrefp) DriveInfo *d = drive_info_new(); + if (!d) + return log_oom(); r = resolve_disk_driver(arg_image_disk_type, image_fn, d); if (r < 0) @@ -2376,10 +2341,9 @@ static int prepare_primary_drive(const char *runtime_dir, DriveInfos *drives) { if (r < 0) return log_error_errno(r, "Expected regular file or block device for image: %s", arg_image); - d->path = arg_image; - d->format = image_format_to_string(arg_image_format); - d->node_name = strdup("vmspawn"); - if (!d->node_name) + d->path = strdup(arg_image); + d->format = strdup(ASSERT_PTR(image_format_to_string(arg_image_format))); + if (!d->path || !d->format) return log_oom(); d->fd = TAKE_FD(image_fd); if (S_ISBLK(st.st_mode)) @@ -2406,6 +2370,7 @@ static int prepare_primary_drive(const char *runtime_dir, DriveInfos *drives) { d->flags |= QMP_DRIVE_NO_FLUSH; } + drives->drives[drives->n_drives++] = TAKE_PTR(d); return 0; } @@ -2414,7 +2379,6 @@ static int prepare_extra_drives(DriveInfos *drives) { assert(drives); - size_t extra_idx = 0; FOREACH_ARRAY(drive, arg_extra_drives.drives, arg_extra_drives.n_drives) { _cleanup_free_ char *drive_fn = NULL; r = path_extract_filename(drive->path, &drive_fn); @@ -2423,8 +2387,9 @@ static int prepare_extra_drives(DriveInfos *drives) { DiskType dt = drive->disk_type >= 0 ? drive->disk_type : arg_image_disk_type; - DriveInfo *d = &drives->drives[drives->n_drives++]; - *d = (DriveInfo) { .fd = -EBADF, .overlay_fd = -EBADF }; + _cleanup_(drive_info_unrefp) DriveInfo *d = drive_info_new(); + if (!d) + return log_oom(); r = resolve_disk_driver(dt, drive_fn, d); if (r < 0) @@ -2445,15 +2410,16 @@ static int prepare_extra_drives(DriveInfos *drives) { "Block device '%s' cannot be used with 'qcow2' format, only 'raw' is supported.", drive->path); - d->path = drive->path; - d->format = image_format_to_string(drive->format); + d->path = strdup(drive->path); + d->format = strdup(ASSERT_PTR(image_format_to_string(drive->format))); + if (!d->path || !d->format) + return log_oom(); d->fd = TAKE_FD(drive_fd); if (S_ISBLK(drive_st.st_mode)) d->flags |= QMP_DRIVE_BLOCK_DEVICE; d->flags |= QMP_DRIVE_NO_FLUSH; - if (asprintf(&d->node_name, "vmspawn_extra_%zu", extra_idx++) < 0) - return log_oom(); + drives->drives[drives->n_drives++] = TAKE_PTR(d); } return 0; @@ -2474,19 +2440,15 @@ static int assign_pcie_ports(MachineConfig *c) { size_t port = 0; - /* Drives: non-SCSI drives get individual ports, SCSI controller gets one port */ - bool need_scsi = false; + /* Non-SCSI drives get individual ports. SCSI controllers (if any) allocate + * from the hotplug-spares pool on demand at device-add time. */ FOREACH_ARRAY(d, drives->drives, drives->n_drives) { - if (STR_IN_SET(d->disk_driver, "scsi-hd", "scsi-cd")) { - need_scsi = true; + DriveInfo *drive = *d; + if (STR_IN_SET(drive->disk_driver, "scsi-hd", "scsi-cd")) continue; - } - if (asprintf(&d->pcie_port, "vmspawn-pcieport-%zu", port++) < 0) + if (asprintf(&drive->pcie_port, "vmspawn-pcieport-%zu", port++) < 0) return log_oom(); } - if (need_scsi) - if (asprintf(&drives->scsi_pcie_port, "vmspawn-pcieport-%zu", port++) < 0) - return log_oom(); if (network->type) if (asprintf(&network->pcie_port, "vmspawn-pcieport-%zu", port++) < 0) @@ -2513,7 +2475,7 @@ static int prepare_device_info(const char *runtime_dir, MachineConfig *c) { /* Build drive info for QMP-based setup. vmspawn opens all image files and * passes fds to QEMU via add-fd — QEMU never needs filesystem access. */ - drives->drives = new0(DriveInfo, 1 + arg_extra_drives.n_drives); + drives->drives = new0(DriveInfo*, 1 + arg_extra_drives.n_drives); if (!drives->drives) return log_oom();