systemd-vmspawn: QMP-varlink bridge for VM runtime control (#41449)

systemd-vmspawn currently has zero runtime control over the VMs it
launches. It can kill QEMU (SIGTERM) or SSH in, but it cannot pause,
resume, request a graceful power-off, query status, or
react to VM events. QEMU exposes all of this via its QMP protocol;
systemd's native IPC is varlink. This series bridges the two.

  Architecture

```
  machinectl → machined (Machine.List → discovers controlAddress)
  machinectl → vmspawn varlink socket (direct connection)
                 ├── io.systemd.MachineInstance       (generic VM control)
                 ├── io.systemd.VirtualMachineInstance (placeholder)
                 └── io.systemd.QemuMachineInstance   (QEMU-specific; AcquireQMP stub)
  vmspawn internally → socketpair → QEMU QMP
```

machined stores the controlAddress but never connects to vmspawn.
machinectl discovers the address from Machine.List and connects
directly. Socket mode 0600 is the access-control boundary —
the socket is rooted in vmspawn's $RUNTIME_DIRECTORY, so only the UID
that launched the VM can talk to it.

  QMP client library (src/shared/qmp-client.{c,h})

  A small non-blocking QMP client modeled on sd-varlink's pump contract:

- Reference-counted QmpClient with an explicit five-state machine:
HANDSHAKE_INITIAL → HANDSHAKE_GREETING_RECEIVED →
HANDSHAKE_CAPABILITIES_SENT → RUNNING → DISCONNECTED.
- qmp_client_connect_fd() is non-blocking: it wraps the fd in a
JsonStream and returns immediately. The greeting + qmp_capabilities
handshake is driven lazily on the first
qmp_client_invoke() or by the event loop — whichever comes first — so
callers never block during connect.
- qmp_client_attach_event() attaches to sd_event for async operation;
qmp_client_process() performs one pump step (write → dispatch → parse →
read → disconnect) with the same contract as
sd_varlink_process(); qmp_client_wait() blocks until the next I/O event.
- qmp_client_invoke() sends an async command and fires the registered
qmp_command_callback_t with (result, error_desc, error, userdata) on
completion. Synchronous callers drive
  process()/wait() in a loop until qmp_client_is_idle() is true.
- QmpClientArgs bundles the JSON arguments and an FD list for a single
command; the QMP_CLIENT_ARGS_FD() macro hands one fd to the callee for
SCM_RIGHTS passing. On partial-stage failure the
args list is narrowed so the caller's cleanup closes only the
untransferred tail.
- Event broadcast to a registered callback via qmp_client_bind_event();
transport loss surfaces through qmp_client_bind_disconnect().
- qmp_schema_has_member() walks the query-qmp-schema result for optional
runtime capability probes.

  vmspawn device setup via QMP

vmspawn starts QEMU paused (-S), sets up devices via QMP, then resumes
with cont. The entire device plane moves off the legacy INI config path
and onto the bridge.

A new MachineConfig aggregate in vmspawn-qmp.h groups the per-device
info (DriveInfos, NetworkInfo, VirtiofsInfos, VsockInfo) with a single
machine_config_done() cleanup that chains the
sub-structure destructors; each conversion patch populates exactly the
field it owns.

  What the conversion enables:

- FD-based device passing via add-fd / getfd + SCM_RIGHTS — vmspawn
opens every image file, TAP, VSOCK, and virtiofs socket itself and hands
the fd to QEMU. QEMU never needs filesystem
  access.
- Ephemeral overlays via blockdev-create + async job-concluded
continuations on anonymous O_TMPFILE / memfd backings — no named overlay
files on disk.
- PCIe root-port pre-allocation for q35/virt machine types so
hotplug-capable slots exist at boot (NVMe, virtio-scsi, etc.).
- io_uring availability probing with automatic fallback to the default
AIO backend if QEMU's build doesn't support it.

Per-command callbacks call sd_event_exit() on setup failure so vmspawn
shuts down cleanly if any device can't be attached.

  machinectl integration

- machinectl pause / resume / poweroff / reboot / terminate go through
the varlink control socket for VMs.
- D-Bus fallback for containers: poweroff sends SIGRTMIN+4, terminate
calls the existing TerminateMachine method — unchanged container
behavior.
- Multi-machine parallel dispatch via sd_event for bulk operations
(machinectl pause vm1 vm2 ...) so one slow VM doesn't serialize the
rest.
- SubscribeEvents streaming with per-subscriber event-name filters
(importd Pull-style pattern: initial {ready:true} notify, fan out via
varlink_many_notifybo(), lazy init — QMP event pump
  runs only while subscribers exist).

  Tests

- Unit test with a mock QMP server covering handshake, command/response,
events, and EOF.
- Integration test against real QEMU (-machine none) exercising
handshake + query-qmp-schema (~200 KB reply, validates the buffered
reader across multiple read()s) and query-status.
- Integration test for the machinectl verbs end-to-end: pause / resume /
describe / subscribe / terminate.
- Integration test for the multi-drive pipeline and ephemeral overlays
(blockdev-create async job continuations).
- Stress test: 5 cycles of start → 3× (pause/describe/resume/describe) →
terminate.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
Christian Brauner
2026-04-15 12:33:35 +02:00
committed by GitHub
41 changed files with 4759 additions and 323 deletions
+82
View File
@@ -2779,6 +2779,88 @@ SPDX-License-Identifier: LGPL-2.1-or-later
- translate SIGTERM to clean ACPI shutdown event
- implement hotkeys ^]^]r and ^]^]p like nspawn
- **vmspawn disk hotplug:**
- virtio-blk-pci — the simplest path. Each disk is an independent PCI
device. QMP sequence (two steps):
blockdev-add {driver: "raw", node-name: "disk1",
file: {driver: "file", filename: "/path/to/img"}}
device_add {driver: "virtio-blk-pci", id: "disk1", drive: "disk1"}
Removal (three steps):
device_del {id: "disk1"}
... wait for DEVICE_DELETED event (guest acknowledges unplug) ...
blockdev-del {node-name: "disk1"}
Works on both i440fx (legacy PCI) and q35 (PCIe) machine types. PCI
address auto-assigned by QEMU — no topology pre-configuration needed.
Each disk independently hotpluggable. Guest sees a virtio block
device (/dev/vdX). Well-tested path — used by libvirt, Incus, and all
major VM managers. No special boot-time setup required.
- NVMe — two-level model: controller + namespace(s). The controller is
a PCIe device; namespaces live on an internal NVMe bus attached to
the controller. Key limitation: namespaces are NOT hotpluggable —
TYPE_NVME_BUS has no HotplugHandler, so device_add of nvme-ns at
runtime fails with "Bus does not support hotplugging". The only
option is hotplugging the entire controller, which embeds one
namespace via its "drive" property:
blockdev-add {driver: "raw", node-name: "disk1",
file: {driver: "file", filename: "/path/to/img"}}
device_add {driver: "nvme", id: "disk1", drive: "disk1", serial: "disk1"}
Same two-step pattern as virtio-blk, with these limitations:
1. PCIe-only (implements INTERFACE_PCIE_DEVICE). Does not work on
i440fx. Requires q35 or virt (aarch64).
2. Requires pre-configured PCIe root ports that exist at boot.
Without them, device_add fails with "no slot/function available".
vmspawn would need to create empty root ports at QEMU startup to
reserve hotplug slots.
3. No namespace-level granularity. Each hotplugged disk burns a full
PCIe slot (controller + one namespace). Cannot add multiple
namespaces to a single controller at runtime.
4. Serial property required (up to 20 chars). virtio-blk does not
require it.
- virtio-scsi — shared virtio-scsi-pci controller with individual
scsi-hd devices attached. Incus uses this as its default bus. The
controller must exist at boot, but individual disks (LUNs) can be
hotplugged onto it without burning PCI slots. Scales better than
virtio-blk when many disks are needed, but adds complexity
(controller management, LUN assignment).
- **vmspawn AcquireQMP():** implement as id-rewriting proxy with FD
passing. vmspawn acts as a QMP multiplexer. When a client calls
AcquireQMP():
1. Create socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0).
2. Return one end to the client via sd_varlink_push_fd() +
sd_varlink_reply().
3. Add an sd-event I/O source on vmspawn's end for the client socket.
4. Send a synthetic QMP greeting on the client socket, handle
qmp_capabilities locally. Maybe we don't even need that and just
document that it's a fully initialized connection.
5. For client commands: read from the client socket, rewrite id to
vmspawn's internal counter (store mapping internal_id ->
(client_fd, original_id_json_variant)), forward to QEMU.
6. For QEMU responses: match internal id, look up original client id,
rewrite back, send to the correct client socket.
7. Broadcast QMP events (no id) to all AcquireQMP clients AND to
SubscribeEvents subscribers.
8. On client EOF: remove the I/O source, clean up id mappings.
This keeps vmspawn in full control of the QMP connection — VMControl
handlers and multiple AcquireQMP clients can coexist without id
collisions. The server needs SD_VARLINK_SERVER_ALLOW_FD_PASSING_OUTPUT
(already set in machined's pattern).
AcquireQMP() also requires server-side Varlink protocol upgrades.
mvo's WIP branch:
<https://github.com/systemd/systemd/compare/main...mvo5:varlink-protocol-upgrade-server-side?expand=1>
- we probably needs .pcrpkeyrd or so as additional PE section in UKIs,
which contains a separate public key for PCR values that only apply in the
initrd, i.e. in the boot phase "enter-initrd". Then, consumers in userspace
+39 -14
View File
@@ -262,16 +262,17 @@
<varlistentry>
<term><command>poweroff</command> <replaceable>NAME</replaceable>…</term>
<listitem><para>Power off one or more containers. This will
trigger a shutdown by sending SIGRTMIN+4 to the container's init
process, which causes systemd-compatible init systems to shut
down cleanly. Use <command>stop</command> as alias for <command>poweroff</command>.
This operation does not work on containers that do not run a
<listitem><para>Power off one or more machines. For VMs managed by
<citerefentry><refentrytitle>systemd-vmspawn</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
this sends an ACPI powerdown request via QMP. For containers, this
sends SIGRTMIN+4 to the container's init process, which causes
systemd-compatible init systems to shut down cleanly. This
operation does not work on containers that do not run a
<citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>-compatible
init system, such as sysvinit. Use
<command>terminate</command> (see below) to immediately
terminate a container or VM, without cleanly shutting it
down.</para>
<command>stop</command> as alias for <command>poweroff</command>.
Use <command>terminate</command> (see below) to immediately
terminate a machine without cleanly shutting it down.</para>
<xi:include href="version-info.xml" xpointer="v212"/></listitem>
</varlistentry>
@@ -279,16 +280,40 @@
<varlistentry>
<term><command>reboot</command> <replaceable>NAME</replaceable>…</term>
<listitem><para>Reboot one or more containers. This will
trigger a reboot by sending SIGINT to the container's init
process, which is roughly equivalent to pressing Ctrl+Alt+Del
on a non-containerized system, and is compatible with
containers running any system manager. Use <command>restart</command> as alias
for <command>reboot</command>.</para>
<listitem><para>Reboot one or more machines. For VMs managed by
<citerefentry><refentrytitle>systemd-vmspawn</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
this sends a system reset request via QMP. For containers, this
sends SIGINT to the container's init process, which is roughly
equivalent to pressing Ctrl+Alt+Del on a non-containerized
system, and is compatible with containers running any system
manager. Use <command>restart</command> as alias for
<command>reboot</command>.</para>
<xi:include href="version-info.xml" xpointer="v209"/></listitem>
</varlistentry>
<varlistentry>
<term><command>pause</command> <replaceable>NAME</replaceable>…</term>
<listitem><para>Pause one or more machines. For VMs managed by
<citerefentry><refentrytitle>systemd-vmspawn</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
this freezes vCPU execution at the hypervisor level — the guest
operating system is not notified and does not observe an ACPI suspend.
From the guest's perspective time simply stops until the machine is
resumed with <command>resume</command>.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><command>resume</command> <replaceable>NAME</replaceable>…</term>
<listitem><para>Resume one or more previously paused machines.
This restarts execution after a <command>pause</command>.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><command>terminate</command> <replaceable>NAME</replaceable>…</term>
+1
View File
@@ -55,3 +55,4 @@ Packages=
tpm2-tools
# kernel-bootcfg --add-uri= is just too useful
virt-firmware
virtiofsd
@@ -70,3 +70,4 @@ Packages=
vim-common
# kernel-bootcfg --add-uri= is just too useful
virt-firmware
virtiofsd
@@ -75,3 +75,4 @@ Packages=
tgt
tpm2-tools
tzdata
virtiofsd
+1
View File
@@ -91,5 +91,6 @@ Packages=
veritysetup
# kernel-bootcfg --add-uri= is just too useful
virt-firmware
virtiofsd
xz
zypper
+1 -1
View File
@@ -45,7 +45,7 @@ _machinectl() {
local -A VERBS=(
[STANDALONE]='list list-images clean pull-tar pull-raw list-transfers cancel-transfer import-fs'
[MACHINES]='status show start stop login shell enable disable poweroff reboot terminate kill
[MACHINES]='status show start stop login shell enable disable poweroff reboot pause resume terminate kill
image-status show-image remove export-tar export-raw'
[MACHINES_OR_FILES]='edit cat'
[MACHINE_ONLY]='clone rename set-limit'
+3 -1
View File
@@ -38,6 +38,8 @@
"disable:Disable automatic container start at boot"
"poweroff:Power off one or more VMs/containers"
"reboot:Reboot one or more VMs/containers"
"pause:Pause one or more machines"
"resume:Resume one or more previously paused machines"
"terminate:Terminate one or more VMs/containers"
"kill:Send signal to process or a VM/container"
"copy-to:Copy files from the host to a container"
@@ -77,7 +79,7 @@
start|enable|disable)
_machinectl_images ;;
status|show|poweroff|reboot|terminate|kill)
status|show|poweroff|reboot|pause|resume|terminate|kill)
_sd_machines ;;
login|shell)
-10
View File
@@ -35,10 +35,6 @@ struct JsonStreamQueueItem {
int fds[];
};
static const char* json_stream_description(const JsonStream *s) {
return (s ? s->description : NULL) ?: "json-stream";
}
/* Returns the size of the framing delimiter in bytes: strlen(delimiter) for multi-char
* delimiters (e.g. "\r\n"), or 1 for the default NUL-byte delimiter (delimiter == NULL). */
static size_t json_stream_delimiter_size(const JsonStream *s) {
@@ -54,12 +50,6 @@ static usec_t json_stream_now(const JsonStream *s) {
return now(CLOCK_MONOTONIC);
}
#define json_stream_log(s, fmt, ...) \
log_debug("%s: " fmt, json_stream_description(s), ##__VA_ARGS__)
#define json_stream_log_errno(s, error, fmt, ...) \
log_debug_errno((error), "%s: " fmt, json_stream_description(s), ##__VA_ARGS__)
sd_json_variant** json_stream_queue_item_get_data(JsonStreamQueueItem *q) {
assert(q);
return &q->data;
+11
View File
@@ -6,6 +6,7 @@
#include "sd-forward.h"
#include "list.h"
#include "log.h"
/* JsonStream provides the transport layer used by sd-varlink (and other consumers like
* the QMP client) for exchanging length-delimited JSON messages over a pair of file
@@ -132,6 +133,16 @@ void json_stream_done(JsonStream *s);
int json_stream_set_description(JsonStream *s, const char *description);
const char* json_stream_get_description(const JsonStream *s);
static inline const char* json_stream_description(const JsonStream *s) {
return (s ? s->description : NULL) ?: "json-stream";
}
#define json_stream_log(s, fmt, ...) \
log_debug("%s: " fmt, json_stream_description(s), ##__VA_ARGS__)
#define json_stream_log_errno(s, error, fmt, ...) \
log_debug_errno((error), "%s: " fmt, json_stream_description(s), ##__VA_ARGS__)
/* fd ownership */
int json_stream_attach_fds(JsonStream *s, int input_fd, int output_fd);
+1
View File
@@ -142,6 +142,7 @@ int vl_method_register(sd_varlink *link, sd_json_variant *parameters, sd_varlink
{ "vSockCid", _SD_JSON_VARIANT_TYPE_INVALID, machine_cid, offsetof(Machine, vsock_cid), 0 },
{ "sshAddress", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(Machine, ssh_address), SD_JSON_STRICT },
{ "sshPrivateKeyPath", SD_JSON_VARIANT_STRING, json_dispatch_path, offsetof(Machine, ssh_private_key_path), 0 },
{ "controlAddress", SD_JSON_VARIANT_STRING, json_dispatch_path, offsetof(Machine, control_address), SD_JSON_STRICT },
{ "allocateUnit", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(Machine, allocate_unit), 0 },
VARLINK_DISPATCH_POLKIT_FIELD,
{}
+3
View File
@@ -154,6 +154,7 @@ Machine* machine_free(Machine *m) {
free(m->netif);
free(m->ssh_address);
free(m->ssh_private_key_path);
free(m->control_address);
return mfree(m);
}
@@ -245,6 +246,7 @@ int machine_save(Machine *m) {
env_file_fputs_assignment(f, "SSH_ADDRESS=", m->ssh_address);
env_file_fputs_assignment(f, "SSH_PRIVATE_KEY_PATH=", m->ssh_private_key_path);
env_file_fputs_assignment(f, "CONTROL_ADDRESS=", m->control_address);
r = flink_tmpfile(f, temp_path, m->state_file, LINK_TMPFILE_REPLACE);
if (r < 0)
@@ -338,6 +340,7 @@ int machine_load(Machine *m) {
"VSOCK_CID", &vsock_cid,
"SSH_ADDRESS", &m->ssh_address,
"SSH_PRIVATE_KEY_PATH", &m->ssh_private_key_path,
"CONTROL_ADDRESS", &m->control_address,
"UID", &uid);
if (r == -ENOENT)
return 0;
+1
View File
@@ -96,6 +96,7 @@ typedef struct Machine {
unsigned vsock_cid;
char *ssh_address;
char *ssh_private_key_path;
char *control_address;
LIST_HEAD(Operation, operations);
+163 -25
View File
@@ -9,6 +9,7 @@
#include "sd-bus.h"
#include "sd-event.h"
#include "sd-journal.h"
#include "sd-varlink.h"
#include "alloc-util.h"
#include "ask-password-agent.h"
@@ -45,6 +46,7 @@
#include "pager.h"
#include "parse-argument.h"
#include "parse-util.h"
#include "path-lookup.h"
#include "path-util.h"
#include "pidref.h"
#include "polkit-agent.h"
@@ -1106,34 +1108,73 @@ static int verb_kill_machine(int argc, char *argv[], uintptr_t _data, void *user
return 0;
}
static int verb_machine_control_one(const char *machine_name, const char *method);
static int verb_reboot_machine(int argc, char *argv[], uintptr_t data, void *userdata) {
if (arg_runner == RUNNER_VMSPAWN)
return log_error_errno(
SYNTHETIC_ERRNO(EOPNOTSUPP),
"%s only support supported for --runner=nspawn",
streq(argv[0], "reboot") ? "Reboot" : "Restart");
arg_kill_whom = "leader";
arg_signal = SIGINT; /* sysvinit + systemd */
return verb_kill_machine(argc, argv, data, userdata);
}
static int verb_poweroff_machine(int argc, char *argv[], uintptr_t data, void *userdata) {
arg_kill_whom = "leader";
arg_signal = SIGRTMIN+4; /* only systemd */
return verb_kill_machine(argc, argv, data, userdata);
}
static int verb_terminate_machine(int argc, char *argv[], uintptr_t _data, void *userdata) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
sd_bus *bus = ASSERT_PTR(userdata);
int r;
(void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
for (int i = 1; i < argc; i++) {
r = verb_machine_control_one(argv[i], "io.systemd.MachineInstance.Reboot");
if (r >= 0)
continue;
if (r != -EOPNOTSUPP)
return r;
/* Container fallback: SIGINT to init (sysvinit + systemd compatible) */
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
r = bus_call_method(bus, bus_machine_mgr, "KillMachine", &error, NULL,
"ssi", argv[i], "leader", (int32_t) SIGINT);
if (r < 0)
return log_error_errno(r, "Could not reboot machine '%s': %s", argv[i], bus_error_message(&error, r));
}
return 0;
}
static int verb_poweroff_machine(int argc, char *argv[], uintptr_t data, void *userdata) {
sd_bus *bus = ASSERT_PTR(userdata);
int r;
(void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
for (int i = 1; i < argc; i++) {
/* VM with varlink control socket: QMP graceful powerdown */
r = verb_machine_control_one(argv[i], "io.systemd.MachineInstance.PowerOff");
if (r >= 0)
continue;
if (r != -EOPNOTSUPP)
return r;
/* Not a VM: signal-based poweroff */
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
r = bus_call_method(bus, bus_machine_mgr, "KillMachine", &error, NULL,
"ssi", argv[i], "leader", (int32_t) (SIGRTMIN+4));
if (r < 0)
return log_error_errno(r, "Could not kill machine: %s", bus_error_message(&error, r));
}
return 0;
}
static int verb_terminate_machine(int argc, char *argv[], uintptr_t _data, void *userdata) {
sd_bus *bus = ASSERT_PTR(userdata);
int r;
(void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
for (int i = 1; i < argc; i++) {
/* VM with varlink control socket: QMP quit (immediate termination) */
r = verb_machine_control_one(argv[i], "io.systemd.MachineInstance.Terminate");
if (r >= 0)
continue;
if (r != -EOPNOTSUPP)
return r;
/* Not a VM or no varlink socket: fall back to machined */
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
r = bus_call_method(bus, bus_machine_mgr, "TerminateMachine", &error, NULL, "s", argv[i]);
if (r < 0)
return log_error_errno(r, "Could not terminate machine: %s", bus_error_message(&error, r));
@@ -1142,6 +1183,99 @@ static int verb_terminate_machine(int argc, char *argv[], uintptr_t _data, void
return 0;
}
/* Look up the controlAddress of a machine by calling machined's Machine.List varlink interface. */
static int machine_get_control_address(const char *machine_name, char **ret) {
_cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL;
sd_json_variant *reply = NULL;
const char *error_id = NULL;
int r;
assert(machine_name);
assert(ret);
if (arg_transport != BUS_TRANSPORT_LOCAL)
return -EOPNOTSUPP;
_cleanup_free_ char *p = NULL;
r = runtime_directory_generic(arg_runtime_scope, "systemd/machine/io.systemd.Machine", &p);
if (r < 0)
return log_error_errno(r, "Failed to determine Machine varlink socket path: %m");
r = sd_varlink_connect_address(&vl, p);
if (r < 0)
return log_error_errno(r, "Failed to connect to machined varlink: %m");
r = sd_varlink_callbo(
vl,
"io.systemd.Machine.List",
&reply,
&error_id,
SD_JSON_BUILD_PAIR_STRING("name", machine_name));
if (r < 0)
return log_error_errno(r, "Failed to list machine: %m");
if (error_id)
return log_error_errno(sd_varlink_error_to_errno(error_id, reply),
"Failed to look up machine '%s': %s", machine_name, error_id);
sd_json_variant *addr = sd_json_variant_by_key(reply, "controlAddress");
if (!addr || !sd_json_variant_is_string(addr))
return -EOPNOTSUPP; /* No varlink control socket, caller decides whether to log */
char *a = strdup(sd_json_variant_string(addr));
if (!a)
return log_oom();
*ret = a;
return 0;
}
static int verb_machine_control_one(const char *machine_name, const char *method) {
_cleanup_free_ char *address = NULL;
int r;
r = machine_get_control_address(machine_name, &address);
if (r < 0)
return r;
_cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL;
r = sd_varlink_connect_address(&vl, address);
if (r < 0)
return log_error_errno(r, "Failed to connect to machine control socket: %m");
_cleanup_(sd_json_variant_unrefp) sd_json_variant *reply = NULL;
const char *error_id = NULL;
r = sd_varlink_call(vl, method, /* parameters= */ NULL, &reply, &error_id);
if (r < 0)
return log_error_errno(r, "Failed to call %s: %m", method);
if (error_id)
return log_error_errno(sd_varlink_error_to_errno(error_id, reply),
"Machine control call failed: %s", error_id);
return 0;
}
static int verb_vm_control(int argc, char *argv[], const char *method) {
int r;
for (int i = 1; i < argc; i++) {
r = verb_machine_control_one(argv[i], method);
if (r == -EOPNOTSUPP)
return log_error_errno(r, "Machine '%s' does not expose a varlink control socket.", argv[i]);
if (r < 0)
return r;
}
return 0;
}
static int verb_pause(int argc, char *argv[], uintptr_t _data, void *userdata) {
return verb_vm_control(argc, argv, "io.systemd.MachineInstance.Pause");
}
static int verb_resume(int argc, char *argv[], uintptr_t _data, void *userdata) {
return verb_vm_control(argc, argv, "io.systemd.MachineInstance.Resume");
}
static const char *select_copy_method(bool copy_from, bool force) {
if (force)
return copy_from ? "CopyFromMachineWithFlags" : "CopyToMachineWithFlags";
@@ -2077,10 +2211,12 @@ static int help(void) {
" or on the local host\n"
" enable NAME... Enable automatic container start at boot\n"
" disable NAME... Disable automatic container start at boot\n"
" poweroff NAME... Power off one or more containers\n"
" reboot NAME... Reboot one or more containers\n"
" terminate NAME... Terminate one or more VMs/containers\n"
" kill NAME... Send signal to processes of a VM/container\n"
" poweroff NAME... Power off one or more machines\n"
" reboot NAME... Reboot one or more machines\n"
" pause NAME... Pause one or more machines\n"
" resume NAME... Resume one or more paused machines\n"
" terminate NAME... Terminate one or more machines\n"
" kill NAME... Send signal to processes of a machine\n"
" copy-to NAME PATH [PATH] Copy files from the host to a container\n"
" copy-from NAME PATH [PATH] Copy files from a container to the host\n"
" bind NAME PATH [PATH] Bind mount a path from the host into a container\n"
@@ -2465,6 +2601,8 @@ static int machinectl_main(int argc, char *argv[], sd_bus *bus) {
{ "poweroff", 2, VERB_ANY, 0, verb_poweroff_machine },
{ "stop", 2, VERB_ANY, 0, verb_poweroff_machine }, /* Convenience alias */
{ "kill", 2, VERB_ANY, 0, verb_kill_machine },
{ "pause", 2, VERB_ANY, 0, verb_pause },
{ "resume", 2, VERB_ANY, 0, verb_resume },
{ "login", VERB_ANY, 2, 0, verb_login_machine },
{ "shell", VERB_ANY, VERB_ANY, 0, verb_shell_machine },
{ "bind", 3, 4, 0, verb_bind_mount },
+1
View File
@@ -489,6 +489,7 @@ static int list_machine_one_and_maybe_read_metadata(sd_varlink *link, Machine *m
JSON_BUILD_PAIR_UNSIGNED_NOT_EQUAL("vSockCid", m->vsock_cid, VMADDR_CID_ANY),
JSON_BUILD_PAIR_STRING_NON_EMPTY("sshAddress", m->ssh_address),
JSON_BUILD_PAIR_STRING_NON_EMPTY("sshPrivateKeyPath", m->ssh_private_key_path),
JSON_BUILD_PAIR_STRING_NON_EMPTY("controlAddress", m->control_address),
JSON_BUILD_PAIR_VARIANT_NON_NULL("addresses", addr_array),
JSON_BUILD_PAIR_STRV_ENV_PAIR_NON_EMPTY("OSRelease", os_release),
JSON_BUILD_PAIR_UNSIGNED_NOT_EQUAL("UIDShift", shift, UID_INVALID),
+1
View File
@@ -187,6 +187,7 @@ int register_machine(
SD_JSON_BUILD_PAIR_CONDITION(!!reg->root_directory, "rootDirectory", SD_JSON_BUILD_STRING(reg->root_directory)),
SD_JSON_BUILD_PAIR_CONDITION(!!reg->ssh_address, "sshAddress", SD_JSON_BUILD_STRING(reg->ssh_address)),
SD_JSON_BUILD_PAIR_CONDITION(!!reg->ssh_private_key_path, "sshPrivateKeyPath", SD_JSON_BUILD_STRING(reg->ssh_private_key_path)),
SD_JSON_BUILD_PAIR_CONDITION(!!reg->control_address, "controlAddress", SD_JSON_BUILD_STRING(reg->control_address)),
SD_JSON_BUILD_PAIR_CONDITION(isatty_safe(STDIN_FILENO), "allowInteractiveAuthentication", SD_JSON_BUILD_BOOLEAN(true)),
SD_JSON_BUILD_PAIR_CONDITION(reg->allocate_unit, "allocateUnit", SD_JSON_BUILD_BOOLEAN(true)),
SD_JSON_BUILD_PAIR_CONDITION(pidref_is_set(reg->pidref), "leaderProcessId", JSON_BUILD_PIDREF(reg->pidref)));
+1
View File
@@ -17,6 +17,7 @@ typedef struct MachineRegistration {
int local_ifindex;
const char *ssh_address;
const char *ssh_private_key_path;
const char *control_address;
bool allocate_unit;
} MachineRegistration;
+4
View File
@@ -165,6 +165,7 @@ shared_sources = files(
'printk-util.c',
'prompt-util.c',
'ptyfwd.c',
'qmp-client.c',
'qrcode-util.c',
'quota-util.c',
'reboot-util.c',
@@ -216,6 +217,7 @@ shared_sources = files(
'varlink-io.systemd.Login.c',
'varlink-io.systemd.Machine.c',
'varlink-io.systemd.MachineImage.c',
'varlink-io.systemd.MachineInstance.c',
'varlink-io.systemd.ManagedOOM.c',
'varlink-io.systemd.Manager.c',
'varlink-io.systemd.Metrics.c',
@@ -226,6 +228,7 @@ 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',
@@ -234,6 +237,7 @@ shared_sources = files(
'varlink-io.systemd.Udev.c',
'varlink-io.systemd.Unit.c',
'varlink-io.systemd.UserDatabase.c',
'varlink-io.systemd.VirtualMachineInstance.c',
'varlink-io.systemd.oom.c',
'varlink-io.systemd.oom.Prekill.c',
'varlink-io.systemd.service.c',
+845
View File
@@ -0,0 +1,845 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-event.h"
#include "sd-json.h"
#include "alloc-util.h"
#include "fd-util.h"
#include "hash-funcs.h"
#include "json-stream.h"
#include "json-util.h"
#include "qmp-client.h"
#include "set.h"
#include "siphash24.h"
#include "string-util.h"
typedef enum QmpClientState {
QMP_CLIENT_HANDSHAKE_INITIAL, /* waiting for QMP greeting */
QMP_CLIENT_HANDSHAKE_GREETING_RECEIVED, /* greeting received, sending qmp_capabilities */
QMP_CLIENT_HANDSHAKE_CAPABILITIES_SENT, /* waiting for qmp_capabilities response */
QMP_CLIENT_RUNNING, /* connected, ready for commands */
QMP_CLIENT_DISCONNECTED, /* connection closed */
_QMP_CLIENT_STATE_MAX,
_QMP_CLIENT_STATE_INVALID = -EINVAL,
} QmpClientState;
/* States routed to dispatch_handshake. */
#define QMP_CLIENT_STATE_IS_HANDSHAKE(s) \
IN_SET(s, \
QMP_CLIENT_HANDSHAKE_INITIAL, \
QMP_CLIENT_HANDSHAKE_GREETING_RECEIVED, \
QMP_CLIENT_HANDSHAKE_CAPABILITIES_SENT)
typedef struct QmpSlot {
uint64_t id;
qmp_command_callback_t callback;
void *userdata;
} QmpSlot;
struct QmpClient {
unsigned n_ref;
JsonStream stream;
sd_event_source *quit_event_source;
sd_event_source *defer_event_source;
uint64_t next_id;
Set *slots; /* QmpSlot* entries indexed by id, for async dispatch */
qmp_event_callback_t event_callback;
void *event_userdata;
qmp_disconnect_callback_t disconnect_callback;
void *disconnect_userdata;
unsigned next_fdset_id; /* monotonic fdset-id allocator for add-fd */
QmpClientState state;
sd_json_variant *current; /* most recently parsed message, pending dispatch */
};
static void qmp_slot_hash_func(const QmpSlot *p, struct siphash *state) {
siphash24_compress_typesafe(p->id, state);
}
static int qmp_slot_compare_func(const QmpSlot *a, const QmpSlot *b) {
return CMP(a->id, b->id);
}
DEFINE_PRIVATE_HASH_OPS(qmp_slot_hash_ops,
QmpSlot, qmp_slot_hash_func, qmp_slot_compare_func);
static void qmp_client_clear(QmpClient *c);
static QmpClient* qmp_client_destroy(QmpClient *c) {
if (!c)
return NULL;
qmp_client_clear(c);
return mfree(c);
}
DEFINE_PRIVATE_TRIVIAL_REF_FUNC(QmpClient, qmp_client);
DEFINE_TRIVIAL_UNREF_FUNC(QmpClient, qmp_client, qmp_client_destroy);
static void qmp_client_clear_current(QmpClient *c) {
assert(c);
c->current = sd_json_variant_unref(c->current);
}
static void qmp_client_dispatch_event(QmpClient *c, sd_json_variant *v) {
int r;
assert(c);
assert(v);
if (!c->event_callback)
return;
struct {
const char *event;
sd_json_variant *data;
} p = {};
static const sd_json_dispatch_field table[] = {
{ "event", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, event), SD_JSON_MANDATORY },
{ "data", SD_JSON_VARIANT_OBJECT, sd_json_dispatch_variant_noref, voffsetof(p, data), 0 },
{},
};
r = sd_json_dispatch(v, table, SD_JSON_ALLOW_EXTENSIONS|SD_JSON_LOG|SD_JSON_DEBUG, &p);
if (r < 0)
return;
r = c->event_callback(c, p.event, p.data, c->event_userdata);
if (r < 0)
json_stream_log_errno(&c->stream, r, "Event callback returned error, ignoring: %m");
}
/* QEMU's error "class" is effectively always "GenericError"; only "desc" carries useful info. */
static const char* qmp_extract_error_description(sd_json_variant *v) {
sd_json_variant *error = sd_json_variant_by_key(v, "error");
if (!error)
return NULL;
sd_json_variant *desc = sd_json_variant_by_key(error, "desc");
if (desc)
return sd_json_variant_string(desc);
return "unspecified error";
}
/* Returns 1 with id set; 0 if absent (e.g. pre-parse error responses); -EBADMSG on wrong type. */
static int qmp_extract_response_id(sd_json_variant *v, uint64_t *ret) {
sd_json_variant *id_variant;
assert(v);
assert(ret);
id_variant = sd_json_variant_by_key(v, "id");
if (!id_variant) {
*ret = 0;
return 0;
}
if (!sd_json_variant_is_unsigned(id_variant))
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG),
"QMP response 'id' field is not an unsigned integer.");
*ret = sd_json_variant_unsigned(id_variant);
return 1;
}
/* Returns 0 on success (ret_result = "return" value), -EIO on QMP error (reterr_desc set). */
static int qmp_parse_response(sd_json_variant *v, sd_json_variant **ret_result, const char **reterr_desc) {
const char *desc;
desc = qmp_extract_error_description(v);
if (desc) {
if (reterr_desc)
*reterr_desc = desc;
return -EIO;
}
if (ret_result)
*ret_result = sd_json_variant_by_key(v, "return");
return 0;
}
static int qmp_client_build_command(
QmpClient *c,
const char *command,
sd_json_variant *arguments,
sd_json_variant **ret,
uint64_t *ret_id) {
uint64_t id;
int r;
assert(c);
assert(command);
assert(ret);
assert(ret_id);
id = c->next_id++;
r = sd_json_buildo(
ret,
SD_JSON_BUILD_PAIR_STRING("execute", command),
SD_JSON_BUILD_PAIR_CONDITION(!!arguments, "arguments", SD_JSON_BUILD_VARIANT(arguments)),
SD_JSON_BUILD_PAIR_UNSIGNED("id", id));
if (r < 0)
return r;
*ret_id = id;
return 0;
}
/* Route c->current to event callback or matching async slot. Returns 1 on dispatch. */
static int qmp_client_dispatch_reply(QmpClient *c) {
sd_json_variant *result = NULL;
const char *desc = NULL;
uint64_t id;
int error, r;
assert(c);
if (!c->current)
return 0;
/* Events have an "event" key */
if (sd_json_variant_by_key(c->current, "event")) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = TAKE_PTR(c->current);
qmp_client_dispatch_event(c, v);
return 1;
}
/* Command responses carry an "id" matching a request we sent */
r = qmp_extract_response_id(c->current, &id);
if (r < 0) {
qmp_client_clear_current(c);
return json_stream_log_errno(&c->stream, r, "Discarding QMP response with malformed id: %m");
}
if (r == 0) {
qmp_client_clear_current(c);
json_stream_log(&c->stream, "Discarding unrecognized QMP message");
return 1;
}
_cleanup_free_ QmpSlot *pending = set_remove(c->slots, &(QmpSlot) { .id = id });
if (!pending) {
qmp_client_clear_current(c);
json_stream_log(&c->stream, "Discarding QMP response with unknown id %" PRIu64, id);
return 1;
}
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = TAKE_PTR(c->current);
error = qmp_parse_response(v, &result, &desc);
r = pending->callback(c, result, desc, error, pending->userdata);
if (r < 0)
json_stream_log_errno(&c->stream, r, "Command callback returned error, ignoring: %m");
return 1;
}
/* Fail all pending async commands with the given error. Called on disconnect. */
static void qmp_client_fail_pending(QmpClient *c, int error) {
QmpSlot *p;
int r;
assert(c);
while ((p = set_steal_first(c->slots))) {
r = p->callback(c, /* result= */ NULL, /* error_desc= */ NULL, error, p->userdata);
if (r < 0)
json_stream_log_errno(&c->stream, r, "Command callback returned error, ignoring: %m");
free(p);
}
}
/* Synthetic SHUTDOWN on unexpected disconnect so subscribers learn the VM is gone. */
static void qmp_client_emit_synthetic_shutdown(QmpClient *c) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *data = NULL;
int r;
assert(c);
if (!c->event_callback)
return;
r = sd_json_buildo(
&data,
SD_JSON_BUILD_PAIR_BOOLEAN("guest", false),
SD_JSON_BUILD_PAIR_STRING("reason", "disconnected"));
if (r < 0) {
json_stream_log_errno(&c->stream, r, "Failed to build synthetic SHUTDOWN event data, skipping: %m");
return;
}
r = c->event_callback(c, "SHUTDOWN", data, c->event_userdata);
if (r < 0)
json_stream_log_errno(&c->stream, r, "Event callback returned error, ignoring: %m");
}
static bool qmp_client_handle_disconnect(QmpClient *c) {
assert(c);
if (c->state == QMP_CLIENT_DISCONNECTED)
return false;
c->state = QMP_CLIENT_DISCONNECTED;
/* Disable defer event source so we don't busy-loop on the EOF condition. */
if (c->defer_event_source)
(void) sd_event_source_set_enabled(c->defer_event_source, SD_EVENT_OFF);
qmp_client_fail_pending(c, -ECONNRESET);
qmp_client_emit_synthetic_shutdown(c);
if (c->disconnect_callback)
c->disconnect_callback(c, c->disconnect_userdata);
return true;
}
static bool qmp_client_test_disconnect(QmpClient *c) {
assert(c);
/* Already disconnected? */
if (c->state == QMP_CLIENT_DISCONNECTED)
return false;
if (!json_stream_should_disconnect(&c->stream))
return false;
return qmp_client_handle_disconnect(c);
}
/* INITIAL → greeting → GREETING_RECEIVED → qmp_capabilities → CAPABILITIES_SENT → response → RUNNING. */
static int qmp_client_dispatch_handshake(QmpClient *c) {
int r;
assert(c);
assert(QMP_CLIENT_STATE_IS_HANDSHAKE(c->state));
if (!c->current)
return 0;
/* Defensive: QEMU shouldn't emit events during capability negotiation, but if one
* arrives, dispatch it as an event rather than mis-parsing it as a handshake reply. */
if (sd_json_variant_by_key(c->current, "event")) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = TAKE_PTR(c->current);
qmp_client_dispatch_event(c, v);
return 1;
}
switch (c->state) {
case QMP_CLIENT_HANDSHAKE_INITIAL: {
/* Waiting for QMP greeting. Take ownership so by_key()'s borrowed pointer
* stays valid through the case scope. */
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = TAKE_PTR(c->current);
if (!sd_json_variant_by_key(v, "QMP"))
return json_stream_log_errno(&c->stream, SYNTHETIC_ERRNO(EPROTO),
"Expected QMP greeting, got something else");
c->state = QMP_CLIENT_HANDSHAKE_GREETING_RECEIVED;
/* Fall through to immediately send capabilities */
_fallthrough_;
}
case QMP_CLIENT_HANDSHAKE_GREETING_RECEIVED: {
/* Send qmp_capabilities command */
_cleanup_(sd_json_variant_unrefp) sd_json_variant *cmd = NULL;
r = sd_json_buildo(
&cmd,
SD_JSON_BUILD_PAIR_STRING("execute", "qmp_capabilities"),
SD_JSON_BUILD_PAIR_UNSIGNED("id", c->next_id++));
if (r < 0)
return r;
r = json_stream_enqueue(&c->stream, cmd);
if (r < 0)
return r;
c->state = QMP_CLIENT_HANDSHAKE_CAPABILITIES_SENT;
return 1;
}
case QMP_CLIENT_HANDSHAKE_CAPABILITIES_SENT: {
/* Take ownership so desc (borrowed from v's "error.desc") survives the format string. */
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = TAKE_PTR(c->current);
const char *desc = NULL;
r = qmp_parse_response(v, /* ret_result= */ NULL, &desc);
if (r < 0)
return json_stream_log_errno(&c->stream, SYNTHETIC_ERRNO(EPROTO),
"qmp_capabilities failed: %s", desc);
c->state = QMP_CLIENT_RUNNING;
return 1;
}
default:
assert_not_reached();
}
}
static int qmp_client_dispatch(QmpClient *c) {
assert(c);
if (!c->current)
return 0;
if (QMP_CLIENT_STATE_IS_HANDSHAKE(c->state))
return qmp_client_dispatch_handshake(c);
return qmp_client_dispatch_reply(c);
}
/* Single step: write → dispatch → parse → read → disconnect. Matches sd_varlink_process(). */
int qmp_client_process(QmpClient *c) {
int r;
assert(c);
if (c->state < 0 || c->state == QMP_CLIENT_DISCONNECTED)
return -ENOTCONN;
/* Pin against a callback dropping the last ref mid-dispatch. Matches sd_varlink_process(). */
qmp_client_ref(c);
/* 1. Write — drain output buffer */
r = json_stream_write(&c->stream);
if (r < 0)
json_stream_log_errno(&c->stream, r, "Failed to write to QMP socket: %m");
if (r != 0)
goto finish;
/* 2. Dispatch — route based on state */
r = qmp_client_dispatch(c);
if (r < 0)
json_stream_log_errno(&c->stream, r, "Failed to dispatch QMP message: %m");
if (r != 0)
goto finish;
/* 3. Parse — extract one complete message into c->current */
if (!c->current) {
r = json_stream_parse(&c->stream, &c->current);
if (r < 0)
json_stream_log_errno(&c->stream, r, "Failed to parse QMP message: %m");
if (r != 0)
goto finish;
}
/* 4. Read — fill input buffer from fd */
if (!c->current) {
r = json_stream_read(&c->stream);
if (r < 0)
json_stream_log_errno(&c->stream, r, "Failed to read from QMP socket: %m");
if (r != 0)
goto finish;
}
/* 5. Test disconnect */
if (qmp_client_test_disconnect(c)) {
r = 1;
goto finish;
}
finish:
/* Re-arm defer source on progress so we get called again next iteration. */
if (r >= 0 && c->defer_event_source) {
int q;
q = sd_event_source_set_enabled(c->defer_event_source, r > 0 ? SD_EVENT_ON : SD_EVENT_OFF);
if (q < 0)
r = json_stream_log_errno(&c->stream, q, "Failed to enable deferred event source: %m");
}
/* -ENOBUFS is the buffered stream's 16 MiB cap, not a transport error — propagate without disconnecting. */
if (r < 0 && r != -ENOBUFS && c->state != QMP_CLIENT_DISCONNECTED)
qmp_client_handle_disconnect(c);
qmp_client_unref(c);
return r;
}
int qmp_client_wait(QmpClient *c, uint64_t timeout_usec) {
assert(c);
if (c->state == QMP_CLIENT_DISCONNECTED)
return -ENOTCONN;
return json_stream_wait(&c->stream, timeout_usec);
}
bool qmp_client_is_idle(QmpClient *c) {
assert(c);
return set_isempty(c->slots);
}
bool qmp_client_is_disconnected(QmpClient *c) {
assert(c);
return c->state == QMP_CLIENT_DISCONNECTED;
}
/* 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);
/* A parsed-but-undispatched message is mid-processing, not waiting on the wire. */
if (c->current)
return JSON_STREAM_PHASE_OTHER;
/* During handshake we're waiting for the greeting or qmp_capabilities response. */
if (QMP_CLIENT_STATE_IS_HANDSHAKE(c->state))
return JSON_STREAM_PHASE_AWAITING_REPLY;
/* Running with pending async commands — waiting for their responses. */
if (c->state == QMP_CLIENT_RUNNING && !set_isempty(c->slots))
return JSON_STREAM_PHASE_AWAITING_REPLY;
/* Running with no pending commands — waiting for unsolicited events. */
if (c->state == QMP_CLIENT_RUNNING)
return JSON_STREAM_PHASE_READING;
return JSON_STREAM_PHASE_OTHER;
}
static int qmp_client_dispatch_cb(void *userdata) {
QmpClient *c = ASSERT_PTR(userdata);
return qmp_client_process(c);
}
static int qmp_client_defer_callback(sd_event_source *source, void *userdata) {
QmpClient *c = ASSERT_PTR(userdata);
assert(source);
(void) qmp_client_process(c);
return 1;
}
/* Drive handshake to completion. Matches sd-bus's bus_ensure_running(). */
static int qmp_client_ensure_running(QmpClient *c) {
int r;
assert(c);
if (c->state == QMP_CLIENT_RUNNING)
return 1;
for (;;) {
if (c->state < 0 || c->state == QMP_CLIENT_DISCONNECTED)
return -ENOTCONN;
r = qmp_client_process(c);
if (r < 0)
return r;
if (c->state == QMP_CLIENT_RUNNING)
return 1;
if (r > 0)
continue;
r = qmp_client_wait(c, USEC_INFINITY);
if (r < 0)
return r;
}
}
static void qmp_client_detach_event(QmpClient *c) {
if (!c)
return;
c->defer_event_source = sd_event_source_disable_unref(c->defer_event_source);
c->quit_event_source = sd_event_source_disable_unref(c->quit_event_source);
json_stream_detach_event(&c->stream);
}
static void qmp_client_clear(QmpClient *c) {
assert(c);
qmp_client_handle_disconnect(c);
qmp_client_detach_event(c);
qmp_client_clear_current(c);
json_stream_done(&c->stream);
c->slots = set_free(c->slots);
}
/* Blocks until output buffer is empty. Matches sd_varlink_flush(). */
static int qmp_client_flush(QmpClient *c) {
if (!c)
return 0;
if (c->state == QMP_CLIENT_DISCONNECTED)
return -ENOTCONN;
return json_stream_flush(&c->stream);
}
/* Notify callbacks, fire disconnect, detach sources, close fd. Matches sd_varlink_close(). */
static int qmp_client_close(QmpClient *c) {
if (!c)
return 0;
/* Take a temporary ref to prevent destruction mid-callback,
* matching sd_varlink_close()'s pattern. */
qmp_client_ref(c);
qmp_client_clear(c);
qmp_client_unref(c);
return 1;
}
static int qmp_client_quit_callback(sd_event_source *source, void *userdata) {
QmpClient *c = ASSERT_PTR(userdata);
assert(source);
qmp_client_flush(c);
qmp_client_close(c);
return 1;
}
int qmp_client_connect_fd(QmpClient **ret, int fd) {
_cleanup_(qmp_client_unrefp) QmpClient *c = NULL;
int r;
assert(ret);
assert(fd >= 0);
c = new(QmpClient, 1);
if (!c)
return -ENOMEM;
*c = (QmpClient) {
.n_ref = 1,
.state = QMP_CLIENT_HANDSHAKE_INITIAL,
.next_id = 1,
};
const JsonStreamParams params = {
.delimiter = "\r\n",
.phase = qmp_client_phase,
.dispatch = qmp_client_dispatch_cb,
.userdata = c,
};
r = json_stream_init(&c->stream, &params);
if (r < 0)
return r;
r = json_stream_connect_fd_pair(&c->stream, fd, fd);
if (r < 0)
return r;
*ret = TAKE_PTR(c);
return 0;
}
int qmp_client_attach_event(QmpClient *c, sd_event *event, int64_t priority) {
int r;
assert(c);
assert(event);
assert(!json_stream_get_event(&c->stream));
r = json_stream_attach_event(&c->stream, event, priority);
if (r < 0)
return r;
sd_event *ev = json_stream_get_event(&c->stream);
r = sd_event_add_exit(ev, &c->quit_event_source, qmp_client_quit_callback, c);
if (r < 0)
goto fail;
r = sd_event_source_set_priority(c->quit_event_source, priority);
if (r < 0)
goto fail;
(void) sd_event_source_set_description(c->quit_event_source, "qmp-client-quit");
r = sd_event_add_defer(ev, &c->defer_event_source, qmp_client_defer_callback, c);
if (r < 0)
goto fail;
r = sd_event_source_set_priority(c->defer_event_source, priority);
if (r < 0)
goto fail;
r = sd_event_source_set_enabled(c->defer_event_source, SD_EVENT_OFF);
if (r < 0)
goto fail;
(void) sd_event_source_set_description(c->defer_event_source, "qmp-client-defer");
return 0;
fail:
qmp_client_detach_event(c);
return r;
}
/* Cleanup hook: closes any fds in *args not yet transferred to the stream. */
static QmpClientArgs* qmp_client_args_close_fds(QmpClientArgs *p) {
assert(p);
close_many_unset(p->fds_consume, p->n_fds);
return NULL;
}
DEFINE_TRIVIAL_CLEANUP_FUNC(QmpClientArgs*, qmp_client_args_close_fds);
/* Transfer fds to the stream. On partial failure narrow args to the unstaged tail so
* the caller's cleanup closes only the untransferred fds. */
static int qmp_client_stage_fds(QmpClient *c, QmpClientArgs *args) {
int r;
assert(c);
if (!args || args->n_fds == 0)
return 0;
assert(args->fds_consume);
for (size_t i = 0; i < args->n_fds; i++) {
r = json_stream_push_fd(&c->stream, args->fds_consume[i]);
if (r < 0) {
/* Already-staged are owned by the stream; narrow args to the rest. */
json_stream_reset_pushed_fds(&c->stream);
args->fds_consume = &args->fds_consume[i];
args->n_fds -= i;
return r;
}
}
args->n_fds = 0;
return 0;
}
int qmp_client_invoke(
QmpClient *c,
const char *command,
QmpClientArgs *args,
qmp_command_callback_t callback,
void *userdata) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *cmd = NULL;
_cleanup_free_ QmpSlot *pending = NULL;
/* Closes any fds in args not yet handed to the stream on every early-return path;
* TAKE_PTR()'d on the success path below once stage_fds has consumed them. */
_cleanup_(qmp_client_args_close_fdsp) QmpClientArgs *fds_owner = args;
uint64_t id;
int r;
assert(c);
assert(command);
assert(callback);
r = qmp_client_ensure_running(c);
if (r < 0)
return r;
r = qmp_client_build_command(c, command, args ? args->arguments : NULL, &cmd, &id);
if (r < 0)
return r;
pending = new(QmpSlot, 1);
if (!pending)
return -ENOMEM;
*pending = (QmpSlot) {
.id = id,
.callback = callback,
.userdata = userdata,
};
r = set_ensure_put(&c->slots, &qmp_slot_hash_ops, pending);
if (r < 0)
return r;
assert(r > 0);
/* Stage AFTER ensure_running() drained internal enqueues so the next enqueue is ours. */
r = qmp_client_stage_fds(c, args);
if (r < 0) {
set_remove(c->slots, pending);
return r;
}
r = json_stream_enqueue(&c->stream, cmd);
if (r < 0) {
json_stream_reset_pushed_fds(&c->stream);
set_remove(c->slots, pending);
return r;
}
/* Arm defer so process() drains the output on the next iteration. */
if (c->defer_event_source)
(void) sd_event_source_set_enabled(c->defer_event_source, SD_EVENT_ON);
TAKE_PTR(pending);
TAKE_PTR(fds_owner);
return 0;
}
void qmp_client_bind_event(QmpClient *c, qmp_event_callback_t callback, void *userdata) {
assert(c);
c->event_callback = callback;
c->event_userdata = userdata;
}
void qmp_client_bind_disconnect(QmpClient *c, qmp_disconnect_callback_t callback, void *userdata) {
assert(c);
c->disconnect_callback = callback;
c->disconnect_userdata = userdata;
}
int qmp_client_set_description(QmpClient *c, const char *description) {
assert(c);
return json_stream_set_description(&c->stream, description);
}
sd_event* qmp_client_get_event(QmpClient *c) {
assert(c);
return json_stream_get_event(&c->stream);
}
unsigned qmp_client_next_fdset_id(QmpClient *c) {
assert(c);
return c->next_fdset_id++;
}
bool qmp_schema_has_member(sd_json_variant *schema, const char *member_name) {
sd_json_variant *entry;
assert(member_name);
if (!sd_json_variant_is_array(schema))
return false;
JSON_VARIANT_ARRAY_FOREACH(entry, schema) {
if (!sd_json_variant_is_object(entry))
continue;
sd_json_variant *meta = sd_json_variant_by_key(entry, "meta-type");
if (!meta || !streq_ptr(sd_json_variant_string(meta), "object"))
continue;
sd_json_variant *members = sd_json_variant_by_key(entry, "members");
if (!sd_json_variant_is_array(members))
continue;
sd_json_variant *m;
JSON_VARIANT_ARRAY_FOREACH(m, members) {
if (!sd_json_variant_is_object(m))
continue;
sd_json_variant *mn = sd_json_variant_by_key(m, "name");
if (mn && streq_ptr(sd_json_variant_string(mn), member_name))
return true;
}
}
return false;
}
+78
View File
@@ -0,0 +1,78 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "shared-forward.h"
typedef int (*qmp_event_callback_t)(
QmpClient *client,
const char *event,
sd_json_variant *data,
void *userdata);
typedef void (*qmp_disconnect_callback_t)(
QmpClient *client,
void *userdata);
/* Success: (result, NULL, 0). QMP error: (NULL, desc, -EIO). Transport: (NULL, NULL, -errno). */
typedef int (*qmp_command_callback_t)(
QmpClient *client,
sd_json_variant *result,
const char *error_desc,
int error,
void *userdata);
/* Bundles arguments + fds for one command. Construct fresh per invoke via the macros below. */
typedef struct QmpClientArgs {
sd_json_variant *arguments;
int *fds_consume;
size_t n_fds;
} QmpClientArgs;
#define QMP_CLIENT_ARGS(args_) \
(&(QmpClientArgs){ .arguments = (args_) })
#define QMP_CLIENT_ARGS_FD(args_, fd_) \
(&(QmpClientArgs){ .arguments = (args_), .fds_consume = (int[]){ (fd_) }, .n_fds = 1 })
/* Takes ownership of fd; handshake runs lazily on first invoke or from the event loop. */
int qmp_client_connect_fd(QmpClient **ret, int fd);
int qmp_client_attach_event(QmpClient *c, sd_event *event, int64_t priority);
/* Single non-blocking pump step: write → dispatch → parse → read → disconnect. Returns >0 if
* progress was made, 0 if idle, <0 on error (-ENOTCONN on disconnect). Same contract as
* sd_varlink_process(). */
int qmp_client_process(QmpClient *c);
/* Block on the transport fd until readable, or until timeout (USEC_INFINITY for no timeout).
* Same contract as sd_varlink_wait(). */
int qmp_client_wait(QmpClient *c, uint64_t timeout_usec);
/* True iff there are no outstanding command replies (slots set is empty). Useful as the pump-loop
* exit condition for callers driving the client synchronously via process() + wait(). */
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);
/* Async send. Returns 0 on send (callback will fire later), negative errno on failure. */
int qmp_client_invoke(
QmpClient *client,
const char *command,
QmpClientArgs *args,
qmp_command_callback_t callback,
void *userdata);
void qmp_client_bind_event(QmpClient *c, qmp_event_callback_t callback, void *userdata);
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);
QmpClient* qmp_client_unref(QmpClient *p);
DEFINE_TRIVIAL_CLEANUP_FUNC(QmpClient *, qmp_client_unref);
/* Returns true iff any object entry in schema (result of query-qmp-schema) has a member with this
* name. QEMU's introspection replaces type names with opaque numeric ids, so lookup-by-type-name is
* impossible — but member names are real. Use only when the member name is unique in the schema. */
bool qmp_schema_has_member(sd_json_variant *schema, const char *member_name);
+1
View File
@@ -78,6 +78,7 @@ typedef struct MountOptions MountOptions;
typedef struct MStack MStack;
typedef struct OpenFile OpenFile;
typedef struct Pkcs11EncryptedKey Pkcs11EncryptedKey;
typedef struct QmpClient QmpClient;
typedef struct Table Table;
typedef struct Tpm2Context Tpm2Context;
typedef struct Tpm2Handle Tpm2Handle;
+4
View File
@@ -57,6 +57,8 @@ 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_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),
VARLINK_DEFINE_POLKIT_INPUT);
@@ -107,6 +109,8 @@ static SD_VARLINK_DEFINE_METHOD_FULL(
SD_VARLINK_DEFINE_OUTPUT(sshAddress, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("Path to private SSH key"),
SD_VARLINK_DEFINE_OUTPUT(sshPrivateKeyPath, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("Varlink socket address for direct machine control, implementing io.systemd.MachineInstance and optionally further interfaces"),
SD_VARLINK_DEFINE_OUTPUT(controlAddress, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("List of addresses of the machine"),
SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(addresses, Address, SD_VARLINK_ARRAY | SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("OS release information of the machine. It contains an array of key value pairs read from the os-release(5) file in the image."),
@@ -0,0 +1,51 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "varlink-io.systemd.MachineInstance.h"
static SD_VARLINK_DEFINE_METHOD(Terminate);
static SD_VARLINK_DEFINE_METHOD(PowerOff);
static SD_VARLINK_DEFINE_METHOD(Reboot);
static SD_VARLINK_DEFINE_METHOD(Pause);
static SD_VARLINK_DEFINE_METHOD(Resume);
static SD_VARLINK_DEFINE_METHOD(
Describe,
SD_VARLINK_FIELD_COMMENT("True iff vCPUs are executing"),
SD_VARLINK_DEFINE_OUTPUT(running, SD_VARLINK_BOOL, 0),
SD_VARLINK_FIELD_COMMENT("Backend-specific state string (e.g. 'running', 'paused', 'shutdown'); 'unknown' if unavailable"),
SD_VARLINK_DEFINE_OUTPUT(status, SD_VARLINK_STRING, 0));
static SD_VARLINK_DEFINE_METHOD_FULL(
SubscribeEvents,
SD_VARLINK_REQUIRES_MORE,
SD_VARLINK_FIELD_COMMENT("If specified, only deliver events whose name matches one of these strings; null means all events"),
SD_VARLINK_DEFINE_INPUT(filter, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY),
SD_VARLINK_FIELD_COMMENT("Name of the event"),
SD_VARLINK_DEFINE_OUTPUT(event, SD_VARLINK_STRING, 0),
SD_VARLINK_FIELD_COMMENT("Event-specific payload"),
SD_VARLINK_DEFINE_OUTPUT(data, SD_VARLINK_OBJECT, SD_VARLINK_NULLABLE));
static SD_VARLINK_DEFINE_ERROR(NotConnected);
static SD_VARLINK_DEFINE_ERROR(NotSupported);
SD_VARLINK_DEFINE_INTERFACE(
io_systemd_MachineInstance,
"io.systemd.MachineInstance",
SD_VARLINK_SYMBOL_COMMENT("Forcefully terminate the machine immediately"),
&vl_method_Terminate,
SD_VARLINK_SYMBOL_COMMENT("Request a clean shutdown of the machine"),
&vl_method_PowerOff,
SD_VARLINK_SYMBOL_COMMENT("Reboot the machine"),
&vl_method_Reboot,
SD_VARLINK_SYMBOL_COMMENT("Pause/freeze the machine"),
&vl_method_Pause,
SD_VARLINK_SYMBOL_COMMENT("Resume a paused machine"),
&vl_method_Resume,
SD_VARLINK_SYMBOL_COMMENT("Query the current status of the machine"),
&vl_method_Describe,
SD_VARLINK_SYMBOL_COMMENT("Subscribe to machine events. Returns a stream of events as they occur."),
&vl_method_SubscribeEvents,
SD_VARLINK_SYMBOL_COMMENT("The connection to the machine backend is not available"),
&vl_error_NotConnected,
SD_VARLINK_SYMBOL_COMMENT("The requested operation is not supported"),
&vl_error_NotSupported);
@@ -0,0 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "sd-varlink-idl.h"
extern const sd_varlink_interface vl_interface_io_systemd_MachineInstance;
@@ -0,0 +1,17 @@
/* 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);
@@ -0,0 +1,6 @@
/* 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;
@@ -0,0 +1,9 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "varlink-io.systemd.VirtualMachineInstance.h"
/* VM-specific control interface. Currently empty — reserved for methods that apply to virtual
* machines generically but not to containers (e.g. snapshot, migration, device hotplug). */
SD_VARLINK_DEFINE_INTERFACE(
io_systemd_VirtualMachineInstance,
"io.systemd.VirtualMachineInstance");
@@ -0,0 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "sd-varlink-idl.h"
extern const sd_varlink_interface vl_interface_io_systemd_VirtualMachineInstance;
+8
View File
@@ -431,6 +431,14 @@ executables += [
test_template + {
'sources' : files('test-progress-bar.c'),
},
test_template + {
'sources' : files('test-qmp-client.c'),
},
test_template + {
'sources' : files('test-qmp-client-qemu.c'),
'objects' : ['systemd-vmspawn'],
'conditions' : ['ENABLE_VMSPAWN'],
},
test_template + {
'sources' : files('test-qrcode-util.c'),
'dependencies' : libdl,
+264
View File
@@ -0,0 +1,264 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/* Integration test for the QMP client library against a real QEMU instance.
*
* Launches QEMU with -machine none (no bootable image needed) to get a live QMP monitor, then exercises the
* client library against it. Validates the blocking handshake, large response buffering (~200KB for
* query-qmp-schema), response correlation by id, and async command execution.
*
* Skipped automatically if QEMU is not installed. */
#include <signal.h>
#include <sys/socket.h>
#include "sd-event.h"
#include "sd-json.h"
#include "fd-util.h"
#include "pidref.h"
#include "process-util.h"
#include "qmp-client.h"
#include "string-util.h"
#include "tests.h"
#include "time-util.h"
#include "vmspawn-util.h"
static int start_qemu(const char *qemu_binary, int fd, PidRef *ret) {
_cleanup_free_ char *chardev_arg = NULL;
int r;
assert(qemu_binary);
assert(fd >= 0);
assert(ret);
if (asprintf(&chardev_arg, "socket,id=qmp,fd=%d", fd) < 0)
return -ENOMEM;
r = pidref_safe_fork_full(
"(qemu)",
(const int[3]) { STDIN_FILENO, -EBADF, -EBADF },
&fd, 1,
FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_RLIMIT_NOFILE_SAFE|FORK_CLOEXEC_OFF,
ret);
if (r < 0)
return r;
if (r == 0) {
/* Child */
execl(qemu_binary, qemu_binary,
"-machine", "none",
"-nographic",
"-nodefaults",
"-chardev", chardev_arg,
"-mon", "chardev=qmp,mode=control",
NULL);
log_error_errno(errno, "Failed to exec %s: %m", qemu_binary);
_exit(EXIT_FAILURE);
}
return 0;
}
/* Test helper: tracks an async QMP command result and signals completion. */
typedef struct {
sd_json_variant *result;
int error;
bool done;
} QmpTestResult;
static int on_test_result(
QmpClient *client,
sd_json_variant *result,
const char *error_desc,
int error,
void *userdata) {
QmpTestResult *t = ASSERT_PTR(userdata);
t->error = error;
if (result)
t->result = sd_json_variant_ref(result);
t->done = true;
return 0;
}
static void qmp_test_wait(sd_event *event, QmpTestResult *t) {
assert(event);
assert(t);
usec_t deadline = usec_add(now(CLOCK_MONOTONIC), 5 * USEC_PER_MINUTE);
while (!t->done) {
usec_t n = now(CLOCK_MONOTONIC);
ASSERT_LT(n, deadline);
ASSERT_OK(sd_event_run(event, usec_sub_unsigned(deadline, n)));
}
}
static void qmp_test_result_done(QmpTestResult *t) {
assert(t);
sd_json_variant_unref(t->result);
*t = (QmpTestResult) {};
}
TEST(qmp_client_qemu_handshake_and_schema) {
_cleanup_free_ char *qemu = NULL;
_cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
_cleanup_(sd_event_unrefp) sd_event *event = NULL;
_cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
QmpTestResult t = {};
_cleanup_close_pair_ int qmp_fds[2] = EBADF_PAIR;
int r;
if (find_qemu_binary(&qemu) < 0) {
log_tests_skipped("QEMU not found");
return;
}
log_info("Using QEMU: %s", qemu);
ASSERT_OK(sd_event_new(&event));
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
ASSERT_OK(start_qemu(qemu, qmp_fds[1], &pidref));
qmp_fds[1] = safe_close(qmp_fds[1]);
r = qmp_client_connect_fd(&client, qmp_fds[0]);
if (r < 0) {
log_tests_skipped_errno(r, "QMP connect failed (QEMU may not support -machine none)");
return;
}
TAKE_FD(qmp_fds[0]);
ASSERT_OK(qmp_client_attach_event(client, event, SD_EVENT_PRIORITY_NORMAL));
/* query-qmp-schema returns ~200KB -- validates the buffered reader handles large multi-read()
* responses correctly. The handshake completes transparently inside invoke(). */
r = qmp_client_invoke(client, "query-qmp-schema", NULL, on_test_result, &t);
if (r < 0) {
log_tests_skipped_errno(r, "QMP invoke failed (handshake or send)");
return;
}
qmp_test_wait(event, &t);
ASSERT_EQ(t.error, 0);
ASSERT_NOT_NULL(t.result);
ASSERT_TRUE(sd_json_variant_is_array(t.result));
ASSERT_GT(sd_json_variant_elements(t.result), (size_t) 0);
log_info("query-qmp-schema returned %zu entries", sd_json_variant_elements(t.result));
/* Smoke-test the schema walker against the real schema. node-name is on every BlockdevOptions*
* object since blockdev-add was introduced. Don't assert discard-no-unref — CI may have QEMU < 8.1. */
ASSERT_TRUE(qmp_schema_has_member(t.result, "node-name"));
ASSERT_FALSE(qmp_schema_has_member(t.result, "definitely-not-a-real-field"));
qmp_test_result_done(&t);
/* Clean shutdown */
ASSERT_OK(qmp_client_invoke(client, "quit", NULL, on_test_result, &t));
qmp_test_wait(event, &t);
ASSERT_EQ(t.error, 0);
qmp_test_result_done(&t);
siginfo_t si = {};
ASSERT_OK(pidref_wait_for_terminate(&pidref, &si));
ASSERT_EQ(si.si_code, CLD_EXITED);
ASSERT_EQ(si.si_status, EXIT_SUCCESS);
pidref_done(&pidref);
}
TEST(qmp_client_qemu_query_status) {
_cleanup_free_ char *qemu = NULL;
_cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
_cleanup_(sd_event_unrefp) sd_event *event = NULL;
_cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
QmpTestResult t = {};
sd_json_variant *running, *status;
_cleanup_close_pair_ int qmp_fds[2] = EBADF_PAIR;
int r;
if (find_qemu_binary(&qemu) < 0) {
log_tests_skipped("QEMU not found");
return;
}
ASSERT_OK(sd_event_new(&event));
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
ASSERT_OK(start_qemu(qemu, qmp_fds[1], &pidref));
qmp_fds[1] = safe_close(qmp_fds[1]);
r = qmp_client_connect_fd(&client, qmp_fds[0]);
if (r < 0) {
log_tests_skipped_errno(r, "QMP connect failed");
return;
}
TAKE_FD(qmp_fds[0]);
ASSERT_OK(qmp_client_attach_event(client, event, SD_EVENT_PRIORITY_NORMAL));
/* query-status validates response parsing against real QEMU output format.
* The handshake completes transparently inside invoke(). */
r = qmp_client_invoke(client, "query-status", NULL, on_test_result, &t);
if (r < 0) {
log_tests_skipped_errno(r, "QMP invoke failed (handshake or send)");
return;
}
qmp_test_wait(event, &t);
ASSERT_EQ(t.error, 0);
ASSERT_NOT_NULL(t.result);
status = ASSERT_NOT_NULL(sd_json_variant_by_key(t.result, "status"));
ASSERT_TRUE(sd_json_variant_is_string(status));
running = ASSERT_NOT_NULL(sd_json_variant_by_key(t.result, "running"));
ASSERT_TRUE(sd_json_variant_is_boolean(running));
log_info("QEMU status: %s, running: %s",
sd_json_variant_string(status),
true_false(sd_json_variant_boolean(running)));
qmp_test_result_done(&t);
/* Test stop + cont to exercise command sequencing and id correlation */
ASSERT_OK(qmp_client_invoke(client, "stop", NULL, on_test_result, &t));
qmp_test_wait(event, &t);
ASSERT_EQ(t.error, 0);
qmp_test_result_done(&t);
/* Verify status changed */
ASSERT_OK(qmp_client_invoke(client, "query-status", NULL, on_test_result, &t));
qmp_test_wait(event, &t);
ASSERT_EQ(t.error, 0);
ASSERT_NOT_NULL(t.result);
running = ASSERT_NOT_NULL(sd_json_variant_by_key(t.result, "running"));
ASSERT_FALSE(sd_json_variant_boolean(running));
log_info("After stop: running=%s", true_false(sd_json_variant_boolean(running)));
qmp_test_result_done(&t);
ASSERT_OK(qmp_client_invoke(client, "cont", NULL, on_test_result, &t));
qmp_test_wait(event, &t);
ASSERT_EQ(t.error, 0);
qmp_test_result_done(&t);
/* Clean shutdown */
ASSERT_OK(qmp_client_invoke(client, "quit", NULL, on_test_result, &t));
qmp_test_wait(event, &t);
ASSERT_EQ(t.error, 0);
qmp_test_result_done(&t);
siginfo_t si = {};
ASSERT_OK(pidref_wait_for_terminate(&pidref, &si));
ASSERT_EQ(si.si_code, CLD_EXITED);
ASSERT_EQ(si.si_status, EXIT_SUCCESS);
pidref_done(&pidref);
}
static int intro(void) {
/* QEMU dies between our last write and read on the QMP socket — without this we'd
* get killed by the SIGPIPE the kernel raises on write-after-EOF. */
ASSERT_TRUE(signal(SIGPIPE, SIG_IGN) != SIG_ERR);
return 0;
}
DEFINE_TEST_MAIN_FULL(LOG_DEBUG, intro, NULL);
+516
View File
@@ -0,0 +1,516 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <fcntl.h>
#include <signal.h>
#include <sys/socket.h>
#include "sd-event.h"
#include "sd-json.h"
#include "errno-util.h"
#include "fd-util.h"
#include "io-util.h"
#include "pidref.h"
#include "process-util.h"
#include "qmp-client.h"
#include "socket-util.h"
#include "string-util.h"
#include "tests.h"
/* Mock QMP server: runs in the child process of a fork, communicates via one end of a socketpair. */
static void mock_qmp_write_json(int fd, sd_json_variant *v) {
_cleanup_free_ char *s = NULL;
ASSERT_OK(sd_json_variant_format(v, 0, &s));
ASSERT_NOT_NULL(strextend(&s, "\r\n"));
ASSERT_OK(loop_write(fd, s, SIZE_MAX));
}
static void mock_qmp_write_literal(int fd, const char *msg) {
ASSERT_OK(loop_write(fd, msg, SIZE_MAX));
ASSERT_OK(loop_write(fd, "\r\n", 2));
}
/* Read a command from the QMP client, verify it contains the expected command name, extract the id,
* and send a reply with that id. If reply_data is NULL, an empty return object is sent. */
static void mock_qmp_expect_and_reply(int fd, const char *expected_command, sd_json_variant *reply_data) {
_cleanup_free_ char *buf = NULL;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *cmd = NULL, *reply_obj = NULL, *response = NULL;
buf = ASSERT_NOT_NULL(new(char, 4096));
ssize_t n = read(fd, buf, 4095);
assert_se(n > 0);
buf[n] = '\0';
ASSERT_OK(sd_json_parse(buf, 0, &cmd, NULL, NULL));
sd_json_variant *execute = ASSERT_NOT_NULL(sd_json_variant_by_key(cmd, "execute"));
ASSERT_STREQ(sd_json_variant_string(execute), expected_command);
sd_json_variant *id = ASSERT_NOT_NULL(sd_json_variant_by_key(cmd, "id"));
if (!reply_data)
ASSERT_OK(sd_json_variant_new_object(&reply_obj, NULL, 0));
ASSERT_OK(sd_json_buildo(
&response,
SD_JSON_BUILD_PAIR("return", SD_JSON_BUILD_VARIANT(reply_data ?: reply_obj)),
SD_JSON_BUILD_PAIR("id", SD_JSON_BUILD_VARIANT(id))));
mock_qmp_write_json(fd, response);
}
static _noreturn_ void mock_qmp_server(int fd) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *status_return = NULL;
/* Send QMP greeting */
mock_qmp_write_literal(fd,
"{\"QMP\": {\"version\": {\"qemu\": {\"micro\": 0, \"minor\": 2, \"major\": 9}}, \"capabilities\": [\"oob\"]}}");
/* Accept qmp_capabilities */
mock_qmp_expect_and_reply(fd, "qmp_capabilities", NULL);
/* Accept query-status, reply with running state */
ASSERT_OK(sd_json_buildo(
&status_return,
SD_JSON_BUILD_PAIR_BOOLEAN("running", true),
SD_JSON_BUILD_PAIR_STRING("status", "running")));
mock_qmp_expect_and_reply(fd, "query-status", status_return);
/* Accept stop */
mock_qmp_expect_and_reply(fd, "stop", NULL);
/* Send a STOP event */
mock_qmp_write_literal(fd,
"{\"event\": \"STOP\", \"timestamp\": {\"seconds\": 1234, \"microseconds\": 5678}}");
/* Accept cont */
mock_qmp_expect_and_reply(fd, "cont", NULL);
/* Close to trigger EOF */
safe_close(fd);
_exit(EXIT_SUCCESS);
}
/* Test helper: tracks an async QMP command result and signals completion. */
typedef struct {
sd_json_variant *result;
char *error_desc;
int error;
bool done;
} QmpTestResult;
static int on_test_result(
QmpClient *client,
sd_json_variant *result,
const char *error_desc,
int error,
void *userdata) {
QmpTestResult *t = ASSERT_PTR(userdata);
t->error = error;
if (result)
t->result = sd_json_variant_ref(result);
if (error_desc)
t->error_desc = strdup(error_desc);
t->done = true;
return 0;
}
/* Run the event loop until the test result callback fires. */
static void qmp_test_wait(sd_event *event, QmpTestResult *t) {
assert(event);
assert(t);
while (!t->done)
ASSERT_OK(sd_event_run(event, UINT64_MAX));
}
static void qmp_test_result_done(QmpTestResult *t) {
assert(t);
sd_json_variant_unref(t->result);
free(t->error_desc);
*t = (QmpTestResult) {};
}
static int test_event_callback(
QmpClient *client,
const char *event,
sd_json_variant *data,
void *userdata) {
bool *event_received = ASSERT_PTR(userdata);
/* We may also receive a synthetic SHUTDOWN event when the mock server closes the connection;
* only validate the STOP event we actually care about. */
if (streq(event, "STOP"))
*event_received = true;
return 0;
}
TEST(qmp_client_basic) {
_cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
_cleanup_(sd_event_unrefp) sd_event *event = NULL;
_cleanup_(pidref_done) PidRef pid = PIDREF_NULL;
QmpTestResult t = {};
sd_json_variant *running, *status;
int qmp_fds[2];
int r;
ASSERT_OK(sd_event_new(&event));
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
r = pidref_safe_fork("(mock-qmp)", FORK_DEATHSIG_SIGKILL|FORK_LOG, &pid);
ASSERT_OK(r);
if (r == 0) {
safe_close(qmp_fds[0]);
mock_qmp_server(qmp_fds[1]);
}
safe_close(qmp_fds[1]);
/* Connect then attach to event loop — handshake completes transparently
* inside the first call()/invoke(). */
ASSERT_OK(qmp_client_connect_fd(&client, qmp_fds[0]));
ASSERT_OK(qmp_client_attach_event(client, event, SD_EVENT_PRIORITY_NORMAL));
/* Set event callback to catch STOP event during cont */
bool event_received = false;
qmp_client_bind_event(client, test_event_callback, &event_received);
/* Execute query-status */
ASSERT_OK(qmp_client_invoke(client, "query-status", NULL, on_test_result, &t));
qmp_test_wait(event, &t);
ASSERT_EQ(t.error, 0);
ASSERT_NOT_NULL(t.result);
running = ASSERT_NOT_NULL(sd_json_variant_by_key(t.result, "running"));
ASSERT_TRUE(sd_json_variant_boolean(running));
status = ASSERT_NOT_NULL(sd_json_variant_by_key(t.result, "status"));
ASSERT_STREQ(sd_json_variant_string(status), "running");
qmp_test_result_done(&t);
/* Execute stop */
ASSERT_OK(qmp_client_invoke(client, "stop", NULL, on_test_result, &t));
qmp_test_wait(event, &t);
ASSERT_EQ(t.error, 0);
qmp_test_result_done(&t);
/* Execute cont -- the STOP event should be dispatched by the IO callback */
ASSERT_OK(qmp_client_invoke(client, "cont", NULL, on_test_result, &t));
qmp_test_wait(event, &t);
ASSERT_EQ(t.error, 0);
qmp_test_result_done(&t);
/* Verify the STOP event was received */
ASSERT_TRUE(event_received);
/* Wait for child and verify clean exit */
siginfo_t si = {};
ASSERT_OK(pidref_wait_for_terminate(&pid, &si));
ASSERT_EQ(si.si_code, CLD_EXITED);
ASSERT_EQ(si.si_status, EXIT_SUCCESS);
}
TEST(qmp_client_eof) {
_cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
_cleanup_(sd_event_unrefp) sd_event *event = NULL;
_cleanup_(pidref_done) PidRef pid = PIDREF_NULL;
QmpTestResult t = {};
int qmp_fds[2];
int r;
ASSERT_OK(sd_event_new(&event));
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
r = pidref_safe_fork("(mock-qmp-eof)", FORK_DEATHSIG_SIGKILL|FORK_LOG, &pid);
ASSERT_OK(r);
if (r == 0) {
safe_close(qmp_fds[0]);
/* Send greeting and accept capabilities, then die */
mock_qmp_write_literal(qmp_fds[1],
"{\"QMP\": {\"version\": {\"qemu\": {\"micro\": 0, \"minor\": 0, \"major\": 9}}, \"capabilities\": []}}");
mock_qmp_expect_and_reply(qmp_fds[1], "qmp_capabilities", NULL);
/* Close immediately to trigger EOF */
safe_close(qmp_fds[1]);
_exit(EXIT_SUCCESS);
}
safe_close(qmp_fds[1]);
ASSERT_OK(qmp_client_connect_fd(&client, qmp_fds[0]));
ASSERT_OK(qmp_client_attach_event(client, event, SD_EVENT_PRIORITY_NORMAL));
/* Executing a command should fail with a disconnect error because the server
* closed. The handshake may succeed or fail inside invoke() — either way the
* invoke itself or the async callback should report a disconnect. */
r = qmp_client_invoke(client, "query-status", NULL, on_test_result, &t);
if (r < 0)
ASSERT_TRUE(ERRNO_IS_NEG_DISCONNECT(r));
else {
qmp_test_wait(event, &t);
ASSERT_TRUE(ERRNO_IS_NEG_DISCONNECT(t.error));
qmp_test_result_done(&t);
}
siginfo_t si = {};
ASSERT_OK(pidref_wait_for_terminate(&pid, &si));
ASSERT_EQ(si.si_code, CLD_EXITED);
ASSERT_EQ(si.si_status, EXIT_SUCCESS);
}
/* Read one QMP command from fd (one recvmsg, expecting it fits in the buffer for typical
* test commands). Returns the number of SCM_RIGHTS fds that arrived attached to the read,
* stores the first received fd in *ret_received_fd (or -EBADF if none) and closes any extras,
* and parses the JSON into *ret_cmd. */
static size_t mock_qmp_recv_command(int fd, sd_json_variant **ret_cmd, int *ret_received_fd) {
char buf[4096];
char ctrl[CMSG_SPACE(sizeof(int) * 4)];
struct iovec iov = { .iov_base = buf, .iov_len = sizeof(buf) - 1 };
struct msghdr mh = {
.msg_iov = &iov, .msg_iovlen = 1,
.msg_control = ctrl, .msg_controllen = sizeof(ctrl),
};
size_t n_fds = 0;
int received_fd = -EBADF;
ssize_t n = recvmsg(fd, &mh, MSG_CMSG_CLOEXEC);
assert_se(n > 0);
buf[n] = '\0';
struct cmsghdr *cmsg;
CMSG_FOREACH(cmsg, &mh) {
if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS)
continue;
size_t k = (cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int);
int *fds = (int*) CMSG_DATA(cmsg);
for (size_t i = 0; i < k; i++) {
if (received_fd < 0)
received_fd = fds[i];
else
safe_close(fds[i]);
}
n_fds += k;
}
ASSERT_OK(sd_json_parse(buf, 0, ret_cmd, NULL, NULL));
if (ret_received_fd)
*ret_received_fd = received_fd;
else if (received_fd >= 0)
safe_close(received_fd);
return n_fds;
}
/* Mock QMP server for the fd-on-first-invoke regression. Drives the wire dance:
* greeting → (recv qmp_capabilities, expect 0 fds) → reply →
* (recv add-fd, expect exactly 1 fd) → reply
* Asserts the cmsg fd counts directly so a regression flips the child to
* exit_failure and the parent test fails on the wait-for-terminate. */
static _noreturn_ void mock_qmp_server_fd_first(int fd) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *cap_cmd = NULL,
*addfd_cmd = NULL,
*cap_reply = NULL,
*addfd_return = NULL,
*addfd_reply = NULL;
size_t n_fds;
int received_fd = -EBADF;
/* Greeting */
mock_qmp_write_literal(fd,
"{\"QMP\": {\"version\": {\"qemu\": {\"micro\": 0, \"minor\": 0, \"major\": 9}}, \"capabilities\": []}}");
/* Receive qmp_capabilities — must arrive with NO fds attached. */
n_fds = mock_qmp_recv_command(fd, &cap_cmd, /* ret_received_fd= */ NULL);
ASSERT_EQ(n_fds, (size_t) 0);
ASSERT_STREQ(sd_json_variant_string(sd_json_variant_by_key(cap_cmd, "execute")), "qmp_capabilities");
sd_json_variant *cap_id = ASSERT_NOT_NULL(sd_json_variant_by_key(cap_cmd, "id"));
ASSERT_OK(sd_json_buildo(
&cap_reply,
SD_JSON_BUILD_PAIR("return", SD_JSON_BUILD_EMPTY_OBJECT),
SD_JSON_BUILD_PAIR("id", SD_JSON_BUILD_VARIANT(cap_id))));
mock_qmp_write_json(fd, cap_reply);
/* Receive add-fd — must arrive with EXACTLY ONE fd attached. */
n_fds = mock_qmp_recv_command(fd, &addfd_cmd, &received_fd);
ASSERT_EQ(n_fds, (size_t) 1);
ASSERT_TRUE(received_fd >= 0);
ASSERT_STREQ(sd_json_variant_string(sd_json_variant_by_key(addfd_cmd, "execute")), "add-fd");
safe_close(received_fd);
sd_json_variant *addfd_id = ASSERT_NOT_NULL(sd_json_variant_by_key(addfd_cmd, "id"));
ASSERT_OK(sd_json_buildo(
&addfd_return,
SD_JSON_BUILD_PAIR_UNSIGNED("fdset-id", 0),
SD_JSON_BUILD_PAIR_UNSIGNED("fd", 42)));
ASSERT_OK(sd_json_buildo(
&addfd_reply,
SD_JSON_BUILD_PAIR("return", SD_JSON_BUILD_VARIANT(addfd_return)),
SD_JSON_BUILD_PAIR("id", SD_JSON_BUILD_VARIANT(addfd_id))));
mock_qmp_write_json(fd, addfd_reply);
safe_close(fd);
_exit(EXIT_SUCCESS);
}
/* Regression: pass an fd in the very first qmp_client_invoke() against a fresh client
* (lazy-bootstrap state, handshake not yet done). The previous push_fd+invoke split would
* stage the fd on the stream BEFORE qmp_client_ensure_running() drove the handshake; the
* handshake's qmp_capabilities enqueue would then steal the staged fd onto its own
* sendmsg. The new QmpClientArgs API stages fds inside invoke AFTER ensure_running, so
* the fd lands on add-fd's sendmsg as it should. */
TEST(qmp_client_first_invoke_with_fd) {
_cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
_cleanup_(sd_event_unrefp) sd_event *event = NULL;
_cleanup_(pidref_done) PidRef pid = PIDREF_NULL;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL;
_cleanup_close_ int fd_to_pass = -EBADF;
QmpTestResult t = {};
int qmp_fds[2];
int r;
ASSERT_OK(sd_event_new(&event));
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
r = pidref_safe_fork("(mock-qmp-fd-first)", FORK_DEATHSIG_SIGKILL|FORK_LOG, &pid);
ASSERT_OK(r);
if (r == 0) {
safe_close(qmp_fds[0]);
mock_qmp_server_fd_first(qmp_fds[1]);
}
safe_close(qmp_fds[1]);
/* Open a real fd to pass — /dev/null is universally available. */
fd_to_pass = open("/dev/null", O_RDWR|O_CLOEXEC);
ASSERT_OK(fd_to_pass);
ASSERT_OK(qmp_client_connect_fd(&client, qmp_fds[0]));
ASSERT_OK(qmp_client_attach_event(client, event, SD_EVENT_PRIORITY_NORMAL));
/* Build add-fd args. The fdset-id value is irrelevant — the mock server only
* cares that the fd arrived with the correct sendmsg. */
ASSERT_OK(sd_json_buildo(&args, SD_JSON_BUILD_PAIR_UNSIGNED("fdset-id", 0)));
/* THIS is the previously-broken pattern: very first invoke against the client,
* carrying an fd, with the handshake still pending. */
ASSERT_OK(qmp_client_invoke(client, "add-fd",
QMP_CLIENT_ARGS_FD(args, TAKE_FD(fd_to_pass)),
on_test_result, &t));
qmp_test_wait(event, &t);
ASSERT_EQ(t.error, 0);
ASSERT_NOT_NULL(t.result);
qmp_test_result_done(&t);
/* Wait for the mock server child. If it received fds in the wrong order it
* exited via the test-assertion failure path and si.si_status will be non-zero. */
siginfo_t si = {};
ASSERT_OK(pidref_wait_for_terminate(&pid, &si));
ASSERT_EQ(si.si_code, CLD_EXITED);
ASSERT_EQ(si.si_status, EXIT_SUCCESS);
}
/* Regression: when qmp_client_invoke() fails before stage_fds runs (e.g.
* ensure_running() returns -ENOTCONN because the peer closed mid-handshake), the
* caller-supplied fds — already TAKE_FD()'d through QMP_CLIENT_ARGS_FD() — must be
* closed inside invoke. Otherwise they leak. */
TEST(qmp_client_invoke_failure_closes_fds) {
_cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL;
_cleanup_close_ int fd_to_pass = -EBADF;
QmpTestResult t = {};
int qmp_fds[2];
int saved_fd_value;
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
/* Close the peer end immediately so ensure_running()'s read sees EOF and
* the client transitions straight to DISCONNECTED inside the first invoke. */
safe_close(qmp_fds[1]);
ASSERT_OK(qmp_client_connect_fd(&client, qmp_fds[0]));
/* Deliberately do NOT attach to an event loop — invoke uses ensure_running()'s
* synchronous process+wait pump for the handshake. */
fd_to_pass = open("/dev/null", O_RDWR|O_CLOEXEC);
ASSERT_OK(fd_to_pass);
saved_fd_value = fd_to_pass; /* remember the int value for the closed-check */
ASSERT_OK(sd_json_buildo(&args, SD_JSON_BUILD_PAIR_UNSIGNED("fdset-id", 0)));
/* invoke must fail because the peer is gone. The TAKE_FD inside the macro
* has already zeroed our local fd_to_pass; if invoke leaked the fd here,
* the fd would stay open in our process. */
int r = qmp_client_invoke(client, "add-fd",
QMP_CLIENT_ARGS_FD(args, TAKE_FD(fd_to_pass)),
on_test_result, &t);
ASSERT_TRUE(r < 0);
ASSERT_TRUE(ERRNO_IS_NEG_DISCONNECT(r));
/* fd_to_pass should now be -EBADF (TAKE_FD'd) and the underlying kernel fd
* should have been closed by the qmp_client_args_close_fds cleanup in
* qmp_client_invoke(). fcntl on the old int returns EBADF only if the slot
* is genuinely free. */
ASSERT_EQ(fd_to_pass, -EBADF);
ASSERT_EQ(fcntl(saved_fd_value, F_GETFD), -1);
ASSERT_EQ(errno, EBADF);
}
TEST(qmp_schema_has_member) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *schema = NULL;
/* QEMU introspection uses opaque numeric type ids ("0", "1", ...) — only member names are
* the actual QAPI strings. Verify we walk all object entries and find the member by name. */
ASSERT_OK(sd_json_build(&schema,
SD_JSON_BUILD_ARRAY(
SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR_STRING("name", "0"),
SD_JSON_BUILD_PAIR_STRING("meta-type", "object"),
SD_JSON_BUILD_PAIR("members", SD_JSON_BUILD_ARRAY(
SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR_STRING("name", "offset"),
SD_JSON_BUILD_PAIR_STRING("type", "int"))))),
SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR_STRING("name", "SomeEnum"),
SD_JSON_BUILD_PAIR_STRING("meta-type", "enum")),
SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR_STRING("name", "1"),
SD_JSON_BUILD_PAIR_STRING("meta-type", "object"),
SD_JSON_BUILD_PAIR("members", SD_JSON_BUILD_ARRAY(
SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR_STRING("name", "lazy-refcounts"),
SD_JSON_BUILD_PAIR_STRING("type", "bool")),
SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR_STRING("name", "discard-no-unref"),
SD_JSON_BUILD_PAIR_STRING("type", "bool"))))))));
ASSERT_TRUE(qmp_schema_has_member(schema, "discard-no-unref"));
ASSERT_TRUE(qmp_schema_has_member(schema, "offset"));
ASSERT_FALSE(qmp_schema_has_member(schema, "definitely-not-a-real-field"));
ASSERT_FALSE(qmp_schema_has_member(NULL, "discard-no-unref"));
}
static int intro(void) {
/* Ignore SIGPIPE so that write() to a closed socket returns EPIPE instead of killing us */
ASSERT_TRUE(signal(SIGPIPE, SIG_IGN) != SIG_ERR);
return 0;
}
DEFINE_TEST_MAIN_FULL(LOG_DEBUG, intro, NULL);
+2
View File
@@ -7,6 +7,8 @@ endif
vmspawn_sources = files(
'vmspawn.c',
'vmspawn-qemu-config.c',
'vmspawn-qmp.c',
'vmspawn-varlink.c',
'vmspawn-settings.c',
'vmspawn-scope.c',
'vmspawn-mount.c',
File diff suppressed because it is too large Load Diff
+145
View File
@@ -0,0 +1,145 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include <net/ethernet.h>
#include "shared-forward.h"
/* 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);
typedef void (*pending_job_free_t)(void *userdata);
typedef struct PendingJob {
pending_job_callback_t on_concluded;
pending_job_free_t free_userdata;
void *userdata;
} PendingJob;
PendingJob* pending_job_free(PendingJob *j);
DEFINE_TRIVIAL_CLEANUP_FUNC(PendingJob *, pending_job_free);
typedef enum VmspawnQmpFeatureFlags {
VMSPAWN_QMP_FEATURE_IO_URING = 1u << 0,
VMSPAWN_QMP_FEATURE_DISCARD_NO_UNREF = 1u << 1,
} VmspawnQmpFeatureFlags;
typedef struct VmspawnQmpBridge {
QmpClient *qmp;
Hashmap *pending_jobs; /* job_id (string, owned) -> PendingJob* */
VmspawnQmpFeatureFlags features;
} VmspawnQmpBridge;
VmspawnQmpBridge* vmspawn_qmp_bridge_free(VmspawnQmpBridge *b);
DEFINE_TRIVIAL_CLEANUP_FUNC(VmspawnQmpBridge *, vmspawn_qmp_bridge_free);
QmpClient* vmspawn_qmp_bridge_get_qmp(VmspawnQmpBridge *b);
/* Phase 1: Connect to VMM backend. Returns an opaque bridge ready for device setup. */
int vmspawn_qmp_init(VmspawnQmpBridge **ret, int fd, sd_event *event);
/* Phase 1b: Feature probing. Fires one-shot QMP commands and drives the client
* synchronously until every reply has been delivered. Populates bridge->features.
* Must run before the device-setup phase; both io_uring and discard-no-unref flags
* are consumed by vmspawn_qmp_setup_drives(). */
int vmspawn_qmp_probe_features(VmspawnQmpBridge *bridge);
/* Phase 3: Resume vCPUs. All commands are async — responses arrive during sd_event_loop(). */
int vmspawn_qmp_start(VmspawnQmpBridge *bridge);
int vmspawn_qmp_bridge_register_job(
VmspawnQmpBridge *b,
const char *job_id,
pending_job_callback_t on_concluded,
void *userdata,
pending_job_free_t free_userdata);
typedef enum QmpDriveFlags {
QMP_DRIVE_BLOCK_DEVICE = 1u << 0,
QMP_DRIVE_READ_ONLY = 1u << 1,
QMP_DRIVE_DISCARD = 1u << 2,
QMP_DRIVE_NO_FLUSH = 1u << 3,
QMP_DRIVE_BOOT = 1u << 4,
QMP_DRIVE_IO_URING = 1u << 5,
QMP_DRIVE_DISCARD_NO_UNREF = 1u << 6, /* qcow2 only */
} QmpDriveFlags;
/* Drive info for QMP-based drive setup */
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) */
QmpDriveFlags flags;
} DriveInfo;
void drive_info_done(DriveInfo *info);
typedef struct DriveInfos {
DriveInfo *drives;
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);
/* Network info for QMP-based network setup. Covers privileged TAP (by name),
* nsresourced TAP (by FD via getfd), and user-mode networking. The no-network
* case (-nic none) stays on the QEMU command line. */
typedef struct NetworkInfo {
const char *type; /* "tap" or "user" — points to a string literal */
char *ifname; /* owned: TAP interface name (tap by name only, NULL if unset) */
struct ether_addr mac; /* VM-side MAC address (tap only, valid iff mac_set) */
bool mac_set;
char *pcie_port; /* owned: pcie-root-port id for device_add bus (NULL on non-PCIe) */
int fd; /* TAP fd to pass via getfd (tap by fd only, -EBADF if unused) */
} NetworkInfo;
void network_info_done(NetworkInfo *info);
/* Virtiofs device info for QMP-based chardev + device setup */
typedef struct VirtiofsInfo {
char *id; /* owned: chardev and device id (e.g. "rootdir", "mnt0") */
char *socket_path; /* owned: virtiofsd listen socket path */
char *tag; /* owned: virtiofs mount tag visible to guest */
char *pcie_port; /* owned: pcie-root-port id for device_add bus (NULL on non-PCIe) */
} VirtiofsInfo;
void virtiofs_info_done(VirtiofsInfo *info);
typedef struct VirtiofsInfos {
VirtiofsInfo *entries;
size_t n_entries;
} VirtiofsInfos;
void virtiofs_infos_done(VirtiofsInfos *infos);
/* VSOCK device info for QMP-based setup via getfd + device_add */
typedef struct VsockInfo {
int fd; /* vhost-vsock fd to pass via getfd (-EBADF if unused) */
unsigned cid; /* guest CID */
char *pcie_port; /* owned: pcie-root-port id for device_add bus (NULL on non-PCIe) */
} VsockInfo;
void vsock_info_done(VsockInfo *info);
/* Aggregate of the per-device info structures populated before the bridge-based
* device setup phase. Keeps lifetime and cleanup of all device state in one place. */
typedef struct MachineConfig {
DriveInfos drives;
NetworkInfo network;
VirtiofsInfos virtiofs;
VsockInfo vsock;
} MachineConfig;
void machine_config_done(MachineConfig *c);
/* Phase 2: Device setup — call any subset in any order before vmspawn_qmp_start(). */
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);
+14 -1
View File
@@ -59,20 +59,33 @@
# define KERNEL_CMDLINE_SIZE 512
#endif
/* ARCHITECTURE_NEEDS_PCIE_ROOT_PORTS is co-located with QEMU_MACHINE_TYPE so they stay in
* sync: q35 and virt machine types need pcie-root-port bridges for QMP device_add hotplug.
* Exception: m68k's "virt" uses virtio-mmio, not PCIe, so it doesn't need root ports. */
#if defined(__x86_64__) || defined(__i386__)
# define QEMU_MACHINE_TYPE "q35"
#elif defined(__arm__) || defined(__aarch64__) || defined(__riscv) || defined(__loongarch64) || defined(__m68k__)
# define ARCHITECTURE_NEEDS_PCIE_ROOT_PORTS 1
#elif defined(__m68k__)
# define QEMU_MACHINE_TYPE "virt"
# define ARCHITECTURE_NEEDS_PCIE_ROOT_PORTS 0
#elif defined(__arm__) || defined(__aarch64__) || defined(__riscv) || defined(__loongarch64)
# define QEMU_MACHINE_TYPE "virt"
# define ARCHITECTURE_NEEDS_PCIE_ROOT_PORTS 1
#elif defined(__s390__) || defined(__s390x__)
# define QEMU_MACHINE_TYPE "s390-ccw-virtio"
# define ARCHITECTURE_NEEDS_PCIE_ROOT_PORTS 0
#elif defined(__powerpc__) || defined(__powerpc64__)
# define QEMU_MACHINE_TYPE "pseries"
# define ARCHITECTURE_NEEDS_PCIE_ROOT_PORTS 0
#elif defined(__mips__)
# define QEMU_MACHINE_TYPE "malta"
# define ARCHITECTURE_NEEDS_PCIE_ROOT_PORTS 0
#elif defined(__sparc__)
# define QEMU_MACHINE_TYPE "sun4u"
# define ARCHITECTURE_NEEDS_PCIE_ROOT_PORTS 0
#else
# define QEMU_MACHINE_TYPE "none"
# define ARCHITECTURE_NEEDS_PCIE_ROOT_PORTS 0
#endif
#if defined(__arm__) || defined(__aarch64__)
+410
View File
@@ -0,0 +1,410 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "alloc-util.h"
#include "errno-util.h"
#include "hashmap.h"
#include "log.h"
#include "path-util.h"
#include "qmp-client.h"
#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-varlink.h"
DEFINE_PRIVATE_HASH_OPS_FULL(
varlink_subscriber_hash_ops,
void, trivial_hash_func, trivial_compare_func, sd_varlink_close_unref,
char*, strv_free);
struct VmspawnVarlinkContext {
sd_varlink_server *varlink_server;
VmspawnQmpBridge *bridge;
/* Key: sd_varlink* (ref'd), Value: strv filter (NULL = all events).
* varlink_subscriber_hash_ops handles cleanup of both on removal. */
Hashmap *subscribed;
};
/* Translate a QMP async completion into a varlink error reply */
static int qmp_error_to_varlink(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 == -EIO)
log_warning("QMP command failed: %s", strna(error_desc));
return sd_varlink_error_errno(link, error);
}
/* Shared async completion for simple QMP commands that return no data.
* Errors are translated to varlink replies, not propagated through sd_event. */
static int on_qmp_simple_complete(
QmpClient *client,
sd_json_variant *result,
const char *error_desc,
int error,
void *userdata) {
sd_varlink *link = ASSERT_PTR(userdata);
assert(client);
if (error == 0)
(void) sd_varlink_reply(link, NULL);
else
(void) qmp_error_to_varlink(link, error_desc, error);
sd_varlink_unref(link);
return 0;
}
static int qmp_execute_varlink_async(
VmspawnVarlinkContext *ctx,
sd_varlink *link,
const char *command,
sd_json_variant *arguments,
qmp_command_callback_t callback) {
int r;
sd_varlink_ref(link);
r = qmp_client_invoke(ctx->bridge->qmp, command, QMP_CLIENT_ARGS(arguments), callback, link);
if (r < 0)
sd_varlink_unref(link);
return r;
}
static int qmp_execute_simple_async(sd_varlink *link, VmspawnVarlinkContext *ctx, const char *qmp_command) {
assert(link);
assert(ctx);
assert(qmp_command);
return qmp_execute_varlink_async(ctx, link, qmp_command, /* arguments= */ NULL, on_qmp_simple_complete);
}
static int vl_method_terminate(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
return qmp_execute_simple_async(link, ASSERT_PTR(userdata), "quit");
}
static int vl_method_pause(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
return qmp_execute_simple_async(link, ASSERT_PTR(userdata), "stop");
}
static int vl_method_resume(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
return qmp_execute_simple_async(link, ASSERT_PTR(userdata), "cont");
}
static int vl_method_power_off(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
return qmp_execute_simple_async(link, ASSERT_PTR(userdata), "system_powerdown");
}
static int vl_method_reboot(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
return qmp_execute_simple_async(link, ASSERT_PTR(userdata), "system_reset");
}
/* Async completion for query-status: extract running/status from QMP result */
static int on_qmp_describe_complete(
QmpClient *client,
sd_json_variant *result,
const char *error_desc,
int error,
void *userdata) {
_cleanup_(sd_varlink_unrefp) sd_varlink *link = ASSERT_PTR(userdata);
assert(client);
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");
(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"));
return 0;
}
static int vl_method_describe(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
VmspawnVarlinkContext *ctx = ASSERT_PTR(userdata);
return qmp_execute_varlink_async(ctx, link, "query-status", /* arguments= */ NULL, on_qmp_describe_complete);
}
static int vl_method_subscribe_events(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
VmspawnVarlinkContext *ctx = ASSERT_PTR(userdata);
_cleanup_strv_free_ char **filter = NULL;
int r;
/* SD_VARLINK_REQUIRES_MORE in the IDL rejects non-streaming callers before we get here */
r = sd_varlink_dispatch(link, parameters, (const sd_json_dispatch_field[]) {
{ "filter", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_strv, 0, SD_JSON_NULLABLE },
{},
}, &filter);
if (r != 0)
return r;
sd_varlink_ref(link);
r = hashmap_ensure_put(&ctx->subscribed, &varlink_subscriber_hash_ops, link, filter);
if (r < 0) {
sd_varlink_unref(link);
return r;
}
TAKE_PTR(filter);
r = sd_varlink_notifybo(link, SD_JSON_BUILD_PAIR_STRING("event", "READY"));
if (r < 0) {
strv_free(hashmap_remove(ctx->subscribed, link));
sd_varlink_close_unref(link);
return r;
}
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);
assert(server);
assert(link);
/* Only subscribers hold an extra ref on the link (taken in vl_method_subscribe_events).
* Non-subscriber connections (one-shot commands like Pause, Describe) must not be unref'd
* here — their extra ref is consumed by the async completion callback. Only unref, never
* close — the server handles close after this callback returns (matching resolved's
* vl_on_notification_disconnect pattern).
*
* Use hashmap_remove2() so the returned key (non-NULL iff the entry was present)
* disambiguates "no filter subscriber" (value=NULL) from "not a subscriber". */
void *removed_key = NULL;
strv_free(hashmap_remove2(ctx->subscribed, link, &removed_key));
if (!removed_key)
return;
sd_varlink_unref(link);
}
static int on_job_dismiss_complete(
QmpClient *client,
sd_json_variant *result,
const char *error_desc,
int error,
void *userdata) {
if (error < 0)
log_debug_errno(error, "job-dismiss failed: %s", strna(error_desc));
return 0;
}
static int dispatch_pending_job(VmspawnQmpBridge *bridge, sd_json_variant *data) {
const char *job_id, *status;
int r;
assert(bridge);
if (!data)
return 0;
job_id = sd_json_variant_string(sd_json_variant_by_key(data, "id"));
status = sd_json_variant_string(sd_json_variant_by_key(data, "status"));
if (!job_id || !streq_ptr(status, "concluded"))
return 0;
_cleanup_free_ char *key = NULL;
_cleanup_(pending_job_freep) PendingJob *job = hashmap_remove2(bridge->pending_jobs, job_id, (void**) &key);
if (!job)
return 0;
log_debug("QMP job '%s' concluded, firing continuation", job_id);
/* Dismiss the concluded job before running the continuation */
_cleanup_(sd_json_variant_unrefp) sd_json_variant *dismiss_args = NULL;
r = sd_json_buildo(&dismiss_args, SD_JSON_BUILD_PAIR_STRING("id", job_id));
if (r < 0)
return sd_event_exit(qmp_client_get_event(bridge->qmp), r);
r = qmp_client_invoke(bridge->qmp, "job-dismiss", QMP_CLIENT_ARGS(dismiss_args),
on_job_dismiss_complete, /* userdata= */ NULL);
if (r < 0)
return sd_event_exit(qmp_client_get_event(bridge->qmp), r);
if (!job->on_concluded)
return 1;
r = job->on_concluded(bridge->qmp, TAKE_PTR(job->userdata));
if (r < 0) {
log_error_errno(r, "Job continuation failed: %m");
return sd_event_exit(qmp_client_get_event(bridge->qmp), r);
}
return 1;
}
static int on_qmp_event(
QmpClient *client,
const char *event,
sd_json_variant *data,
void *userdata) {
VmspawnVarlinkContext *ctx = ASSERT_PTR(userdata);
_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);
if (hashmap_isempty(ctx->subscribed))
return 0;
r = sd_json_buildo(
&notification,
SD_JSON_BUILD_PAIR_STRING("event", event),
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");
return 0;
}
HASHMAP_FOREACH_KEY(filter, link, ctx->subscribed) {
if (filter && !strv_contains(filter, event))
continue;
r = sd_varlink_notify(link, notification);
if (r < 0)
log_warning_errno(r, "Failed to notify event subscriber, ignoring: %m");
}
return 0;
}
/* 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) {
assert(subscribed);
*subscribed = hashmap_free(*subscribed);
}
static void on_qmp_disconnect(QmpClient *client, void *userdata) {
VmspawnVarlinkContext *ctx = ASSERT_PTR(userdata);
assert(client);
log_debug("Backend connection lost");
/* Propagate connection loss by closing all subscriber connections */
drain_event_subscribers(&ctx->subscribed);
}
int vmspawn_varlink_setup(
VmspawnVarlinkContext **ret,
VmspawnQmpBridge *bridge,
const char *runtime_dir,
char **ret_control_address) {
_cleanup_(vmspawn_varlink_context_freep) VmspawnVarlinkContext *ctx = NULL;
_cleanup_free_ char *listen_address = NULL;
int r;
assert(ret);
assert(bridge);
assert(runtime_dir);
sd_event *event = qmp_client_get_event(bridge->qmp);
assert(event);
ctx = new0(VmspawnVarlinkContext, 1);
if (!ctx)
return log_oom();
/* Create varlink server for VM control */
r = varlink_server_new(&ctx->varlink_server,
SD_VARLINK_SERVER_INHERIT_USERDATA,
ctx);
if (r < 0)
return log_error_errno(r, "Failed to create varlink server: %m");
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);
if (r < 0)
return log_error_errno(r, "Failed to add varlink interfaces: %m");
r = sd_varlink_server_bind_method_many(
ctx->varlink_server,
"io.systemd.MachineInstance.Terminate", vl_method_terminate,
"io.systemd.MachineInstance.PowerOff", vl_method_power_off,
"io.systemd.MachineInstance.Pause", vl_method_pause,
"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);
if (r < 0)
return log_error_errno(r, "Failed to bind varlink methods: %m");
r = sd_varlink_server_bind_disconnect(ctx->varlink_server, vl_disconnect);
if (r < 0)
return log_error_errno(r, "Failed to bind disconnect handler: %m");
listen_address = path_join(runtime_dir, "control");
if (!listen_address)
return log_oom();
r = sd_varlink_server_listen_address(ctx->varlink_server, listen_address, 0600);
if (r < 0)
return log_error_errno(r, "Failed to listen on %s: %m", listen_address);
r = sd_varlink_server_attach_event(ctx->varlink_server, event, SD_EVENT_PRIORITY_NORMAL);
if (r < 0)
return log_error_errno(r, "Failed to attach varlink server to event loop: %m");
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);
log_debug("Varlink control server listening on %s", listen_address);
if (ret_control_address)
*ret_control_address = TAKE_PTR(listen_address);
*ret = TAKE_PTR(ctx);
return 0;
}
VmspawnVarlinkContext* vmspawn_varlink_context_free(VmspawnVarlinkContext *ctx) {
if (!ctx)
return NULL;
sd_varlink_server_unref(ctx->varlink_server);
vmspawn_qmp_bridge_free(ctx->bridge);
drain_event_subscribers(&ctx->subscribed);
return mfree(ctx);
}
+19
View File
@@ -0,0 +1,19 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "cleanup-util.h"
#include "shared-forward.h"
#include "vmspawn-qmp.h"
typedef struct VmspawnVarlinkContext VmspawnVarlinkContext;
/* Varlink server for VM control on top of an established bridge connection */
int vmspawn_varlink_setup(
VmspawnVarlinkContext **ret,
VmspawnQmpBridge *bridge,
const char *runtime_dir,
char **ret_control_address);
VmspawnVarlinkContext* vmspawn_varlink_context_free(VmspawnVarlinkContext *ctx);
DEFINE_TRIVIAL_CLEANUP_FUNC(VmspawnVarlinkContext *, vmspawn_varlink_context_free);
+444 -271
View File
@@ -53,6 +53,7 @@
#include "machine-credential.h"
#include "machine-register.h"
#include "main-func.h"
#include "memfd-util.h"
#include "mkdir.h"
#include "namespace-util.h"
#include "netif-util.h"
@@ -91,14 +92,20 @@
#include "utf8.h"
#include "vmspawn-mount.h"
#include "vmspawn-qemu-config.h"
#include "vmspawn-qmp.h"
#include "vmspawn-scope.h"
#include "vmspawn-settings.h"
#include "vmspawn-util.h"
#include "vmspawn-varlink.h"
#define VM_TAP_HASH_KEY SD_ID128_MAKE(01,d0,c6,4c,2b,df,24,fb,c0,f8,b2,09,7d,59,b2,93)
#define DISK_SERIAL_MAX_LEN_SCSI 30
#define DISK_SERIAL_MAX_LEN_NVME 20
#define DISK_SERIAL_MAX_LEN_SCSI 30
#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. */
@@ -2288,6 +2295,264 @@ static int cmdline_add_ovmf(FILE *config_file, const OvmfConfig *ovmf_config, ch
return 0;
}
/* Create a QMP control socketpair, add QEMU's end to pass_fds, and write the chardev + monitor
* config sections. Returns with bridge_fds populated: [0] is vmspawn's end, [1] is QEMU's end
* (also in pass_fds). FORK_CLOEXEC_OFF clears CLOEXEC on pass_fds in the child. */
static int qemu_config_add_qmp_monitor(FILE *config_file, int bridge_fds[2], int **pass_fds, size_t *n_pass_fds) {
int r;
assert(config_file);
assert(bridge_fds);
assert(pass_fds);
assert(n_pass_fds);
if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, bridge_fds) < 0)
return log_error_errno(errno, "Failed to create QMP socketpair: %m");
if (!GREEDY_REALLOC(*pass_fds, *n_pass_fds + 1))
return log_oom();
(*pass_fds)[(*n_pass_fds)++] = bridge_fds[1];
r = qemu_config_section(config_file, "chardev", "qmp",
"backend", "socket");
if (r < 0)
return r;
r = qemu_config_keyf(config_file, "fd", "%d", bridge_fds[1]);
if (r < 0)
return r;
return qemu_config_section(config_file, "mon", "qmp",
"chardev", "qmp",
"mode", "control");
}
static int resolve_disk_driver(DiskType dt, const char *filename, DriveInfo *info) {
int r;
assert(filename);
assert(info);
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;
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;
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;
break;
case DISK_TYPE_VIRTIO_SCSI_CDROM:
info->disk_driver = "scsi-cd";
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();
}
return 0;
}
static int prepare_primary_drive(const char *runtime_dir, DriveInfos *drives) {
int r;
assert(runtime_dir);
assert(drives);
if (!arg_image)
return 0;
_cleanup_free_ char *image_fn = NULL;
r = path_extract_filename(arg_image, &image_fn);
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 };
r = resolve_disk_driver(arg_image_disk_type, image_fn, d);
if (r < 0)
return log_error_errno(r, "Failed to resolve disk driver for '%s': %m", image_fn);
int open_flags = ((arg_ephemeral || FLAGS_SET(d->flags, QMP_DRIVE_READ_ONLY)) ? O_RDONLY : O_RDWR) | O_CLOEXEC | O_NOCTTY;
_cleanup_close_ int image_fd = open(arg_image, open_flags);
if (image_fd < 0)
return log_error_errno(errno, "Failed to open '%s': %m", arg_image);
struct stat st;
if (fstat(image_fd, &st) < 0)
return log_error_errno(errno, "Failed to stat '%s': %m", arg_image);
r = stat_verify_regular_or_block(&st);
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)
return log_oom();
d->fd = TAKE_FD(image_fd);
if (S_ISBLK(st.st_mode))
d->flags |= QMP_DRIVE_BLOCK_DEVICE;
if (arg_discard_disk && !FLAGS_SET(d->flags, QMP_DRIVE_READ_ONLY))
d->flags |= QMP_DRIVE_DISCARD;
d->flags |= QMP_DRIVE_BOOT;
/* For ephemeral mode, create an anonymous overlay file. QEMU will format it
* as qcow2 via blockdev-create, so no filesystem path is needed.
* Skip for read-only drives (e.g. CDROM) where overlays are not meaningful. */
if (arg_ephemeral && !FLAGS_SET(d->flags, QMP_DRIVE_READ_ONLY)) {
_cleanup_close_ int overlay_fd = open(runtime_dir, O_TMPFILE | O_RDWR | O_CLOEXEC, 0600);
if (overlay_fd < 0) {
if (!ERRNO_IS_NOT_SUPPORTED(errno))
return log_error_errno(errno, "Failed to create ephemeral overlay in '%s': %m", runtime_dir);
/* Fallback to memfd if O_TMPFILE is not supported */
overlay_fd = memfd_new("vmspawn-overlay");
if (overlay_fd < 0)
return log_error_errno(overlay_fd, "Failed to create ephemeral overlay via memfd: %m");
}
d->overlay_fd = TAKE_FD(overlay_fd);
d->flags |= QMP_DRIVE_NO_FLUSH;
}
return 0;
}
static int prepare_extra_drives(DriveInfos *drives) {
int r;
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);
if (r < 0)
return log_error_errno(r, "Failed to extract filename from path '%s': %m", drive->path);
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 };
r = resolve_disk_driver(dt, drive_fn, d);
if (r < 0)
return log_error_errno(r, "Failed to resolve disk driver for '%s': %m", drive_fn);
_cleanup_close_ int drive_fd = open(drive->path, (FLAGS_SET(d->flags, QMP_DRIVE_READ_ONLY) ? O_RDONLY : O_RDWR) | O_CLOEXEC | O_NOCTTY);
if (drive_fd < 0)
return log_error_errno(errno, "Failed to open '%s': %m", drive->path);
struct stat drive_st;
if (fstat(drive_fd, &drive_st) < 0)
return log_error_errno(errno, "Failed to stat '%s': %m", drive->path);
r = stat_verify_regular_or_block(&drive_st);
if (r < 0)
return log_error_errno(r, "Expected regular file or block device, not '%s'.", drive->path);
if (S_ISBLK(drive_st.st_mode) && drive->format == IMAGE_FORMAT_QCOW2)
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"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->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();
}
return 0;
}
/* Assign PCIe root port names to devices. The ports were pre-allocated in the config
* file. Each PCI device that will be hotplugged via QMP device_add gets a port. */
static int assign_pcie_ports(MachineConfig *c) {
assert(c);
if (!ARCHITECTURE_NEEDS_PCIE_ROOT_PORTS)
return 0;
DriveInfos *drives = &c->drives;
NetworkInfo *network = &c->network;
VirtiofsInfos *virtiofs = &c->virtiofs;
VsockInfo *vsock = &c->vsock;
size_t port = 0;
/* Drives: non-SCSI drives get individual ports, SCSI controller gets one port */
bool need_scsi = false;
FOREACH_ARRAY(d, drives->drives, drives->n_drives) {
if (STR_IN_SET(d->disk_driver, "scsi-hd", "scsi-cd")) {
need_scsi = true;
continue;
}
if (asprintf(&d->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)
return log_oom();
FOREACH_ARRAY(v, virtiofs->entries, virtiofs->n_entries)
if (asprintf(&v->pcie_port, "vmspawn-pcieport-%zu", port++) < 0)
return log_oom();
if (vsock->fd >= 0)
if (asprintf(&vsock->pcie_port, "vmspawn-pcieport-%zu", port++) < 0)
return log_oom();
return 0;
}
static int prepare_device_info(const char *runtime_dir, MachineConfig *c) {
int r;
assert(runtime_dir);
assert(c);
DriveInfos *drives = &c->drives;
/* 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);
if (!drives->drives)
return log_oom();
r = prepare_primary_drive(runtime_dir, drives);
if (r < 0)
return r;
r = prepare_extra_drives(drives);
if (r < 0)
return r;
return assign_pcie_ports(c);
}
static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
_cleanup_(ovmf_config_freep) OvmfConfig *ovmf_config = NULL;
_cleanup_free_ char *qemu_binary = NULL, *mem = NULL;
@@ -2297,6 +2562,10 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
_cleanup_close_ int notify_sock_fd = -EBADF;
_cleanup_strv_free_ char **cmdline = NULL;
_cleanup_free_ int *pass_fds = NULL;
_cleanup_(machine_config_done) MachineConfig config = {
.network = { .fd = -EBADF },
.vsock = { .fd = -EBADF },
};
sd_event_source **children = NULL;
size_t n_children = 0, n_pass_fds = 0;
int r;
@@ -2386,8 +2655,8 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
if (asprintf(&mem, "%" PRIu64 "M", DIV_ROUND_UP(arg_ram, U64_MB)) < 0)
return log_oom();
/* Create our runtime directory. We need this for the QEMU config file, TPM state, virtiofsd
* sockets, runtime mounts, and SSH key material.
/* Create our runtime directory. We need this for the QMP varlink control socket, the QEMU
* config file, TPM state, virtiofsd sockets, runtime mounts, and SSH key material.
*
* Use runtime_directory() (not _generic()) so that when vmspawn runs in a systemd service
* with RuntimeDirectory= set, we pick up $RUNTIME_DIRECTORY and place our stuff into the
@@ -2522,8 +2791,10 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
return r;
}
/* Start building the cmdline for items that must remain as command line arguments */
/* Start building the cmdline for items that must remain as command line arguments.
* -S starts QEMU with vCPUs paused — we set up devices via QMP then resume with "cont". */
cmdline = strv_new(qemu_binary,
"-S",
"-no-user-config");
if (!cmdline)
return log_oom();
@@ -2533,8 +2804,14 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
return log_oom();
_cleanup_close_ int delegate_userns_fd = -EBADF, tap_fd = -EBADF;
_cleanup_free_ char *tap_name = NULL;
struct ether_addr mac_vm = {};
if (arg_network_stack == NETWORK_STACK_TAP) {
if (have_effective_cap(CAP_NET_ADMIN) <= 0) {
/* Without CAP_NET_ADMIN we use nsresourced to create a TAP device.
* The TAP fd is passed to QEMU via QMP getfd + SCM_RIGHTS after
* the handshake, then referenced by name in netdev_add. */
delegate_userns_fd = userns_acquire_self_root();
if (delegate_userns_fd < 0)
return log_error_errno(delegate_userns_fd, "Failed to acquire userns: %m");
@@ -2556,65 +2833,39 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
if (tap_fd < 0)
return log_error_errno(tap_fd, "Failed to allocate network tap device: %m");
r = strv_extend(&cmdline, "-netdev");
if (r < 0)
return log_oom();
r = strv_extendf(&cmdline, "tap,id=net0,fd=%i", tap_fd);
if (r < 0)
return log_oom();
r = strv_extend_many(&cmdline, "-device", "virtio-net-pci,netdev=net0");
if (r < 0)
return log_oom();
if (!GREEDY_REALLOC(pass_fds, n_pass_fds + 1))
return log_oom();
pass_fds[n_pass_fds++] = tap_fd;
config.network = (NetworkInfo) {
.type = "tap",
.fd = TAKE_FD(tap_fd),
};
} else {
_cleanup_free_ char *tap_name = NULL;
struct ether_addr mac_vm = {};
/* With CAP_NET_ADMIN we create the TAP interface by name.
* Configure via QMP after QEMU starts. */
tap_name = strjoin("vt-", arg_machine);
if (!tap_name)
return log_oom();
(void) net_shorten_ifname(tap_name, /* check_naming_scheme= */ false);
if (ether_addr_is_null(&arg_network_provided_mac)){
if (ether_addr_is_null(&arg_network_provided_mac)) {
r = net_generate_mac(arg_machine, &mac_vm, VM_TAP_HASH_KEY, 0);
if (r < 0)
return log_error_errno(r, "Failed to generate predictable MAC address for VM side: %m");
} else
mac_vm = arg_network_provided_mac;
r = qemu_config_section(config_file, "netdev", "net0",
"type", "tap",
"ifname", tap_name,
"script", "no",
"downscript", "no");
if (r < 0)
return r;
r = qemu_config_section(config_file, "device", "nic0",
"driver", "virtio-net-pci",
"netdev", "net0",
"mac", ETHER_ADDR_TO_STR(&mac_vm));
if (r < 0)
return r;
config.network = (NetworkInfo) {
.type = "tap",
.ifname = TAKE_PTR(tap_name),
.mac = mac_vm,
.mac_set = true,
.fd = -EBADF,
};
}
} else if (arg_network_stack == NETWORK_STACK_USER) {
r = qemu_config_section(config_file, "netdev", "net0",
"type", "user");
if (r < 0)
return r;
r = qemu_config_section(config_file, "device", "nic0",
"driver", "virtio-net-pci",
"netdev", "net0");
if (r < 0)
return r;
config.network = (NetworkInfo) {
.type = "user",
.fd = -EBADF,
};
} else {
r = strv_extend_many(&cmdline, "-nic", "none");
if (r < 0)
@@ -2671,40 +2922,21 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
return r;
}
_cleanup_close_ int child_vsock_fd = -EBADF;
unsigned child_cid = arg_vsock_cid;
if (use_vsock) {
int device_fd = vhost_device_fd;
config.vsock.fd = TAKE_FD(vhost_device_fd);
if (device_fd < 0) {
child_vsock_fd = open("/dev/vhost-vsock", O_RDWR|O_CLOEXEC);
if (child_vsock_fd < 0)
if (config.vsock.fd < 0) {
config.vsock.fd = open("/dev/vhost-vsock", O_RDWR|O_CLOEXEC);
if (config.vsock.fd < 0)
return log_error_errno(errno, "Failed to open /dev/vhost-vsock as read/write: %m");
device_fd = child_vsock_fd;
}
r = vsock_fix_child_cid(device_fd, &child_cid, arg_machine);
r = vsock_fix_child_cid(config.vsock.fd, &child_cid, arg_machine);
if (r < 0)
return log_error_errno(r, "Failed to fix CID for the guest VSOCK socket: %m");
r = qemu_config_section(config_file, "device", "vsock0",
"driver", "vhost-vsock-pci");
if (r < 0)
return r;
r = qemu_config_keyf(config_file, "guest-cid", "%u", child_cid);
if (r < 0)
return r;
r = qemu_config_keyf(config_file, "vhostfd", "%d", device_fd);
if (r < 0)
return r;
if (!GREEDY_REALLOC(pass_fds, n_pass_fds + 1))
return log_oom();
pass_fds[n_pass_fds++] = device_fd;
config.vsock.cid = child_cid;
}
/* -cpu stays on cmdline since not all flags are supported in config */
@@ -2846,24 +3078,6 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
}
}
bool need_scsi_controller =
IN_SET(arg_image_disk_type, DISK_TYPE_VIRTIO_SCSI, DISK_TYPE_VIRTIO_SCSI_CDROM) && arg_image;
if (!need_scsi_controller)
FOREACH_ARRAY(drive, arg_extra_drives.drives, arg_extra_drives.n_drives) {
DiskType dt = drive->disk_type >= 0 ? drive->disk_type : arg_image_disk_type;
if (IN_SET(dt, DISK_TYPE_VIRTIO_SCSI, DISK_TYPE_VIRTIO_SCSI_CDROM)) {
need_scsi_controller = true;
break;
}
}
if (need_scsi_controller) {
r = qemu_config_section(config_file, "device", "vmspawn_scsi",
"driver", "virtio-scsi-pci");
if (r < 0)
return r;
}
if (arg_image) {
assert(!arg_directory);
@@ -2875,74 +3089,6 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
arg_image);
}
if (arg_image_disk_type == DISK_TYPE_VIRTIO_SCSI_CDROM)
r = qemu_config_section(config_file, "drive", "vmspawn",
"if", "none",
"file", arg_image,
"format", image_format_to_string(arg_image_format),
"media", "cdrom",
"readonly", "on");
else
r = qemu_config_section(config_file, "drive", "vmspawn",
"if", "none",
"file", arg_image,
"format", image_format_to_string(arg_image_format),
"discard", on_off(arg_discard_disk),
"snapshot", on_off(arg_ephemeral));
if (r < 0)
return r;
_cleanup_free_ char *image_fn = NULL;
r = path_extract_filename(arg_image, &image_fn);
if (r < 0)
return log_error_errno(r, "Failed to extract filename from path '%s': %m", image_fn);
const char *disk_driver;
_cleanup_free_ char *serial = NULL;
switch (arg_image_disk_type) {
case DISK_TYPE_VIRTIO_BLK:
disk_driver = "virtio-blk-pci";
serial = strdup(image_fn);
if (!serial)
return log_oom();
break;
case DISK_TYPE_VIRTIO_SCSI:
disk_driver = "scsi-hd";
r = disk_serial(image_fn, DISK_SERIAL_MAX_LEN_SCSI, &serial);
if (r < 0)
return log_oom();
break;
case DISK_TYPE_NVME:
disk_driver = "nvme";
r = disk_serial(image_fn, DISK_SERIAL_MAX_LEN_NVME, &serial);
if (r < 0)
return log_oom();
break;
case DISK_TYPE_VIRTIO_SCSI_CDROM:
disk_driver = "scsi-cd";
r = disk_serial(image_fn, DISK_SERIAL_MAX_LEN_SCSI, &serial);
if (r < 0)
return log_oom();
break;
default:
assert_not_reached();
}
r = qemu_config_section(config_file, "device", "vmspawn-disk",
"driver", disk_driver,
"drive", "vmspawn",
"bootindex", "1",
"serial", serial);
if (r < 0)
return r;
if (IN_SET(arg_image_disk_type, DISK_TYPE_VIRTIO_SCSI, DISK_TYPE_VIRTIO_SCSI_CDROM)) {
r = qemu_config_key(config_file, "bus", "vmspawn_scsi.0");
if (r < 0)
return r;
}
if (arg_image_disk_type != DISK_TYPE_VIRTIO_SCSI_CDROM) {
r = grow_image(arg_image, arg_grow_image);
if (r < 0)
@@ -3012,104 +3158,25 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
pidref_done(&child);
children[n_children++] = TAKE_PTR(source);
r = qemu_config_section(config_file, "chardev", "rootdir",
"backend", "socket",
"path", listen_address);
if (r < 0)
return r;
_cleanup_free_ char *id = strdup("rootdir"), *tag = strdup("root");
if (!id || !tag)
return log_oom();
r = qemu_config_section(config_file, "device", "rootdir",
"driver", "vhost-user-fs-pci",
"queue-size", "1024",
"chardev", "rootdir",
"tag", "root");
if (r < 0)
return r;
if (!GREEDY_REALLOC(config.virtiofs.entries, config.virtiofs.n_entries + 1))
return log_oom();
config.virtiofs.entries[config.virtiofs.n_entries++] = (VirtiofsInfo) {
.id = TAKE_PTR(id),
.socket_path = TAKE_PTR(listen_address),
.tag = TAKE_PTR(tag),
};
if (strv_extend(&arg_kernel_cmdline_extra, "root=root rootfstype=virtiofs rw") < 0)
return log_oom();
}
size_t i = 0;
FOREACH_ARRAY(drive, arg_extra_drives.drives, arg_extra_drives.n_drives) {
if (strv_extend(&cmdline, "-blockdev") < 0)
return log_oom();
_cleanup_free_ char *escaped_drive = escape_qemu_value(drive->path);
if (!escaped_drive)
return log_oom();
struct stat st;
if (stat(drive->path, &st) < 0)
return log_error_errno(errno, "Failed to stat '%s': %m", drive->path);
const char *driver = NULL;
if (S_ISREG(st.st_mode))
driver = "file";
else if (S_ISBLK(st.st_mode)) {
if (drive->format == IMAGE_FORMAT_QCOW2)
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"Block device '%s' cannot be used with 'qcow2' format, only 'raw' is supported.",
drive->path);
driver = "host_device";
} else
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected regular file or block device, not '%s'.", drive->path);
DiskType dt = drive->disk_type >= 0 ? drive->disk_type : arg_image_disk_type;
if (strv_extendf(&cmdline, "driver=%s,cache.direct=off,cache.no-flush=on,file.driver=%s,file.filename=%s,node-name=vmspawn_extra_%zu%s",
image_format_to_string(drive->format), driver, escaped_drive, i,
dt == DISK_TYPE_VIRTIO_SCSI_CDROM ? ",read-only=on" : "") < 0)
return log_oom();
_cleanup_free_ char *drive_fn = NULL;
r = path_extract_filename(drive->path, &drive_fn);
if (r < 0)
return log_error_errno(r, "Failed to extract filename from path '%s': %m", drive->path);
_cleanup_free_ char *escaped_drive_fn = escape_qemu_value(drive_fn);
if (!escaped_drive_fn)
return log_oom();
if (strv_extend(&cmdline, "-device") < 0)
return log_oom();
switch (dt) {
case DISK_TYPE_VIRTIO_BLK:
if (strv_extendf(&cmdline, "virtio-blk-pci,drive=vmspawn_extra_%zu,serial=%s", i++, escaped_drive_fn) < 0)
return log_oom();
break;
case DISK_TYPE_VIRTIO_SCSI: {
_cleanup_free_ char *serial = NULL;
r = disk_serial(escaped_drive_fn, DISK_SERIAL_MAX_LEN_SCSI, &serial);
if (r < 0)
return log_oom();
if (strv_extendf(&cmdline, "scsi-hd,bus=vmspawn_scsi.0,drive=vmspawn_extra_%zu,serial=%s", i++, serial) < 0)
return log_oom();
break;
}
case DISK_TYPE_NVME: {
_cleanup_free_ char *serial = NULL;
r = disk_serial(escaped_drive_fn, DISK_SERIAL_MAX_LEN_NVME, &serial);
if (r < 0)
return log_oom();
if (strv_extendf(&cmdline, "nvme,drive=vmspawn_extra_%zu,serial=%s", i++, serial) < 0)
return log_oom();
break;
}
case DISK_TYPE_VIRTIO_SCSI_CDROM: {
_cleanup_free_ char *serial = NULL;
r = disk_serial(escaped_drive_fn, DISK_SERIAL_MAX_LEN_SCSI, &serial);
if (r < 0)
return log_oom();
if (strv_extendf(&cmdline, "scsi-cd,bus=vmspawn_scsi.0,drive=vmspawn_extra_%zu,serial=%s", i++, serial) < 0)
return log_oom();
break;
}
default:
assert_not_reached();
}
}
/* Extra drive validation is done in the post-fork drive info construction loop
* to avoid stat()'ing each drive twice. */
if (!IN_SET(arg_console_mode, CONSOLE_GUI, CONSOLE_HEADLESS)) {
r = strv_prepend(&arg_kernel_cmdline_extra,
@@ -3141,7 +3208,7 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
for (size_t j = 0; j < arg_runtime_mounts.n_mounts; j++) {
RuntimeMount *m = arg_runtime_mounts.mounts + j;
_cleanup_free_ char *listen_address = NULL;
_cleanup_free_ char *listen_address = NULL, *id = NULL, *tag = NULL;
_cleanup_(fork_notify_terminate) PidRef child = PIDREF_NULL;
if (!GREEDY_REALLOC(children, n_children + 1))
@@ -3167,23 +3234,12 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
pidref_done(&child);
children[n_children++] = TAKE_PTR(source);
_cleanup_free_ char *id = NULL;
if (asprintf(&id, "mnt%zu", j) < 0)
return log_oom();
r = qemu_config_section(config_file, "chardev", id,
"backend", "socket",
"path", listen_address);
if (r < 0)
return r;
r = qemu_config_section(config_file, "device", id,
"driver", "vhost-user-fs-pci",
"queue-size", "1024",
"chardev", id,
"tag", id);
if (r < 0)
return r;
tag = strdup(id);
if (!tag)
return log_oom();
/* fstab uses whitespace as field separator, so octal-escape spaces in paths */
_cleanup_free_ char *escaped_target = octescape_full(m->target, SIZE_MAX, " \t");
@@ -3193,6 +3249,15 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
if (strextendf(&fstab_extra, "%s %s virtiofs %s,x-initrd.mount\n",
id, escaped_target, m->read_only ? "ro" : "rw") < 0)
return log_oom();
if (!GREEDY_REALLOC(config.virtiofs.entries, config.virtiofs.n_entries + 1))
return log_oom();
config.virtiofs.entries[config.virtiofs.n_entries++] = (VirtiofsInfo) {
.id = TAKE_PTR(id),
.socket_path = TAKE_PTR(listen_address),
.tag = TAKE_PTR(tag),
};
}
if (fstab_extra) {
@@ -3443,6 +3508,63 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
if (r < 0)
return r;
_cleanup_close_pair_ int bridge_fds[2] = EBADF_PAIR;
r = qemu_config_add_qmp_monitor(config_file, bridge_fds, &pass_fds, &n_pass_fds);
if (r < 0)
return r;
/* Pre-allocate PCIe root ports for QMP device_add hotplug. On PCIe machine types
* (q35, virt), QMP device_add is always hotplug — the root bus (pcie.0) does not support
* it. Each root port provides one slot for hotplug. We create enough ports for all devices
* that will be set up via QMP, plus VMSPAWN_PCIE_HOTPLUG_SPARES spare ports for future
* runtime hotplug. */
if (ARCHITECTURE_NEEDS_PCIE_ROOT_PORTS) {
/* Count maximum possible PCI devices: root image + extra drives + SCSI controller +
* network + virtiofs mounts + vsock. The actual count may be lower (e.g. no network,
* no SCSI), but unused ports have negligible overhead. */
size_t n_pcie_ports = 1 +
arg_extra_drives.n_drives + /* drives */
1 + /* SCSI controller */
1 + /* network */
(arg_directory ? 1 : 0) + /* rootdir virtiofs */
arg_runtime_mounts.n_mounts + /* extra virtiofs mounts */
1 + /* vsock */
VMSPAWN_PCIE_HOTPLUG_SPARES; /* reserved for future hotplug */
/* Guard the unsigned subtraction below against future refactors that might drop the
* fixed additions. */
assert(n_pcie_ports >= VMSPAWN_PCIE_HOTPLUG_SPARES);
/* QEMU's pcie-root-port chassis/slot are uint8_t — i+1 must fit. */
if (n_pcie_ports > UINT8_MAX)
return log_error_errno(SYNTHETIC_ERRNO(E2BIG),
"Too many PCIe root ports requested (%zu, max 255). "
"Reduce the number of extra drives or runtime mounts.",
n_pcie_ports);
size_t n_builtin_ports = n_pcie_ports - VMSPAWN_PCIE_HOTPLUG_SPARES;
for (size_t i = 0; i < n_pcie_ports; i++) {
char id[STRLEN("vmspawn-hotplug-pci-root-port-") + DECIMAL_STR_MAX(size_t)];
if (i < n_builtin_ports)
xsprintf(id, "vmspawn-pcieport-%zu", i);
else
xsprintf(id, "vmspawn-hotplug-pci-root-port-%zu", i - n_builtin_ports);
r = qemu_config_section(config_file, "device", id,
"driver", "pcie-root-port");
if (r < 0)
return r;
r = qemu_config_keyf(config_file, "chassis", "%zu", i + 1);
if (r < 0)
return r;
r = qemu_config_keyf(config_file, "slot", "%zu", i + 1);
if (r < 0)
return r;
}
}
/* Finalize the config file and add -readconfig to the cmdline */
r = fflush_and_check(config_file);
if (r < 0)
@@ -3485,7 +3607,8 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
return log_error_errno(child_pty, "Failed to open PTY slave: %m");
}
_cleanup_(pidref_done) PidRef child_pidref = PIDREF_NULL;
/* SIGTERM, not SIGKILL — let QEMU flush state on error-path early exits. */
_cleanup_(pidref_done_sigterm_wait) PidRef child_pidref = PIDREF_NULL;
r = pidref_safe_fork_full(
qemu_binary,
child_pty >= 0 ? (const int[]) { child_pty, child_pty, child_pty } : NULL,
@@ -3507,10 +3630,59 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
_exit(EXIT_FAILURE);
}
/* Close relevant fds we passed to qemu in the parent. We don't need them anymore. */
/* Close QEMU's end of the QMP socketpair in the parent. We don't need it anymore. */
child_pty = safe_close(child_pty);
child_vsock_fd = safe_close(child_vsock_fd);
tap_fd = safe_close(tap_fd);
bridge_fds[1] = safe_close(bridge_fds[1]);
r = prepare_device_info(runtime_dir, &config);
if (r < 0)
return r;
/* Connect to VMM backend */
_cleanup_(vmspawn_qmp_bridge_freep) VmspawnQmpBridge *bridge = NULL;
r = vmspawn_qmp_init(&bridge, bridge_fds[0], event);
if (r < 0)
return r;
TAKE_FD(bridge_fds[0]);
/* Probe QEMU feature availability synchronously before device setup consumes the flags. */
r = vmspawn_qmp_probe_features(bridge);
if (r < 0)
return r;
/* Device setup — all before resuming vCPUs */
r = vmspawn_qmp_setup_drives(bridge, &config.drives);
if (r < 0)
return r;
if (config.network.type) {
r = vmspawn_qmp_setup_network(bridge, &config.network);
if (r < 0)
return r;
}
r = vmspawn_qmp_setup_virtiofs(bridge, &config.virtiofs);
if (r < 0)
return r;
r = vmspawn_qmp_setup_vsock(bridge, &config.vsock);
if (r < 0)
return r;
/* Resume vCPUs and switch to async event processing */
r = vmspawn_qmp_start(bridge);
if (r < 0)
return r;
/* Varlink server for VM control */
_cleanup_(vmspawn_varlink_context_freep) VmspawnVarlinkContext *varlink_ctx = NULL;
_cleanup_free_ char *control_address = NULL;
r = vmspawn_varlink_setup(&varlink_ctx, bridge, runtime_dir, &control_address);
if (r < 0)
return r;
TAKE_PTR(bridge);
if (!arg_keep_unit) {
/* When a new scope is created for this container, then we'll be registered as its controller, in which
@@ -3575,6 +3747,7 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
.vsock_cid = child_cid,
.ssh_address = child_cid != VMADDR_CID_ANY ? vm_address : NULL,
.ssh_private_key_path = ssh_private_key_path,
.control_address = control_address,
.allocate_unit = !arg_keep_unit,
};
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
# Test vmspawn QMP-based multi-drive setup and ephemeral overlay.
#
# Exercises the async QMP command pipeline with multiple drives:
# - Multiple fdset allocations (counter correctness)
# - Pipelined blockdev-add commands (FIFO ordering)
# - io_uring retry callbacks (if QEMU lacks io_uring support)
# - Multiple device_add commands
# - blockdev-create job watching with deferred continuation (ephemeral)
set -eux
set -o pipefail
# shellcheck source=test/units/util.sh
. "$(dirname "$0")"/util.sh
if [[ -v ASAN_OPTIONS ]]; then
echo "vmspawn launches QEMU which doesn't work under ASan, skipping"
exit 0
fi
if ! command -v systemd-vmspawn >/dev/null 2>&1; then
echo "systemd-vmspawn not found, skipping"
exit 0
fi
if ! find_qemu_binary; then
echo "QEMU not found, skipping"
exit 0
fi
if ! command -v mke2fs >/dev/null 2>&1; then
echo "mke2fs not found, skipping"
exit 0
fi
# Find a kernel for direct boot
KERNEL=""
for k in /usr/lib/modules/"$(uname -r)"/vmlinuz /boot/vmlinuz-"$(uname -r)" /boot/vmlinuz; do
if [[ -f "$k" ]]; then
KERNEL="$k"
break
fi
done
if [[ -z "$KERNEL" ]]; then
echo "No kernel found for direct VM boot, skipping"
exit 0
fi
echo "Using kernel: $KERNEL"
WORKDIR="$(mktemp -d)"
at_exit() {
set +e
for m in "${MACHINE_MULTI:-}" "${MACHINE_EPHEMERAL:-}"; do
[[ -n "$m" ]] || continue
if machinectl status "$m" &>/dev/null; then
machinectl terminate "$m" 2>/dev/null
timeout 10 bash -c "while machinectl status '$m' &>/dev/null; do sleep .5; done" 2>/dev/null
fi
done
[[ -n "${VMSPAWN_MULTI_PID:-}" ]] && kill "$VMSPAWN_MULTI_PID" 2>/dev/null && wait "$VMSPAWN_MULTI_PID" 2>/dev/null
[[ -n "${VMSPAWN_EPHEMERAL_PID:-}" ]] && kill "$VMSPAWN_EPHEMERAL_PID" 2>/dev/null && wait "$VMSPAWN_EPHEMERAL_PID" 2>/dev/null
rm -rf "$WORKDIR"
}
trap at_exit EXIT
# Create a minimal root filesystem directory, then bake it into a raw ext4 image.
# The guest doesn't need to fully boot — 'sleep infinity' keeps QEMU alive for QMP testing.
mkdir -p "$WORKDIR/rootfs/sbin"
cat >"$WORKDIR/rootfs/sbin/init" <<'INITEOF'
#!/bin/sh
exec sleep infinity
INITEOF
chmod +x "$WORKDIR/rootfs/sbin/init"
truncate -s 256M "$WORKDIR/root.raw"
mke2fs -t ext4 -q -d "$WORKDIR/rootfs" "$WORKDIR/root.raw"
# Create extra raw drive images (different sizes to be distinguishable)
truncate -s 64M "$WORKDIR/extra1.raw"
truncate -s 32M "$WORKDIR/extra2.raw"
wait_for_machine() {
local machine="$1" pid="$2" log="$3"
timeout 30 bash -c "
while ! machinectl list --no-legend 2>/dev/null | grep >/dev/null '$machine'; do
if ! kill -0 $pid 2>/dev/null; then
echo 'vmspawn exited before machine registration'
cat '$log'
exit 1
fi
sleep .5
done
"
}
# --- Test 1: Multi-drive setup (root + 2 extra drives) ---
# Verifies that --image with multiple --extra-drive flags works with the async
# QMP pipeline. Three drives means three fdset allocations, three blockdev-add
# file nodes (each with io_uring retry), three blockdev-add format nodes, and
# three device_add commands — all pipelined without waiting for responses.
MACHINE_MULTI="test-vmspawn-drives-$$"
systemd-vmspawn \
--machine="$MACHINE_MULTI" \
--ram=256M \
--image="$WORKDIR/root.raw" \
--extra-drive="$WORKDIR/extra1.raw" \
--extra-drive="$WORKDIR/extra2.raw" \
--linux="$KERNEL" \
--tpm=no \
--console=headless \
root=/dev/vda rw \
&>"$WORKDIR/vmspawn-multi.log" &
VMSPAWN_MULTI_PID=$!
wait_for_machine "$MACHINE_MULTI" "$VMSPAWN_MULTI_PID" "$WORKDIR/vmspawn-multi.log"
echo "Multi-drive machine '$MACHINE_MULTI' registered with machined"
# Verify varlink control address is present and the VM is running
VARLINK_ADDR=$(varlinkctl call /run/systemd/machine/io.systemd.Machine \
io.systemd.Machine.List "{\"name\":\"$MACHINE_MULTI\"}" | jq -r '.controlAddress')
assert_neq "$VARLINK_ADDR" "null"
STATUS=$(varlinkctl call "$VARLINK_ADDR" io.systemd.MachineInstance.Describe '{}')
echo "$STATUS" | jq -e '.running == true'
echo "Multi-drive VM running — async QMP drive pipeline succeeded"
# Verify no on_setup_complete failures in the vmspawn log
if grep -E '(add-fd|blockdev-add|blockdev-create|device_add|getfd|netdev_add|chardev-add) failed:' "$WORKDIR/vmspawn-multi.log"; then
echo "Full vmspawn log:"
cat "$WORKDIR/vmspawn-multi.log"
exit 1
fi
echo "No QMP device setup errors in log"
machinectl terminate "$MACHINE_MULTI"
timeout 10 bash -c "while machinectl status '$MACHINE_MULTI' &>/dev/null; do sleep .5; done"
timeout 10 bash -c "while kill -0 '$VMSPAWN_MULTI_PID' 2>/dev/null; do sleep .5; done"
echo "Multi-drive VM terminated cleanly"
# --- Test 2: Ephemeral overlay (blockdev-create job continuation) ---
# Verifies that --image with --ephemeral works. This is the most complex async
# path: blockdev-create returns immediately, the qcow2 overlay is formatted in a
# background job, JOB_STATUS_CHANGE events are watched, and when the job
# concludes the deferred continuation fires blockdev-add (overlay format) +
# device_add. If any step fails, the root drive is never attached and the kernel
# panics — vmspawn exits without registering.
MACHINE_EPHEMERAL="test-vmspawn-ephemeral-$$"
systemd-vmspawn \
--machine="$MACHINE_EPHEMERAL" \
--ram=256M \
--image="$WORKDIR/root.raw" \
--ephemeral \
--linux="$KERNEL" \
--tpm=no \
--console=headless \
root=/dev/vda rw \
&>"$WORKDIR/vmspawn-ephemeral.log" &
VMSPAWN_EPHEMERAL_PID=$!
wait_for_machine "$MACHINE_EPHEMERAL" "$VMSPAWN_EPHEMERAL_PID" "$WORKDIR/vmspawn-ephemeral.log"
echo "Ephemeral machine '$MACHINE_EPHEMERAL' registered with machined"
VARLINK_ADDR_E=$(varlinkctl call /run/systemd/machine/io.systemd.Machine \
io.systemd.Machine.List "{\"name\":\"$MACHINE_EPHEMERAL\"}" | jq -r '.controlAddress')
assert_neq "$VARLINK_ADDR_E" "null"
STATUS_E=$(varlinkctl call "$VARLINK_ADDR_E" io.systemd.MachineInstance.Describe '{}')
echo "$STATUS_E" | jq -e '.running == true'
echo "Ephemeral VM running — blockdev-create job continuation succeeded"
if grep -E '(add-fd|blockdev-add|blockdev-create|device_add|getfd|netdev_add|chardev-add) failed:' "$WORKDIR/vmspawn-ephemeral.log"; then
echo "Full vmspawn log:"
cat "$WORKDIR/vmspawn-ephemeral.log"
exit 1
fi
echo "No QMP device setup errors in ephemeral log"
machinectl terminate "$MACHINE_EPHEMERAL"
timeout 10 bash -c "while machinectl status '$MACHINE_EPHEMERAL' &>/dev/null; do sleep .5; done"
timeout 10 bash -c "while kill -0 '$VMSPAWN_EPHEMERAL_PID' 2>/dev/null; do sleep .5; done"
echo "Ephemeral VM terminated cleanly"
echo "All vmspawn drive setup tests passed"
+319
View File
@@ -0,0 +1,319 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
# Test vmspawn QMP-varlink bridge and machinectl VM control verbs.
set -eux
set -o pipefail
# shellcheck source=test/units/util.sh
. "$(dirname "$0")"/util.sh
if [[ -v ASAN_OPTIONS ]]; then
echo "vmspawn launches QEMU which doesn't work under ASan, skipping"
exit 0
fi
if ! command -v systemd-vmspawn >/dev/null 2>&1; then
echo "systemd-vmspawn not found, skipping"
exit 0
fi
if ! find_qemu_binary; then
echo "QEMU not found, skipping"
exit 0
fi
# --directory= needs virtiofsd (on Fedora it lives in /usr/libexec, not in PATH)
if ! command -v virtiofsd >/dev/null 2>&1 &&
! test -x /usr/libexec/virtiofsd &&
! test -x /usr/lib/virtiofsd; then
echo "virtiofsd not found, skipping"
exit 0
fi
# Find a kernel for direct boot
KERNEL=""
for k in /usr/lib/modules/"$(uname -r)"/vmlinuz /boot/vmlinuz-"$(uname -r)" /boot/vmlinuz; do
if [[ -f "$k" ]]; then
KERNEL="$k"
break
fi
done
if [[ -z "$KERNEL" ]]; then
echo "No kernel found for direct VM boot, skipping"
exit 0
fi
echo "Using kernel: $KERNEL"
MACHINE="test-vmspawn-qmp-$$"
WORKDIR="$(mktemp -d)"
at_exit() {
set +e
for m in "$MACHINE" "${MACHINE2:-}" "${STRESS_MACHINE:-}"; do
[[ -n "$m" ]] || continue
if machinectl status "$m" &>/dev/null; then
machinectl terminate "$m" 2>/dev/null
timeout 10 bash -c "while machinectl status '$m' &>/dev/null; do sleep .5; done" 2>/dev/null
fi
done
[[ -n "${SUBSCRIBE_ALL_PID:-}" ]] && kill "$SUBSCRIBE_ALL_PID" 2>/dev/null && wait "$SUBSCRIBE_ALL_PID" 2>/dev/null
[[ -n "${SUBSCRIBE_FILTER_PID:-}" ]] && kill "$SUBSCRIBE_FILTER_PID" 2>/dev/null && wait "$SUBSCRIBE_FILTER_PID" 2>/dev/null
[[ -n "${STRESS_PID:-}" ]] && kill "$STRESS_PID" 2>/dev/null && wait "$STRESS_PID" 2>/dev/null
[[ -n "${VMSPAWN_PID:-}" ]] && kill "$VMSPAWN_PID" 2>/dev/null && wait "$VMSPAWN_PID" 2>/dev/null
[[ -n "${VMSPAWN2_PID:-}" ]] && kill "$VMSPAWN2_PID" 2>/dev/null && wait "$VMSPAWN2_PID" 2>/dev/null
rm -rf "$WORKDIR"
}
trap at_exit EXIT
# Create a minimal root filesystem. The guest does not need to fully boot -- we only need QEMU running
# with QMP. A trivial init that sleeps is sufficient.
mkdir -p "$WORKDIR/root/sbin"
cat >"$WORKDIR/root/sbin/init" <<'EOF'
#!/bin/sh
exec sleep infinity
EOF
chmod +x "$WORKDIR/root/sbin/init"
# Wait for a vmspawn machine to register with machined.
# Skips the test gracefully if vmspawn fails due to missing vhost-user-fs support (nested VM).
wait_for_machine() {
local machine="$1" pid="$2" log="$3"
timeout 30 bash -c "
while ! machinectl list --no-legend 2>/dev/null | grep >/dev/null '$machine'; do
if ! kill -0 $pid 2>/dev/null; then
if grep >/dev/null 'virtiofs.*QMP\|vhost-user-fs-pci' '$log'; then
echo 'vhost-user-fs not supported (nested VM?), skipping'
exit 77
fi
echo 'vmspawn exited before registering'
cat '$log'
exit 1
fi
sleep .5
done
" || {
local rc=$?
if [[ $rc -eq 77 ]]; then exit 0; fi
exit "$rc"
}
}
# Launch vmspawn in the background with direct kernel boot and headless console.
systemd-vmspawn \
--machine="$MACHINE" \
--ram=256M \
--directory="$WORKDIR/root" \
--linux="$KERNEL" \
--tpm=no \
--console=headless \
&>"$WORKDIR/vmspawn.log" &
VMSPAWN_PID=$!
wait_for_machine "$MACHINE" "$VMSPAWN_PID" "$WORKDIR/vmspawn.log"
echo "Machine '$MACHINE' registered with machined"
# Verify that controlAddress is present in Machine.List output
varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.List "{\"name\":\"$MACHINE\"}" | grep >/dev/null controlAddress
echo "controlAddress exposed in Machine.List"
# Exercise the MachineInstance varlink interface directly via varlinkctl.
# Look up the varlink address from machined. Do this BEFORE machinectl poweroff since poweroff
# is destructive (either kills the machine via signal or sends ACPI shutdown).
VARLINK_ADDR=$(varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.List "{\"name\":\"$MACHINE\"}" | jq -r '.controlAddress')
assert_neq "$VARLINK_ADDR" "null"
# Describe should reflect a running VM
STATUS=$(varlinkctl call "$VARLINK_ADDR" io.systemd.MachineInstance.Describe '{}')
echo "$STATUS" | jq -e '.running == true'
echo "$STATUS" | jq -e '.status == "running"'
echo "Describe returned running state"
# Pause, verify, resume via varlinkctl
varlinkctl call "$VARLINK_ADDR" io.systemd.MachineInstance.Pause '{}'
STATUS=$(varlinkctl call "$VARLINK_ADDR" io.systemd.MachineInstance.Describe '{}')
echo "$STATUS" | jq -e '.running == false'
echo "Verified paused state via Describe"
varlinkctl call "$VARLINK_ADDR" io.systemd.MachineInstance.Resume '{}'
STATUS=$(varlinkctl call "$VARLINK_ADDR" io.systemd.MachineInstance.Describe '{}')
echo "$STATUS" | jq -e '.running == true'
echo "Verified resumed state via Describe"
# --- SubscribeEvents tests ---
# Subscribe to all events in the background, collect output
varlinkctl call --more --timeout=10 "$VARLINK_ADDR" io.systemd.MachineInstance.SubscribeEvents '{}' \
>"$WORKDIR/events-all.json" 2>&1 &
SUBSCRIBE_ALL_PID=$!
sleep 0.5
# Trigger STOP + RESUME events via pause/resume
varlinkctl call "$VARLINK_ADDR" io.systemd.MachineInstance.Pause '{}'
sleep 0.2
varlinkctl call "$VARLINK_ADDR" io.systemd.MachineInstance.Resume '{}'
sleep 0.5
# Kill the subscriber and check output
kill "$SUBSCRIBE_ALL_PID" 2>/dev/null; wait "$SUBSCRIBE_ALL_PID" 2>/dev/null || true
cat "$WORKDIR/events-all.json"
# Verify initial READY event
grep >/dev/null '"READY"' "$WORKDIR/events-all.json"
echo "SubscribeEvents sent READY event"
# Verify we got both STOP and RESUME events
grep >/dev/null '"STOP"' "$WORKDIR/events-all.json"
grep >/dev/null '"RESUME"' "$WORKDIR/events-all.json"
echo "SubscribeEvents received STOP and RESUME events"
# Test filtered subscription: only STOP events
varlinkctl call --more --timeout=10 "$VARLINK_ADDR" io.systemd.MachineInstance.SubscribeEvents '{"filter":["STOP"]}' \
>"$WORKDIR/events-filtered.json" 2>&1 &
SUBSCRIBE_FILTER_PID=$!
sleep 0.5
# Trigger both events again
varlinkctl call "$VARLINK_ADDR" io.systemd.MachineInstance.Pause '{}'
sleep 0.2
varlinkctl call "$VARLINK_ADDR" io.systemd.MachineInstance.Resume '{}'
sleep 0.5
kill "$SUBSCRIBE_FILTER_PID" 2>/dev/null; wait "$SUBSCRIBE_FILTER_PID" 2>/dev/null || true
cat "$WORKDIR/events-filtered.json"
# Should have STOP but not RESUME
grep >/dev/null '"STOP"' "$WORKDIR/events-filtered.json"
(! grep >/dev/null '"RESUME"' "$WORKDIR/events-filtered.json")
echo "Filtered subscription correctly received only STOP events"
# Test machinectl pause/resume
machinectl pause "$MACHINE"
echo "machinectl pause succeeded"
machinectl resume "$MACHINE"
echo "machinectl resume succeeded"
# Test machinectl poweroff -- sends ACPI powerdown via QMP (system_powerdown).
# The guest won't handle it (our init is just 'sleep infinity'), but the QMP command should succeed.
machinectl poweroff "$MACHINE"
echo "machinectl poweroff succeeded"
# --- Stress test: repeated start/pause/resume/terminate cycles ---
# Exercises the varlink disconnect path, QMP reconnection, and ref counting under repeated use.
# This catches use-after-free and double-close bugs that only manifest after multiple cycles.
machinectl terminate "$MACHINE" 2>/dev/null
timeout 10 bash -c "while machinectl status '$MACHINE' &>/dev/null; do sleep .5; done" 2>/dev/null
timeout 10 bash -c "while kill -0 '$VMSPAWN_PID' 2>/dev/null; do sleep .5; done" 2>/dev/null
for i in $(seq 1 5); do
echo "Stress cycle $i/5"
STRESS_MACHINE="test-vmspawn-stress-$i-$$"
systemd-vmspawn \
--machine="$STRESS_MACHINE" \
--ram=256M \
--directory="$WORKDIR/root" \
--linux="$KERNEL" \
--tpm=no \
--console=headless \
&>"$WORKDIR/vmspawn-stress.log" &
STRESS_PID=$!
wait_for_machine "$STRESS_MACHINE" "$STRESS_PID" "$WORKDIR/vmspawn-stress.log"
STRESS_ADDR=$(varlinkctl call /run/systemd/machine/io.systemd.Machine \
io.systemd.Machine.List "{\"name\":\"$STRESS_MACHINE\"}" | jq -r '.controlAddress')
assert_neq "$STRESS_ADDR" "null"
# Rapid pause/resume/describe cycles
for _ in $(seq 1 3); do
machinectl pause "$STRESS_MACHINE"
varlinkctl call "$STRESS_ADDR" io.systemd.MachineInstance.Describe '{}' | jq -e '.running == false' >/dev/null
machinectl resume "$STRESS_MACHINE"
varlinkctl call "$STRESS_ADDR" io.systemd.MachineInstance.Describe '{}' | jq -e '.running == true' >/dev/null
done
machinectl terminate "$STRESS_MACHINE"
timeout 10 bash -c "while machinectl status '$STRESS_MACHINE' &>/dev/null; do sleep .5; done"
timeout 10 bash -c "while kill -0 '$STRESS_PID' 2>/dev/null; do sleep .5; done"
echo "Stress cycle $i/5 passed"
done
echo "All stress cycles passed"
# Restart a fresh VM for the remaining tests
systemd-vmspawn \
--machine="$MACHINE" \
--ram=256M \
--directory="$WORKDIR/root" \
--linux="$KERNEL" \
--tpm=no \
--console=headless \
&>"$WORKDIR/vmspawn.log" &
VMSPAWN_PID=$!
wait_for_machine "$MACHINE" "$VMSPAWN_PID" "$WORKDIR/vmspawn.log"
VARLINK_ADDR=$(varlinkctl call /run/systemd/machine/io.systemd.Machine \
io.systemd.Machine.List "{\"name\":\"$MACHINE\"}" | jq -r '.controlAddress')
assert_neq "$VARLINK_ADDR" "null"
# --- Parallel multi-machine dispatch tests ---
# Launch a second VM to test machinectl operating on multiple machines simultaneously.
# Use a separate rootfs so each VM gets independent sidecar state (TPM, EFI NVRAM).
MACHINE2="test-vmspawn-qmp2-$$"
mkdir -p "$WORKDIR/root2/sbin"
cp "$WORKDIR/root/sbin/init" "$WORKDIR/root2/sbin/init"
systemd-vmspawn \
--machine="$MACHINE2" \
--ram=256M \
--directory="$WORKDIR/root2" \
--linux="$KERNEL" \
--tpm=no \
--console=headless \
&>"$WORKDIR/vmspawn2.log" &
VMSPAWN2_PID=$!
wait_for_machine "$MACHINE2" "$VMSPAWN2_PID" "$WORKDIR/vmspawn2.log"
echo "Second machine '$MACHINE2' registered"
VARLINK_ADDR2=$(varlinkctl call /run/systemd/machine/io.systemd.Machine \
io.systemd.Machine.List "{\"name\":\"$MACHINE2\"}" | jq -r '.controlAddress')
assert_neq "$VARLINK_ADDR2" "null"
# Parallel pause: both machines at once
machinectl pause "$MACHINE" "$MACHINE2"
echo "Parallel pause of two machines succeeded"
# Verify both are paused
varlinkctl call "$VARLINK_ADDR" io.systemd.MachineInstance.Describe '{}' | jq -e '.running == false'
varlinkctl call "$VARLINK_ADDR2" io.systemd.MachineInstance.Describe '{}' | jq -e '.running == false'
echo "Both machines verified paused"
# Parallel resume
machinectl resume "$MACHINE" "$MACHINE2"
echo "Parallel resume of two machines succeeded"
# Verify both resumed
varlinkctl call "$VARLINK_ADDR" io.systemd.MachineInstance.Describe '{}' | jq -e '.running == true'
varlinkctl call "$VARLINK_ADDR2" io.systemd.MachineInstance.Describe '{}' | jq -e '.running == true'
echo "Both machines verified running"
# --- Terminate and verify cleanup ---
# Parallel terminate: both machines at once (QMP quit)
machinectl terminate "$MACHINE" "$MACHINE2"
timeout 10 bash -c "
while machinectl status '$MACHINE' &>/dev/null || machinectl status '$MACHINE2' &>/dev/null; do
sleep .5
done
"
echo "Parallel terminate succeeded, both VMs gone"
# Both vmspawn processes should have exited
timeout 10 bash -c "while kill -0 '$VMSPAWN_PID' 2>/dev/null; do sleep .5; done"
timeout 10 bash -c "while kill -0 '$VMSPAWN2_PID' 2>/dev/null; do sleep .5; done"
echo "Both vmspawn processes exited"
echo "All vmspawn QMP-varlink bridge tests passed"
+33
View File
@@ -525,3 +525,36 @@ check_nss_module() (
return 0
)
find_qemu_binary() {
# Mirrors find_qemu_binary() from src/vmspawn/vmspawn-util.c.
# Returns 0 if a usable QEMU binary exists, 1 otherwise.
for binary in qemu qemu-kvm; do
if command -v "$binary" >/dev/null 2>&1; then
return 0
fi
done
if test -x /usr/libexec/qemu-kvm; then
return 0
fi
local arch
case "$(uname -m)" in
x86_64) arch=x86_64 ;;
i?86) arch=i386 ;;
aarch64) arch=aarch64 ;;
armv*l|arm*) arch=arm ;;
alpha) arch=alpha ;;
loongarch64) arch=loongarch64 ;;
mips*) arch=mips ;;
parisc*) arch=hppa ;;
ppc64*|ppc*) arch=ppc ;;
riscv32) arch=riscv32 ;;
riscv64) arch=riscv64 ;;
s390x) arch=s390x ;;
*) return 1 ;;
esac
command -v "qemu-system-$arch" >/dev/null 2>&1
}