diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml index 2475a0464f..3c5519fd88 100644 --- a/man/systemd-vmspawn.xml +++ b/man/systemd-vmspawn.xml @@ -169,6 +169,10 @@ already as large or larger than requested. The specified size accepts the usual K, M, G suffixes (to the base of 1024). Specified values are rounded up to multiples of 4096. + If is specified, the original image file is left untouched and + the requested size is applied to the ephemeral copy instead. In that case qcow2 images are + supported too. + diff --git a/src/vmspawn/vmspawn-qmp.c b/src/vmspawn/vmspawn-qmp.c index 7dc58a8ccb..092f8dae44 100644 --- a/src/vmspawn/vmspawn-qmp.c +++ b/src/vmspawn/vmspawn-qmp.c @@ -13,6 +13,7 @@ #include "errno-util.h" #include "ether-addr-util.h" #include "fd-util.h" +#include "format-util.h" #include "hashmap.h" #include "json-util.h" #include "log.h" @@ -493,6 +494,13 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D if (r < 0) return r; + /* Growing the overlay leaves the base image untouched, which is the whole point of ephemeral mode. */ + if (drive->grow_to > virtual_size) { + log_debug("Growing ephemeral overlay of '%s' from %s to %s.", + drive->path, FORMAT_BYTES(virtual_size), FORMAT_BYTES(drive->grow_to)); + virtual_size = drive->grow_to; + } + /* Step 1-2: Pass both fds to QEMU */ _cleanup_free_ char *base_path = NULL; uint64_t base_fdset_id; diff --git a/src/vmspawn/vmspawn-qmp.h b/src/vmspawn/vmspawn-qmp.h index bf9919f93a..df0242bbe0 100644 --- a/src/vmspawn/vmspawn-qmp.h +++ b/src/vmspawn/vmspawn-qmp.h @@ -98,6 +98,7 @@ typedef struct DriveInfo { char *pcie_port; /* pcie-root-port id for device_add bus (NULL on non-PCIe) */ int fd; /* pre-opened image fd (-EBADF if unused) */ int overlay_fd; /* pre-opened anonymous overlay fd for ephemeral (-EBADF if unused) */ + uint64_t grow_to; /* ephemeral: minimum virtual size of the overlay (0 if unset) */ QmpDriveFlags flags; /* Per-add-op state (populated by the add flow; zeroed at CLI-parse time) */ diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 1e1283c327..9d90cbed92 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -423,6 +423,11 @@ static int parse_argv(int argc, char *argv[]) { r = parse_size(opts.arg, 1024, &arg_grow_image); if (r < 0) return log_error_errno(r, "Failed to parse --grow-image= parameter: %s", opts.arg); + + if (arg_grow_image > UINT64_MAX - 4095) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), + "Specified --grow-image= size too large: %s", opts.arg); + arg_grow_image = ROUND_UP(arg_grow_image, 4096); break; OPTION_GROUP("Host Configuration"): {} @@ -2088,12 +2093,6 @@ static int grow_image(const char *path, uint64_t size) { if (size == 0) return 0; - /* Round up to multiple of 4K */ - size = DIV_ROUND_UP(size, 4096); - if (size > UINT64_MAX / 4096) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified file size too large, refusing."); - size *= 4096; - _cleanup_close_ int fd = xopenat_full(AT_FDCWD, path, O_RDWR|O_CLOEXEC, XO_REGULAR, /* mode= */ 0); if (fd < 0) return log_error_errno(fd, "Failed to open image file '%s': %m", path); @@ -2418,6 +2417,7 @@ static int prepare_primary_drive(const char *runtime_dir, DriveInfos *drives) { } d->overlay_fd = TAKE_FD(overlay_fd); d->flags |= QMP_DRIVE_NO_FLUSH; + d->grow_to = arg_grow_image; } drives->drives[drives->n_drives++] = TAKE_PTR(d); @@ -3249,14 +3249,18 @@ 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) { + if (arg_image_disk_type == DISK_TYPE_VIRTIO_SCSI_CDROM) { + /* CD-ROMs are read-only, so override any "rw" on the kernel command line. */ + if (strv_contains(arg_kernel_cmdline_extra, "rw") && + strv_extend(&arg_kernel_cmdline_extra, "ro") < 0) + return log_oom(); + } else if (!arg_ephemeral) { + /* In ephemeral mode the original image is never written to, the requested size is + * applied to the qcow2 overlay instead, see prepare_primary_drive(). */ r = grow_image(arg_image, arg_grow_image); if (r < 0) return r; - /* CD-ROMs are read-only, so override any "rw" on the kernel command line. */ - } else if (strv_contains(arg_kernel_cmdline_extra, "rw") && - strv_extend(&arg_kernel_cmdline_extra, "ro") < 0) - return log_oom(); + } } _cleanup_(sd_event_unrefp) sd_event *event = NULL; @@ -4225,7 +4229,9 @@ static int verify_arguments(void) { log_warning("--grow-image has no effect with --image-disk-type=scsi-cd (CD-ROMs are read-only)."); } - if (arg_grow_image && arg_image_format == IMAGE_FORMAT_QCOW2) + /* In ephemeral mode the size is picked when creating the qcow2 overlay, so the base image format + * doesn't matter. */ + if (arg_grow_image && arg_image_format == IMAGE_FORMAT_QCOW2 && !arg_ephemeral) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--grow-image is not supported for qcow2 images, use 'qemu-img resize FILE SIZE'."); diff --git a/test/units/TEST-87-AUX-UTILS-VM.vmspawn-drives.sh b/test/units/TEST-87-AUX-UTILS-VM.vmspawn-drives.sh index 5ec2fd2f7b..31b3bf8b51 100755 --- a/test/units/TEST-87-AUX-UTILS-VM.vmspawn-drives.sh +++ b/test/units/TEST-87-AUX-UTILS-VM.vmspawn-drives.sh @@ -53,7 +53,7 @@ WORKDIR="$(mktemp -d)" at_exit() { set +e - for m in "${MACHINE_MULTI:-}" "${MACHINE_EPHEMERAL:-}"; do + for m in "${MACHINE_MULTI:-}" "${MACHINE_EPHEMERAL:-}" "${MACHINE_GROW:-}"; do [[ -n "$m" ]] || continue if machinectl status "$m" &>/dev/null; then machinectl terminate "$m" 2>/dev/null @@ -62,6 +62,7 @@ at_exit() { 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 + [[ -n "${VMSPAWN_GROW_PID:-}" ]] && kill "$VMSPAWN_GROW_PID" 2>/dev/null && wait "$VMSPAWN_GROW_PID" 2>/dev/null rm -rf "$WORKDIR" } trap at_exit EXIT @@ -185,4 +186,59 @@ timeout 10 bash -c "while machinectl status '$MACHINE_EPHEMERAL' &>/dev/null; do timeout 10 bash -c "while kill -0 '$VMSPAWN_EPHEMERAL_PID' 2>/dev/null; do sleep .5; done" echo "Ephemeral VM terminated cleanly" +# --- Test 3: Ephemeral overlay grown with --grow-image= --- +# In ephemeral mode the requested size has to land on the qcow2 overlay. The +# image passed to --image= is opened read-only and has to come out of the run +# at its original size. + +MACHINE_GROW="test-vmspawn-grow-$$" +GROW_SIZE=$((512 * 1024 * 1024)) +IMAGE_SIZE="$(stat -c %s "$WORKDIR/root.raw")" + +systemd-vmspawn \ + --machine="$MACHINE_GROW" \ + --ram=256M \ + --image="$WORKDIR/root.raw" \ + --ephemeral \ + --grow-image="$GROW_SIZE" \ + --linux="$KERNEL" \ + --tpm=no \ + --console=headless \ + root=/dev/vda rw \ + &>"$WORKDIR/vmspawn-grow.log" & +VMSPAWN_GROW_PID=$! + +wait_for_machine "$MACHINE_GROW" "$VMSPAWN_GROW_PID" "$WORKDIR/vmspawn-grow.log" +echo "Grown ephemeral machine '$MACHINE_GROW' registered with machined" + +assert_eq "$(stat -c %s "$WORKDIR/root.raw")" "$IMAGE_SIZE" +echo "Image passed to --image= was left at its original size" + +# The overlay is unlinked, so QEMU's fd is the only way to reach it. vmspawn +# registers QEMU itself as the machine leader, so machined knows its PID. +QEMU_PID="$(varlinkctl call /run/systemd/machine/io.systemd.Machine \ + io.systemd.Machine.List "{\"name\":\"$MACHINE_GROW\"}" | jq -r '.leader.pid')" + +OVERLAY="" +for fd in /proc/"$QEMU_PID"/fd/*; do + target="$(readlink "$fd" 2>/dev/null)" || continue + # O_TMPFILE in the runtime directory, or the memfd fallback. + [[ "$target" == */vmspawn/"$MACHINE_GROW"/#* || "$target" == /memfd:vmspawn-overlay* ]] || continue + OVERLAY="$fd" + break +done +assert_neq "$OVERLAY" "" +echo "Ephemeral overlay is QEMU fd ${OVERLAY##*/}" + +# qcow2 header: the magic, followed by the virtual size as a big endian u64 at +# offset 24. Read it out of the header directly, qemu-img may not be installed. +assert_eq "$(od -An -tx1 -N4 "$OVERLAY" | tr -d ' ')" "514649fb" +assert_eq "$(od -An -tu8 -j24 -N8 --endian=big "$OVERLAY" | tr -d ' ')" "$GROW_SIZE" +echo "Ephemeral overlay was created at the requested size" + +machinectl terminate "$MACHINE_GROW" +timeout 10 bash -c "while machinectl status '$MACHINE_GROW' &>/dev/null; do sleep .5; done" +timeout 10 bash -c "while kill -0 '$VMSPAWN_GROW_PID' 2>/dev/null; do sleep .5; done" +echo "Grown ephemeral VM terminated cleanly" + echo "All vmspawn drive setup tests passed"