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.
This commit is contained in:
Kai Lüke
2026-01-29 09:02:13 +09:00
committed by Yu Watanabe
parent b0b2a71d81
commit 16b3472b3f
5 changed files with 117 additions and 20 deletions
+17 -2
View File
@@ -113,6 +113,18 @@
<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>
@@ -464,9 +476,12 @@
</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>
+1
View 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
+16
View 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",
+20
View 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);
+63 -18
View 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;
r = parse_path_argument(optarg, /* suppress_root= */ false, &drive_path);
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(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)