From e4a58c38e9434e834b726622efc4c9f21d316574 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 24 Mar 2026 14:25:50 +0100 Subject: [PATCH 1/2] vmspawn: add virtio-scsi disk type support Add --image-disk-type= to select the disk type for the root disk, and allow specifying the disk type as a colon-separated prefix on --extra-drive=: systemd-vmspawn --image-disk-type=virtio-scsi --image=image.raw systemd-vmspawn --image=image.raw --extra-drive=virtio-scsi:data.raw For --extra-drive=, the format and disk type prefixes can appear in any order since the value sets don't overlap: --extra-drive=raw:virtio-scsi:/path --extra-drive=virtio-scsi:raw:/path Extra drives inherit --image-disk-type= by default unless overridden with an explicit prefix. vmspawn originally used virtio-scsi for all drives but switched to virtio-blk in 1f24a954e4 for simplicity and direct kernel boot compatibility. This makes virtio-scsi available again as an explicit option for cases where a SCSI storage topology is desired. For virtio-scsi, a shared virtio-scsi-pci controller is created and drives are attached as scsi-hd devices. The SCSI serial number is limited to 30 characters, so filenames exceeding this are hashed with SHA-256. Signed-off-by: Christian Brauner --- man/systemd-vmspawn.xml | 22 ++++- shell-completion/bash/systemd-vmspawn | 3 + src/vmspawn/vmspawn-settings.c | 7 ++ src/vmspawn/vmspawn-settings.h | 9 ++ src/vmspawn/vmspawn.c | 129 +++++++++++++++++++++++--- 5 files changed, 151 insertions(+), 19 deletions(-) diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml index 331c7c16fd..80798f7354 100644 --- a/man/systemd-vmspawn.xml +++ b/man/systemd-vmspawn.xml @@ -125,6 +125,17 @@ + + + + Specifies the disk type to use for the root disk passed to . + Extra drives added via inherit this disk type unless overridden + with an explicit disk type prefix. Takes one of virtio-blk or + virtio-scsi. Defaults to virtio-blk. + + + + @@ -506,13 +517,14 @@ - + 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 - raw or qcow2 and a colon. The format defaults to - raw. Note that qcow2 is only supported for regular files, not - block devices. + 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 raw and the disk type defaults to the value of + (which itself defaults to virtio-blk). + 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 b17586de14..955c59bef5 100644 --- a/shell-completion/bash/systemd-vmspawn +++ b/shell-completion/bash/systemd-vmspawn @@ -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' 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' elif __contains_word "$prev" ${OPTS[ARG]}; then comps='' else diff --git a/src/vmspawn/vmspawn-settings.c b/src/vmspawn/vmspawn-settings.c index 46dda4bfc3..2e594e59b2 100644 --- a/src/vmspawn/vmspawn-settings.c +++ b/src/vmspawn/vmspawn-settings.c @@ -10,6 +10,13 @@ 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", +}; + +DEFINE_STRING_TABLE_LOOKUP(disk_type, DiskType); + void extra_drive_context_done(ExtraDriveContext *ctx) { assert(ctx); diff --git a/src/vmspawn/vmspawn-settings.h b/src/vmspawn/vmspawn-settings.h index ee937c993a..2fe3b84297 100644 --- a/src/vmspawn/vmspawn-settings.h +++ b/src/vmspawn/vmspawn-settings.h @@ -10,9 +10,17 @@ typedef enum ImageFormat { _IMAGE_FORMAT_INVALID = -EINVAL, } ImageFormat; +typedef enum DiskType { + DISK_TYPE_VIRTIO_BLK, + DISK_TYPE_VIRTIO_SCSI, + _DISK_TYPE_MAX, + _DISK_TYPE_INVALID = -EINVAL, +} DiskType; + typedef struct ExtraDrive { char *path; ImageFormat format; + DiskType disk_type; } ExtraDrive; typedef struct ExtraDriveContext { @@ -42,4 +50,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); diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index a65f879449..4abc54675b 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -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)\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,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=[FORMAT:]PATH\n" + " --extra-drive=[FORMAT:][DISKTYPE:]PATH\n" " Adds an additional disk to the virtual machine\n" - " (FORMAT: raw, qcow2; default: raw)\n" + " (FORMAT: raw, qcow2; DISKTYPE: virtio-blk, virtio-scsi)\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 +339,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 +349,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 +440,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 +583,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 +626,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 +1994,31 @@ static int make_sidecar_path(const char *suffix, char **ret) { return 0; } +/* Device serial numbers have length limits (e.g. 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; @@ -2476,6 +2530,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); @@ -2510,8 +2580,22 @@ 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; + } + default: + assert_not_reached(); + } r = grow_image(arg_image, arg_grow_image); if (r < 0) @@ -2637,8 +2721,25 @@ 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; + } + default: + assert_not_reached(); + } } if (arg_console_mode != CONSOLE_GUI) { From b76e1732f7c4781db4eb1611f7fdbbd7d8f830ad Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 24 Mar 2026 14:44:34 +0100 Subject: [PATCH 2/2] vmspawn: add nvme disk type support Extend --image-disk-type= and the --extra-drive= disk type prefix to support nvme in addition to virtio-blk and virtio-scsi: systemd-vmspawn --image-disk-type=nvme --image=image.raw systemd-vmspawn --image=image.raw --extra-drive=nvme:data.raw The NVMe serial number is limited to 20 characters by the NVMe spec. If the image filename exceeds this, it is hashed with SHA-256 and truncated to 20 hex characters via the disk_serial() helper introduced in the previous commit. Signed-off-by: Christian Brauner --- man/systemd-vmspawn.xml | 5 +++-- shell-completion/bash/systemd-vmspawn | 2 +- src/vmspawn/vmspawn-settings.c | 1 + src/vmspawn/vmspawn-settings.h | 1 + src/vmspawn/vmspawn.c | 26 ++++++++++++++++++++++---- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml index 80798f7354..eec52f1374 100644 --- a/man/systemd-vmspawn.xml +++ b/man/systemd-vmspawn.xml @@ -130,8 +130,9 @@ Specifies the disk type to use for the root disk passed to . Extra drives added via inherit this disk type unless overridden - with an explicit disk type prefix. Takes one of virtio-blk or - virtio-scsi. Defaults to virtio-blk. + with an explicit disk type prefix. Takes one of virtio-blk, + virtio-scsi, or nvme. Defaults to + virtio-blk. diff --git a/shell-completion/bash/systemd-vmspawn b/shell-completion/bash/systemd-vmspawn index 955c59bef5..718cb300ce 100644 --- a/shell-completion/bash/systemd-vmspawn +++ b/shell-completion/bash/systemd-vmspawn @@ -61,7 +61,7 @@ _systemd_vmspawn() { 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' + comps='virtio-blk virtio-scsi nvme' elif __contains_word "$prev" ${OPTS[ARG]}; then comps='' else diff --git a/src/vmspawn/vmspawn-settings.c b/src/vmspawn/vmspawn-settings.c index 2e594e59b2..7c30ed753f 100644 --- a/src/vmspawn/vmspawn-settings.c +++ b/src/vmspawn/vmspawn-settings.c @@ -13,6 +13,7 @@ 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); diff --git a/src/vmspawn/vmspawn-settings.h b/src/vmspawn/vmspawn-settings.h index 2fe3b84297..252ceecceb 100644 --- a/src/vmspawn/vmspawn-settings.h +++ b/src/vmspawn/vmspawn-settings.h @@ -13,6 +13,7 @@ typedef enum ImageFormat { typedef enum DiskType { DISK_TYPE_VIRTIO_BLK, DISK_TYPE_VIRTIO_SCSI, + DISK_TYPE_NVME, _DISK_TYPE_MAX, _DISK_TYPE_INVALID = -EINVAL, } DiskType; diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 4abc54675b..72aa39e18a 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -209,7 +209,7 @@ static int help(void) { " -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)\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" @@ -251,8 +251,9 @@ static int help(void) { " --bind-ro=SOURCE[:TARGET]\n" " Mount a file or directory, but read-only\n" " --extra-drive=[FORMAT:][DISKTYPE:]PATH\n" - " Adds an additional disk to the virtual machine\n" - " (FORMAT: raw, qcow2; DISKTYPE: virtio-blk, virtio-scsi)\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" @@ -1994,7 +1995,7 @@ static int make_sidecar_path(const char *suffix, char **ret) { return 0; } -/* Device serial numbers have length limits (e.g. 30 for SCSI). +/* 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 @@ -2593,6 +2594,14 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { 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(); } @@ -2737,6 +2746,15 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { 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(); }