From: Kai Lüke Date: Sun, 25 Jan 2026 22:09:16 +0000 (+0100) Subject: vmspawn: Add image format option to support qcow2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=16b3472b3f0714b44165e37c215116dc9edbd662;p=thirdparty%2Fsystemd.git vmspawn: Add image format option to support qcow2 A QEMU qcow2 VM image can be internally sparse and compressed. Support such images in vmspawn for both the main disk and any extra disks. --- diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml index fce3827e4cf..eb0d4bff764 100644 --- a/man/systemd-vmspawn.xml +++ b/man/systemd-vmspawn.xml @@ -113,6 +113,18 @@ + + + + + Specifies the format of the disk image passed to . + Takes either raw or qcow2. Defaults to + raw. Note that qcow2 is only supported for regular files, + not block devices. + + + + @@ -464,9 +476,12 @@ - + - Takes a disk image or block device on the host and supplies it to the virtual machine as another drive. + Takes a disk image or block device on the host and supplies it to the virtual + machine as another drive. Optionally, the image format can be specified by appending a colon and + the format (raw or qcow2). Defaults to raw. + Note that qcow2 is only supported for regular files, not block devices. diff --git a/shell-completion/bash/systemd-vmspawn b/shell-completion/bash/systemd-vmspawn index 5cbb342d1e4..46a178f50cc 100644 --- a/shell-completion/bash/systemd-vmspawn +++ b/shell-completion/bash/systemd-vmspawn @@ -37,6 +37,7 @@ _systemd_vmspawn() { [SSH_KEY]='--ssh-key' [CONSOLE]='--console' [ARG]='--cpus --ram --vsock-cid -M --machine --uuid --private-users --background --set-credential --load-credential' + [IMAGE_FORMAT]='--image-format' ) _init_completion || return diff --git a/src/vmspawn/vmspawn-settings.c b/src/vmspawn/vmspawn-settings.c index 780df553aac..46dda4bfc32 100644 --- a/src/vmspawn/vmspawn-settings.c +++ b/src/vmspawn/vmspawn-settings.c @@ -3,6 +3,22 @@ #include "string-table.h" #include "vmspawn-settings.h" +static const char *const image_format_table[_IMAGE_FORMAT_MAX] = { + [IMAGE_FORMAT_RAW] = "raw", + [IMAGE_FORMAT_QCOW2] = "qcow2", +}; + +DEFINE_STRING_TABLE_LOOKUP(image_format, ImageFormat); + +void extra_drive_context_done(ExtraDriveContext *ctx) { + assert(ctx); + + FOREACH_ARRAY(drive, ctx->drives, ctx->n_drives) + free(drive->path); + + free(ctx->drives); +} + static const char *const console_mode_table[_CONSOLE_MODE_MAX] = { [CONSOLE_INTERACTIVE] = "interactive", [CONSOLE_READ_ONLY] = "read-only", diff --git a/src/vmspawn/vmspawn-settings.h b/src/vmspawn/vmspawn-settings.h index d60f3e18781..1cfe4ffd729 100644 --- a/src/vmspawn/vmspawn-settings.h +++ b/src/vmspawn/vmspawn-settings.h @@ -3,6 +3,25 @@ #include "shared-forward.h" +typedef enum ImageFormat { + IMAGE_FORMAT_RAW, + IMAGE_FORMAT_QCOW2, + _IMAGE_FORMAT_MAX, + _IMAGE_FORMAT_INVALID = -EINVAL, +} ImageFormat; + +typedef struct ExtraDrive { + char *path; + ImageFormat format; +} ExtraDrive; + +typedef struct ExtraDriveContext { + ExtraDrive *drives; + size_t n_drives; +} ExtraDriveContext; + +void extra_drive_context_done(ExtraDriveContext *ctx); + typedef enum ConsoleMode { CONSOLE_INTERACTIVE, /* ptyfwd */ CONSOLE_READ_ONLY, /* ptyfwd, but in read-only mode */ @@ -22,3 +41,4 @@ typedef enum SettingsMask { } SettingsMask; DECLARE_STRING_TABLE_LOOKUP(console_mode, ConsoleMode); +DECLARE_STRING_TABLE_LOOKUP(image_format, ImageFormat); diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 2b6055349f0..06f6961b1a8 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -63,6 +63,7 @@ #include "rm-rf.h" #include "signal-util.h" #include "socket-util.h" +#include "stat-util.h" #include "stdio-util.h" #include "string-util.h" #include "strv.h" @@ -104,6 +105,7 @@ static bool arg_quiet = false; static PagerFlags arg_pager_flags = 0; static char *arg_directory = NULL; static char *arg_image = NULL; +static ImageFormat arg_image_format = IMAGE_FORMAT_RAW; static char *arg_machine = NULL; static char *arg_slice = NULL; static char **arg_property = NULL; @@ -127,7 +129,7 @@ static bool arg_register = true; static bool arg_keep_unit = false; static sd_id128_t arg_uuid = {}; static char **arg_kernel_cmdline_extra = NULL; -static char **arg_extra_drives = NULL; +static ExtraDriveContext arg_extra_drives = {}; static char *arg_background = NULL; static bool arg_pass_ssh_key = true; static char *arg_ssh_key_type = NULL; @@ -157,7 +159,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_initrds, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_runtime_mounts, runtime_mount_context_done); STATIC_DESTRUCTOR_REGISTER(arg_forward_journal, freep); STATIC_DESTRUCTOR_REGISTER(arg_kernel_cmdline_extra, strv_freep); -STATIC_DESTRUCTOR_REGISTER(arg_extra_drives, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_extra_drives, extra_drive_context_done); STATIC_DESTRUCTOR_REGISTER(arg_background, freep); STATIC_DESTRUCTOR_REGISTER(arg_ssh_key_type, freep); STATIC_DESTRUCTOR_REGISTER(arg_smbios11, strv_freep); @@ -189,6 +191,7 @@ static int help(void) { "\n%3$sImage:%4$s\n" " -D --directory=PATH Root directory for the VM\n" " -i --image=FILE|DEVICE Root file system disk image or device for the VM\n" + " --image-format=FORMAT Specify disk image format (raw, qcow2; default: raw)\n" "\n%3$sHost Configuration:%4$s\n" " --cpus=CPUS Configure number of CPUs in guest\n" " --ram=BYTES Configure guest's RAM size\n" @@ -227,7 +230,9 @@ static int help(void) { " Mount a file or directory from the host into the VM\n" " --bind-ro=SOURCE[:TARGET]\n" " Mount a file or directory, but read-only\n" - " --extra-drive=PATH Adds an additional disk to the virtual machine\n" + " --extra-drive=PATH[:FORMAT]\n" + " Adds an additional disk to the virtual machine\n" + " (format: raw, qcow2; default: raw)\n" " --bind-user=NAME Bind user from host to virtual machine\n" " --bind-user-shell=BOOL|PATH\n" " Configure the shell to use for --bind-user= users\n" @@ -312,6 +317,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_BIND_USER_GROUP, ARG_SYSTEM, ARG_USER, + ARG_IMAGE_FORMAT, }; static const struct option options[] = { @@ -320,6 +326,7 @@ static int parse_argv(int argc, char *argv[]) { { "quiet", no_argument, NULL, 'q' }, { "no-pager", no_argument, NULL, ARG_NO_PAGER }, { "image", required_argument, NULL, 'i' }, + { "image-format", required_argument, NULL, ARG_IMAGE_FORMAT }, { "directory", required_argument, NULL, 'D' }, { "machine", required_argument, NULL, 'M' }, { "slice", required_argument, NULL, 'S' }, @@ -401,6 +408,13 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_IMAGE_FORMAT: + arg_image_format = image_format_from_string(optarg); + if (arg_image_format < 0) + return log_error_errno(arg_image_format, + "Invalid image format: %s", optarg); + break; + case 'M': if (isempty(optarg)) arg_machine = mfree(arg_machine); @@ -532,15 +546,34 @@ static int parse_argv(int argc, char *argv[]) { break; case ARG_EXTRA_DRIVE: { - _cleanup_free_ char *drive_path = NULL; + _cleanup_free_ char *buf = NULL, *drive_path = NULL; + ImageFormat format = IMAGE_FORMAT_RAW; + + const char *colon = strrchr(optarg, ':'); + if (colon) { + ImageFormat f = image_format_from_string(colon + 1); + if (f < 0) + log_debug_errno(f, "Failed to parse image format '%s', assuming it is a part of path, ignoring: %m", colon + 1); + else { + format = f; + buf = strndup(optarg, colon - optarg); + if (!buf) + return log_oom(); + } + } - r = parse_path_argument(optarg, /* suppress_root= */ false, &drive_path); + r = parse_path_argument(buf ?: optarg, /* suppress_root= */ false, &drive_path); if (r < 0) return r; - r = strv_consume(&arg_extra_drives, TAKE_PTR(drive_path)); - if (r < 0) + if (!GREEDY_REALLOC(arg_extra_drives.drives, arg_extra_drives.n_drives + 1)) return log_oom(); + + arg_extra_drives.drives[arg_extra_drives.n_drives++] = (ExtraDrive) { + .path = TAKE_PTR(drive_path), + .format = format, + }; + break; } @@ -2271,6 +2304,14 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { if (arg_image) { assert(!arg_directory); + if (arg_image_format == IMAGE_FORMAT_QCOW2) { + r = verify_regular_at(AT_FDCWD, arg_image, /* follow= */ true); + if (r < 0) + return log_error_errno(r, + "Block device '%s' cannot be used with 'qcow2' format, only 'raw' is supported: %m", + arg_image); + } + if (strv_extend(&cmdline, "-drive") < 0) return log_oom(); @@ -2278,7 +2319,7 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { if (!escaped_image) return log_oom(); - if (strv_extendf(&cmdline, "if=none,id=vmspawn,file=%s,format=raw,discard=%s", escaped_image, on_off(arg_discard_disk)) < 0) + if (strv_extendf(&cmdline, "if=none,id=vmspawn,file=%s,format=%s,discard=%s", escaped_image, image_format_to_string(arg_image_format), on_off(arg_discard_disk)) < 0) return log_oom(); _cleanup_free_ char *image_fn = NULL; @@ -2367,33 +2408,37 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { } size_t i = 0; - STRV_FOREACH(drive, arg_extra_drives) { + 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); + _cleanup_free_ char *escaped_drive = escape_qemu_value(drive->path); if (!escaped_drive) return log_oom(); struct stat st; - if (stat(*drive, &st) < 0) - return log_error_errno(errno, "Failed to stat '%s': %m", *drive); + 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)) + 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); + } else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected regular file or block device, not '%s'.", drive->path); - if (strv_extendf(&cmdline, "driver=raw,cache.direct=off,cache.no-flush=on,file.driver=%s,file.filename=%s,node-name=vmspawn_extra_%zu", driver, escaped_drive, i) < 0) + if (strv_extendf(&cmdline, "driver=%s,cache.direct=off,cache.no-flush=on,file.driver=%s,file.filename=%s,node-name=vmspawn_extra_%zu", image_format_to_string(drive->format), driver, escaped_drive, i) < 0) return log_oom(); _cleanup_free_ char *drive_fn = NULL; - r = path_extract_filename(*drive, &drive_fn); + 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); + 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)