vmspawn: prepare QMP infrastructure for runtime block-device hotplug (#41763)

The block-device hotplug work (#NNNNN) needs a number of cross-cutting
  changes to the QMP plumbing that aren't hotplug-specific in
  themselves: a refcounted DriveInfo so async stage callbacks can keep
  slot refs, a counter-based naming scheme so multiple drives can share
  the same backing path, pipelined remove-fd so QEMU's fdsets get
  released at blockdev-del time, and a generic-completion callback that
  doesn't tear the VM down on a runtime QMP error. None of these change
  behaviour from a user's point of view, none of them depend on the
  varlink hotplug methods landing, and several are wins on their own
  (the fdset leak in particular is observable today with --extra-drive
  under long-running VMs). Pulling them out of the hotplug PR keeps
  that PR focused on the IDL + server-side method handlers, and lets
  this preparatory work land on its own merits without waiting for the
  larger feature review.

  Cleanup pieces that fall out for free:

    qmp-client: widen next_fdset_id to uint64_t
    vmspawn: move VMSPAWN_PCIE_HOTPLUG_SPARES to vmspawn-qmp.h
    vmspawn-varlink: use error < 0 in async QMP completion callbacks
    vmspawn-varlink: simplify on_qmp_describe_complete result extraction
    vmspawn-varlink: extract notify_event_subscribers from on_qmp_event
    vmspawn-varlink: treat empty event subscription filter as catch-all
    vmspawn-qmp: pass bridge to on_cont_complete via invoke userdata

  Infrastructure the hotplug add path will sit on top of:

    vmspawn-qmp: convert DriveInfo to a refcounted object
    vmspawn-qmp: derive QMP node and device ids from a bridge counter
    vmspawn-qmp: pipeline remove-fd after each blockdev-add
    vmspawn-qmp: keep the event loop running on post-setup QMP failures
    vmspawn-qmp: add the hotplug-capable block-device add machinery
    vmspawn-qmp: add vmspawn_qmp_remove_block_device

  The two final commits introduce vmspawn_qmp_add_block_device() and
  vmspawn_qmp_remove_block_device() but leave them without varlink
  callers — the io.systemd.VirtualMachineInstance method handlers that
  forward into them land with the rest of the hotplug PR. Boot-time
  drive setup is rewritten on top of vmspawn_qmp_add_block_device() so
  the hotplug and boot paths share a single staged-add pipeline from
  day one.
This commit is contained in:
Christian Brauner
2026-04-24 17:38:54 +02:00
committed by GitHub
14 changed files with 964 additions and 356 deletions
+102
View File
@@ -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;
}
+32
View File
@@ -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);
+1 -1
View File
@@ -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',
+19 -2
View File
@@ -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++;
}
+4 -1
View File
@@ -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);
+1 -1
View File
@@ -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),
@@ -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);
@@ -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;
+671 -175
View File
File diff suppressed because it is too large Load Diff
+49 -13
View File
@@ -3,8 +3,11 @@
#include <net/ethernet.h>
#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-<N>-*) */
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-<counter>-storage" */
char *qmp_device_id; /* "vmspawn-<counter>-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);
-16
View File
@@ -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);
+1 -18
View File
@@ -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);
+48 -32
View File
@@ -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(
&notification,
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);
+36 -74
View File
@@ -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();