diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml index 23ecc51bac..79ae2274d7 100644 --- a/man/systemd-vmspawn.xml +++ b/man/systemd-vmspawn.xml @@ -125,6 +125,18 @@ + + + + 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, + virtio-scsi, or nvme. Defaults to + virtio-blk. + + + + @@ -506,13 +518,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 08e92e0a4b..0e2e46a84c 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 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 diff --git a/src/vmspawn/vmspawn-settings.c b/src/vmspawn/vmspawn-settings.c index 1c4bc102b9..d19a65d55d 100644 --- a/src/vmspawn/vmspawn-settings.c +++ b/src/vmspawn/vmspawn-settings.c @@ -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); diff --git a/src/vmspawn/vmspawn-settings.h b/src/vmspawn/vmspawn-settings.h index 1d59db6418..cfcee6fbb6 100644 --- a/src/vmspawn/vmspawn-settings.h +++ b/src/vmspawn/vmspawn-settings.h @@ -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); diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index d41a920389..91da71d669 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, 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)) {