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:
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
@@ -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++;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+49
-13
@@ -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);
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
+36
-74
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user