]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
vmspawn: Add image format option to support qcow2
authorKai Lüke <kailueke@riseup.net>
Sun, 25 Jan 2026 22:09:16 +0000 (23:09 +0100)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Thu, 29 Jan 2026 00:02:13 +0000 (09:02 +0900)
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.

man/systemd-vmspawn.xml
shell-completion/bash/systemd-vmspawn
src/vmspawn/vmspawn-settings.c
src/vmspawn/vmspawn-settings.h
src/vmspawn/vmspawn.c

index fce3827e4cf5521d6c29d71f452f082972191a3d..eb0d4bff7644b563d753efbd1af43a8b6b27c432 100644 (file)
           <xi:include href="version-info.xml" xpointer="v255"/>
           </listitem>
         </varlistentry>
+
+        <varlistentry>
+          <term><option>--image-format=<replaceable>FORMAT</replaceable></option></term>
+
+          <listitem><para>Specifies the format of the disk image passed to <option>--image=</option>.
+          Takes either <literal>raw</literal> or <literal>qcow2</literal>. Defaults to
+          <literal>raw</literal>. Note that <literal>qcow2</literal> is only supported for regular files,
+          not block devices.</para>
+
+          <xi:include href="version-info.xml" xpointer="v260"/></listitem>
+        </varlistentry>
+
       </variablelist>
     </refsect2>
 
         </varlistentry>
 
         <varlistentry>
-          <term><option>--extra-drive=<replaceable>PATH</replaceable></option></term>
+          <term><option>--extra-drive=<replaceable>PATH</replaceable>[:<replaceable>FORMAT</replaceable>]</option></term>
 
-          <listitem><para>Takes a disk image or block device on the host and supplies it to the virtual machine as another drive.</para>
+          <listitem><para>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 (<literal>raw</literal> or <literal>qcow2</literal>). Defaults to <literal>raw</literal>.
+          Note that <literal>qcow2</literal> is only supported for regular files, not block devices.</para>
 
           <xi:include href="version-info.xml" xpointer="v256"/></listitem>
         </varlistentry>
index 5cbb342d1e4ba82fa5306e1804c52ac0e6bed16d..46a178f50cc959465a5a600d19c2185015d791f0 100644 (file)
@@ -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
index 780df553aac3d99a3aba97981a93afde5deda008..46dda4bfc325ff4fca6b56fae7568c1ad49c7a3f 100644 (file)
@@ -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",
index d60f3e18781e0abfcde5915c8780e7416503e30b..1cfe4ffd72978c37c9973d3081f54af986dbec51 100644 (file)
@@ -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);
index 2b6055349f0ae335fd81b1df034d8dd87e5694b0..06f6961b1a8b3228d804707366737725aebd71e1 100644 (file)
@@ -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)