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:
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -91,5 +91,6 @@ Packages=
|
||||
veritysetup
|
||||
# kernel-bootcfg --add-uri= is just too useful
|
||||
virt-firmware
|
||||
virtiofsd
|
||||
xz
|
||||
zypper
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
{}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 },
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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, ¶ms);
|
||||
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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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
@@ -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);
|
||||
@@ -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__)
|
||||
|
||||
@@ -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(
|
||||
¬ification,
|
||||
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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
Executable
+319
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user