vmspawn: add disk type selection for root and extra drives (#41301)

vmspawn previously hardcoded virtio-blk for all drives. This adds
  --image-disk-type= to select the root disk type (virtio-blk,
  virtio-scsi, or nvme) and allows per-drive overrides via a
  colon-separated prefix on --extra-drive=. The format and disk type
  prefixes can appear in any order since their value sets don't overlap.

  For virtio-scsi, a single shared controller is created with drives
  attached as scsi-hd devices. For nvme, each drive gets its own
controller. Both have serial number length limits (30 and 20 characters
  respectively), so long filenames are replaced with a truncated SHA-256
  hex digest.
This commit is contained in:
Lennart Poettering
2026-03-24 21:48:00 +01:00
committed by GitHub
5 changed files with 173 additions and 20 deletions
+18 -5
View File
@@ -125,6 +125,18 @@
<xi:include href="version-info.xml" xpointer="v260"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--image-disk-type=<replaceable>TYPE</replaceable></option></term>
<listitem><para>Specifies the disk type to use for the root disk passed to <option>--image=</option>.
Extra drives added via <option>--extra-drive=</option> inherit this disk type unless overridden
with an explicit disk type prefix. Takes one of <literal>virtio-blk</literal>,
<literal>virtio-scsi</literal>, or <literal>nvme</literal>. Defaults to
<literal>virtio-blk</literal>.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>-x</option></term>
<term><option>--ephemeral</option></term>
@@ -506,13 +518,14 @@
</varlistentry>
<varlistentry>
<term><option>--extra-drive=[<replaceable>FORMAT</replaceable>:]<replaceable>PATH</replaceable></option></term>
<term><option>--extra-drive=[<replaceable>FORMAT</replaceable>:][<replaceable>DISKTYPE</replaceable>:]<replaceable>PATH</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. Optionally, the image format can be specified by prefixing the path with
<literal>raw</literal> or <literal>qcow2</literal> and a colon. The format defaults to
<literal>raw</literal>. Note that <literal>qcow2</literal> is only supported for regular files, not
block devices.</para>
machine as another drive. Optionally, the image format and/or disk type can be specified by prefixing
the path with their values separated by colons. The format and disk type prefixes can appear in any
order. The format defaults to <literal>raw</literal> and the disk type defaults to the value of
<option>--image-disk-type=</option> (which itself defaults to <literal>virtio-blk</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>
+3
View File
@@ -38,6 +38,7 @@ _systemd_vmspawn() {
[CONSOLE]='--console'
[ARG]='--cpus --ram --vsock-cid -M --machine --uuid --private-users --background --set-credential --load-credential'
[IMAGE_FORMAT]='--image-format'
[IMAGE_DISK_TYPE]='--image-disk-type'
)
_init_completion || return
@@ -59,6 +60,8 @@ _systemd_vmspawn() {
comps='interactive native gui read-only headless'
elif __contains_word "$prev" ${OPTS[IMAGE_FORMAT]}; then
comps='raw qcow2'
elif __contains_word "$prev" ${OPTS[IMAGE_DISK_TYPE]}; then
comps='virtio-blk virtio-scsi nvme'
elif __contains_word "$prev" ${OPTS[ARG]}; then
comps=''
else
+8
View File
@@ -10,6 +10,14 @@ static const char *const image_format_table[_IMAGE_FORMAT_MAX] = {
DEFINE_STRING_TABLE_LOOKUP(image_format, ImageFormat);
static const char *const disk_type_table[_DISK_TYPE_MAX] = {
[DISK_TYPE_VIRTIO_BLK] = "virtio-blk",
[DISK_TYPE_VIRTIO_SCSI] = "virtio-scsi",
[DISK_TYPE_NVME] = "nvme",
};
DEFINE_STRING_TABLE_LOOKUP(disk_type, DiskType);
void extra_drive_context_done(ExtraDriveContext *ctx) {
assert(ctx);
+10
View File
@@ -10,9 +10,18 @@ typedef enum ImageFormat {
_IMAGE_FORMAT_INVALID = -EINVAL,
} ImageFormat;
typedef enum DiskType {
DISK_TYPE_VIRTIO_BLK,
DISK_TYPE_VIRTIO_SCSI,
DISK_TYPE_NVME,
_DISK_TYPE_MAX,
_DISK_TYPE_INVALID = -EINVAL,
} DiskType;
typedef struct ExtraDrive {
char *path;
ImageFormat format;
DiskType disk_type;
} ExtraDrive;
typedef struct ExtraDriveContext {
@@ -43,4 +52,5 @@ typedef enum SettingsMask {
} SettingsMask;
DECLARE_STRING_TABLE_LOOKUP(console_mode, ConsoleMode);
DECLARE_STRING_TABLE_LOOKUP(disk_type, DiskType);
DECLARE_STRING_TABLE_LOOKUP(image_format, ImageFormat);
+134 -15
View File
@@ -65,6 +65,7 @@
#include "ptyfwd.h"
#include "random-util.h"
#include "rm-rf.h"
#include "sha256.h"
#include "signal-util.h"
#include "snapshot-util.h"
#include "socket-util.h"
@@ -143,6 +144,7 @@ static char *arg_background = NULL;
static bool arg_pass_ssh_key = true;
static char *arg_ssh_key_type = NULL;
static bool arg_discard_disk = true;
static DiskType arg_image_disk_type = DISK_TYPE_VIRTIO_BLK;
static struct ether_addr arg_network_provided_mac = {};
static char **arg_smbios11 = NULL;
static uint64_t arg_grow_image = 0;
@@ -206,6 +208,8 @@ static int help(void) {
" -x --ephemeral Run VM with snapshot of the disk or directory\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"
" --image-disk-type=TYPE\n"
" Specify disk type (virtio-blk, virtio-scsi, nvme; default: virtio-blk)\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"
@@ -246,9 +250,10 @@ 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=[FORMAT:]PATH\n"
" Adds an additional disk to the virtual machine\n"
" (FORMAT: raw, qcow2; default: raw)\n"
" --extra-drive=[FORMAT:][DISKTYPE:]PATH\n"
" Adds an additional disk to the VM\n"
" FORMAT: raw, qcow2\n"
" DISKTYPE: virtio-blk, virtio-scsi, nvme\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"
@@ -335,6 +340,7 @@ static int parse_argv(int argc, char *argv[]) {
ARG_SYSTEM,
ARG_USER,
ARG_IMAGE_FORMAT,
ARG_IMAGE_DISK_TYPE,
};
static const struct option options[] = {
@@ -344,6 +350,7 @@ static int parse_argv(int argc, char *argv[]) {
{ "no-pager", no_argument, NULL, ARG_NO_PAGER },
{ "image", required_argument, NULL, 'i' },
{ "image-format", required_argument, NULL, ARG_IMAGE_FORMAT },
{ "image-disk-type", required_argument, NULL, ARG_IMAGE_DISK_TYPE },
{ "ephemeral", no_argument, NULL, 'x' },
{ "directory", required_argument, NULL, 'D' },
{ "machine", required_argument, NULL, 'M' },
@@ -434,6 +441,13 @@ static int parse_argv(int argc, char *argv[]) {
"Invalid image format: %s", optarg);
break;
case ARG_IMAGE_DISK_TYPE:
arg_image_disk_type = disk_type_from_string(optarg);
if (arg_image_disk_type < 0)
return log_error_errno(arg_image_disk_type,
"Invalid image disk type: %s", optarg);
break;
case 'M':
if (isempty(optarg))
arg_machine = mfree(arg_machine);
@@ -570,21 +584,36 @@ static int parse_argv(int argc, char *argv[]) {
case ARG_EXTRA_DRIVE: {
ImageFormat format = IMAGE_FORMAT_RAW;
DiskType extra_disk_type = _DISK_TYPE_INVALID;
const char *dp = optarg;
const char *colon = strchr(dp, ':');
if (colon) {
_cleanup_free_ char *fs = strndup(optarg, colon - optarg);
if (!fs)
/* Parse optional colon-separated prefixes. The format and disk type
* value sets don't overlap, so they can appear in any order. */
for (;;) {
const char *colon = strchr(dp, ':');
if (!colon)
break;
_cleanup_free_ char *prefix = strndup(dp, colon - dp);
if (!prefix)
return log_oom();
ImageFormat f = image_format_from_string(fs);
if (f < 0)
log_debug_errno(f, "Cannot parse '%s' as an image format, assuming it is a part of path, ignoring.", fs);
else {
ImageFormat f = image_format_from_string(prefix);
if (f >= 0) {
format = f;
dp = colon + 1;
continue;
}
DiskType dt = disk_type_from_string(prefix);
if (dt >= 0) {
extra_disk_type = dt;
dp = colon + 1;
continue;
}
/* Not a recognized prefix, treat the rest as the path */
break;
}
_cleanup_free_ char *drive_path = NULL;
@@ -598,6 +627,7 @@ static int parse_argv(int argc, char *argv[]) {
arg_extra_drives.drives[arg_extra_drives.n_drives++] = (ExtraDrive) {
.path = TAKE_PTR(drive_path),
.format = format,
.disk_type = extra_disk_type,
};
break;
@@ -1965,6 +1995,31 @@ static int make_sidecar_path(const char *suffix, char **ret) {
return 0;
}
/* Device serial numbers have length limits (e.g. 20 for NVMe, 30 for SCSI).
* If the filename fits, use it directly; otherwise hash it with SHA-256 and
* take the first max_len hex characters. max_len must be even and <= 64.
* The filename should already be QEMU-escaped (commas doubled) so that the
* result can be embedded directly in a -device argument. */
static int disk_serial(const char *filename, size_t max_len, char **ret) {
assert(filename);
assert(ret);
assert(max_len % 2 == 0);
assert(max_len <= SHA256_DIGEST_SIZE * 2);
if (strlen(filename) <= max_len)
return strdup_to(ret, filename);
uint8_t hash[SHA256_DIGEST_SIZE];
sha256_direct(filename, strlen(filename), hash);
_cleanup_free_ char *serial = hexmem(hash, max_len / 2);
if (!serial)
return -ENOMEM;
*ret = TAKE_PTR(serial);
return 0;
}
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, *kernel = NULL;
@@ -2483,6 +2538,22 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
}
}
bool need_scsi_controller =
arg_image_disk_type == DISK_TYPE_VIRTIO_SCSI && 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 (dt == DISK_TYPE_VIRTIO_SCSI) {
need_scsi_controller = true;
break;
}
}
if (need_scsi_controller) {
if (strv_extend_many(&cmdline, "-device", "virtio-scsi-pci,id=vmspawn_scsi") < 0)
return log_oom();
}
if (arg_image) {
assert(!arg_directory);
@@ -2517,8 +2588,30 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
if (strv_extend(&cmdline, "-device") < 0)
return log_oom();
if (strv_extend_joined(&cmdline, "virtio-blk-pci,drive=vmspawn,bootindex=1,serial=", escaped_image_fn) < 0)
return log_oom();
switch (arg_image_disk_type) {
case DISK_TYPE_VIRTIO_BLK:
if (strv_extend_joined(&cmdline, "virtio-blk-pci,drive=vmspawn,bootindex=1,serial=", escaped_image_fn) < 0)
return log_oom();
break;
case DISK_TYPE_VIRTIO_SCSI: {
_cleanup_free_ char *serial = NULL;
if (disk_serial(escaped_image_fn, 30, &serial) < 0)
return log_oom();
if (strv_extend_joined(&cmdline, "scsi-hd,bus=vmspawn_scsi.0,drive=vmspawn,bootindex=1,serial=", serial) < 0)
return log_oom();
break;
}
case DISK_TYPE_NVME: {
_cleanup_free_ char *serial = NULL;
if (disk_serial(escaped_image_fn, 20, &serial) < 0)
return log_oom();
if (strv_extend_joined(&cmdline, "nvme,drive=vmspawn,bootindex=1,serial=", serial) < 0)
return log_oom();
break;
}
default:
assert_not_reached();
}
r = grow_image(arg_image, arg_grow_image);
if (r < 0)
@@ -2644,8 +2737,34 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
if (strv_extend(&cmdline, "-device") < 0)
return log_oom();
if (strv_extendf(&cmdline, "virtio-blk-pci,drive=vmspawn_extra_%zu,serial=%s", i++, escaped_drive_fn) < 0)
return log_oom();
DiskType dt = drive->disk_type >= 0 ? drive->disk_type : arg_image_disk_type;
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, 30, &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, 20, &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;
}
default:
assert_not_reached();
}
}
if (!IN_SET(arg_console_mode, CONSOLE_GUI, CONSOLE_HEADLESS)) {