Files
cloud-hypervisor/vmm/src/config.rs
Pascal Scholz d419338a47 vmm: Add parsing logic for zone_updates
This commit adds support for parsing `zone_updates` from the CLI
for the live migration and restore paths.

Signed-off-by: Pascal Scholz <pascal.scholz@cyberus-technology.de>
On-behalf-of: SAP pascal.scholz@sap.
2026-07-20 16:06:55 +00:00

7065 lines
245 KiB
Rust

// Copyright © 2019 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
//
use std::collections::{BTreeSet, HashMap, HashSet};
#[cfg(feature = "ivshmem")]
use std::fs;
use std::path::PathBuf;
use std::result;
use std::str::FromStr;
use std::sync::LazyLock;
use arch::CpuProfile;
use block::ImageType;
use clap::ArgMatches;
use log::{debug, warn};
use option_parser::{
ByteSized, IntegerList, OptionParser, OptionParserError, StringList, Toggle, Tuple, TupleList,
};
use pci::NUM_DEVICE_IDS;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use virtio_bindings::virtio_blk::VIRTIO_BLK_ID_BYTES;
use virtio_bindings::virtio_ids::*;
use virtio_devices::block::MINIMUM_BLOCK_QUEUE_SIZE;
use virtio_devices::vhost_user::VIRTIO_FS_TAG_LEN;
use virtio_devices::{RateLimiterConfig, TokenBucketConfig, net, vhost_user};
use crate::landlock::LandlockAccess;
use crate::vm_config::*;
const MAX_NUM_PCI_SEGMENTS: u16 = 96;
const MAX_IOMMU_ADDRESS_WIDTH_BITS: u8 = 64;
// Maximum queue size is largest power of 2 that fits into a u16
const VIRTIO_MAX_QUEUE_SIZE: u16 = 32768;
#[cfg(all(feature = "kvm", target_arch = "x86_64"))]
const MAX_SUPPORTED_CPUS: u32 = 8192;
#[cfg(not(all(feature = "kvm", target_arch = "x86_64")))]
const MAX_SUPPORTED_CPUS: u32 = 255;
/// Errors associated with VM configuration parameters.
#[derive(Debug, Error)]
pub enum Error {
/// Filesystem tag is missing
#[error("Error parsing --fs: tag missing")]
ParseFsTagMissing,
/// Filesystem tag is too long
#[error("Error parsing --fs: max tag length is {VIRTIO_FS_TAG_LEN}")]
ParseFsTagTooLong,
/// Filesystem socket is missing
#[error("Error parsing --fs: socket missing")]
ParseFsSockMissing,
/// Generic vhost-user device type is invalid
#[error(
"Error parsing --generic-vhost-user: device_type {0:?} invalid (leading zeros or unknown string)"
)]
ParseGenericVhostUserVirtioIdInvalid(String),
/// Generic vhost-user device type is unsupported
#[error(
"Error parsing --generic-vhost-user: device with device_type {0:?} cannot be implemented via vhost-user"
)]
ParseGenericVhostUserVirtioIdUnsupported(String),
/// Generic vhost-user socket is missing
#[error("Error parsing --generic-vhost-user: socket missing")]
ParseGenericVhostUserSockMissing,
/// Generic vhost-user number of queues is missing
#[error("Error parsing --generic-vhost-user: number of queues missing")]
ParseGenericVhostUserNumResponseQueuesMissing,
/// Generic vhost-user device type is missing
#[error("Error parsing --generic-vhost-user: device_type missing")]
ParseGenericVhostUserVirtioIdMissing,
/// Generic vhost-user available features is missing
#[error("Error parsing --generic-vhost-user: available features missing")]
ParseGenericVhostUserAvailFeaturesMissing,
/// Generic vhost-user queue size missing
#[error("Error parsing --generic-vhost-user: queue size missing")]
ParseGenericVhostUserQueueSizeMissing,
/// Missing persistent memory file parameter.
#[error("Error parsing --pmem: file missing")]
ParsePmemFileMissing,
/// Missing vsock socket path parameter.
#[error("Error parsing --vsock: socket missing")]
ParseVsockSockMissing,
/// Missing vsock cid parameter.
#[error("Error parsing --vsock: cid missing")]
ParseVsockCidMissing,
/// Missing restore source_url parameter.
#[error("Error parsing --restore: source_url missing")]
ParseRestoreSourceUrlMissing,
/// Error parsing CPU options
#[error("Error parsing --cpus")]
ParseCpus(#[source] OptionParserError),
/// Invalid CPU features
#[error("Invalid feature in --cpus features list: {0}")]
InvalidCpuFeatures(String),
/// Error parsing memory options
#[error("Error parsing --memory")]
ParseMemory(#[source] OptionParserError),
/// Error parsing memory zone options
#[error("Error parsing --memory-zone")]
ParseMemoryZone(#[source] OptionParserError),
/// Missing 'id' from memory zone
#[error("Error parsing --memory-zone: id missing")]
ParseMemoryZoneIdMissing,
/// Error parsing rate-limiter group options
#[error("Error parsing --rate-limit-group")]
ParseRateLimiterGroup(#[source] OptionParserError),
/// Error parsing disk options
#[error("Error parsing --disk")]
ParseDisk(#[source] OptionParserError),
/// Error parsing network options
#[error("Error parsing --net")]
ParseNetwork(#[source] OptionParserError),
/// Error parsing RNG options
#[error("Error parsing --rng")]
ParseRng(#[source] OptionParserError),
/// Error parsing RTC options
#[error("Error parsing --rtc")]
ParseRtc(#[source] OptionParserError),
/// Error parsing balloon options
#[error("Error parsing --balloon")]
ParseBalloon(#[source] OptionParserError),
/// Error parsing filesystem parameters
#[error("Error parsing --fs")]
ParseFileSystem(#[source] OptionParserError),
/// Error parsing persistent memory parameters
#[error("Error parsing --pmem")]
ParsePersistentMemory(#[source] OptionParserError),
/// Error parsing generic vhost-user parameters
#[error("Error parsing --generic-vhost-user")]
ParseGenericVhostUser(#[source] OptionParserError),
/// Failed parsing console parameters
#[error("Error parsing --console")]
ParseConsole(#[source] OptionParserError),
/// Failed parsing serial parameters
#[error("Error parsing --serial")]
ParseSerial(#[source] OptionParserError),
#[cfg(target_arch = "x86_64")]
/// Failed parsing debug-console
#[error("Error parsing --debug-console")]
ParseDebugConsole(#[source] OptionParserError),
/// No mode given for console
#[error("Error parsing --console: invalid console mode given")]
ParseConsoleInvalidModeGiven,
/// Failed parsing device parameters
#[error("Error parsing --device")]
ParseDevice(#[source] OptionParserError),
/// Failed parsing vsock parameters
#[error("Error parsing --vsock")]
ParseVsock(#[source] OptionParserError),
/// Failed parsing restore parameters
#[error("Error parsing --restore")]
ParseRestore(#[source] OptionParserError),
/// Failed parsing NUMA parameters
#[error("Error parsing --numa")]
ParseNuma(#[source] OptionParserError),
/// Failed validating configuration
#[error("Error validating configuration")]
Validation(#[source] ValidationError),
#[cfg(feature = "sev_snp")]
#[error("Error parsing --sev_snp")]
/// Failed parsing SEV-SNP config
ParseSevSnp(#[source] OptionParserError),
#[cfg(feature = "tdx")]
#[error("Error parsing --tdx")]
/// Failed parsing TDX config
ParseTdx(#[source] OptionParserError),
#[cfg(feature = "tdx")]
#[error("TDX firmware missing")]
/// No TDX firmware
FirmwarePathMissing,
/// Failed parsing userspace device
#[error("Error parsing --user-device")]
ParseUserDevice(#[source] OptionParserError),
/// Missing socket for userspace device
#[error("Error parsing --user-device: socket missing")]
ParseUserDeviceSocketMissing,
/// Error parsing pci segment options
#[error("Error parsing --pci-segment")]
ParsePciSegment(#[source] OptionParserError),
/// Failed parsing platform parameters
#[error("Error parsing --platform")]
ParsePlatform(#[source] OptionParserError),
/// Failed parsing vDPA device
#[error("Error parsing --vdpa")]
ParseVdpa(#[source] OptionParserError),
/// Missing path for vDPA device
#[error("Error parsing --vdpa: path missing")]
ParseVdpaPathMissing,
/// Failed parsing TPM device
#[error("Error parsing --tpm")]
ParseTpm(#[source] OptionParserError),
#[cfg(feature = "ivshmem")]
/// Failed parsing ivsmem device
#[error("Error parsing --ivshmem")]
ParseIvshmem(#[source] OptionParserError),
/// Missing path for TPM device
#[error("Error parsing --tpm: path missing")]
ParseTpmPathMissing,
#[cfg(feature = "ivshmem")]
/// Missing path for ivsmem device
#[error("Error parsing --ivshmem: path missing")]
ParseIvshmemPathMissing,
/// Error parsing Landlock rules
#[error("Error parsing --landlock-rules")]
ParseLandlockRules(#[source] OptionParserError),
/// Missing fields in Landlock rules
#[error("Error parsing --landlock-rules: path/access field missing")]
ParseLandlockMissingFields,
#[cfg(feature = "fw_cfg")]
/// Failed Parsing FwCfgItem config
#[error("Error parsing --fw-cfg-config items")]
ParseFwCfgItem(#[source] OptionParserError),
#[error("Error parsing common PCI device config")]
ParsePciDeviceCommonConfig(#[source] OptionParserError),
}
#[derive(Debug, PartialEq, Eq, Error)]
pub enum ValidationError {
/// Missing file value for console
#[error("Path missing when using file console mode")]
ConsoleFileMissing,
/// Missing socket path for console
#[error("Path missing when using socket console mode")]
ConsoleSocketPathMissing,
/// Max is less than boot
#[error("Max CPUs ({0}) lower than boot CPUs ({1})")]
CpusMaxLowerThanBoot(u32 /* max vCPUs */, u32 /* boot vCPUs */),
/// Too many CPUs.
#[error("Too many CPUs: specified {0} but {MAX_SUPPORTED_CPUS} is the limit")]
TooManyCpus(u32 /* specified CPUs */),
/// Missing file value for debug-console
#[cfg(target_arch = "x86_64")]
#[error("Path missing when using file mode for debug console")]
DebugconFileMissing,
/// Both socket and path specified
#[error("Disk path and vhost socket both provided")]
DiskSocketAndPath,
/// Using vhost user requires shared memory
#[error("Using vhost-user requires using shared memory or huge pages")]
VhostUserRequiresSharedMemory,
/// No socket provided for vhost_use
#[error("No socket provided when using vhost-user")]
VhostUserMissingSocket,
/// Trying to use IOMMU without PCI
#[error("Using an IOMMU without PCI support is unsupported")]
IommuUnsupported,
/// Trying to use VFIO without PCI
#[error("Using VFIO without PCI support is unsupported")]
VfioUnsupported,
/// CPU topology count doesn't match max
#[error("Product of CPU topology parts does not match maximum vCPU")]
CpuTopologyCount,
/// CPU topology uses too many threads per core
#[error("CPU topology supports at most 2 threads per core")]
CpuTopologyThreadsPerCore,
/// One part of the CPU topology was zero
#[error("No part of the CPU topology can be zero")]
CpuTopologyZeroPart,
#[cfg(target_arch = "aarch64")]
/// Dies per package must be 1
#[error("Dies per package must be 1")]
CpuTopologyDiesPerPackage,
/// Virtio needs a min of 2 queues
#[error("Number of queues ({0}) to virtio_net should be higher than 2")]
VnetQueueLowerThan2(usize),
/// The input queue number for virtio_net must match the number of input fds
#[error("Number of queues ({0}) to virtio_net does not match the number of FDs ({1})")]
VnetQueueFdMismatch(usize /* num of queues */, usize /* FD num */),
/// Using reserved fd
#[error("Reserved fd number (<= 2): {0}")]
VnetReservedFd(i32),
/// Hardware checksum offload is disabled.
#[error("\"offload_tso\" and \"offload_ufo\" depend on \"offload_csum\"")]
NoHardwareChecksumOffload,
/// Hugepages not turned on
#[error("Huge page size specified but huge pages not enabled")]
HugePageSizeWithoutHugePages,
/// Huge page size is not power of 2
#[error("Huge page size is not power of 2: {0}")]
InvalidHugePageSize(u64),
/// CPU Hotplug is not permitted with TDX
#[cfg(feature = "tdx")]
#[error("CPU hotplug is not permitted with TDX")]
TdxNoCpuHotplug,
/// Missing firmware for TDX
#[cfg(feature = "tdx")]
#[error("No TDX firmware specified")]
TdxFirmwareMissing,
/// Insufficient vCPUs for queues
#[error("Queue count ({0}) must not exceed boot vCPUs ({1})")]
TooManyQueues(usize /* queues */, usize /* vCPUs */),
/// Queue size is not a power of 2 within the spec-permitted range
#[error("Queue size must be a power of 2 no greater than {VIRTIO_MAX_QUEUE_SIZE}: {0}")]
InvalidQueueSize(u16),
/// Block queue size too small to advertise a usable seg_max
#[error("Block queue size must be greater than {MINIMUM_BLOCK_QUEUE_SIZE}: {0}")]
BlockQueueSizeTooSmall(u16),
/// Need shared memory for vfio-user
#[error("Using user devices requires using shared memory or huge pages")]
UserDevicesRequireSharedMemory,
/// VSOCK Context Identifier has a special meaning, unsuitable for a VM.
#[error("{0} is a special VSOCK CID")]
VsockSpecialCid(u32),
/// Memory zone is reused across NUMA nodes
#[error("Memory zone: {0} belongs to multiple NUMA nodes: {1} and {2}")]
MemoryZoneReused(String, u32, u32),
/// Invalid number of PCI segments
#[error("Number of PCI segments ({0}) not in range of 1 to {MAX_NUM_PCI_SEGMENTS}")]
InvalidNumPciSegments(u16),
/// Invalid PCI segment id
#[error("Invalid PCI segment id: {0}")]
InvalidPciSegment(u16),
/// Invalid PCI segment aperture weight
#[error("Invalid PCI segment aperture weight: {0}")]
InvalidPciSegmentApertureWeight(u32),
/// Invalid VFIO excluded-mmap BAR index
#[error("Invalid VFIO excluded-mmap BAR index: {0}")]
InvalidDeviceExcludeMmapBar(u64),
/// Invalid IOMMU address width in bits
#[error(
"IOMMU address width in bits ({0}) should be less than or equal to {MAX_IOMMU_ADDRESS_WIDTH_BITS}"
)]
InvalidIommuAddressWidthBits(u8),
/// Balloon too big
#[error("Balloon size ({0}) greater than RAM ({1})")]
BalloonLargerThanRam(u64, u64),
/// On a IOMMU segment but not behind IOMMU
#[error("Device is on an IOMMU PCI segment ({0}) but not placed behind IOMMU")]
OnIommuSegment(u16),
/// GPUDirect clique requires P2P DMA
#[error("Device with x_nv_gpudirect_clique requires vfio_p2p_dma=on")]
GpuDirectCliqueRequiresP2pDma,
// Identifier is not unique
#[error("Identifier {0} is not unique")]
IdentifierNotUnique(String),
/// Invalid identifier
#[error("Identifier {0} is not invalid")]
InvalidIdentifier(String),
/// Placing the device behind a virtual IOMMU is not supported
#[error("Device does not support being placed behind IOMMU")]
IommuNotSupported,
/// Duplicated device path (device added twice)
#[error("Duplicated device path: {0}")]
DuplicateDevicePath(String),
/// A DeviceConfig specified neither `path` nor `fd`.
#[error("VFIO device config must specify `path=` or `fd=`")]
VfioDeviceNeitherPathNorFd,
/// A DeviceConfig specified both `path` and `fd`.
#[error("VFIO device config must specify at most one of `path=` or `fd=`")]
VfioDeviceBothPathAndFd,
/// FD-based VFIO requires an externally-supplied iommufd FD so the
/// cdev's bind state can survive across an in-process VM reboot.
#[error("VFIO device `fd=` requires platform `iommufd_fd=<fd>`")]
VfioFdRequiresIommufdFd,
/// `iommufd_fd` was provided without also enabling the iommufd backend.
#[error("Platform `iommufd_fd=<fd>` requires `iommufd=on`")]
IommufdFdRequiresIommufd,
/// Provided MTU is lower than what the VIRTIO specification expects
#[error("Provided MTU {0} is lower than 1280 (expected by VIRTIO specification)")]
InvalidMtu(u16),
/// PCI segment is reused across NUMA nodes
#[error("PCI segment: {0} belongs to multiple NUMA nodes {1} and {2}")]
PciSegmentReused(u16, u32, u32),
/// Default PCI segment is assigned to NUMA node other than 0.
#[error("Default PCI segment assigned to non-zero NUMA node {0}")]
DefaultPciSegmentInvalidNode(u32),
/// Invalid rate-limiter group
#[error("Invalid rate-limiter group")]
InvalidRateLimiterGroup,
/// Rate limiting is not supported with vhost-user
#[error("Rate limiting is not supported with vhost-user")]
VhostUserRateLimiterNotSupported,
/// The specified I/O port was invalid. It should be provided in hex, such as `0xe9`.
#[cfg(target_arch = "x86_64")]
#[error("The IO port was not properly provided in hex or a `0x` prefix is missing: {0}")]
InvalidIoPortHex(String),
#[cfg(feature = "sev_snp")]
#[error("Invalid host data format")]
InvalidHostData,
#[cfg(feature = "sev_snp")]
#[error("SEV-SNP requires an IGVM payload (--payload igvm=<path>)")]
SevSnpRequiresIgvm,
/// Restore expects all net ids that have fds
#[error("Net id {0} is associated with FDs and is required")]
RestoreMissingRequiredNetId(String),
/// Number of FDs passed during Restore are incorrect to the NetConfig
#[error("Number of Net FDs passed for '{0}' during Restore: {1}. Expected: {2}")]
RestoreNetFdCountMismatch(String, usize, usize),
/// vfio_fds entry does not match any DeviceConfig.id in the snapshot
#[error("VFIO device id '{0}' in 'vfio_fds' does not match any device in the snapshot")]
RestoreUnknownVfioId(String),
/// A device saved with an FD has no replacement FD for the restore
#[error("VFIO device '{0}' was FD backed and needs a new fd in 'vfio_fds'")]
RestoreMissingVfioFd(String),
/// Prefault cannot be combined with on-demand restore
#[error("'prefault' cannot be combined with 'memory_restore_mode=ondemand'")]
InvalidRestorePrefaultWithOnDemand,
/// Path provided in landlock-rules doesn't exist
#[error("Path {0:?} provided in landlock-rules does not exist")]
LandlockPathDoesNotExist(PathBuf),
/// Access provided in landlock-rules in invalid
#[error("Invalid landlock access: {0}")]
InvalidLandlockAccess(String),
/// Invalid block device serial length
#[error("Block device serial length ({0}) exceeds maximum allowed length ({1})")]
InvalidSerialLength(usize, usize),
#[cfg(feature = "ivshmem")]
/// Invalid Ivshmem input size
#[error("Invalid ivshmem input size: {0}")]
InvalidIvshmemInputSize(u64),
#[cfg(feature = "ivshmem")]
/// Invalid Ivshmem backend file size
#[error("Invalid ivshmem backend file size: {0}")]
InvalidIvshmemSize(u64),
#[cfg(feature = "ivshmem")]
/// Invalid Ivshmem backend file path
#[error("Invalid ivshmem backend file path")]
InvalidIvshmemPath,
#[error("Payload configuration is not bootable")]
PayloadError(#[from] PayloadConfigError),
#[error("Mask provided without an IP")]
MaskProvidedWithoutIp,
#[error("IP provided without a mask")]
IpProvidedWithoutMask,
/// Invalid NUMA Configuration
#[error("Invalid NUMA configuration: {0}")]
InvalidNumaConfig(String),
/// The supplied PCI ID was greater then the max. supported number
/// of devices per Bus
#[error("Given PCI device ID ({0}) is out of the supported range of 0..{NUM_DEVICE_IDS}")]
InvalidPciDeviceId(u8),
/// The supplied PCI ID is reserved
#[error("Given PCI device ID ({0}) is reserved")]
ReservedPciDeviceId(u8),
/// Invalid to set both 'mergeable' and 'shared' for memory
#[error("Invalid to set both 'mergeable' and 'shared' for memory")]
InvalidSharedMemoryWithMergeable,
/// More than one update was specified for one or more MemoryZone
#[error("Multiple updates for the same zone defined")]
MultipleMemoryZoneUpdates,
/// More than one update was specified that contains an empty memory zone ID
#[error("At least one memory zone update with an empty ID was defined")]
MemoryZoneUpdatesEmptyId,
}
type ValidationResult<T> = result::Result<T, ValidationError>;
pub fn add_to_config<T>(items: &mut Option<Vec<T>>, item: T) {
if let Some(items) = items {
items.push(item);
} else {
*items = Some(vec![item]);
}
}
/// Check that the PCI device supplied is neither out of range nor does
/// it use any reserved device ID.
fn validate_pci_device_id(device_id: u8) -> ValidationResult<()> {
if device_id >= pci::NUM_DEVICE_IDS {
// Check the given ID is not out of range
return Err(ValidationError::InvalidPciDeviceId(device_id));
} else if device_id == pci::PCI_ROOT_DEVICE_ID {
// Check the ID isn't any reserved one. Currently, only the device ID
// for the root device is reserved.
return Err(ValidationError::ReservedPciDeviceId(device_id));
}
Ok(())
}
/// Reject a virtio queue size that is not a power of 2 as required by the
/// spec. The `u16` type already handles the maximum queue size.
fn validate_queue_size(queue_size: u16) -> ValidationResult<()> {
if !queue_size.is_power_of_two() {
return Err(ValidationError::InvalidQueueSize(queue_size));
}
Ok(())
}
pub type Result<T> = result::Result<T, Error>;
pub struct VmParams<'a> {
pub cpus: &'a str,
pub memory: &'a str,
pub memory_zones: Option<Vec<&'a str>>,
pub firmware: Option<&'a str>,
pub kernel: Option<&'a str>,
pub initramfs: Option<&'a str>,
pub cmdline: Option<&'a str>,
pub rate_limit_groups: Option<Vec<&'a str>>,
pub disks: Option<Vec<&'a str>>,
pub net: Option<Vec<&'a str>>,
pub rng: &'a str,
pub balloon: Option<&'a str>,
pub fs: Option<Vec<&'a str>>,
pub generic_vhost_user: Option<Vec<&'a str>>,
pub pmem: Option<Vec<&'a str>>,
pub serial: &'a str,
pub console: &'a str,
#[cfg(target_arch = "x86_64")]
pub debug_console: &'a str,
pub devices: Option<Vec<&'a str>>,
pub user_devices: Option<Vec<&'a str>>,
pub vdpa: Option<Vec<&'a str>>,
pub vsock: Option<&'a str>,
#[cfg(feature = "pvmemcontrol")]
pub pvmemcontrol: bool,
pub pvpanic: bool,
pub numa: Option<Vec<&'a str>>,
pub watchdog: bool,
pub rtc: Option<&'a str>,
#[cfg(feature = "guest_debug")]
pub gdb: bool,
pub pci_segments: Option<Vec<&'a str>>,
pub platform: Option<&'a str>,
pub tpm: Option<&'a str>,
#[cfg(feature = "igvm")]
pub igvm: Option<&'a str>,
#[cfg(feature = "sev_snp")]
pub host_data: Option<&'a str>,
pub landlock_enable: bool,
pub landlock_rules: Option<Vec<&'a str>>,
#[cfg(feature = "fw_cfg")]
pub fw_cfg_config: Option<&'a str>,
#[cfg(feature = "ivshmem")]
pub ivshmem: Option<&'a str>,
}
impl<'a> VmParams<'a> {
pub fn from_arg_matches(args: &'a ArgMatches) -> Self {
// These .unwrap()s cannot fail as there is a default value defined
let cpus = args.get_one::<String>("cpus").unwrap();
let memory = args.get_one::<String>("memory").unwrap();
let memory_zones: Option<Vec<&str>> = args
.get_many::<String>("memory-zone")
.map(|x| x.map(|y| y as &str).collect());
let rng = args.get_one::<String>("rng").unwrap();
let serial = args.get_one::<String>("serial").unwrap();
let firmware = args.get_one::<String>("firmware").map(|x| x as &str);
let kernel = args.get_one::<String>("kernel").map(|x| x as &str);
let initramfs = args.get_one::<String>("initramfs").map(|x| x as &str);
let cmdline = args.get_one::<String>("cmdline").map(|x| x as &str);
let rate_limit_groups: Option<Vec<&str>> = args
.get_many::<String>("rate-limit-group")
.map(|x| x.map(|y| y as &str).collect());
let disks: Option<Vec<&str>> = args
.get_many::<String>("disk")
.map(|x| x.map(|y| y as &str).collect());
let net: Option<Vec<&str>> = args
.get_many::<String>("net")
.map(|x| x.map(|y| y as &str).collect());
let console = args.get_one::<String>("console").unwrap();
#[cfg(target_arch = "x86_64")]
let debug_console = args.get_one::<String>("debug-console").unwrap().as_str();
let balloon = args.get_one::<String>("balloon").map(|x| x as &str);
let fs: Option<Vec<&str>> = args
.get_many::<String>("fs")
.map(|x| x.map(|y| y as &str).collect());
let generic_vhost_user: Option<Vec<&str>> = args
.get_many::<String>("generic-vhost-user")
.map(|x| x.map(|y| y as &str).collect());
let pmem: Option<Vec<&str>> = args
.get_many::<String>("pmem")
.map(|x| x.map(|y| y as &str).collect());
let devices: Option<Vec<&str>> = args
.get_many::<String>("device")
.map(|x| x.map(|y| y as &str).collect());
let user_devices: Option<Vec<&str>> = args
.get_many::<String>("user-device")
.map(|x| x.map(|y| y as &str).collect());
let vdpa: Option<Vec<&str>> = args
.get_many::<String>("vdpa")
.map(|x| x.map(|y| y as &str).collect());
let vsock: Option<&str> = args.get_one::<String>("vsock").map(|x| x as &str);
#[cfg(feature = "pvmemcontrol")]
let pvmemcontrol = args.get_flag("pvmemcontrol");
let pvpanic = args.get_flag("pvpanic");
let numa: Option<Vec<&str>> = args
.get_many::<String>("numa")
.map(|x| x.map(|y| y as &str).collect());
let watchdog = args.get_flag("watchdog");
let rtc: Option<&str> = args.get_one::<String>("rtc").map(|x| x as &str);
let pci_segments: Option<Vec<&str>> = args
.get_many::<String>("pci-segment")
.map(|x| x.map(|y| y as &str).collect());
let platform = args.get_one::<String>("platform").map(|x| x as &str);
#[cfg(feature = "guest_debug")]
let gdb = args.contains_id("gdb");
let tpm: Option<&str> = args.get_one::<String>("tpm").map(|x| x as &str);
#[cfg(feature = "igvm")]
let igvm = args.get_one::<String>("igvm").map(|x| x as &str);
#[cfg(feature = "sev_snp")]
let host_data = args.get_one::<String>("host-data").map(|x| x as &str);
let landlock_enable = args.get_flag("landlock");
let landlock_rules: Option<Vec<&str>> = args
.get_many::<String>("landlock-rules")
.map(|x| x.map(|y| y as &str).collect());
#[cfg(feature = "fw_cfg")]
let fw_cfg_config: Option<&str> =
args.get_one::<String>("fw-cfg-config").map(|x| x as &str);
#[cfg(feature = "ivshmem")]
let ivshmem: Option<&str> = args.get_one::<String>("ivshmem").map(|x| x as &str);
VmParams {
cpus,
memory,
memory_zones,
firmware,
kernel,
initramfs,
cmdline,
rate_limit_groups,
disks,
net,
rng,
balloon,
fs,
generic_vhost_user,
pmem,
serial,
console,
#[cfg(target_arch = "x86_64")]
debug_console,
devices,
user_devices,
vdpa,
vsock,
#[cfg(feature = "pvmemcontrol")]
pvmemcontrol,
pvpanic,
numa,
watchdog,
rtc,
#[cfg(feature = "guest_debug")]
gdb,
pci_segments,
platform,
tpm,
#[cfg(feature = "igvm")]
igvm,
#[cfg(feature = "sev_snp")]
host_data,
landlock_enable,
landlock_rules,
#[cfg(feature = "fw_cfg")]
fw_cfg_config,
#[cfg(feature = "ivshmem")]
ivshmem,
}
}
}
#[derive(Debug)]
pub enum ParseHotplugMethodError {
InvalidValue(String),
}
impl FromStr for HotplugMethod {
type Err = ParseHotplugMethodError;
fn from_str(s: &str) -> result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"acpi" => Ok(HotplugMethod::Acpi),
"virtio-mem" => Ok(HotplugMethod::VirtioMem),
_ => Err(ParseHotplugMethodError::InvalidValue(s.to_owned())),
}
}
}
pub enum ParseCoreSchedulingError {
InvalidValue(String),
}
impl FromStr for CoreScheduling {
type Err = ParseCoreSchedulingError;
fn from_str(s: &str) -> result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"vm" => Ok(CoreScheduling::Vm),
"vcpu" => Ok(CoreScheduling::Vcpu),
"off" => Ok(CoreScheduling::Off),
_ => Err(ParseCoreSchedulingError::InvalidValue(s.to_owned())),
}
}
}
pub enum CpuTopologyParseError {
InvalidValue(String),
}
impl FromStr for CpuTopology {
type Err = CpuTopologyParseError;
fn from_str(s: &str) -> result::Result<Self, Self::Err> {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 4 {
return Err(Self::Err::InvalidValue(s.to_owned()));
}
let t = CpuTopology {
threads_per_core: parts[0]
.parse()
.map_err(|_| Self::Err::InvalidValue(s.to_owned()))?,
cores_per_die: parts[1]
.parse()
.map_err(|_| Self::Err::InvalidValue(s.to_owned()))?,
dies_per_package: parts[2]
.parse()
.map_err(|_| Self::Err::InvalidValue(s.to_owned()))?,
packages: parts[3]
.parse()
.map_err(|_| Self::Err::InvalidValue(s.to_owned()))?,
};
Ok(t)
}
}
impl CpusConfig {
pub fn parse(cpus: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add("boot")
.add("max")
.add("topology")
.add("kvm_hyperv")
.add("max_phys_bits")
.add("affinity")
.add("features")
.add("nested")
.add("core_scheduling")
.add("profile");
parser.parse(cpus).map_err(Error::ParseCpus)?;
let boot_vcpus: u32 = parser
.convert("boot")
.map_err(Error::ParseCpus)?
.unwrap_or(DEFAULT_VCPUS);
let max_vcpus: u32 = parser
.convert("max")
.map_err(Error::ParseCpus)?
.unwrap_or(boot_vcpus);
let topology = parser.convert("topology").map_err(Error::ParseCpus)?;
let kvm_hyperv = parser
.convert::<Toggle>("kvm_hyperv")
.map_err(Error::ParseCpus)?
.unwrap_or(Toggle(false))
.0;
let max_phys_bits = parser
.convert::<u8>("max_phys_bits")
.map_err(Error::ParseCpus)?
.unwrap_or(DEFAULT_MAX_PHYS_BITS);
let affinity = parser
.convert::<TupleList<u32, Vec<usize>>>("affinity")
.map_err(Error::ParseCpus)?
.map(|v| {
v.0.iter()
.map(|Tuple(e1, e2)| CpuAffinity {
vcpu: *e1,
host_cpus: e2.clone().into_boxed_slice(),
})
.collect()
});
let profile = parser
.convert::<CpuProfile>("profile")
.map_err(Error::ParseCpus)?
.unwrap_or_default();
let features_list = parser
.convert::<StringList>("features")
.map_err(Error::ParseCpus)?
.unwrap_or_default();
#[allow(unused_mut)]
let mut features = CpuFeatures::default();
{
#[cfg(target_arch = "x86_64")]
for feature in features_list.0 {
match feature.as_str() {
"amx" => features.amx = true,
_ => return Err(Error::InvalidCpuFeatures(feature)),
}
}
#[cfg(not(target_arch = "x86_64"))]
if let Some(feature) = features_list.0.into_iter().next() {
return Err(Error::InvalidCpuFeatures(feature));
}
}
let nested = parser
.convert::<Toggle>("nested")
.map_err(Error::ParseCpus)?
.is_none_or(|toggle| toggle.0);
let core_scheduling = parser
.convert("core_scheduling")
.map_err(Error::ParseCpus)?
.unwrap_or(CoreScheduling::Vm);
Ok(CpusConfig {
boot_vcpus,
max_vcpus,
topology,
kvm_hyperv,
max_phys_bits,
affinity,
features,
nested,
core_scheduling,
profile,
})
}
}
impl PciSegmentConfig {
pub const SYNTAX: &'static str = "PCI Segment parameters \
\"pci_segment=<segment_id>,mmio32_aperture_weight=<scale>,mmio64_aperture_weight=<scale>\"";
pub fn parse(disk: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add("mmio32_aperture_weight")
.add("mmio64_aperture_weight")
.add("pci_segment");
parser.parse(disk).map_err(Error::ParsePciSegment)?;
let pci_segment = parser
.convert("pci_segment")
.map_err(Error::ParsePciSegment)?
.unwrap_or_default();
let mmio32_aperture_weight = parser
.convert("mmio32_aperture_weight")
.map_err(Error::ParsePciSegment)?
.unwrap_or(DEFAULT_PCI_SEGMENT_APERTURE_WEIGHT);
let mmio64_aperture_weight = parser
.convert("mmio64_aperture_weight")
.map_err(Error::ParsePciSegment)?
.unwrap_or(DEFAULT_PCI_SEGMENT_APERTURE_WEIGHT);
Ok(PciSegmentConfig {
pci_segment,
mmio32_aperture_weight,
mmio64_aperture_weight,
})
}
pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
let num_pci_segments = match &vm_config.platform {
Some(platform_config) => platform_config.num_pci_segments,
None => 1,
};
if self.pci_segment >= num_pci_segments {
return Err(ValidationError::InvalidPciSegment(self.pci_segment));
}
if self.mmio32_aperture_weight == 0 {
return Err(ValidationError::InvalidPciSegmentApertureWeight(
self.mmio32_aperture_weight,
));
}
if self.mmio64_aperture_weight == 0 {
return Err(ValidationError::InvalidPciSegmentApertureWeight(
self.mmio64_aperture_weight,
));
}
Ok(())
}
}
impl PlatformConfig {
pub fn syntax() -> &'static str {
static SYNTAX: LazyLock<String> = LazyLock::new(|| {
let mut syntax = "Platform configuration parameters \
\"num_pci_segments=<num_pci_segments>,iommu_segments=<list_of_segments>,\
iommu_address_width=<bits>,iommufd=on|off,iommufd_fd=<fd>,vfio_p2p_dma=on|off,\
system_manufacturer=<dmi_system_manufacturer>,\
system_product_name=<dmi_system_product_name>,system_version=<dmi_system_version>,\
system_serial_number=<dmi_system_serial_number>,system_uuid=<dmi_system_uuid>,\
system_sku_number=<dmi_system_sku_number>,system_family=<dmi_system_family>,\
oem_strings=<list_of_strings>,chassis_asset_tag=<dmi_chassis_asset_tag>"
.to_string();
if cfg!(feature = "tdx") {
syntax.push_str(",tdx=on|off");
}
if cfg!(feature = "sev_snp") {
syntax.push_str(",sev_snp=on|off");
}
syntax.push('"');
syntax
});
&SYNTAX
}
pub fn parse(platform: &str) -> Result<Self> {
struct StringField {
key: &'static str,
apply: fn(&mut PlatformConfig, String),
}
const SMBIOS_STRING_FIELDS: &[StringField] = &[
StringField {
key: "system_manufacturer",
apply: |config, value| config.system_manufacturer = Some(value),
},
StringField {
key: "system_product_name",
apply: |config, value| config.system_product_name = Some(value),
},
StringField {
key: "system_version",
apply: |config, value| config.system_version = Some(value),
},
StringField {
key: "system_serial_number",
apply: |config, value| config.system_serial_number = Some(value),
},
StringField {
key: "system_uuid",
apply: |config, value| config.system_uuid = Some(value),
},
StringField {
key: "system_sku_number",
apply: |config, value| config.system_sku_number = Some(value),
},
StringField {
key: "system_family",
apply: |config, value| config.system_family = Some(value),
},
StringField {
key: "chassis_asset_tag",
apply: |config, value| config.chassis_asset_tag = Some(value),
},
];
let mut parser = OptionParser::new();
parser
.add("num_pci_segments")
.add("iommu_segments")
.add("iommu_address_width")
.add("serial_number")
.add("uuid")
.add("oem_strings")
.add("iommufd")
.add("iommufd_fd")
.add("vfio_p2p_dma");
for field in SMBIOS_STRING_FIELDS {
parser.add(field.key);
}
#[cfg(feature = "tdx")]
parser.add("tdx");
#[cfg(feature = "sev_snp")]
parser.add("sev_snp");
parser.parse(platform).map_err(Error::ParsePlatform)?;
let num_pci_segments: u16 = parser
.convert("num_pci_segments")
.map_err(Error::ParsePlatform)?
.unwrap_or(DEFAULT_NUM_PCI_SEGMENTS);
let iommu_segments = parser
.convert::<IntegerList>("iommu_segments")
.map_err(Error::ParsePlatform)?
.map(|v| v.0.iter().map(|e| *e as u16).collect());
let iommu_address_width_bits: u8 = parser
.convert("iommu_address_width")
.map_err(Error::ParsePlatform)?
.unwrap_or(MAX_IOMMU_ADDRESS_WIDTH_BITS);
let oem_strings = parser
.convert::<StringList>("oem_strings")
.map_err(Error::ParsePlatform)?
.map(|v| v.0.into_boxed_slice());
let iommufd_fd = parser
.convert::<i32>("iommufd_fd")
.map_err(Error::ParsePlatform)?;
// `iommufd_fd=<n>` implies `iommufd=on` unless the user explicitly set the value.
let iommufd = parser
.convert::<Toggle>("iommufd")
.map_err(Error::ParsePlatform)?
.map_or(iommufd_fd.is_some(), |Toggle(v)| v);
let vfio_p2p_dma = parser
.convert::<Toggle>("vfio_p2p_dma")
.map_err(Error::ParsePlatform)?
.unwrap_or(Toggle(true))
.0;
#[cfg(feature = "tdx")]
let tdx = parser
.convert::<Toggle>("tdx")
.map_err(Error::ParsePlatform)?
.unwrap_or(Toggle(false))
.0;
#[cfg(feature = "sev_snp")]
let sev_snp = parser
.convert::<Toggle>("sev_snp")
.map_err(Error::ParsePlatform)?
.unwrap_or(Toggle(false))
.0;
let mut platform_config = PlatformConfig {
num_pci_segments,
iommu_segments,
iommu_address_width_bits,
system_serial_number: None,
system_uuid: None,
oem_strings,
system_manufacturer: None,
system_product_name: None,
system_version: None,
system_family: None,
system_sku_number: None,
chassis_asset_tag: None,
iommufd,
iommufd_fd,
#[cfg(feature = "tdx")]
tdx,
#[cfg(feature = "sev_snp")]
sev_snp,
vfio_p2p_dma,
};
for field in SMBIOS_STRING_FIELDS {
if let Some(value) = parser
.convert::<String>(field.key)
.map_err(Error::ParsePlatform)?
{
(field.apply)(&mut platform_config, value);
}
}
let legacy_serial_number = parser
.convert::<String>("serial_number")
.map_err(Error::ParsePlatform)?;
if legacy_serial_number.is_some() {
warn!("'serial_number' in --platform is deprecated; use 'system_serial_number'.");
}
platform_config.system_serial_number = platform_config
.system_serial_number
.or(legacy_serial_number);
let legacy_uuid = parser
.convert::<String>("uuid")
.map_err(Error::ParsePlatform)?;
if legacy_uuid.is_some() {
warn!("'uuid' in --platform is deprecated; use 'system_uuid'.");
}
platform_config.system_uuid = platform_config.system_uuid.or(legacy_uuid);
Ok(platform_config)
}
pub fn validate(&self) -> ValidationResult<()> {
if self.num_pci_segments == 0 || self.num_pci_segments > MAX_NUM_PCI_SEGMENTS {
return Err(ValidationError::InvalidNumPciSegments(
self.num_pci_segments,
));
}
if let Some(iommu_segments) = &self.iommu_segments {
for segment in iommu_segments {
if *segment >= self.num_pci_segments {
return Err(ValidationError::InvalidPciSegment(*segment));
}
}
}
if self.iommu_address_width_bits > MAX_IOMMU_ADDRESS_WIDTH_BITS {
return Err(ValidationError::InvalidIommuAddressWidthBits(
self.iommu_address_width_bits,
));
}
if self.iommufd_fd.is_some() && !self.iommufd {
return Err(ValidationError::IommufdFdRequiresIommufd);
}
Ok(())
}
}
impl MemoryConfig {
#[expect(clippy::needless_pass_by_value)]
pub fn parse(memory: &str, memory_zones: Option<Vec<&str>>) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add("size")
.add("file")
.add("mergeable")
.add("hotplug_method")
.add("hotplug_size")
.add("hotplugged_size")
.add("shared")
.add("hugepages")
.add("hugepage_size")
.add("prefault")
.add("reserve")
.add("thp");
parser.parse(memory).map_err(Error::ParseMemory)?;
let size = parser
.convert::<ByteSized>("size")
.map_err(Error::ParseMemory)?
.unwrap_or(ByteSized(DEFAULT_MEMORY_MB << 20))
.0;
let mergeable = parser
.convert::<Toggle>("mergeable")
.map_err(Error::ParseMemory)?
.unwrap_or(Toggle(false))
.0;
let hotplug_method = parser
.convert("hotplug_method")
.map_err(Error::ParseMemory)?
.unwrap_or_default();
let hotplug_size = parser
.convert::<ByteSized>("hotplug_size")
.map_err(Error::ParseMemory)?
.map(|v| v.0);
let hotplugged_size = parser
.convert::<ByteSized>("hotplugged_size")
.map_err(Error::ParseMemory)?
.map(|v| v.0);
let shared = parser
.convert::<Toggle>("shared")
.map_err(Error::ParseMemory)?
.unwrap_or(Toggle(false))
.0;
let hugepages = parser
.convert::<Toggle>("hugepages")
.map_err(Error::ParseMemory)?
.unwrap_or(Toggle(false))
.0;
let hugepage_size = parser
.convert::<ByteSized>("hugepage_size")
.map_err(Error::ParseMemory)?
.map(|v| v.0);
let prefault = parser
.convert::<Toggle>("prefault")
.map_err(Error::ParseMemory)?
.unwrap_or(Toggle(false))
.0;
let reserve = parser
.convert::<Toggle>("reserve")
.map_err(Error::ParseMemory)?
.unwrap_or(Toggle(false))
.0;
let thp = parser
.convert::<Toggle>("thp")
.map_err(Error::ParseMemory)?
.unwrap_or(Toggle(true))
.0;
let zones: Option<Vec<MemoryZoneConfig>> = if let Some(memory_zones) = &memory_zones {
let mut zones = Vec::new();
for memory_zone in memory_zones.iter() {
let mut parser = OptionParser::new();
parser
.add("id")
.add("size")
.add("file")
.add("shared")
.add("hugepages")
.add("hugepage_size")
.add("host_numa_node")
.add("hotplug_size")
.add("hotplugged_size")
.add("prefault")
.add("reserve")
.add("mergeable");
parser.parse(memory_zone).map_err(Error::ParseMemoryZone)?;
let id = parser.get("id").ok_or(Error::ParseMemoryZoneIdMissing)?;
let size = parser
.convert::<ByteSized>("size")
.map_err(Error::ParseMemoryZone)?
.unwrap_or(ByteSized(DEFAULT_MEMORY_MB << 20))
.0;
let file = parser.get("file").map(PathBuf::from);
let shared = parser
.convert::<Toggle>("shared")
.map_err(Error::ParseMemoryZone)?
.unwrap_or(Toggle(false))
.0;
let hugepages = parser
.convert::<Toggle>("hugepages")
.map_err(Error::ParseMemoryZone)?
.unwrap_or(Toggle(false))
.0;
let hugepage_size = parser
.convert::<ByteSized>("hugepage_size")
.map_err(Error::ParseMemoryZone)?
.map(|v| v.0);
let host_numa_node = parser
.convert::<u32>("host_numa_node")
.map_err(Error::ParseMemoryZone)?;
let hotplug_size = parser
.convert::<ByteSized>("hotplug_size")
.map_err(Error::ParseMemoryZone)?
.map(|v| v.0);
let hotplugged_size = parser
.convert::<ByteSized>("hotplugged_size")
.map_err(Error::ParseMemoryZone)?
.map(|v| v.0);
let prefault = parser
.convert::<Toggle>("prefault")
.map_err(Error::ParseMemoryZone)?
.unwrap_or(Toggle(false))
.0;
let reserve = parser
.convert::<Toggle>("reserve")
.map_err(Error::ParseMemoryZone)?
.unwrap_or(Toggle(false))
.0;
let mergeable = parser
.convert::<Toggle>("mergeable")
.map_err(Error::ParseMemoryZone)?
.unwrap_or(Toggle(mergeable))
.0;
zones.push(MemoryZoneConfig {
id,
size,
file,
shared,
hugepages,
hugepage_size,
host_numa_node,
hotplug_size,
hotplugged_size,
prefault,
reserve,
mergeable,
});
}
Some(zones)
} else {
None
};
Ok(MemoryConfig {
size,
mergeable,
hotplug_method,
hotplug_size,
hotplugged_size,
shared,
hugepages,
hugepage_size,
prefault,
reserve,
zones,
thp,
})
}
pub fn total_size(&self) -> u64 {
self.size
+ self
.zones
.iter()
.flatten()
.map(|zone| zone.size)
.sum::<u64>()
+ self.hotplugged_size()
}
pub fn hotplugged_size(&self) -> u64 {
self.hotplugged_size.unwrap_or(0)
+ self
.zones
.iter()
.flatten()
.filter_map(|zone| zone.hotplugged_size)
.sum::<u64>()
}
}
impl RateLimiterGroupConfig {
pub const SYNTAX: &'static str = "Rate Limit Group parameters \
\"bw_size=<bytes>,bw_one_time_burst=<bytes>,bw_refill_time=<ms>,\
ops_size=<io_ops>,ops_one_time_burst=<io_ops>,ops_refill_time=<ms>,\
id=<device_id>\"";
pub fn parse(rate_limit_group: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add("bw_size")
.add("bw_one_time_burst")
.add("bw_refill_time")
.add("ops_size")
.add("ops_one_time_burst")
.add("ops_refill_time")
.add("id");
parser
.parse(rate_limit_group)
.map_err(Error::ParseRateLimiterGroup)?;
let id = parser.get("id").unwrap_or_default();
let bw_size = parser
.convert("bw_size")
.map_err(Error::ParseRateLimiterGroup)?
.unwrap_or_default();
let bw_one_time_burst = parser
.convert("bw_one_time_burst")
.map_err(Error::ParseRateLimiterGroup)?
.unwrap_or_default();
let bw_refill_time = parser
.convert("bw_refill_time")
.map_err(Error::ParseRateLimiterGroup)?
.unwrap_or_default();
let ops_size = parser
.convert("ops_size")
.map_err(Error::ParseRateLimiterGroup)?
.unwrap_or_default();
let ops_one_time_burst = parser
.convert("ops_one_time_burst")
.map_err(Error::ParseRateLimiterGroup)?
.unwrap_or_default();
let ops_refill_time = parser
.convert("ops_refill_time")
.map_err(Error::ParseRateLimiterGroup)?
.unwrap_or_default();
let bw_tb_config = if bw_size != 0 && bw_refill_time != 0 {
Some(TokenBucketConfig {
size: bw_size,
one_time_burst: Some(bw_one_time_burst),
refill_time: bw_refill_time,
})
} else {
None
};
let ops_tb_config = if ops_size != 0 && ops_refill_time != 0 {
Some(TokenBucketConfig {
size: ops_size,
one_time_burst: Some(ops_one_time_burst),
refill_time: ops_refill_time,
})
} else {
None
};
Ok(RateLimiterGroupConfig {
id,
rate_limiter_config: RateLimiterConfig {
bandwidth: bw_tb_config,
ops: ops_tb_config,
},
})
}
pub fn validate(&self, _vm_config: &VmConfig) -> ValidationResult<()> {
if self.rate_limiter_config.bandwidth.is_none() && self.rate_limiter_config.ops.is_none() {
return Err(ValidationError::InvalidRateLimiterGroup);
}
if self.id.is_empty() {
return Err(ValidationError::InvalidRateLimiterGroup);
}
Ok(())
}
}
impl PciDeviceCommonConfig {
const OPTIONS: &[&str] = &["id", "pci_segment", "pci_device_id"];
const OPTIONS_IOMMU: &[&str] = &["id", "iommu", "pci_segment", "pci_device_id"];
pub fn parse(input: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser.add_all(Self::OPTIONS_IOMMU);
parser
.parse_subset(input)
.map_err(Error::ParsePciDeviceCommonConfig)?;
let id = parser.get("id");
let iommu = parser
.convert::<Toggle>("iommu")
.map_err(Error::ParsePciDeviceCommonConfig)?
.unwrap_or(Toggle(false))
.0;
let pci_segment = parser
.convert("pci_segment")
.map_err(Error::ParsePciDeviceCommonConfig)?
.unwrap_or_default();
let pci_device_id = parser
.convert::<u8>("pci_device_id")
.map_err(Error::ParsePciDeviceCommonConfig)?;
Ok(Self {
id,
iommu,
pci_segment,
pci_device_id,
})
}
pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
let num_pci_segments = vm_config
.platform
.as_ref()
.map_or(DEFAULT_NUM_PCI_SEGMENTS, |platform_config| {
platform_config.num_pci_segments
});
if self.pci_segment >= num_pci_segments {
return Err(ValidationError::InvalidPciSegment(self.pci_segment));
}
if let Some(platform_config) = vm_config.platform.as_ref()
&& let Some(iommu_segments) = platform_config.iommu_segments.as_ref()
&& iommu_segments.contains(&self.pci_segment)
&& !self.iommu
{
return Err(ValidationError::OnIommuSegment(self.pci_segment));
}
if let Some(device_id) = self.pci_device_id {
validate_pci_device_id(device_id)?;
}
Ok(())
}
}
impl DiskConfig {
pub const SYNTAX: &'static str = "Disk parameters \
\"path=<disk_image_path>,readonly=on|off,direct=on|off,iommu=on|off,\
num_queues=<number_of_queues>,queue_size=<size_of_each_queue>,\
vhost_user=on|off,socket=<vhost_user_socket_path>,\
bw_size=<bytes>,bw_one_time_burst=<bytes>,bw_refill_time=<ms>,\
ops_size=<io_ops>,ops_one_time_burst=<io_ops>,ops_refill_time=<ms>,\
id=<device_id>,pci_segment=<segment_id>,pci_device_id=<pci_slot>,\
rate_limit_group=<group_id>,\
queue_affinity=<list_of_queue_indices_with_their_associated_cpuset>,\
serial=<serial_number>,backing_files=on|off,sparse=on|off,\
image_type=<raw,qcow2,vhd,vhdx>,lock_granularity=byte-range|full";
pub fn parse(disk: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add("path")
.add("readonly")
.add("direct")
.add("queue_size")
.add("num_queues")
.add("vhost_user")
.add("socket")
.add("bw_size")
.add("bw_one_time_burst")
.add("bw_refill_time")
.add("ops_size")
.add("ops_one_time_burst")
.add("ops_refill_time")
.add("_disable_io_uring")
.add("_disable_aio")
.add("serial")
.add("rate_limit_group")
.add("queue_affinity")
.add("backing_files")
.add("sparse")
.add("image_type")
.add("lock_granularity")
.add_all(PciDeviceCommonConfig::OPTIONS_IOMMU);
parser.parse(disk).map_err(Error::ParseDisk)?;
let path = parser.get("path").map(PathBuf::from);
let readonly = parser
.convert::<Toggle>("readonly")
.map_err(Error::ParseDisk)?
.unwrap_or(Toggle(false))
.0;
let direct = parser
.convert::<Toggle>("direct")
.map_err(Error::ParseDisk)?
.unwrap_or(Toggle(false))
.0;
let queue_size = parser
.convert("queue_size")
.map_err(Error::ParseDisk)?
.unwrap_or_else(default_diskconfig_queue_size);
let num_queues = parser
.convert("num_queues")
.map_err(Error::ParseDisk)?
.unwrap_or_else(default_diskconfig_num_queues);
let vhost_user = parser
.convert::<Toggle>("vhost_user")
.map_err(Error::ParseDisk)?
.unwrap_or(Toggle(false))
.0;
let vhost_socket = parser.get("socket");
let disable_io_uring = parser
.convert::<Toggle>("_disable_io_uring")
.map_err(Error::ParseDisk)?
.unwrap_or(Toggle(false))
.0;
let disable_aio = parser
.convert::<Toggle>("_disable_aio")
.map_err(Error::ParseDisk)?
.unwrap_or(Toggle(false))
.0;
let rate_limit_group = parser.get("rate_limit_group");
let bw_size = parser
.convert("bw_size")
.map_err(Error::ParseDisk)?
.unwrap_or_default();
let bw_one_time_burst = parser
.convert("bw_one_time_burst")
.map_err(Error::ParseDisk)?
.unwrap_or_default();
let bw_refill_time = parser
.convert("bw_refill_time")
.map_err(Error::ParseDisk)?
.unwrap_or_default();
let ops_size = parser
.convert("ops_size")
.map_err(Error::ParseDisk)?
.unwrap_or_default();
let ops_one_time_burst = parser
.convert("ops_one_time_burst")
.map_err(Error::ParseDisk)?
.unwrap_or_default();
let ops_refill_time = parser
.convert("ops_refill_time")
.map_err(Error::ParseDisk)?
.unwrap_or_default();
let serial = parser.get("serial");
let queue_affinity = parser
.convert::<TupleList<u16, Vec<usize>>>("queue_affinity")
.map_err(Error::ParseDisk)?
.map(|v| {
v.0.iter()
.map(|Tuple(e1, e2)| VirtQueueAffinity {
queue_index: *e1,
host_cpus: e2.clone().into_boxed_slice(),
})
.collect()
});
let backing_files = parser
.convert::<Toggle>("backing_files")
.map_err(Error::ParseDisk)?
.unwrap_or(Toggle(false))
.0;
let image_type = if vhost_socket.is_none() {
parser
.convert::<ImageType>("image_type")
.map_err(Error::ParseDisk)?
.unwrap_or(ImageType::Unknown)
} else {
ImageType::Unknown
};
let lock_granularity = parser
.convert::<LockGranularityChoice>("lock_granularity")
.map_err(Error::ParseDisk)?
.unwrap_or_default();
let bw_tb_config = if bw_size != 0 && bw_refill_time != 0 {
Some(TokenBucketConfig {
size: bw_size,
one_time_burst: Some(bw_one_time_burst),
refill_time: bw_refill_time,
})
} else {
None
};
let ops_tb_config = if ops_size != 0 && ops_refill_time != 0 {
Some(TokenBucketConfig {
size: ops_size,
one_time_burst: Some(ops_one_time_burst),
refill_time: ops_refill_time,
})
} else {
None
};
let rate_limiter_config = if bw_tb_config.is_some() || ops_tb_config.is_some() {
Some(RateLimiterConfig {
bandwidth: bw_tb_config,
ops: ops_tb_config,
})
} else {
None
};
let sparse = parser
.convert::<Toggle>("sparse")
.map_err(Error::ParseDisk)?
.unwrap_or_else(|| Toggle(default_diskconfig_sparse()))
.0;
let pci_common = PciDeviceCommonConfig::parse(disk)?;
Ok(DiskConfig {
pci_common,
path,
readonly,
direct,
num_queues,
queue_size,
vhost_user,
vhost_socket,
rate_limit_group,
rate_limiter_config,
disable_io_uring,
disable_aio,
serial,
queue_affinity,
backing_files,
sparse,
image_type,
lock_granularity,
})
}
pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
self.pci_common.validate(vm_config)?;
if self.num_queues > vm_config.cpus.boot_vcpus as usize {
return Err(ValidationError::TooManyQueues(
self.num_queues,
vm_config.cpus.boot_vcpus as usize,
));
}
validate_queue_size(self.queue_size)?;
if self.queue_size <= MINIMUM_BLOCK_QUEUE_SIZE {
return Err(ValidationError::BlockQueueSizeTooSmall(self.queue_size));
}
if self.vhost_user && self.pci_common.iommu {
return Err(ValidationError::IommuNotSupported);
}
if self.vhost_user && self.rate_limiter_config.is_some() {
return Err(ValidationError::VhostUserRateLimiterNotSupported);
}
if self.vhost_user && self.rate_limit_group.is_some() {
return Err(ValidationError::VhostUserRateLimiterNotSupported);
}
if self.rate_limiter_config.is_some() && self.rate_limit_group.is_some() {
return Err(ValidationError::InvalidRateLimiterGroup);
}
// Check Block device serial length
if let Some(ref serial) = self.serial
&& serial.len() > VIRTIO_BLK_ID_BYTES as usize
{
return Err(ValidationError::InvalidSerialLength(
serial.len(),
VIRTIO_BLK_ID_BYTES as usize,
));
}
Ok(())
}
}
#[derive(Debug)]
pub enum ParseVhostModeError {
InvalidValue(String),
}
impl FromStr for VhostMode {
type Err = ParseVhostModeError;
fn from_str(s: &str) -> result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"client" => Ok(VhostMode::Client),
"server" => Ok(VhostMode::Server),
_ => Err(ParseVhostModeError::InvalidValue(s.to_owned())),
}
}
}
impl NetConfig {
pub const SYNTAX: &'static str = "Network parameters \
\"tap=<if_name>,ip=<ip_addr>,mask=<net_mask>,mac=<mac_addr>,fd=<[fd1,fd2,...]>,iommu=on|off,\
num_queues=<number_of_queues>,queue_size=<size_of_each_queue>,id=<device_id>,\
vhost_user=<vhost_user_enable>,socket=<vhost_user_socket_path>,vhost_mode=client|server,\
bw_size=<bytes>,bw_one_time_burst=<bytes>,bw_refill_time=<ms>,\
ops_size=<io_ops>,ops_one_time_burst=<io_ops>,ops_refill_time=<ms>,\
pci_segment=<segment_id>,pci_device_id=<pci_slot>,\
offload_tso=on|off,offload_ufo=on|off,offload_csum=on|off\"";
pub fn parse(net: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add("tap")
.add("ip")
.add("mask")
.add("mac")
.add("host_mac")
.add("offload_tso")
.add("offload_ufo")
.add("offload_csum")
.add("mtu")
.add("queue_size")
.add("num_queues")
.add("vhost_user")
.add("socket")
.add("vhost_mode")
.add("fd")
.add("bw_size")
.add("bw_one_time_burst")
.add("bw_refill_time")
.add("ops_size")
.add("ops_one_time_burst")
.add("ops_refill_time")
.add_all(PciDeviceCommonConfig::OPTIONS_IOMMU);
parser.parse(net).map_err(Error::ParseNetwork)?;
let tap = parser.get("tap");
let ip = parser.convert("ip").map_err(Error::ParseNetwork)?;
let mask = parser.convert("mask").map_err(Error::ParseNetwork)?;
let mac = parser
.convert("mac")
.map_err(Error::ParseNetwork)?
.unwrap_or_else(default_netconfig_mac);
let host_mac = parser.convert("host_mac").map_err(Error::ParseNetwork)?;
let offload_tso = parser
.convert::<Toggle>("offload_tso")
.map_err(Error::ParseNetwork)?
.unwrap_or(Toggle(true))
.0;
let offload_ufo = parser
.convert::<Toggle>("offload_ufo")
.map_err(Error::ParseNetwork)?
.unwrap_or(Toggle(true))
.0;
let offload_csum = parser
.convert::<Toggle>("offload_csum")
.map_err(Error::ParseNetwork)?
.unwrap_or(Toggle(true))
.0;
let mtu = parser.convert("mtu").map_err(Error::ParseNetwork)?;
let queue_size = parser
.convert("queue_size")
.map_err(Error::ParseNetwork)?
.unwrap_or_else(default_netconfig_queue_size);
let num_queues = parser
.convert("num_queues")
.map_err(Error::ParseNetwork)?
.unwrap_or_else(default_netconfig_num_queues);
let vhost_user = parser
.convert::<Toggle>("vhost_user")
.map_err(Error::ParseNetwork)?
.unwrap_or(Toggle(false))
.0;
let vhost_socket = parser.get("socket");
let vhost_mode = parser
.convert("vhost_mode")
.map_err(Error::ParseNetwork)?
.unwrap_or_default();
let fds = parser
.convert::<IntegerList>("fd")
.map_err(Error::ParseNetwork)?
.map(|v| v.0.iter().map(|e| *e as i32).collect());
let bw_size = parser
.convert("bw_size")
.map_err(Error::ParseNetwork)?
.unwrap_or_default();
let bw_one_time_burst = parser
.convert("bw_one_time_burst")
.map_err(Error::ParseNetwork)?
.unwrap_or_default();
let bw_refill_time = parser
.convert("bw_refill_time")
.map_err(Error::ParseNetwork)?
.unwrap_or_default();
let ops_size = parser
.convert("ops_size")
.map_err(Error::ParseNetwork)?
.unwrap_or_default();
let ops_one_time_burst = parser
.convert("ops_one_time_burst")
.map_err(Error::ParseNetwork)?
.unwrap_or_default();
let ops_refill_time = parser
.convert("ops_refill_time")
.map_err(Error::ParseNetwork)?
.unwrap_or_default();
let bw_tb_config = if bw_size != 0 && bw_refill_time != 0 {
Some(TokenBucketConfig {
size: bw_size,
one_time_burst: Some(bw_one_time_burst),
refill_time: bw_refill_time,
})
} else {
None
};
let ops_tb_config = if ops_size != 0 && ops_refill_time != 0 {
Some(TokenBucketConfig {
size: ops_size,
one_time_burst: Some(ops_one_time_burst),
refill_time: ops_refill_time,
})
} else {
None
};
let rate_limiter_config = if bw_tb_config.is_some() || ops_tb_config.is_some() {
Some(RateLimiterConfig {
bandwidth: bw_tb_config,
ops: ops_tb_config,
})
} else {
None
};
let pci_common = PciDeviceCommonConfig::parse(net)?;
let config = NetConfig {
pci_common,
tap,
ip,
mask,
mac,
host_mac,
mtu,
num_queues,
queue_size,
vhost_user,
vhost_socket,
vhost_mode,
fds,
rate_limiter_config,
offload_tso,
offload_ufo,
offload_csum,
};
Ok(config)
}
pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
self.pci_common.validate(vm_config)?;
if self.num_queues < 2 {
return Err(ValidationError::VnetQueueLowerThan2(self.num_queues));
}
if let Some(fds) = &self.fds {
let actual_queues = fds.len() * 2;
if actual_queues != self.num_queues {
return Err(ValidationError::VnetQueueFdMismatch(
self.num_queues,
actual_queues,
));
}
for &fd in fds {
if fd <= 2 {
return Err(ValidationError::VnetReservedFd(fd));
}
}
}
if (self.num_queues / 2) > vm_config.cpus.boot_vcpus as usize {
return Err(ValidationError::TooManyQueues(
self.num_queues,
vm_config.cpus.boot_vcpus as usize,
));
}
validate_queue_size(self.queue_size)?;
if self.vhost_user && self.pci_common.iommu {
return Err(ValidationError::IommuNotSupported);
}
if self.vhost_user && self.rate_limiter_config.is_some() {
return Err(ValidationError::VhostUserRateLimiterNotSupported);
}
if let Some(mtu) = self.mtu
&& mtu < net::MIN_MTU
{
return Err(ValidationError::InvalidMtu(mtu));
}
if !self.offload_csum && (self.offload_tso || self.offload_ufo) {
return Err(ValidationError::NoHardwareChecksumOffload);
}
if self.mask.is_some() && self.ip.is_none() {
return Err(ValidationError::MaskProvidedWithoutIp);
}
if self.ip.is_some() && self.mask.is_none() {
return Err(ValidationError::IpProvidedWithoutMask);
}
Ok(())
}
}
impl RngConfig {
pub const SYNTAX: &'static str = "Random number generator parameters \"\
src=<entropy_source_path>,iommu=on|off,pci_segment=<segment_id>,\
pci_device_id=<pci_slot>\"";
pub fn parse(rng: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add("src")
.add_all(PciDeviceCommonConfig::OPTIONS_IOMMU);
parser.parse(rng).map_err(Error::ParseRng)?;
let src = PathBuf::from(
parser
.get("src")
.unwrap_or_else(|| Self::DEFAULT_RNG_SOURCE.to_owned()),
);
let pci_common = PciDeviceCommonConfig::parse(rng)?;
Ok(RngConfig { src, pci_common })
}
pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
self.pci_common.validate(vm_config)
}
}
impl RtcConfig {
pub const SYNTAX: &'static str = "Virtio RTC parameters \"\
iommu=on|off,id=<device_id>,\
pci_segment=<segment_id>,pci_device_id=<pci_slot>\". \
Passing --rtc with no arguments enables the device with default \
settings.";
pub fn parse(rtc: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser.add_all(PciDeviceCommonConfig::OPTIONS_IOMMU);
parser.parse(rtc).map_err(Error::ParseRtc)?;
let pci_common = PciDeviceCommonConfig::parse(rtc)?;
Ok(RtcConfig { pci_common })
}
pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
self.pci_common.validate(vm_config)
}
}
impl BalloonConfig {
pub const SYNTAX: &'static str = "Balloon parameters \"size=<balloon_size>,deflate_on_oom=on|off,\
free_page_reporting=on|off,iommu=on|off,id=<device_id>,pci_segment=<segment_id>,\
pci_device_id=<pci_slot>\"";
pub fn parse(balloon: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser.add("size");
parser.add("deflate_on_oom");
parser.add("free_page_reporting");
parser.add_all(PciDeviceCommonConfig::OPTIONS_IOMMU);
parser.parse(balloon).map_err(Error::ParseBalloon)?;
let size = parser
.convert::<ByteSized>("size")
.map_err(Error::ParseBalloon)?
.map_or(0, |v| v.0);
let deflate_on_oom = parser
.convert::<Toggle>("deflate_on_oom")
.map_err(Error::ParseBalloon)?
.unwrap_or(Toggle(false))
.0;
let free_page_reporting = parser
.convert::<Toggle>("free_page_reporting")
.map_err(Error::ParseBalloon)?
.unwrap_or(Toggle(false))
.0;
let pci_common = PciDeviceCommonConfig::parse(balloon)?;
Ok(BalloonConfig {
pci_common,
size,
deflate_on_oom,
free_page_reporting,
})
}
pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
self.pci_common.validate(vm_config)
}
}
impl GenericVhostUserConfig {
pub const SYNTAX: &'static str = "generic vhost-user parameters \
\"device_type=<ID number for virtio device type (FS, block, net, etc) or symbolic name>,\
socket=<socket_path>,\
queue_sizes=<list of queue sizes>,\
id=<device_id>,pci_segment=<segment_id>,pci_device_id=<pci_slot>\"";
pub fn parse(vhost_user: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add("device_type")
// TODO: Remove 'virtio_id' as a deprecated alias for 'device_type'
.add("virtio_id")
.add("queue_sizes")
.add("socket")
.add_all(PciDeviceCommonConfig::OPTIONS);
parser
.parse(vhost_user)
.map_err(Error::ParseGenericVhostUser)?;
let socket = parser
.get("socket")
.ok_or(Error::ParseGenericVhostUserSockMissing)?;
let IntegerList(queue_sizes) = parser
.convert::<IntegerList<u16>>("queue_sizes")
.map_err(Error::ParseGenericVhostUser)?
.ok_or(Error::ParseGenericVhostUserQueueSizeMissing)?;
let legacy_virtio_id = parser
.convert::<String>("virtio_id")
.map_err(Error::ParseGenericVhostUser)?;
if legacy_virtio_id.is_some() {
warn!("'virtio_id' in --generic-vhost-user is deprecated; use 'device_type'.");
}
let device_type_str = parser
.convert::<String>("device_type")
.map_err(Error::ParseGenericVhostUser)?
.or(legacy_virtio_id)
.ok_or(Error::ParseGenericVhostUserVirtioIdMissing)?;
let device_type = match device_type_str.as_bytes() {
b"net" => VIRTIO_ID_NET,
b"block" => VIRTIO_ID_BLOCK,
b"console" => VIRTIO_ID_CONSOLE,
b"rng" => VIRTIO_ID_RNG,
b"balloon" => VIRTIO_ID_BALLOON,
b"iomem" => VIRTIO_ID_IOMEM,
b"rpmsg" => VIRTIO_ID_RPMSG,
b"scsi" => VIRTIO_ID_SCSI,
b"9p" => VIRTIO_ID_9P,
b"mac80211_wlan" => VIRTIO_ID_MAC80211_WLAN,
b"rproc_serial" => VIRTIO_ID_RPROC_SERIAL,
b"caif" => VIRTIO_ID_CAIF,
b"memory_balloon" => VIRTIO_ID_MEMORY_BALLOON,
b"gpu" => VIRTIO_ID_GPU,
b"clock" => VIRTIO_ID_CLOCK,
b"input" => VIRTIO_ID_INPUT,
b"vsock" => VIRTIO_ID_VSOCK,
b"crypto" => VIRTIO_ID_CRYPTO,
b"signal_dist" => VIRTIO_ID_SIGNAL_DIST,
b"pstore" => VIRTIO_ID_PSTORE,
b"iommu" => VIRTIO_ID_IOMMU,
b"mem" => VIRTIO_ID_MEM,
b"sound" => VIRTIO_ID_SOUND,
b"fs" => VIRTIO_ID_FS,
b"pmem" => VIRTIO_ID_PMEM,
b"rpmb" => VIRTIO_ID_RPMB,
b"mac80211_hwsim" => VIRTIO_ID_MAC80211_HWSIM,
b"video_encoder" => VIRTIO_ID_VIDEO_ENCODER,
b"video_decoder" => VIRTIO_ID_VIDEO_DECODER,
b"scmi" => VIRTIO_ID_SCMI,
b"nitro_sec_mod" => VIRTIO_ID_NITRO_SEC_MOD,
b"i2c" => VIRTIO_ID_I2C_ADAPTER,
b"watchdog" => VIRTIO_ID_WATCHDOG,
b"can" => VIRTIO_ID_CAN,
b"dmabuf" => VIRTIO_ID_DMABUF,
b"param_serv" => VIRTIO_ID_PARAM_SERV,
b"audio_policy" => VIRTIO_ID_AUDIO_POLICY,
b"bt" => VIRTIO_ID_BT,
b"gpio" => VIRTIO_ID_GPIO,
b"rdma" => 42,
b"camera" => 43,
b"ism" => 44,
b"spi" => 45,
b"tee" => 46,
b"cpu_balloon" => 47,
b"media" => 48,
b"usb" => 49,
[b'1'..=b'9', ..] => match device_type_str.parse() {
Ok(id) => id,
Err(_) => return Err(Error::ParseGenericVhostUserVirtioIdInvalid(device_type_str)),
},
_ => return Err(Error::ParseGenericVhostUserVirtioIdInvalid(device_type_str)),
};
match device_type {
// vhost-user devices of these types definitely cannot work.
// Cloud Hypervisor needs to know if an IOMMU exists so that it
// can perform address translation, and a vhost-user device has
// no supported way to reset the guest.
VIRTIO_ID_WATCHDOG | VIRTIO_ID_IOMMU => {
return Err(Error::ParseGenericVhostUserVirtioIdUnsupported(
device_type_str,
));
}
_ => {}
}
let pci_common = PciDeviceCommonConfig::parse(vhost_user)?;
Ok(GenericVhostUserConfig {
pci_common,
socket: socket.into(),
device_type,
queue_sizes,
})
}
pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
if self.pci_common.iommu {
return Err(ValidationError::IommuNotSupported);
}
for &queue_size in &self.queue_sizes {
validate_queue_size(queue_size)?;
}
self.pci_common.validate(vm_config)
}
}
impl FsConfig {
pub const SYNTAX: &'static str = "virtio-fs parameters \
\"tag=<tag_name>,socket=<socket_path>,num_queues=<number_of_queues>,\
queue_size=<size_of_each_queue>,id=<device_id>,\
pci_segment=<segment_id>,pci_device_id=<pci_slot>\"";
pub fn parse(fs: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add("tag")
.add("queue_size")
.add("num_queues")
.add("socket")
.add_all(PciDeviceCommonConfig::OPTIONS);
parser.parse(fs).map_err(Error::ParseFileSystem)?;
let tag = parser.get("tag").ok_or(Error::ParseFsTagMissing)?;
if tag.len() > vhost_user::VIRTIO_FS_TAG_LEN {
return Err(Error::ParseFsTagTooLong);
}
let socket = PathBuf::from(parser.get("socket").ok_or(Error::ParseFsSockMissing)?);
let queue_size = parser
.convert("queue_size")
.map_err(Error::ParseFileSystem)?
.unwrap_or_else(default_fsconfig_queue_size);
let num_queues = parser
.convert("num_queues")
.map_err(Error::ParseFileSystem)?
.unwrap_or_else(default_fsconfig_num_queues);
let pci_common = PciDeviceCommonConfig::parse(fs)?;
Ok(FsConfig {
pci_common,
tag,
socket,
num_queues,
queue_size,
})
}
pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
if self.num_queues > vm_config.cpus.boot_vcpus as usize {
return Err(ValidationError::TooManyQueues(
self.num_queues,
vm_config.cpus.boot_vcpus as usize,
));
}
validate_queue_size(self.queue_size)?;
if self.pci_common.iommu {
return Err(ValidationError::IommuNotSupported);
}
self.pci_common.validate(vm_config)
}
}
#[cfg(feature = "fw_cfg")]
impl FwCfgConfig {
pub const SYNTAX: &'static str = "Boot params to pass to FW CFG device \
\"e820=on|off,kernel=on|off,cmdline=on|off,initramfs=on|off,acpi_table=on|off, \
items=[name=<item_name>,file=<file_path>:name=<item_name>,string=<string_value>]\"";
pub fn parse(fw_cfg_config: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add("e820")
.add("kernel")
.add("cmdline")
.add("initramfs")
.add("acpi_table")
.add("items");
parser.parse(fw_cfg_config).map_err(Error::ParseFwCfgItem)?;
let e820 = parser
.convert::<Toggle>("e820")
.map_err(Error::ParseFwCfgItem)?
.unwrap_or(Toggle(true))
.0;
let kernel = parser
.convert::<Toggle>("kernel")
.map_err(Error::ParseFwCfgItem)?
.unwrap_or(Toggle(true))
.0;
let cmdline = parser
.convert::<Toggle>("cmdline")
.map_err(Error::ParseFwCfgItem)?
.unwrap_or(Toggle(true))
.0;
let initramfs = parser
.convert::<Toggle>("initramfs")
.map_err(Error::ParseFwCfgItem)?
.unwrap_or(Toggle(true))
.0;
let acpi_tables = parser
.convert::<Toggle>("acpi_table")
.map_err(Error::ParseFwCfgItem)?
.unwrap_or(Toggle(true))
.0;
let items = if parser.is_set("items") {
Some(
parser
.convert::<FwCfgItemList>("items")
.map_err(Error::ParseFwCfgItem)?
.unwrap(),
)
} else {
None
};
Ok(FwCfgConfig {
e820,
kernel,
cmdline,
initramfs,
acpi_tables,
items,
})
}
pub fn validate(&self, payload: &PayloadConfig) -> result::Result<(), PayloadConfigError> {
if self.kernel && payload.kernel.is_none() {
return Err(PayloadConfigError::FwCfgMissingKernel);
} else if self.cmdline && payload.cmdline.is_none() {
return Err(PayloadConfigError::FwCfgMissingCmdline);
} else if self.initramfs && payload.initramfs.is_none() {
return Err(PayloadConfigError::FwCfgMissingInitramfs);
}
if let Some(items) = &self.items {
for item in &items.item_list {
if item.file.is_some() == item.string.is_some() {
return Err(PayloadConfigError::FwCfgInvalidItem(item.name.clone()));
}
}
}
Ok(())
}
}
#[cfg(feature = "fw_cfg")]
impl FwCfgItem {
pub fn parse(fw_cfg: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser.add("name").add("file").add("string");
parser.parse(fw_cfg).map_err(Error::ParseFwCfgItem)?;
let name =
parser
.get("name")
.ok_or(Error::ParseFwCfgItem(OptionParserError::InvalidValue(
"missing FwCfgItem name".to_string(),
)))?;
let file = parser.get("file").map(PathBuf::from);
let string = parser.get("string");
Ok(FwCfgItem { name, file, string })
}
}
impl PmemConfig {
pub const SYNTAX: &'static str = "Persistent memory parameters \
\"file=<backing_file_path>,size=<persistent_memory_size>,iommu=on|off,\
discard_writes=on|off,id=<device_id>,\
pci_segment=<segment_id>,pci_device_id=<pci_slot>\"";
pub fn parse(pmem: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add("size")
.add("file")
.add("discard_writes")
.add_all(PciDeviceCommonConfig::OPTIONS_IOMMU);
parser.parse(pmem).map_err(Error::ParsePersistentMemory)?;
let pci_common = PciDeviceCommonConfig::parse(pmem)?;
let file = PathBuf::from(parser.get("file").ok_or(Error::ParsePmemFileMissing)?);
let size = parser
.convert::<ByteSized>("size")
.map_err(Error::ParsePersistentMemory)?
.map(|v| v.0);
let discard_writes = parser
.convert::<Toggle>("discard_writes")
.map_err(Error::ParsePersistentMemory)?
.unwrap_or(Toggle(false))
.0;
Ok(PmemConfig {
pci_common,
file,
size,
discard_writes,
})
}
pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
self.pci_common.validate(vm_config)
}
}
impl CommonConsoleConfig {
const VALUELESS_OPTIONS: &[&str] = &["off", "pty", "tty", "null"];
const VALUE_OPTIONS: &[&str] = &["file", "socket"];
fn parse(console: &str, map_err: impl Fn(OptionParserError) -> Error) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add_all_valueless(Self::VALUELESS_OPTIONS)
.add_all(Self::VALUE_OPTIONS);
parser.parse_subset(console).map_err(map_err)?;
let mut file: Option<PathBuf> = None;
let mut socket: Option<PathBuf> = None;
let mut mode: ConsoleOutputMode = ConsoleOutputMode::Off;
if parser.is_set("off") {
} else if parser.is_set("pty") {
mode = ConsoleOutputMode::Pty;
} else if parser.is_set("tty") {
mode = ConsoleOutputMode::Tty;
} else if parser.is_set("null") {
mode = ConsoleOutputMode::Null;
} else if parser.is_set("file") {
mode = ConsoleOutputMode::File;
file =
Some(PathBuf::from(parser.get("file").ok_or(
Error::Validation(ValidationError::ConsoleFileMissing),
)?));
} else if parser.is_set("socket") {
mode = ConsoleOutputMode::Socket;
socket = Some(PathBuf::from(parser.get("socket").ok_or(
Error::Validation(ValidationError::ConsoleSocketPathMissing),
)?));
} else {
return Err(Error::ParseConsoleInvalidModeGiven);
}
Ok(Self { mode, file, socket })
}
}
impl ConsoleConfig {
pub fn parse(console: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add_all_valueless(CommonConsoleConfig::VALUELESS_OPTIONS)
.add_all(CommonConsoleConfig::VALUE_OPTIONS)
.add_all(PciDeviceCommonConfig::OPTIONS_IOMMU);
parser.parse(console).map_err(Error::ParseConsole)?;
let common = CommonConsoleConfig::parse(console, Error::ParseConsole)?;
let pci_common = PciDeviceCommonConfig::parse(console)?;
Ok(Self { common, pci_common })
}
pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
self.pci_common.validate(vm_config)
}
}
impl SerialConfig {
pub fn parse(serial: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add_all_valueless(CommonConsoleConfig::VALUELESS_OPTIONS)
.add_all(CommonConsoleConfig::VALUE_OPTIONS);
parser.parse(serial).map_err(Error::ParseSerial)?;
let common = CommonConsoleConfig::parse(serial, Error::ParseSerial)?;
Ok(Self { common })
}
}
#[cfg(target_arch = "x86_64")]
impl DebugConsoleConfig {
pub fn parse(debug_console_ops: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add_valueless("off")
.add_valueless("pty")
.add_valueless("tty")
.add_valueless("null")
.add("file")
.add("iobase");
parser
.parse(debug_console_ops)
.map_err(Error::ParseConsole)?;
let mut file: Option<PathBuf> = None;
let mut iobase: Option<u16> = None;
let mut mode: ConsoleOutputMode = ConsoleOutputMode::Off;
if parser.is_set("off") {
} else if parser.is_set("pty") {
mode = ConsoleOutputMode::Pty;
} else if parser.is_set("tty") {
mode = ConsoleOutputMode::Tty;
} else if parser.is_set("null") {
mode = ConsoleOutputMode::Null;
} else if parser.is_set("file") {
mode = ConsoleOutputMode::File;
file =
Some(PathBuf::from(parser.get("file").ok_or(
Error::Validation(ValidationError::ConsoleFileMissing),
)?));
} else {
return Err(Error::ParseConsoleInvalidModeGiven);
}
if parser.is_set("iobase")
&& let Some(iobase_opt) = parser.get("iobase")
{
if !iobase_opt.starts_with("0x") {
return Err(Error::Validation(ValidationError::InvalidIoPortHex(
iobase_opt,
)));
}
iobase =
Some(u16::from_str_radix(&iobase_opt[2..], 16).map_err(|_| {
Error::Validation(ValidationError::InvalidIoPortHex(iobase_opt))
})?);
}
Ok(Self { file, mode, iobase })
}
}
impl DeviceConfig {
pub const SYNTAX: &'static str = "Direct device assignment parameters \
\"path=<device_path>,fd=<vfio_cdev_fd>,iommu=on|off,id=<device_id>,\
pci_segment=<segment_id>,pci_device_id=<pci_slot>,\
x_nv_gpudirect_clique=<clique_id>,\
x_exclude_mmap_bars=[<bar>...]\"";
pub fn parse(device: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add("path")
.add("fd")
.add_all(PciDeviceCommonConfig::OPTIONS_IOMMU)
.add("x_nv_gpudirect_clique")
.add("x_exclude_mmap_bars");
parser.parse(device).map_err(Error::ParseDevice)?;
let pci_common = PciDeviceCommonConfig::parse(device)?;
let path = parser.get("path").map(PathBuf::from);
let fd = parser.convert::<i32>("fd").map_err(Error::ParseDevice)?;
let x_nv_gpudirect_clique = parser
.convert::<u8>("x_nv_gpudirect_clique")
.map_err(Error::ParseDevice)?;
let x_exclude_mmap_bars = parser
.convert::<IntegerList>("x_exclude_mmap_bars")
.map_err(Error::ParseDevice)?
.map(|bars| bars.0)
.unwrap_or_default();
Ok(DeviceConfig {
pci_common,
path,
fd,
x_nv_gpudirect_clique,
x_exclude_mmap_bars,
})
}
pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
self.pci_common.validate(vm_config)?;
match (&self.path, self.fd) {
(None, None) => return Err(ValidationError::VfioDeviceNeitherPathNorFd),
(Some(_), Some(_)) => return Err(ValidationError::VfioDeviceBothPathAndFd),
(None, Some(_)) => {
let iommufd_fd_set = vm_config
.platform
.as_ref()
.is_some_and(|p| p.iommufd_fd.is_some());
if !iommufd_fd_set {
return Err(ValidationError::VfioFdRequiresIommufdFd);
}
}
(Some(_), None) => {}
}
if self.x_nv_gpudirect_clique.is_some() {
let vfio_p2p_dma = vm_config.platform.as_ref().is_none_or(|p| p.vfio_p2p_dma);
if !vfio_p2p_dma {
return Err(ValidationError::GpuDirectCliqueRequiresP2pDma);
}
}
// PCI devices expose six BARs, so only BAR indices 0 through 5 are valid here.
for bar in &self.x_exclude_mmap_bars {
if *bar > 5 {
return Err(ValidationError::InvalidDeviceExcludeMmapBar(*bar));
}
}
Ok(())
}
}
impl UserDeviceConfig {
pub const SYNTAX: &'static str = "Userspace device socket=<socket_path>,id=<device_id>,\
pci_segment=<segment_id>,pci_device_id=<pci_slot>\"";
pub fn parse(user_device: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser.add("socket").add_all(PciDeviceCommonConfig::OPTIONS);
parser.parse(user_device).map_err(Error::ParseUserDevice)?;
let pci_common = PciDeviceCommonConfig::parse(user_device)?;
let socket = parser
.get("socket")
.map(PathBuf::from)
.ok_or(Error::ParseUserDeviceSocketMissing)?;
Ok(UserDeviceConfig { pci_common, socket })
}
pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
if self.pci_common.iommu {
return Err(ValidationError::IommuNotSupported);
}
self.pci_common.validate(vm_config)
}
}
impl VdpaConfig {
pub const SYNTAX: &'static str = "vDPA device \
\"path=<device_path>,num_queues=<number_of_queues>,iommu=on|off,\
id=<device_id>,pci_segment=<segment_id>,pci_device_id=<pci_slot>\"";
pub fn parse(vdpa: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add("path")
.add("num_queues")
.add_all(PciDeviceCommonConfig::OPTIONS_IOMMU);
parser.parse(vdpa).map_err(Error::ParseVdpa)?;
let pci_common = PciDeviceCommonConfig::parse(vdpa)?;
let path = parser
.get("path")
.map(PathBuf::from)
.ok_or(Error::ParseVdpaPathMissing)?;
let num_queues = parser
.convert("num_queues")
.map_err(Error::ParseVdpa)?
.unwrap_or_else(default_vdpaconfig_num_queues);
Ok(VdpaConfig {
pci_common,
path,
num_queues,
})
}
pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
self.pci_common.validate(vm_config)
}
}
impl VsockConfig {
pub const SYNTAX: &'static str = "Virtio VSOCK parameters \
\"cid=<context_id>,socket=<socket_path>,iommu=on|off,id=<device_id>,\
pci_segment=<segment_id>,pci_device_id=<pci_slot>\"";
pub fn parse(vsock: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add("socket")
.add("cid")
.add_all(PciDeviceCommonConfig::OPTIONS_IOMMU);
parser.parse(vsock).map_err(Error::ParseVsock)?;
let pci_common = PciDeviceCommonConfig::parse(vsock)?;
let socket = parser
.get("socket")
.map(PathBuf::from)
.ok_or(Error::ParseVsockSockMissing)?;
let cid = parser
.convert("cid")
.map_err(Error::ParseVsock)?
.ok_or(Error::ParseVsockCidMissing)?;
Ok(VsockConfig {
pci_common,
cid,
socket,
})
}
pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
self.pci_common.validate(vm_config)
}
}
impl NumaConfig {
pub const SYNTAX: &'static str = "Settings related to a given NUMA node \
\"guest_numa_id=<node_id>,cpus=<cpus_id>,distances=<list_of_distances_to_destination_nodes>,\
device_id=<device_id>,\
memory_zones=<list_of_memory_zones>,\
pci_segments=<list_of_pci_segments>\"";
pub fn parse(numa: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add("guest_numa_id")
.add("cpus")
.add("distances")
.add("device_id")
.add("memory_zones")
.add("pci_segments");
parser.parse(numa).map_err(Error::ParseNuma)?;
let guest_numa_id = parser
.convert::<u32>("guest_numa_id")
.map_err(Error::ParseNuma)?
.ok_or_else(|| {
Error::ParseNuma(OptionParserError::InvalidValue(
"guest_numa_id is required for all NUMA nodes".to_string(),
))
})?;
let cpus = parser
.convert::<IntegerList>("cpus")
.map_err(Error::ParseNuma)?
.map(|v| v.0.iter().map(|e| *e as u32).collect());
let distances = parser
.convert::<TupleList<u64, u64>>("distances")
.map_err(Error::ParseNuma)?
.map(|v| {
v.0.iter()
.map(|Tuple(e1, e2)| NumaDistance {
destination: *e1 as u32,
distance: *e2 as u8,
})
.collect()
});
let device_id = parser.get("device_id");
let memory_zones = parser
.convert::<StringList>("memory_zones")
.map_err(Error::ParseNuma)?
.map(|v| v.0.into_boxed_slice());
let pci_segments = parser
.convert::<IntegerList>("pci_segments")
.map_err(Error::ParseNuma)?
.map(|v| v.0.iter().map(|e| *e as u16).collect());
if device_id.is_some() && (cpus.is_some() || memory_zones.is_some()) {
return Err(Error::ParseNuma(OptionParserError::InvalidValue(
"device_id in numa config cannot be used with cpus or memory zones".to_string(),
)));
}
Ok(NumaConfig {
guest_numa_id,
cpus,
distances,
device_id,
memory_zones,
pci_segments,
})
}
pub fn is_generic_initiator(&self) -> bool {
self.device_id.is_some()
}
/// Validates NumaConfig
pub fn validate(&self) -> result::Result<(), ValidationError> {
match (&self.device_id, &self.cpus, &self.memory_zones) {
(Some(device_id), None, None) => {
// Valid generic initiator case
if device_id.is_empty() {
return Err(ValidationError::InvalidNumaConfig(
"device_id in numa config cannot be empty".to_string(),
));
}
Ok(())
}
(None, Some(cpus), _) => {
// Standard NUMA with cpus
if cpus.is_empty() {
return Err(ValidationError::InvalidNumaConfig(
"cpus list in numa config cannot be empty".to_string(),
));
}
Ok(())
}
(None, _, Some(memory_zones)) => {
// Standard NUMA with memory_zones (cpus is None here)
if memory_zones.is_empty() {
return Err(ValidationError::InvalidNumaConfig(
"memory_zones in numa config cannot be empty".to_string(),
));
}
Ok(())
}
_ => {
// Default handles all error cases
if self.device_id.is_some() && (self.cpus.is_some() || self.memory_zones.is_some())
{
Err(ValidationError::InvalidNumaConfig(
"device_id in numa config is mutually exclusive with cpus and memory_zones"
.to_string(),
))
} else {
Err(ValidationError::InvalidNumaConfig(
"numa config must specify either device_id or cpus/memory_zones"
.to_string(),
))
}
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
pub struct RestoredNetConfig {
pub id: String,
#[serde(default)]
pub num_fds: usize,
// Special deserialize handling:
// A serialize-deserialize cycle typically happens across processes.
// Therefore, we don't serialize FDs, and whatever value is here after
// deserialization is invalid.
//
// Valid FDs are transmitted via a different channel (SCM_RIGHTS message)
// and will be populated into this struct on the destination VMM eventually.
#[serde(default, deserialize_with = "deserialize_restorednetconfig_fds")]
pub fds: Option<Vec<i32>>,
}
fn deserialize_restorednetconfig_fds<'de, D>(d: D) -> result::Result<Option<Vec<i32>>, D::Error>
where
D: serde::Deserializer<'de>,
{
let invalid_fds: Option<Vec<i32>> = Option::deserialize(d)?;
if let Some(invalid_fds) = invalid_fds {
// If the live-migration path is used properly, new FDs are passed as
// SCM_RIGHTS message. So, we don't get them from the serialized JSON
// anyway.
debug!(
"FDs in 'RestoredNetConfig' won't be deserialized as they are most likely invalid now. Deserializing them as -1."
);
Ok(Some(vec![-1; invalid_fds.len()]))
} else {
Ok(None)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
pub enum MemoryRestoreMode {
/// Restore by eagerly copying the snapshot into guest RAM before resume.
#[default]
Copy,
/// Restore lazily by faulting snapshot pages into guest RAM on demand.
OnDemand,
}
#[derive(Debug, Error)]
pub enum MemoryRestoreModeParseError {
#[error("Invalid value: {0}")]
InvalidValue(String),
}
impl FromStr for MemoryRestoreMode {
type Err = MemoryRestoreModeParseError;
fn from_str(s: &str) -> result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"copy" => Ok(Self::Copy),
"ondemand" => Ok(Self::OnDemand),
_ => Err(MemoryRestoreModeParseError::InvalidValue(s.to_owned())),
}
}
}
#[derive(Clone, Deserialize, Serialize, Debug, Eq, PartialEq)]
/// Data required for updating memory zone <-> host NUMA node mappings.
pub struct VmMemoryZoneUpdateData {
/// Id of the MemoryZone to update
pub id: String,
/// Host NUMA node to relocate the MemoryZone to
pub host_numa_node: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
pub struct RestoredVfioConfig {
pub id: String,
// FDs are not serialized and any deserialized value is invalid; see NetConfig::fds.
#[serde(default, deserialize_with = "deserialize_restored_fd")]
pub fd: Option<i32>,
}
pub(crate) fn deserialize_restored_fd<'de, D>(d: D) -> result::Result<Option<i32>, D::Error>
where
D: serde::Deserializer<'de>,
{
let invalid_fd: Option<i32> = Option::deserialize(d)?;
if invalid_fd.is_some() {
Ok(Some(-1))
} else {
Ok(None)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
pub struct RestoreConfig {
pub source_url: PathBuf,
#[serde(default)]
pub prefault: bool,
#[serde(default)]
pub memory_restore_mode: MemoryRestoreMode,
#[serde(default)]
pub net_fds: Option<Vec<RestoredNetConfig>>,
#[serde(default)]
pub vfio_fds: Option<Vec<RestoredVfioConfig>>,
// FDs are not serialized and any deserialized value is invalid; see NetConfig::fds.
#[serde(default, deserialize_with = "deserialize_restored_fd")]
pub iommufd_fd: Option<i32>,
#[serde(default)]
pub resume: bool,
#[serde(default)]
pub zone_updates: Vec<VmMemoryZoneUpdateData>,
}
impl RestoreConfig {
pub const SYNTAX: &'static str = "Restore from a VM snapshot. \
\nRestore parameters \"source_url=<source_url>,prefault=on|off,memory_restore_mode=copy|ondemand,\
net_fds=<list_of_net_ids_with_their_associated_fds>,\
vfio_fds=<list_of_vfio_ids_with_their_associated_fd>,iommufd_fd=<fd>,resume=true|false,\
zone_updates=<list_of_updates>\"
\n`source_url` should be a valid URL (e.g file:///foo/bar or tcp://192.168.1.10/foo) \
\n`prefault` controls eager prefaulting for the copy-based restore path (disabled by default) \
\n`memory_restore_mode=copy` preserves the existing eager read-copy restore behavior, while `memory_restore_mode=ondemand` enables lazy demand paging and fails restore if userfaultfd support is unavailable \
\n`net_fds` is a list of net ids with new file descriptors. \
Only net devices backed by FDs directly are needed as input.\
\n`vfio_fds` is a list of VFIO device ids each paired with a new cdev file descriptor, \
e.g. vfio_fds=[vfio0@5,vfio1@6]. Use this to restore a VFIO device onto a different \
sysfs path or host. Requires `iommufd_fd`.\
\n`iommufd_fd` is a new iommufd file descriptor for the restored VM. \
The one saved in the snapshot does not survive serialization.\
\n `resume` controls whether the VM will be directly resumed after restore \
\n `zone_updates` can be used to update NUMA memory zones. Expects a list of elements in the form `id@host_numa_node`";
pub fn parse(restore: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add("source_url")
.add("prefault")
.add("memory_restore_mode")
.add("net_fds")
.add("vfio_fds")
.add("iommufd_fd")
.add("resume")
.add("zone_updates");
parser.parse(restore).map_err(Error::ParseRestore)?;
let source_url = parser
.get("source_url")
.map(PathBuf::from)
.ok_or(Error::ParseRestoreSourceUrlMissing)?;
let prefault = parser
.convert::<Toggle>("prefault")
.map_err(Error::ParseRestore)?
.unwrap_or(Toggle(false))
.0;
let memory_restore_mode = parser
.convert::<MemoryRestoreMode>("memory_restore_mode")
.map_err(Error::ParseRestore)?
.unwrap_or_default();
let net_fds = parser
.convert::<TupleList<String, Vec<u64>>>("net_fds")
.map_err(Error::ParseRestore)?
.map(|v| {
v.0.iter()
.map(|Tuple(id, fds)| RestoredNetConfig {
id: id.clone(),
num_fds: fds.len(),
fds: Some(fds.iter().map(|e| *e as i32).collect()),
})
.collect()
});
let vfio_fds = parser
.convert::<TupleList<String, u64>>("vfio_fds")
.map_err(Error::ParseRestore)?
.map(|v| {
v.0.iter()
.map(|Tuple(id, fd)| RestoredVfioConfig {
id: id.clone(),
fd: Some(*fd as i32),
})
.collect()
});
let iommufd_fd = parser
.convert::<i32>("iommufd_fd")
.map_err(Error::ParseRestore)?;
let resume = parser
.convert::<Toggle>("resume")
.map_err(Error::ParseRestore)?
.unwrap_or(Toggle(false))
.0;
let zone_updates: Vec<VmMemoryZoneUpdateData> = parser
.convert::<TupleList<String, u32>>("zone_updates")
.map_err(Error::ParseRestore)?
.map_or(Vec::new(), |v| {
v.0.iter()
.map(|Tuple(id, host_numa_node)| VmMemoryZoneUpdateData {
id: id.clone(),
host_numa_node: *host_numa_node,
})
.collect()
});
Ok(RestoreConfig {
source_url,
prefault,
memory_restore_mode,
net_fds,
vfio_fds,
iommufd_fd,
resume,
zone_updates,
})
}
// Ensure all net devices from 'VmConfig' backed by FDs have a
// corresponding 'RestoreNetConfig' with a matched 'id' and expected
// number of FDs.
pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
if self.memory_restore_mode == MemoryRestoreMode::OnDemand && self.prefault {
return Err(ValidationError::InvalidRestorePrefaultWithOnDemand);
}
let mut restored_net_with_fds = HashMap::new();
for n in self.net_fds.iter().flatten() {
assert_eq!(
n.num_fds,
n.fds.as_ref().map_or(0, |f| f.len()),
"Invalid 'RestoredNetConfig' with conflicted fields."
);
if restored_net_with_fds.insert(n.id.clone(), n).is_some() {
return Err(ValidationError::IdentifierNotUnique(n.id.clone()));
}
}
for net_fds in vm_config.net.iter().flatten() {
if let Some(expected_fds) = &net_fds.fds {
let expected_id = net_fds
.pci_common
.id
.as_ref()
.expect("Invalid 'NetConfig' with empty 'id' for VM restore.");
if let Some(r) = restored_net_with_fds.remove(expected_id) {
if r.num_fds != expected_fds.len() {
return Err(ValidationError::RestoreNetFdCountMismatch(
expected_id.clone(),
r.num_fds,
expected_fds.len(),
));
}
} else {
return Err(ValidationError::RestoreMissingRequiredNetId(
expected_id.clone(),
));
}
}
}
let unique_zones = self
.zone_updates
.iter()
.map(|update| update.id.as_str())
.collect::<HashSet<_>>();
if self.zone_updates.len() != unique_zones.len() {
return Err(ValidationError::MultipleMemoryZoneUpdates);
}
if unique_zones.contains("") {
return Err(ValidationError::MemoryZoneUpdatesEmptyId);
}
if !restored_net_with_fds.is_empty() {
warn!("Ignoring unused 'net_fds' for VM restore.");
}
let vfio_fds = self.vfio_fds.as_deref().unwrap_or_default();
if !vfio_fds.is_empty() {
// Substituted devices become FD backed, which requires an
// externally supplied iommufd FD and the iommufd backend.
if self.iommufd_fd.is_none() {
return Err(ValidationError::VfioFdRequiresIommufdFd);
}
if !vm_config.platform.as_ref().is_some_and(|p| p.iommufd) {
return Err(ValidationError::IommufdFdRequiresIommufd);
}
let mut seen = HashSet::new();
for v in vfio_fds {
if !seen.insert(v.id.as_str()) {
return Err(ValidationError::IdentifierNotUnique(v.id.clone()));
}
}
let known_ids: HashSet<&str> = vm_config
.devices
.iter()
.flatten()
.filter_map(|d| d.pci_common.id.as_deref())
.collect();
for v in vfio_fds {
if !known_ids.contains(v.id.as_str()) {
return Err(ValidationError::RestoreUnknownVfioId(v.id.clone()));
}
}
}
// A device saved with an FD cannot reuse it, the snapshot carries no
// live descriptor. Each one needs a replacement in vfio_fds.
let substituted: HashSet<&str> = vfio_fds.iter().map(|v| v.id.as_str()).collect();
for d in vm_config.devices.iter().flatten() {
if d.fd.is_some()
&& !d
.pci_common
.id
.as_deref()
.is_some_and(|id| substituted.contains(id))
{
return Err(ValidationError::RestoreMissingVfioFd(
d.pci_common.id.clone().unwrap_or_default(),
));
}
}
Ok(())
}
}
impl TpmConfig {
pub const SYNTAX: &'static str = "TPM device \
\"(UNIX Domain Socket from swtpm) socket=</path/to/a/socket>\"";
pub fn parse(tpm: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser.add("socket");
parser.parse(tpm).map_err(Error::ParseTpm)?;
let socket = parser
.get("socket")
.map(PathBuf::from)
.ok_or(Error::ParseTpmPathMissing)?;
Ok(TpmConfig { socket })
}
}
impl LandlockConfig {
pub const SYNTAX: &'static str = "Landlock parameters \
\"path=<path/to/{file/dir}>,access=[rw]\"";
pub fn parse(landlock_rule: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser.add("path").add("access");
parser
.parse(landlock_rule)
.map_err(Error::ParseLandlockRules)?;
let path = parser
.get("path")
.map(PathBuf::from)
.ok_or(Error::ParseLandlockMissingFields)?;
let access = parser
.get("access")
.ok_or(Error::ParseLandlockMissingFields)?;
if access.chars().count() > 2 {
return Err(Error::ParseLandlockRules(OptionParserError::InvalidValue(
access.to_string(),
)));
}
Ok(LandlockConfig { path, access })
}
pub fn validate(&self) -> ValidationResult<()> {
if !self.path.exists() {
return Err(ValidationError::LandlockPathDoesNotExist(self.path.clone()));
}
LandlockAccess::try_from(self.access.as_str())
.map_err(|e| ValidationError::InvalidLandlockAccess(e.to_string()))?;
Ok(())
}
}
#[cfg(feature = "ivshmem")]
impl IvshmemConfig {
pub const SYNTAX: &'static str = "Ivshmem device. Specify the backend file path and size \
for the shared memory: \"path=</path/to/a/file>,size=<file_size>,id=<device_id>,\
pci_segment=<segment_id>,pci_device_id=<pci_slot>\" \
\nThe <file_size> must be a power of 2 (e.g., 2M, 4M, etc.), as it represents the size \
of the memory region mapped to the guest. Default size is 128M.";
pub fn parse(ivshmem: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser.add("path").add("size");
parser.add_all(PciDeviceCommonConfig::OPTIONS);
parser.parse(ivshmem).map_err(Error::ParseIvshmem)?;
let path = parser
.get("path")
.map(PathBuf::from)
.ok_or(Error::ParseIvshmemPathMissing)?;
let size = parser
.convert::<ByteSized>("size")
.map_err(Error::ParseIvshmem)?
.unwrap_or(ByteSized((DEFAULT_IVSHMEM_SIZE << 20) as u64))
.0;
let pci_common = PciDeviceCommonConfig::parse(ivshmem)?;
Ok(IvshmemConfig {
pci_common,
path,
size: size as usize,
})
}
pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
if self.pci_common.iommu {
return Err(ValidationError::IommuNotSupported);
}
self.pci_common.validate(vm_config)?;
let size = self.size as u64;
let path = &self.path;
// size must = 2^n
if !size.is_power_of_two() {
return Err(ValidationError::InvalidIvshmemInputSize(size));
}
let metadata = fs::metadata(path.to_str().unwrap())
.map_err(|_| ValidationError::InvalidIvshmemPath)?;
if metadata.len() < size {
return Err(ValidationError::InvalidIvshmemSize(metadata.len()));
}
Ok(())
}
}
impl VmConfig {
fn validate_identifier(
id_list: &mut BTreeSet<String>,
id: &Option<String>,
) -> ValidationResult<()> {
if let Some(id) = id.as_ref() {
if id.starts_with("__") {
return Err(ValidationError::InvalidIdentifier(id.clone()));
}
if !id_list.insert(id.clone()) {
return Err(ValidationError::IdentifierNotUnique(id.clone()));
}
}
Ok(())
}
pub fn backed_by_shared_memory(&self) -> bool {
if self.memory.shared || self.memory.hugepages {
return true;
}
if self.memory.size == 0 {
for zone in self.memory.zones.as_ref().unwrap() {
if !zone.shared && !zone.hugepages {
return false;
}
}
true
} else {
false
}
}
// Also enables virtio-iommu if the config needs it
// Returns the list of unique identifiers provided through the
// configuration.
pub fn validate(&mut self) -> ValidationResult<BTreeSet<String>> {
let mut id_list = BTreeSet::new();
// Is the payload configuration bootable?
self.payload
.as_mut()
.ok_or(ValidationError::PayloadError(
PayloadConfigError::MissingBootitem,
))?
.validate()?;
#[cfg(feature = "tdx")]
{
let tdx_enabled = self.platform.as_ref().is_some_and(|p| p.tdx);
// At this point we know payload isn't None.
if tdx_enabled && self.payload.as_ref().unwrap().firmware.is_none() {
return Err(ValidationError::TdxFirmwareMissing);
}
if tdx_enabled && (self.cpus.max_vcpus != self.cpus.boot_vcpus) {
return Err(ValidationError::TdxNoCpuHotplug);
}
}
#[cfg(feature = "sev_snp")]
{
let sev_snp_enabled = self.platform.as_ref().is_some_and(|p| p.sev_snp);
if sev_snp_enabled {
let host_data_opt = &self.payload.as_ref().unwrap().host_data;
if let Some(host_data) = host_data_opt
&& host_data.len() != 64
{
return Err(ValidationError::InvalidHostData);
}
// KVM SEV-SNP requires an IGVM payload to initialise the VMSA.
// Without IGVM the vCPU register state is undefined and VM entry fails.
#[cfg(feature = "igvm")]
if self
.payload
.as_ref()
.and_then(|p| p.igvm.as_ref())
.is_none()
{
return Err(ValidationError::SevSnpRequiresIgvm);
}
}
}
// The 'conflict' check is introduced in commit 24438e0390d3
// (vm-virtio: Enable the vmm support for virtio-console).
//
// Allow simultaneously set serial and console as TTY mode, for
// someone want to use virtio console for better performance, and
// want to keep legacy serial to catch boot stage logs for debug.
// Using such double tty mode, you need to configure the kernel
// properly, such as:
// "console=hvc0 earlyprintk=ttyS0"
let mut tty_consoles = Vec::new();
if self.console.common.mode == ConsoleOutputMode::Tty {
tty_consoles.push("virtio-console");
}
if self.serial.common.mode == ConsoleOutputMode::Tty {
tty_consoles.push("serial-console");
}
#[cfg(target_arch = "x86_64")]
if self.debug_console.mode == ConsoleOutputMode::Tty {
tty_consoles.push("debug-console");
}
if tty_consoles.len() > 1 {
warn!("Using TTY output for multiple consoles: {tty_consoles:?}");
}
if self.console.common.mode == ConsoleOutputMode::File && self.console.common.file.is_none()
{
return Err(ValidationError::ConsoleFileMissing);
}
if self.serial.common.mode == ConsoleOutputMode::File && self.serial.common.file.is_none() {
return Err(ValidationError::ConsoleFileMissing);
}
if self.cpus.max_vcpus < self.cpus.boot_vcpus {
return Err(ValidationError::CpusMaxLowerThanBoot(
self.cpus.max_vcpus,
self.cpus.boot_vcpus,
));
}
if self.cpus.max_vcpus > MAX_SUPPORTED_CPUS {
// Note: historically, Cloud Hypervisor did not support more than 255(254 on x64)
// vCPUs: self.cpus.max_vcpus was of type u8, so 255 was the maximum;
// on x86_64, the legacy mptable/apic was limited to 254 CPUs.
//
// Now the limit is lifted on x86_64 targets. Other targests/archs: TBD.
return Err(ValidationError::TooManyCpus(self.cpus.max_vcpus));
}
if let Some(rate_limit_groups) = &self.rate_limit_groups {
for rate_limit_group in rate_limit_groups {
rate_limit_group.validate(self)?;
Self::validate_identifier(&mut id_list, &Some(rate_limit_group.id.clone()))?;
}
}
if let Some(disks) = &self.disks {
for disk in disks {
if disk.vhost_socket.as_ref().and(disk.path.as_ref()).is_some() {
return Err(ValidationError::DiskSocketAndPath);
}
if disk.vhost_user && !self.backed_by_shared_memory() {
return Err(ValidationError::VhostUserRequiresSharedMemory);
}
if disk.vhost_user && disk.vhost_socket.is_none() {
return Err(ValidationError::VhostUserMissingSocket);
}
if disk.vhost_user
&& (disk.rate_limiter_config.is_some() || disk.rate_limit_group.is_some())
{
return Err(ValidationError::VhostUserRateLimiterNotSupported);
}
if let Some(rate_limit_group) = &disk.rate_limit_group {
if let Some(rate_limit_groups) = &self.rate_limit_groups {
if !rate_limit_groups
.iter()
.any(|cfg| &cfg.id == rate_limit_group)
{
return Err(ValidationError::InvalidRateLimiterGroup);
}
} else {
return Err(ValidationError::InvalidRateLimiterGroup);
}
}
disk.validate(self)?;
self.iommu |= disk.pci_common.iommu;
Self::validate_identifier(&mut id_list, &disk.pci_common.id)?;
}
}
if let Some(nets) = &self.net {
for net in nets {
if net.vhost_user && !self.backed_by_shared_memory() {
return Err(ValidationError::VhostUserRequiresSharedMemory);
}
if net.vhost_user && net.vhost_socket.is_none() {
return Err(ValidationError::VhostUserMissingSocket);
}
if net.vhost_user && net.rate_limiter_config.is_some() {
return Err(ValidationError::VhostUserRateLimiterNotSupported);
}
net.validate(self)?;
self.iommu |= net.pci_common.iommu;
Self::validate_identifier(&mut id_list, &net.pci_common.id)?;
}
}
if let Some(fses) = &self.fs {
if !fses.is_empty() && !self.backed_by_shared_memory() {
return Err(ValidationError::VhostUserRequiresSharedMemory);
}
for fs in fses {
fs.validate(self)?;
Self::validate_identifier(&mut id_list, &fs.pci_common.id)?;
}
}
if let Some(generic_vhost_user_devices) = &self.generic_vhost_user {
if !generic_vhost_user_devices.is_empty() && !self.backed_by_shared_memory() {
return Err(ValidationError::VhostUserRequiresSharedMemory);
}
for generic_vhost_user_device in generic_vhost_user_devices {
generic_vhost_user_device.validate(self)?;
Self::validate_identifier(&mut id_list, &generic_vhost_user_device.pci_common.id)?;
}
}
if let Some(pmems) = &self.pmem {
for pmem in pmems {
pmem.validate(self)?;
self.iommu |= pmem.pci_common.iommu;
Self::validate_identifier(&mut id_list, &pmem.pci_common.id)?;
}
}
self.rng.validate(self)?;
Self::validate_identifier(&mut id_list, &self.rng.pci_common.id)?;
self.iommu |= self.rng.pci_common.iommu;
if let Some(rtc) = &self.rtc {
rtc.validate(self)?;
Self::validate_identifier(&mut id_list, &rtc.pci_common.id)?;
self.iommu |= rtc.pci_common.iommu;
}
self.console.validate(self)?;
Self::validate_identifier(&mut id_list, &self.console.pci_common.id)?;
self.iommu |= self.console.pci_common.iommu;
if let Some(t) = &self.cpus.topology {
if t.threads_per_core == 0
|| t.cores_per_die == 0
|| t.dies_per_package == 0
|| t.packages == 0
{
return Err(ValidationError::CpuTopologyZeroPart);
}
#[cfg(target_arch = "x86_64")]
if t.threads_per_core > 2 {
return Err(ValidationError::CpuTopologyThreadsPerCore);
}
// The setting of dies doesn't apply on AArch64.
// Only '1' value is accepted, so its impact on the vcpu topology
// setting can be ignored.
#[cfg(target_arch = "aarch64")]
if t.dies_per_package != 1 {
return Err(ValidationError::CpuTopologyDiesPerPackage);
}
let total: u32 = (t.threads_per_core as u32)
* (t.cores_per_die as u32)
* (t.dies_per_package as u32)
* (t.packages as u32);
if total != self.cpus.max_vcpus {
return Err(ValidationError::CpuTopologyCount);
}
}
if let Some(hugepage_size) = &self.memory.hugepage_size {
if !self.memory.hugepages {
return Err(ValidationError::HugePageSizeWithoutHugePages);
}
if !hugepage_size.is_power_of_two() {
return Err(ValidationError::InvalidHugePageSize(*hugepage_size));
}
}
if self.memory.shared && self.memory.mergeable {
return Err(ValidationError::InvalidSharedMemoryWithMergeable);
}
if let Some(zones) = &self.memory.zones {
for zone in zones {
if zone.shared && zone.mergeable {
return Err(ValidationError::InvalidSharedMemoryWithMergeable);
}
}
}
if let Some(user_devices) = &self.user_devices {
if !user_devices.is_empty() && !self.backed_by_shared_memory() {
return Err(ValidationError::UserDevicesRequireSharedMemory);
}
for user_device in user_devices {
user_device.validate(self)?;
Self::validate_identifier(&mut id_list, &user_device.pci_common.id)?;
}
}
if let Some(vdpa_devices) = &self.vdpa {
for vdpa_device in vdpa_devices {
vdpa_device.validate(self)?;
self.iommu |= vdpa_device.pci_common.iommu;
Self::validate_identifier(&mut id_list, &vdpa_device.pci_common.id)?;
}
}
if let Some(vsock) = &self.vsock
&& [!0, 0, 1, 2].contains(&vsock.cid)
{
return Err(ValidationError::VsockSpecialCid(vsock.cid));
}
if let Some(balloon) = &self.balloon {
balloon.validate(self)?;
Self::validate_identifier(&mut id_list, &balloon.pci_common.id)?;
self.iommu |= balloon.pci_common.iommu;
let ram_size = self.memory.total_size();
if balloon.size >= ram_size {
return Err(ValidationError::BalloonLargerThanRam(
balloon.size,
ram_size,
));
}
}
if let Some(devices) = &self.devices {
let mut device_paths = BTreeSet::new();
for device in devices {
if let Some(path) = device.path.as_deref()
&& !device_paths.insert(path.to_string_lossy())
{
return Err(ValidationError::DuplicateDevicePath(
path.to_string_lossy().to_string(),
));
}
device.validate(self)?;
self.iommu |= device.pci_common.iommu;
Self::validate_identifier(&mut id_list, &device.pci_common.id)?;
}
}
if let Some(vsock) = &self.vsock {
vsock.validate(self)?;
self.iommu |= vsock.pci_common.iommu;
Self::validate_identifier(&mut id_list, &vsock.pci_common.id)?;
}
let num_pci_segments = match &self.platform {
Some(platform_config) => platform_config.num_pci_segments,
None => 1,
};
if let Some(numa) = &self.numa {
let mut used_numa_node_memory_zones = HashMap::new();
let mut used_pci_segments = HashMap::new();
for numa_node in numa.iter() {
numa_node.validate()?;
if let Some(memory_zones) = numa_node.memory_zones.clone() {
for memory_zone in memory_zones.iter() {
if used_numa_node_memory_zones.contains_key(memory_zone) {
return Err(ValidationError::MemoryZoneReused(
memory_zone.to_string(),
*used_numa_node_memory_zones.get(memory_zone).unwrap(),
numa_node.guest_numa_id,
));
}
used_numa_node_memory_zones
.insert(memory_zone.to_string(), numa_node.guest_numa_id);
}
}
if let Some(pci_segments) = numa_node.pci_segments.clone() {
for pci_segment in pci_segments.iter() {
if *pci_segment >= num_pci_segments {
return Err(ValidationError::InvalidPciSegment(*pci_segment));
}
if *pci_segment == 0 && numa_node.guest_numa_id != 0 {
return Err(ValidationError::DefaultPciSegmentInvalidNode(
numa_node.guest_numa_id,
));
}
if used_pci_segments.contains_key(pci_segment) {
return Err(ValidationError::PciSegmentReused(
*pci_segment,
*used_pci_segments.get(pci_segment).unwrap(),
numa_node.guest_numa_id,
));
}
used_pci_segments.insert(*pci_segment, numa_node.guest_numa_id);
}
}
}
}
if let Some(zones) = &self.memory.zones {
for zone in zones.iter() {
let id = zone.id.clone();
Self::validate_identifier(&mut id_list, &Some(id))?;
}
}
if let Some(pci_segments) = &self.pci_segments {
for pci_segment in pci_segments {
pci_segment.validate(self)?;
}
}
self.platform.as_ref().map(|p| p.validate()).transpose()?;
self.iommu |= self
.platform
.as_ref()
.map(|p| p.iommu_segments.is_some())
.unwrap_or_default();
if let Some(landlock_rules) = &self.landlock_rules {
for landlock_rule in landlock_rules {
landlock_rule.validate()?;
}
}
#[cfg(feature = "ivshmem")]
if let Some(ivshmem_config) = &self.ivshmem {
ivshmem_config.validate(self)?;
Self::validate_identifier(&mut id_list, &ivshmem_config.pci_common.id)?;
}
Ok(id_list)
}
pub fn parse(vm_params: VmParams) -> Result<Self> {
let mut rate_limit_groups: Option<Box<[RateLimiterGroupConfig]>> = None;
if let Some(rate_limit_group_list) = &vm_params.rate_limit_groups {
let mut rate_limit_group_config_list = Vec::new();
for item in rate_limit_group_list.iter() {
let rate_limit_group_config = RateLimiterGroupConfig::parse(item)?;
rate_limit_group_config_list.push(rate_limit_group_config);
}
rate_limit_groups = Some(rate_limit_group_config_list.into_boxed_slice());
}
let mut disks: Option<Vec<DiskConfig>> = None;
if let Some(disk_list) = &vm_params.disks {
let mut disk_config_list = Vec::new();
for item in disk_list.iter() {
let disk_config = DiskConfig::parse(item)?;
disk_config_list.push(disk_config);
}
disks = Some(disk_config_list);
}
#[cfg(feature = "fw_cfg")]
let fw_cfg_config = if let Some(fw_cfg_config_str) = vm_params.fw_cfg_config {
let fw_cfg_config = FwCfgConfig::parse(fw_cfg_config_str)?;
Some(fw_cfg_config)
} else {
None
};
let mut net: Option<Vec<NetConfig>> = None;
if let Some(net_list) = &vm_params.net {
let mut net_config_list = Vec::new();
for item in net_list.iter() {
let net_config = NetConfig::parse(item)?;
net_config_list.push(net_config);
}
net = Some(net_config_list);
}
let rng = RngConfig::parse(vm_params.rng)?;
let mut rtc: Option<RtcConfig> = None;
if let Some(rtc_params) = &vm_params.rtc {
rtc = Some(RtcConfig::parse(rtc_params)?);
}
let mut balloon: Option<BalloonConfig> = None;
if let Some(balloon_params) = &vm_params.balloon {
balloon = Some(BalloonConfig::parse(balloon_params)?);
}
#[cfg(feature = "pvmemcontrol")]
let pvmemcontrol: Option<PvmemcontrolConfig> = vm_params
.pvmemcontrol
.then_some(PvmemcontrolConfig::default());
let mut fs: Option<Vec<FsConfig>> = None;
if let Some(fs_list) = &vm_params.fs {
let mut fs_config_list = Vec::new();
for item in fs_list.iter() {
fs_config_list.push(FsConfig::parse(item)?);
}
fs = Some(fs_config_list);
}
let mut generic_vhost_user: Option<Vec<GenericVhostUserConfig>> = None;
if let Some(generic_vhost_user_list) = &vm_params.generic_vhost_user {
let mut generic_vhost_user_config_list = Vec::new();
for item in generic_vhost_user_list.iter() {
generic_vhost_user_config_list.push(GenericVhostUserConfig::parse(item)?);
}
generic_vhost_user = Some(generic_vhost_user_config_list);
}
let mut pmem: Option<Vec<PmemConfig>> = None;
if let Some(pmem_list) = &vm_params.pmem {
let mut pmem_config_list = Vec::new();
for item in pmem_list.iter() {
let pmem_config = PmemConfig::parse(item)?;
pmem_config_list.push(pmem_config);
}
pmem = Some(pmem_config_list);
}
let console = ConsoleConfig::parse(vm_params.console)?;
let serial = SerialConfig::parse(vm_params.serial)?;
#[cfg(target_arch = "x86_64")]
let debug_console = DebugConsoleConfig::parse(vm_params.debug_console)?;
let mut devices: Option<Vec<DeviceConfig>> = None;
if let Some(device_list) = &vm_params.devices {
let mut device_config_list = Vec::new();
for item in device_list.iter() {
let device_config = DeviceConfig::parse(item)?;
device_config_list.push(device_config);
}
devices = Some(device_config_list);
}
let mut user_devices: Option<Vec<UserDeviceConfig>> = None;
if let Some(user_device_list) = &vm_params.user_devices {
let mut user_device_config_list = Vec::new();
for item in user_device_list.iter() {
let user_device_config = UserDeviceConfig::parse(item)?;
user_device_config_list.push(user_device_config);
}
user_devices = Some(user_device_config_list);
}
let mut vdpa: Option<Vec<VdpaConfig>> = None;
if let Some(vdpa_list) = &vm_params.vdpa {
let mut vdpa_config_list = Vec::new();
for item in vdpa_list.iter() {
let vdpa_config = VdpaConfig::parse(item)?;
vdpa_config_list.push(vdpa_config);
}
vdpa = Some(vdpa_config_list);
}
let mut vsock: Option<VsockConfig> = None;
if let Some(vs) = &vm_params.vsock {
let vsock_config = VsockConfig::parse(vs)?;
vsock = Some(vsock_config);
}
let mut pci_segments: Option<Box<[PciSegmentConfig]>> = None;
if let Some(pci_segment_list) = &vm_params.pci_segments {
let mut pci_segment_config_list = Vec::new();
for item in pci_segment_list.iter() {
let pci_segment_config = PciSegmentConfig::parse(item)?;
pci_segment_config_list.push(pci_segment_config);
}
pci_segments = Some(pci_segment_config_list.into_boxed_slice());
}
let platform = vm_params.platform.map(PlatformConfig::parse).transpose()?;
let mut numa: Option<Box<[NumaConfig]>> = None;
if let Some(numa_list) = &vm_params.numa {
let mut numa_config_list = Vec::new();
for item in numa_list.iter() {
let numa_config = NumaConfig::parse(item)?;
numa_config_list.push(numa_config);
}
numa = Some(numa_config_list.into_boxed_slice());
}
#[cfg(not(feature = "igvm"))]
let payload_present = vm_params.kernel.is_some() || vm_params.firmware.is_some();
#[cfg(feature = "igvm")]
let payload_present =
vm_params.kernel.is_some() || vm_params.firmware.is_some() || vm_params.igvm.is_some();
let payload = if payload_present {
Some(PayloadConfig {
kernel: vm_params.kernel.map(PathBuf::from),
initramfs: vm_params.initramfs.map(PathBuf::from),
cmdline: vm_params.cmdline.map(|s| s.to_string()),
firmware: vm_params.firmware.map(PathBuf::from),
#[cfg(feature = "igvm")]
igvm: vm_params.igvm.map(PathBuf::from),
#[cfg(feature = "sev_snp")]
host_data: vm_params.host_data.map(|s| s.to_string()),
#[cfg(feature = "fw_cfg")]
fw_cfg_config,
})
} else {
None
};
let mut tpm: Option<TpmConfig> = None;
if let Some(tc) = vm_params.tpm {
let tpm_conf = TpmConfig::parse(tc)?;
tpm = Some(TpmConfig {
socket: tpm_conf.socket,
});
}
#[cfg(feature = "guest_debug")]
let gdb = vm_params.gdb;
let mut landlock_rules: Option<Box<[LandlockConfig]>> = None;
if let Some(ll_rules) = vm_params.landlock_rules {
landlock_rules = Some(
ll_rules
.iter()
.map(|rule| LandlockConfig::parse(rule))
.collect::<Result<Vec<LandlockConfig>>>()?
.into_boxed_slice(),
);
}
#[cfg(feature = "ivshmem")]
let mut ivshmem: Option<IvshmemConfig> = None;
#[cfg(feature = "ivshmem")]
if let Some(iv) = vm_params.ivshmem {
let ivshmem_conf = IvshmemConfig::parse(iv)?;
ivshmem = Some(ivshmem_conf);
}
let mut config = VmConfig {
cpus: CpusConfig::parse(vm_params.cpus)?,
memory: MemoryConfig::parse(vm_params.memory, vm_params.memory_zones)?,
payload,
rate_limit_groups,
disks,
net,
rng,
balloon,
generic_vhost_user,
fs,
pmem,
serial,
console,
#[cfg(target_arch = "x86_64")]
debug_console,
devices,
user_devices,
vdpa,
vsock,
#[cfg(feature = "pvmemcontrol")]
pvmemcontrol,
pvpanic: vm_params.pvpanic,
iommu: false, // updated in VmConfig::validate()
numa,
watchdog: vm_params.watchdog,
rtc,
#[cfg(feature = "guest_debug")]
gdb,
pci_segments,
platform,
tpm,
preserved_fds: None,
landlock_enable: vm_params.landlock_enable,
landlock_rules,
#[cfg(feature = "ivshmem")]
ivshmem,
};
config.validate().map_err(Error::Validation)?;
Ok(config)
}
pub fn remove_device(&mut self, id: &str) -> bool {
let mut removed = false;
// Remove if VFIO device
if let Some(devices) = self.devices.as_mut() {
let len = devices.len();
devices.retain(|dev| dev.pci_common.id.as_ref().map(|id| id.as_ref()) != Some(id));
removed |= devices.len() != len;
}
// Remove if VFIO user device
if let Some(user_devices) = self.user_devices.as_mut() {
let len = user_devices.len();
user_devices.retain(|dev| dev.pci_common.id.as_ref().map(|id| id.as_ref()) != Some(id));
removed |= user_devices.len() != len;
}
// Remove if disk device
if let Some(disks) = self.disks.as_mut() {
let len = disks.len();
disks.retain(|dev| dev.pci_common.id.as_ref().map(|id| id.as_ref()) != Some(id));
removed |= disks.len() != len;
}
// Remove if fs device
if let Some(fs) = self.fs.as_mut() {
let len = fs.len();
fs.retain(|dev| dev.pci_common.id.as_ref().map(|id| id.as_ref()) != Some(id));
removed |= fs.len() != len;
}
// Remove if generic vhost-user device
if let Some(generic_vhost_user) = self.generic_vhost_user.as_mut() {
let len = generic_vhost_user.len();
generic_vhost_user
.retain(|dev| dev.pci_common.id.as_ref().map(|id| id.as_ref()) != Some(id));
removed |= generic_vhost_user.len() != len;
}
// Remove if net device
if let Some(net) = self.net.as_mut() {
let len = net.len();
net.retain(|dev| dev.pci_common.id.as_ref().map(|id| id.as_ref()) != Some(id));
removed |= net.len() != len;
}
// Remove if pmem device
if let Some(pmem) = self.pmem.as_mut() {
let len = pmem.len();
pmem.retain(|dev| dev.pci_common.id.as_ref().map(|id| id.as_ref()) != Some(id));
removed |= pmem.len() != len;
}
// Remove if vDPA device
if let Some(vdpa) = self.vdpa.as_mut() {
let len = vdpa.len();
vdpa.retain(|dev| dev.pci_common.id.as_ref().map(|id| id.as_ref()) != Some(id));
removed |= vdpa.len() != len;
}
// Remove if vsock device
if let Some(vsock) = self.vsock.as_ref()
&& vsock.pci_common.id.as_ref().map(|id| id.as_ref()) == Some(id)
{
self.vsock = None;
removed = true;
}
removed
}
/// # Safety
/// To use this safely, the caller must guarantee that the input
/// fds are all valid.
pub unsafe fn add_preserved_fds(&mut self, fds: Vec<i32>) {
if fds.is_empty() {
return;
}
self.preserved_fds
.get_or_insert_with(HashSet::new)
.extend(fds);
}
#[cfg(feature = "tdx")]
pub fn is_tdx_enabled(&self) -> bool {
self.platform.as_ref().is_some_and(|p| p.tdx)
}
#[cfg(feature = "sev_snp")]
pub fn is_sev_snp_enabled(&self) -> bool {
self.platform.as_ref().is_some_and(|p| p.sev_snp)
}
}
impl Clone for VmConfig {
fn clone(&self) -> Self {
VmConfig {
cpus: self.cpus.clone(),
memory: self.memory.clone(),
payload: self.payload.clone(),
rate_limit_groups: self.rate_limit_groups.clone(),
disks: self.disks.clone(),
net: self.net.clone(),
rng: self.rng.clone(),
rtc: self.rtc.clone(),
balloon: self.balloon.clone(),
#[cfg(feature = "pvmemcontrol")]
pvmemcontrol: self.pvmemcontrol.clone(),
fs: self.fs.clone(),
generic_vhost_user: self.generic_vhost_user.clone(),
pmem: self.pmem.clone(),
serial: self.serial.clone(),
console: self.console.clone(),
#[cfg(target_arch = "x86_64")]
debug_console: self.debug_console.clone(),
devices: self.devices.clone(),
user_devices: self.user_devices.clone(),
vdpa: self.vdpa.clone(),
vsock: self.vsock.clone(),
numa: self.numa.clone(),
pci_segments: self.pci_segments.clone(),
platform: self.platform.clone(),
tpm: self.tpm.clone(),
preserved_fds: self
.preserved_fds
.as_ref()
// SAFETY: FFI call with valid FDs
.map(|fds| fds.iter().map(|fd| unsafe { libc::dup(*fd) }).collect()),
landlock_rules: self.landlock_rules.clone(),
#[cfg(feature = "ivshmem")]
ivshmem: self.ivshmem.clone(),
..*self
}
}
}
impl Drop for VmConfig {
fn drop(&mut self) {
if let Some(mut fds) = self.preserved_fds.take() {
for fd in fds.drain() {
// SAFETY: FFI call with valid FDs
unsafe { libc::close(fd) };
}
}
}
}
#[cfg(test)]
mod unit_tests {
use std::fs::File;
use std::os::unix::io::AsRawFd;
use net_util::MacAddr;
use super::*;
#[test]
fn test_cpu_parsing() -> Result<()> {
assert_eq!(CpusConfig::parse("")?, CpusConfig::default());
assert_eq!(
CpusConfig::parse("boot=1")?,
CpusConfig {
boot_vcpus: 1,
max_vcpus: 1,
..Default::default()
}
);
assert_eq!(
CpusConfig::parse("boot=1,max=2")?,
CpusConfig {
boot_vcpus: 1,
max_vcpus: 2,
..Default::default()
}
);
assert_eq!(
CpusConfig::parse("boot=8,topology=2:2:1:2")?,
CpusConfig {
boot_vcpus: 8,
max_vcpus: 8,
topology: Some(CpuTopology {
threads_per_core: 2,
cores_per_die: 2,
dies_per_package: 1,
packages: 2
}),
..Default::default()
}
);
CpusConfig::parse("boot=8,topology=2:2:1").unwrap_err();
CpusConfig::parse("boot=8,topology=2:2:1:x").unwrap_err();
assert_eq!(
CpusConfig::parse("boot=1,kvm_hyperv=on")?,
CpusConfig {
boot_vcpus: 1,
max_vcpus: 1,
kvm_hyperv: true,
..Default::default()
}
);
assert_eq!(
CpusConfig::parse("boot=2,affinity=[0@[0,2],1@[1,3]]")?,
CpusConfig {
boot_vcpus: 2,
max_vcpus: 2,
affinity: Some(Box::new([
CpuAffinity {
vcpu: 0,
host_cpus: Box::new([0, 2]),
},
CpuAffinity {
vcpu: 1,
host_cpus: Box::new([1, 3]),
}
])),
..Default::default()
},
);
// Test core_scheduling parsing
assert_eq!(
CpusConfig::parse("boot=1,core_scheduling=vm")?,
CpusConfig {
boot_vcpus: 1,
max_vcpus: 1,
core_scheduling: CoreScheduling::Vm,
..Default::default()
}
);
assert_eq!(
CpusConfig::parse("boot=1,core_scheduling=vcpu")?,
CpusConfig {
boot_vcpus: 1,
max_vcpus: 1,
core_scheduling: CoreScheduling::Vcpu,
..Default::default()
}
);
assert_eq!(
CpusConfig::parse("boot=1,core_scheduling=off")?,
CpusConfig {
boot_vcpus: 1,
max_vcpus: 1,
core_scheduling: CoreScheduling::Off,
..Default::default()
}
);
// Default (no core_scheduling specified) should be Vm
assert_eq!(
CpusConfig::parse("boot=1")?.core_scheduling,
CoreScheduling::Vm
);
// Invalid value should error
CpusConfig::parse("boot=1,core_scheduling=invalid").unwrap_err();
Ok(())
}
#[test]
fn test_mem_zone_parsing() -> Result<()> {
// mergeable defaults to false
assert_eq!(
MemoryConfig::parse("size=0", Some(vec!["id=mem0,size=1G"]))?,
MemoryConfig {
size: 0,
zones: Some(vec![MemoryZoneConfig {
id: "mem0".to_string(),
size: 1 << 30,
..Default::default()
}]),
..Default::default()
}
);
// mergeable=on
assert_eq!(
MemoryConfig::parse("size=0", Some(vec!["id=mem0,size=1G,mergeable=on"]))?,
MemoryConfig {
size: 0,
zones: Some(vec![MemoryZoneConfig {
id: "mem0".to_string(),
size: 1 << 30,
mergeable: true,
..Default::default()
}]),
..Default::default()
}
);
// mergeable=off is explicit false
assert_eq!(
MemoryConfig::parse("size=0", Some(vec!["id=mem0,size=1G,mergeable=off"]))?,
MemoryConfig {
size: 0,
zones: Some(vec![MemoryZoneConfig {
id: "mem0".to_string(),
size: 1 << 30,
mergeable: false,
..Default::default()
}]),
..Default::default()
}
);
// per-zone mergeable independent of global mergeable
assert_eq!(
MemoryConfig::parse(
"size=1G,mergeable=off",
Some(vec!["id=hotplug,size=0,hotplug_size=4G,mergeable=on"])
)?,
MemoryConfig {
size: 1 << 30,
mergeable: false,
hotplug_method: HotplugMethod::Acpi,
zones: Some(vec![MemoryZoneConfig {
id: "hotplug".to_string(),
size: 0,
hotplug_size: Some(4 << 30),
mergeable: true,
..Default::default()
}]),
..Default::default()
}
);
// global mergeable=on inherited by zone with no explicit mergeable
assert_eq!(
MemoryConfig::parse("size=0,mergeable=on", Some(vec!["id=mem0,size=1G"]))?,
MemoryConfig {
size: 0,
mergeable: true,
zones: Some(vec![MemoryZoneConfig {
id: "mem0".to_string(),
size: 1 << 30,
mergeable: true,
..Default::default()
}]),
..Default::default()
}
);
// reserve=on on a zone
assert_eq!(
MemoryConfig::parse("size=0", Some(vec!["id=mem0,size=1G,reserve=on"]))?,
MemoryConfig {
size: 0,
zones: Some(vec![MemoryZoneConfig {
id: "mem0".to_string(),
size: 1 << 30,
reserve: true,
..Default::default()
}]),
..Default::default()
}
);
Ok(())
}
#[test]
fn test_mem_parsing() -> Result<()> {
assert_eq!(MemoryConfig::parse("", None)?, MemoryConfig::default());
// Default string
assert_eq!(
MemoryConfig::parse("size=512M", None)?,
MemoryConfig::default()
);
assert_eq!(
MemoryConfig::parse("size=512M,mergeable=on", None)?,
MemoryConfig {
size: 512 << 20,
mergeable: true,
..Default::default()
}
);
assert_eq!(
MemoryConfig::parse("mergeable=on", None)?,
MemoryConfig {
mergeable: true,
..Default::default()
}
);
assert_eq!(
MemoryConfig::parse("size=1G,mergeable=off", None)?,
MemoryConfig {
size: 1 << 30,
mergeable: false,
..Default::default()
}
);
assert_eq!(
MemoryConfig::parse("hotplug_method=acpi", None)?,
MemoryConfig {
..Default::default()
}
);
assert_eq!(
MemoryConfig::parse("hotplug_method=acpi,hotplug_size=512M", None)?,
MemoryConfig {
hotplug_size: Some(512 << 20),
..Default::default()
}
);
assert_eq!(
MemoryConfig::parse("hotplug_method=virtio-mem,hotplug_size=512M", None)?,
MemoryConfig {
hotplug_size: Some(512 << 20),
hotplug_method: HotplugMethod::VirtioMem,
..Default::default()
}
);
assert_eq!(
MemoryConfig::parse("hugepages=on,size=1G,hugepage_size=2M", None)?,
MemoryConfig {
hugepage_size: Some(2 << 20),
size: 1 << 30,
hugepages: true,
..Default::default()
}
);
// reserve=on opts out of MAP_NORESERVE
assert_eq!(
MemoryConfig::parse("size=1G,hugepages=on,reserve=on", None)?,
MemoryConfig {
size: 1 << 30,
hugepages: true,
reserve: true,
..Default::default()
}
);
Ok(())
}
#[test]
fn test_rate_limit_group_parsing() -> Result<()> {
assert_eq!(
RateLimiterGroupConfig::parse("id=group0,bw_size=1000,bw_refill_time=100")?,
RateLimiterGroupConfig {
id: "group0".to_string(),
rate_limiter_config: RateLimiterConfig {
bandwidth: Some(TokenBucketConfig {
size: 1000,
one_time_burst: Some(0),
refill_time: 100,
}),
ops: None,
}
}
);
assert_eq!(
RateLimiterGroupConfig::parse("id=group0,ops_size=1000,ops_refill_time=100")?,
RateLimiterGroupConfig {
id: "group0".to_string(),
rate_limiter_config: RateLimiterConfig {
bandwidth: None,
ops: Some(TokenBucketConfig {
size: 1000,
one_time_burst: Some(0),
refill_time: 100,
}),
}
}
);
Ok(())
}
#[test]
fn test_pci_segment_parsing() -> Result<()> {
assert_eq!(
PciSegmentConfig::parse("pci_segment=0")?,
PciSegmentConfig {
pci_segment: 0,
mmio32_aperture_weight: 1,
mmio64_aperture_weight: 1,
}
);
assert_eq!(
PciSegmentConfig::parse(
"pci_segment=0,mmio32_aperture_weight=1,mmio64_aperture_weight=1"
)?,
PciSegmentConfig {
pci_segment: 0,
mmio32_aperture_weight: 1,
mmio64_aperture_weight: 1,
}
);
assert_eq!(
PciSegmentConfig::parse("pci_segment=0,mmio32_aperture_weight=2")?,
PciSegmentConfig {
pci_segment: 0,
mmio32_aperture_weight: 2,
mmio64_aperture_weight: 1,
}
);
assert_eq!(
PciSegmentConfig::parse("pci_segment=0,mmio64_aperture_weight=2")?,
PciSegmentConfig {
pci_segment: 0,
mmio32_aperture_weight: 1,
mmio64_aperture_weight: 2,
}
);
Ok(())
}
fn disk_fixture() -> DiskConfig {
DiskConfig {
pci_common: PciDeviceCommonConfig::default(),
path: Some(PathBuf::from("/path/to_file")),
readonly: false,
direct: false,
num_queues: 1,
queue_size: 128,
vhost_user: false,
vhost_socket: None,
disable_io_uring: false,
disable_aio: false,
rate_limit_group: None,
rate_limiter_config: None,
serial: None,
queue_affinity: None,
backing_files: false,
sparse: true,
image_type: ImageType::Unknown,
lock_granularity: LockGranularityChoice::default(),
}
}
#[test]
fn test_disk_parsing() -> Result<()> {
assert_eq!(
DiskConfig::parse("path=/path/to_file")?,
DiskConfig { ..disk_fixture() }
);
assert_eq!(
DiskConfig::parse("path=/path/to_file,id=mydisk0")?,
DiskConfig {
pci_common: PciDeviceCommonConfig {
id: Some("mydisk0".to_owned()),
..Default::default()
},
..disk_fixture()
}
);
assert_eq!(
DiskConfig::parse("vhost_user=true,socket=/tmp/sock")?,
DiskConfig {
path: None,
vhost_socket: Some(String::from("/tmp/sock")),
vhost_user: true,
image_type: ImageType::Unknown,
..disk_fixture()
}
);
assert_eq!(
DiskConfig::parse("path=/path/to_file,iommu=on")?,
DiskConfig {
pci_common: PciDeviceCommonConfig {
iommu: true,
..Default::default()
},
..disk_fixture()
}
);
assert_eq!(
DiskConfig::parse("path=/path/to_file,iommu=on,queue_size=256")?,
DiskConfig {
pci_common: PciDeviceCommonConfig {
iommu: true,
..Default::default()
},
queue_size: 256,
..disk_fixture()
}
);
assert_eq!(
DiskConfig::parse("path=/path/to_file,iommu=on,queue_size=256,num_queues=4")?,
DiskConfig {
pci_common: PciDeviceCommonConfig {
iommu: true,
..Default::default()
},
queue_size: 256,
num_queues: 4,
..disk_fixture()
}
);
assert_eq!(
DiskConfig::parse("path=/path/to_file,direct=on")?,
DiskConfig {
direct: true,
..disk_fixture()
}
);
assert_eq!(
DiskConfig::parse("path=/path/to_file,serial=test")?,
DiskConfig {
serial: Some(String::from("test")),
..disk_fixture()
}
);
assert_eq!(
DiskConfig::parse("path=/path/to_file,rate_limit_group=group0")?,
DiskConfig {
rate_limit_group: Some("group0".to_string()),
..disk_fixture()
}
);
assert_eq!(
DiskConfig::parse("path=/path/to_file,lock_granularity=full")?,
DiskConfig {
lock_granularity: LockGranularityChoice::Full,
..disk_fixture()
}
);
assert_eq!(
DiskConfig::parse("path=/path/to_file,lock_granularity=byte-range")?,
DiskConfig {
lock_granularity: LockGranularityChoice::ByteRange,
..disk_fixture()
}
);
assert_eq!(
DiskConfig::parse("path=/path/to_file,queue_affinity=[0@[1],1@[2],2@[3,4],3@[5-8]]")?,
DiskConfig {
queue_affinity: Some(Box::new([
VirtQueueAffinity {
queue_index: 0,
host_cpus: Box::new([1]),
},
VirtQueueAffinity {
queue_index: 1,
host_cpus: Box::new([2]),
},
VirtQueueAffinity {
queue_index: 2,
host_cpus: Box::new([3, 4]),
},
VirtQueueAffinity {
queue_index: 3,
host_cpus: Box::new([5, 6, 7, 8]),
}
])),
..disk_fixture()
}
);
Ok(())
}
fn net_fixture() -> NetConfig {
NetConfig {
pci_common: PciDeviceCommonConfig::default(),
tap: None,
ip: None,
mask: None,
mac: MacAddr::parse_str("de:ad:be:ef:12:34").unwrap(),
host_mac: Some(MacAddr::parse_str("12:34:de:ad:be:ef").unwrap()),
mtu: None,
num_queues: 2,
queue_size: 256,
vhost_user: false,
vhost_socket: None,
vhost_mode: VhostMode::Client,
fds: None,
rate_limiter_config: None,
offload_tso: true,
offload_ufo: true,
offload_csum: true,
}
}
#[test]
fn test_net_parsing() -> Result<()> {
// mac address is random
assert_eq!(
NetConfig::parse("mac=de:ad:be:ef:12:34,host_mac=12:34:de:ad:be:ef")?,
net_fixture(),
);
assert_eq!(
NetConfig::parse("mac=de:ad:be:ef:12:34,host_mac=12:34:de:ad:be:ef,id=mynet0")?,
NetConfig {
pci_common: PciDeviceCommonConfig {
id: Some("mynet0".to_owned()),
..Default::default()
},
..net_fixture()
}
);
assert_eq!(
NetConfig::parse(
"mac=de:ad:be:ef:12:34,host_mac=12:34:de:ad:be:ef,tap=tap0,ip=192.168.100.1,mask=255.255.255.128"
)?,
NetConfig {
tap: Some("tap0".to_owned()),
ip: Some("192.168.100.1".parse().unwrap()),
mask: Some("255.255.255.128".parse().unwrap()),
..net_fixture()
}
);
assert_eq!(
NetConfig::parse(
"mac=de:ad:be:ef:12:34,host_mac=12:34:de:ad:be:ef,vhost_user=true,socket=/tmp/sock"
)?,
NetConfig {
vhost_user: true,
vhost_socket: Some("/tmp/sock".to_owned()),
..net_fixture()
}
);
assert_eq!(
NetConfig::parse(
"mac=de:ad:be:ef:12:34,host_mac=12:34:de:ad:be:ef,num_queues=4,queue_size=1024,iommu=on"
)?,
NetConfig {
pci_common: PciDeviceCommonConfig {
iommu: true,
..Default::default()
},
num_queues: 4,
queue_size: 1024,
..net_fixture()
}
);
assert_eq!(
NetConfig::parse("mac=de:ad:be:ef:12:34,fd=[3,7],num_queues=4")?,
NetConfig {
host_mac: None,
fds: Some(vec![3, 7]),
num_queues: 4,
..net_fixture()
}
);
assert_eq!(
NetConfig::parse("mac=de:ad:be:ef:12:34,mask=255.255.255.0")?,
NetConfig {
mask: Some("255.255.255.0".parse().unwrap()),
host_mac: None,
..net_fixture()
}
);
Ok(())
}
#[test]
fn test_parse_rng() -> Result<()> {
assert_eq!(RngConfig::parse("")?, RngConfig::default());
assert_eq!(
RngConfig::parse("src=/dev/random")?,
RngConfig {
src: PathBuf::from("/dev/random"),
..Default::default()
}
);
assert_eq!(
RngConfig::parse("src=/dev/random,iommu=on,pci_segment=1,pci_device_id=7")?,
RngConfig {
src: PathBuf::from("/dev/random"),
pci_common: PciDeviceCommonConfig {
id: None,
iommu: true,
pci_segment: 1,
pci_device_id: Some(7),
},
}
);
assert_eq!(
RngConfig::parse("iommu=on")?,
RngConfig {
pci_common: PciDeviceCommonConfig {
id: None,
iommu: true,
pci_segment: 0,
pci_device_id: None,
},
..Default::default()
}
);
Ok(())
}
#[test]
fn test_parse_balloon() -> Result<()> {
assert_eq!(
BalloonConfig::parse(
"size=128M,deflate_on_oom=on,free_page_reporting=on,pci_segment=1,pci_device_id=7"
)?,
BalloonConfig {
pci_common: PciDeviceCommonConfig {
pci_segment: 1,
pci_device_id: Some(7),
..Default::default()
},
size: 128 << 20,
deflate_on_oom: true,
free_page_reporting: true,
}
);
assert_eq!(
BalloonConfig::parse("size=0,iommu=on")?,
BalloonConfig {
pci_common: PciDeviceCommonConfig {
iommu: true,
..Default::default()
},
size: 0,
deflate_on_oom: false,
free_page_reporting: false,
}
);
Ok(())
}
#[test]
#[cfg(feature = "ivshmem")]
fn test_parse_ivshmem() -> Result<()> {
assert_eq!(
IvshmemConfig::parse("path=/tmp/ivshmem.data,size=2M,pci_segment=1,pci_device_id=7")?,
IvshmemConfig {
pci_common: PciDeviceCommonConfig {
pci_segment: 1,
pci_device_id: Some(7),
..Default::default()
},
path: PathBuf::from("/tmp/ivshmem.data"),
size: 2 << 20,
}
);
Ok(())
}
fn fs_fixture() -> FsConfig {
FsConfig {
pci_common: PciDeviceCommonConfig::default(),
socket: PathBuf::from("/tmp/sock"),
tag: "mytag".to_owned(),
num_queues: 1,
queue_size: 1024,
}
}
#[test]
fn test_parse_fs() -> Result<()> {
// "tag" and "socket" must be supplied
FsConfig::parse("").unwrap_err();
FsConfig::parse("tag=mytag").unwrap_err();
FsConfig::parse("socket=/tmp/sock").unwrap_err();
assert_eq!(FsConfig::parse("tag=mytag,socket=/tmp/sock")?, fs_fixture());
assert_eq!(
FsConfig::parse("tag=mytag,socket=/tmp/sock,num_queues=4,queue_size=1024")?,
FsConfig {
num_queues: 4,
queue_size: 1024,
..fs_fixture()
}
);
Ok(())
}
#[track_caller]
fn make_vhost_user_config(
socket: &str,
virtio_id: u64,
id: &str,
pci_segment: u64,
queue_sizes: &IntegerList,
) {
assert!(!socket.contains(",[]\n\r\0\""));
assert!(!id.contains(",[]\n\r\0\""));
let config = GenericVhostUserConfig::parse(&format!(
"device_type={virtio_id},socket=\"{socket}\",\
id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
));
if pci_segment <= u16::MAX.into()
&& virtio_id <= u32::MAX.into()
&& virtio_id != u64::from(VIRTIO_ID_BALLOON)
&& virtio_id != u64::from(VIRTIO_ID_WATCHDOG)
&& virtio_id != u64::from(VIRTIO_ID_IOMMU)
&& queue_sizes.0.iter().all(|&f| f <= u16::MAX.into())
{
assert_eq!(
config.unwrap(),
GenericVhostUserConfig {
pci_common: PciDeviceCommonConfig {
id: Some(id.to_owned()),
pci_segment: u16::try_from(pci_segment).unwrap(),
..Default::default()
},
socket: socket.into(),
device_type: u32::try_from(virtio_id).unwrap(),
queue_sizes: queue_sizes
.0
.iter()
.map(|&f| u16::try_from(f).unwrap())
.collect(),
}
);
} else {
config.unwrap_err();
}
}
#[test]
fn test_parse_vhost_user() -> Result<()> {
// all parameters must be supplied, except pci_segment
GenericVhostUserConfig::parse("").unwrap_err();
GenericVhostUserConfig::parse("virtio_id=1").unwrap_err();
GenericVhostUserConfig::parse("queue_size=1").unwrap_err();
GenericVhostUserConfig::parse("socket=/tmp/sock").unwrap_err();
GenericVhostUserConfig::parse("id=1").unwrap_err();
make_vhost_user_config(
"/dev/null/doesnotexist",
100,
"Something",
10,
&IntegerList(vec![u16::MAX.into(), 20u16.into()]),
);
make_vhost_user_config(
"/dev/null/doesnotexist",
100,
"Something",
10,
&IntegerList(vec![u16::MAX.into()]),
);
make_vhost_user_config(
"/dev/null/doesnotexist",
u64::from(u32::MAX) + 1,
"Something",
10,
&IntegerList(vec![20u64]),
);
make_vhost_user_config(
"/dev/null/doesnotexist",
u64::from(u32::MAX) + 1,
"Something",
10,
&IntegerList(vec![20u64]),
);
make_vhost_user_config(
"/dev/null/doesnotexist",
u64::from(u32::MAX) + 1,
"Something",
10,
&IntegerList(vec![20u64]),
);
// The deprecated 'virtio_id' key is an alias for 'device_type' and must
// parse to an identical configuration.
assert_eq!(
GenericVhostUserConfig::parse("virtio_id=26,socket=/tmp/sock,queue_sizes=[1024]")
.unwrap(),
GenericVhostUserConfig::parse("device_type=26,socket=/tmp/sock,queue_sizes=[1024]")
.unwrap(),
);
Ok(())
}
fn pmem_fixture() -> PmemConfig {
PmemConfig {
pci_common: PciDeviceCommonConfig::default(),
file: PathBuf::from("/tmp/pmem"),
size: Some(128 << 20),
discard_writes: false,
}
}
#[test]
fn test_pmem_parsing() -> Result<()> {
// Must always give a file and size
PmemConfig::parse("").unwrap_err();
PmemConfig::parse("size=128M").unwrap_err();
assert_eq!(
PmemConfig::parse("file=/tmp/pmem,size=128M")?,
pmem_fixture()
);
assert_eq!(
PmemConfig::parse("file=/tmp/pmem,size=128M,id=mypmem0")?,
PmemConfig {
pci_common: PciDeviceCommonConfig {
id: Some("mypmem0".to_owned()),
..Default::default()
},
..pmem_fixture()
}
);
assert_eq!(
PmemConfig::parse("file=/tmp/pmem,size=128M,iommu=on,discard_writes=on")?,
PmemConfig {
pci_common: PciDeviceCommonConfig {
iommu: true,
..Default::default()
},
discard_writes: true,
..pmem_fixture()
}
);
Ok(())
}
#[test]
fn test_console_parsing() -> Result<()> {
let console_config = |mode, file, socket, iommu| ConsoleConfig {
common: CommonConsoleConfig { file, mode, socket },
pci_common: PciDeviceCommonConfig {
iommu,
..Default::default()
},
};
ConsoleConfig::parse("").unwrap_err();
ConsoleConfig::parse("badmode").unwrap_err();
assert_eq!(
ConsoleConfig::parse("off")?,
console_config(ConsoleOutputMode::Off, None, None, false)
);
assert_eq!(
ConsoleConfig::parse("pty")?,
console_config(ConsoleOutputMode::Pty, None, None, false)
);
assert_eq!(
ConsoleConfig::parse("tty")?,
console_config(ConsoleOutputMode::Tty, None, None, false)
);
assert_eq!(
ConsoleConfig::parse("null")?,
console_config(ConsoleOutputMode::Null, None, None, false)
);
assert_eq!(
ConsoleConfig::parse("file=/tmp/console")?,
console_config(
ConsoleOutputMode::File,
Some(PathBuf::from("/tmp/console")),
None,
false
)
);
assert_eq!(
ConsoleConfig::parse("null,iommu=on")?,
console_config(ConsoleOutputMode::Null, None, None, true)
);
assert_eq!(
ConsoleConfig::parse("file=/tmp/console,iommu=on")?,
console_config(
ConsoleOutputMode::File,
Some(PathBuf::from("/tmp/console")),
None,
true
)
);
assert_eq!(
ConsoleConfig::parse("socket=/tmp/serial.sock,iommu=on")?,
console_config(
ConsoleOutputMode::Socket,
None,
Some(PathBuf::from("/tmp/serial.sock")),
true
)
);
Ok(())
}
fn device_fixture() -> DeviceConfig {
DeviceConfig {
pci_common: PciDeviceCommonConfig::default(),
path: Some(PathBuf::from("/path/to/device")),
fd: None,
x_nv_gpudirect_clique: None,
x_exclude_mmap_bars: Vec::new(),
}
}
#[test]
fn test_device_parsing() -> Result<()> {
// The parser itself is purely syntactic; the "path or fd is
// required" rule is enforced by VmConfig::validate instead.
assert_eq!(
DeviceConfig::parse("")?,
DeviceConfig {
path: None,
..device_fixture()
}
);
assert_eq!(
DeviceConfig::parse("path=/path/to/device")?,
device_fixture()
);
assert_eq!(
DeviceConfig::parse("path=/path/to/device,iommu=on")?,
DeviceConfig {
pci_common: PciDeviceCommonConfig {
iommu: true,
..Default::default()
},
..device_fixture()
}
);
assert_eq!(
DeviceConfig::parse("path=/path/to/device,iommu=on,id=mydevice0")?,
DeviceConfig {
pci_common: PciDeviceCommonConfig {
id: Some("mydevice0".to_owned()),
iommu: true,
..Default::default()
},
..device_fixture()
}
);
assert_eq!(
DeviceConfig::parse("path=/path/to/device,x_exclude_mmap_bars=[2]")?,
DeviceConfig {
x_exclude_mmap_bars: vec![2],
..device_fixture()
}
);
assert_eq!(
DeviceConfig::parse("path=/path/to/device,x_exclude_mmap_bars=[0,2,5]")?,
DeviceConfig {
x_exclude_mmap_bars: vec![0, 2, 5],
..device_fixture()
}
);
assert_eq!(
DeviceConfig::parse("path=/path/to/device,x_exclude_mmap_bars=[6]")?,
DeviceConfig {
x_exclude_mmap_bars: vec![6],
..device_fixture()
}
);
// `fd=` is accepted alongside or in place of `path=`; exclusivity
// is enforced by DeviceConfig::validate, not by the parser.
assert_eq!(
DeviceConfig::parse("fd=7")?,
DeviceConfig {
path: None,
fd: Some(7),
..device_fixture()
}
);
assert_eq!(
DeviceConfig::parse("path=/path/to/device,fd=7")?,
DeviceConfig {
fd: Some(7),
..device_fixture()
}
);
// Non-integer fd fails at parse time.
DeviceConfig::parse("fd=notanint").unwrap_err();
Ok(())
}
fn vdpa_fixture() -> VdpaConfig {
VdpaConfig {
pci_common: PciDeviceCommonConfig::default(),
path: PathBuf::from("/dev/vhost-vdpa"),
num_queues: 1,
}
}
#[test]
fn test_vdpa_parsing() -> Result<()> {
// path is required
VdpaConfig::parse("").unwrap_err();
assert_eq!(VdpaConfig::parse("path=/dev/vhost-vdpa")?, vdpa_fixture());
assert_eq!(
VdpaConfig::parse("path=/dev/vhost-vdpa,num_queues=2,id=my_vdpa")?,
VdpaConfig {
pci_common: PciDeviceCommonConfig {
id: Some("my_vdpa".to_owned()),
..Default::default()
},
num_queues: 2,
..vdpa_fixture()
}
);
Ok(())
}
#[test]
fn test_tpm_parsing() -> Result<()> {
// path is required
TpmConfig::parse("").unwrap_err();
assert_eq!(
TpmConfig::parse("socket=/var/run/tpm.sock")?,
TpmConfig {
socket: PathBuf::from("/var/run/tpm.sock"),
}
);
Ok(())
}
#[test]
fn test_platform_iommufd_fd_parsing() -> Result<()> {
// `iommufd_fd=N` alone implies `iommufd=on`.
let p = PlatformConfig::parse("iommufd_fd=42")?;
assert!(p.iommufd);
assert_eq!(p.iommufd_fd, Some(42));
// Explicit `iommufd=on,iommufd_fd=N` is the same.
let p = PlatformConfig::parse("iommufd=on,iommufd_fd=42")?;
assert!(p.iommufd);
assert_eq!(p.iommufd_fd, Some(42));
// No flags → both default to off.
let p = PlatformConfig::parse("")?;
assert!(!p.iommufd);
assert_eq!(p.iommufd_fd, None);
Ok(())
}
#[test]
fn test_vsock_parsing() -> Result<()> {
// socket and cid is required
VsockConfig::parse("").unwrap_err();
assert_eq!(
VsockConfig::parse("socket=/tmp/sock,cid=3")?,
VsockConfig {
pci_common: PciDeviceCommonConfig::default(),
cid: 3,
socket: PathBuf::from("/tmp/sock"),
}
);
assert_eq!(
VsockConfig::parse("socket=/tmp/sock,cid=3,iommu=on")?,
VsockConfig {
pci_common: PciDeviceCommonConfig {
iommu: true,
..Default::default()
},
cid: 3,
socket: PathBuf::from("/tmp/sock"),
}
);
Ok(())
}
#[test]
fn test_numa_config_parsing() -> Result<()> {
// Error when device_id and cpu/memory are present
let invalid_input = "guest_numa_id=0,cpus=[0,1],distances=[0@25,1@20],\
device_id=vfio0,memory_zones=[mem1],pci_segments=[0]";
NumaConfig::parse(invalid_input).unwrap_err();
// Successful numa config parsing
let standard_input = "guest_numa_id=1,cpus=[2,3],distances=[0@20],\
memory_zones=[mem0],pci_segments=[0]";
let expected_standard = NumaConfig {
guest_numa_id: 1,
cpus: Some(Box::new([2, 3])),
distances: Some(Box::new([NumaDistance {
destination: 0,
distance: 20,
}])),
device_id: None,
memory_zones: Some(Box::new(["mem0".to_string()])),
pci_segments: Some(Box::new([0])),
};
assert_eq!(NumaConfig::parse(standard_input)?, expected_standard);
// Successful generic initiator config parse
let gi_input = "guest_numa_id=2,device_id=vfio1,distances=[0@30],pci_segments=[1]";
let expected_gi = NumaConfig {
guest_numa_id: 2,
cpus: None,
distances: Some(Box::new([NumaDistance {
destination: 0,
distance: 30,
}])),
device_id: Some("vfio1".to_string()),
memory_zones: None,
pci_segments: Some(Box::new([1])),
};
assert_eq!(NumaConfig::parse(gi_input)?, expected_gi);
Ok(())
}
#[test]
fn test_numa_config_generic_initiator_valid() {
// device_id specified, no cpus/memory_zones
let config = NumaConfig {
guest_numa_id: 0,
cpus: None,
distances: Some(Box::new([NumaDistance {
destination: 1,
distance: 20,
}])),
memory_zones: None,
device_id: Some("vfio0".to_string()),
pci_segments: None,
};
config.validate().unwrap();
assert!(config.is_generic_initiator());
}
#[test]
fn test_numa_config_invalid_device_id() {
// empty device_id
let config = NumaConfig {
guest_numa_id: 0,
cpus: None,
distances: None,
memory_zones: None,
device_id: Some(String::new()),
pci_segments: None,
};
assert!(config.validate().is_err());
}
#[test]
fn test_numa_config_invalid_both_device_cpus() {
// device_id and cpus specified
let config = NumaConfig {
guest_numa_id: 0,
cpus: Some(Box::new([0, 1])),
distances: None,
device_id: Some("vfio0".to_string()),
memory_zones: None,
pci_segments: None,
};
assert!(config.validate().is_err());
}
#[test]
fn test_numa_config_invalid_both_device_memory() {
// device_id and memory zones specified
let config = NumaConfig {
guest_numa_id: 0,
cpus: None,
distances: None,
device_id: Some("vfio0".to_string()),
memory_zones: Some(Box::new(["mem0".to_string()])),
pci_segments: None,
};
assert!(config.validate().is_err());
}
#[test]
fn test_numa_config_standard_valid() {
// No device_id
let config = NumaConfig {
guest_numa_id: 0,
cpus: Some(Box::new([0, 1])),
distances: Some(Box::new([NumaDistance {
destination: 1,
distance: 20,
}])),
device_id: None,
memory_zones: Some(Box::new(["mem0".to_string()])),
pci_segments: None,
};
config.validate().unwrap();
}
#[test]
fn test_restore_parsing() -> Result<()> {
assert_eq!(
RestoreConfig::parse("source_url=/path/to/snapshot")?,
RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"),
prefault: false,
memory_restore_mode: MemoryRestoreMode::Copy,
net_fds: None,
vfio_fds: None,
iommufd_fd: None,
resume: false,
zone_updates: vec![],
}
);
assert_eq!(
RestoreConfig::parse(
"source_url=/path/to/snapshot,prefault=off,net_fds=[net0@[3,4],net1@[5,6,7,8]]"
)?,
RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"),
prefault: false,
memory_restore_mode: MemoryRestoreMode::Copy,
net_fds: Some(vec![
RestoredNetConfig {
id: "net0".to_string(),
num_fds: 2,
fds: Some(vec![3, 4]),
},
RestoredNetConfig {
id: "net1".to_string(),
num_fds: 4,
fds: Some(vec![5, 6, 7, 8]),
}
]),
vfio_fds: None,
iommufd_fd: None,
resume: false,
zone_updates: vec![],
}
);
assert_eq!(
RestoreConfig::parse("source_url=/path/to/snapshot,memory_restore_mode=ondemand")?,
RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"),
prefault: false,
memory_restore_mode: MemoryRestoreMode::OnDemand,
net_fds: None,
vfio_fds: None,
iommufd_fd: None,
resume: false,
zone_updates: vec![],
}
);
assert_eq!(
RestoreConfig::parse("source_url=/path/to/snapshot,resume=on,zone_updates=[zone1@1]")?,
RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"),
prefault: false,
memory_restore_mode: MemoryRestoreMode::Copy,
net_fds: None,
vfio_fds: None,
iommufd_fd: None,
resume: true,
zone_updates: vec![VmMemoryZoneUpdateData {
host_numa_node: 1,
id: "zone1".to_string(),
}],
}
);
assert_eq!(
RestoreConfig::parse(
"source_url=/path/to/snapshot,vfio_fds=[vfio0@5,vfio1@6],iommufd_fd=7"
)?,
RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"),
prefault: false,
memory_restore_mode: MemoryRestoreMode::Copy,
net_fds: None,
vfio_fds: Some(vec![
RestoredVfioConfig {
id: "vfio0".to_string(),
fd: Some(5),
},
RestoredVfioConfig {
id: "vfio1".to_string(),
fd: Some(6),
},
]),
iommufd_fd: Some(7),
resume: false,
zone_updates: vec![],
}
);
assert_eq!(
RestoreConfig::parse(
"source_url=/path/to/snapshot,vfio_fds=[vfio0@5,vfio1@6],iommufd_fd=7"
)?,
RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"),
prefault: false,
memory_restore_mode: MemoryRestoreMode::Copy,
net_fds: None,
vfio_fds: Some(vec![
RestoredVfioConfig {
id: "vfio0".to_string(),
fd: Some(5),
},
RestoredVfioConfig {
id: "vfio1".to_string(),
fd: Some(6),
},
]),
iommufd_fd: Some(7),
resume: false,
zone_updates: vec![],
}
);
// Parsing should fail as source_url is a required field
RestoreConfig::parse("prefault=off").unwrap_err();
RestoreConfig::parse("source_url=/path/to/snapshot,memory_restore_mode=bogus").unwrap_err();
RestoreConfig::parse("source_url=/path/to/snapshot,resume=on,zone_updates=[@1]")
.unwrap_err();
RestoreConfig::parse("source_url=/path/to/snapshot,resume=on,zone_updates=[@]")
.unwrap_err();
RestoreConfig::parse("source_url=/path/to/snapshot,resume=on,zone_updates=[id1@]")
.unwrap_err();
RestoreConfig::parse("source_url=/path/to/snapshot,resume=on,zone_updates=[id1 1]")
.unwrap_err();
RestoreConfig::parse("source_url=/path/to/snapshot,resume=on,zone_updates=[[id1@1]]")
.unwrap_err();
RestoreConfig::parse("source_url=/path/to/snapshot,resume=on,zone_updates=id1@1")
.unwrap_err();
Ok(())
}
#[test]
fn test_restore_config_serde() {
assert_eq!(
serde_json::from_str::<RestoreConfig>(r#"{"source_url":"/path/to/snapshot"}"#)
.unwrap()
.memory_restore_mode,
MemoryRestoreMode::Copy
);
assert_eq!(
serde_json::from_str::<RestoreConfig>(
r#"{"source_url":"/path/to/snapshot","memory_restore_mode":"OnDemand"}"#
)
.unwrap()
.memory_restore_mode,
MemoryRestoreMode::OnDemand
);
assert_eq!(
serde_json::from_str::<RestoreConfig>(
r#"{"source_url":"/path/to/snapshot","zone_updates":[{"id": "zone1", "host_numa_node": 1}]}"#
)
.unwrap()
.zone_updates,
vec![VmMemoryZoneUpdateData {
host_numa_node: 1,
id: "zone1".to_string(),
}],
);
}
#[test]
fn test_restore_config_validation() {
// interested in only VmConfig.net, so set rest to default values
let mut snapshot_vm_config = VmConfig {
cpus: CpusConfig::default(),
memory: MemoryConfig::default(),
payload: None,
rate_limit_groups: None,
disks: None,
rng: RngConfig::default(),
generic_vhost_user: None,
balloon: None,
fs: None,
pmem: None,
serial: SerialConfig::default(),
console: ConsoleConfig::default(),
#[cfg(target_arch = "x86_64")]
debug_console: DebugConsoleConfig::default(),
devices: None,
user_devices: None,
vdpa: None,
vsock: None,
#[cfg(feature = "pvmemcontrol")]
pvmemcontrol: None,
pvpanic: false,
iommu: false,
numa: None,
watchdog: false,
rtc: None,
#[cfg(feature = "guest_debug")]
gdb: false,
pci_segments: None,
platform: None,
tpm: None,
preserved_fds: None,
net: Some(vec![
NetConfig {
pci_common: PciDeviceCommonConfig {
id: Some("net0".to_owned()),
..Default::default()
},
num_queues: 2,
fds: Some(vec![-1, -1, -1, -1]),
..net_fixture()
},
NetConfig {
pci_common: PciDeviceCommonConfig {
id: Some("net1".to_owned()),
..Default::default()
},
num_queues: 1,
fds: Some(vec![-1, -1]),
..net_fixture()
},
NetConfig {
pci_common: PciDeviceCommonConfig {
id: Some("net2".to_owned()),
..Default::default()
},
fds: None,
..net_fixture()
},
]),
landlock_enable: false,
landlock_rules: None,
#[cfg(feature = "ivshmem")]
ivshmem: None,
};
let valid_config = RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"),
prefault: false,
memory_restore_mode: MemoryRestoreMode::Copy,
net_fds: Some(vec![
RestoredNetConfig {
id: "net0".to_string(),
num_fds: 4,
fds: Some(vec![3, 4, 5, 6]),
},
RestoredNetConfig {
id: "net1".to_string(),
num_fds: 2,
fds: Some(vec![7, 8]),
},
]),
vfio_fds: None,
iommufd_fd: None,
resume: false,
zone_updates: vec![],
};
valid_config.validate(&snapshot_vm_config).unwrap();
let mut invalid_config = valid_config.clone();
invalid_config.net_fds = Some(vec![RestoredNetConfig {
id: "netx".to_string(),
num_fds: 4,
fds: Some(vec![3, 4, 5, 6]),
}]);
assert_eq!(
invalid_config.validate(&snapshot_vm_config),
Err(ValidationError::RestoreMissingRequiredNetId(
"net0".to_string()
))
);
invalid_config.net_fds = Some(vec![
RestoredNetConfig {
id: "net0".to_string(),
num_fds: 4,
fds: Some(vec![3, 4, 5, 6]),
},
RestoredNetConfig {
id: "net0".to_string(),
num_fds: 4,
fds: Some(vec![3, 4, 5, 6]),
},
]);
assert_eq!(
invalid_config.validate(&snapshot_vm_config),
Err(ValidationError::IdentifierNotUnique("net0".to_string()))
);
invalid_config.net_fds = Some(vec![RestoredNetConfig {
id: "net0".to_string(),
num_fds: 4,
fds: Some(vec![3, 4, 5, 6]),
}]);
assert_eq!(
invalid_config.validate(&snapshot_vm_config),
Err(ValidationError::RestoreMissingRequiredNetId(
"net1".to_string()
))
);
invalid_config.net_fds = Some(vec![RestoredNetConfig {
id: "net0".to_string(),
num_fds: 2,
fds: Some(vec![3, 4]),
}]);
assert_eq!(
invalid_config.validate(&snapshot_vm_config),
Err(ValidationError::RestoreNetFdCountMismatch(
"net0".to_string(),
2,
4
))
);
let another_valid_config = RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"),
prefault: false,
memory_restore_mode: MemoryRestoreMode::Copy,
net_fds: None,
vfio_fds: None,
iommufd_fd: None,
resume: false,
zone_updates: vec![],
};
snapshot_vm_config.net = Some(vec![NetConfig {
pci_common: PciDeviceCommonConfig {
id: Some("net2".to_owned()),
..Default::default()
},
fds: None,
..net_fixture()
}]);
another_valid_config.validate(&snapshot_vm_config).unwrap();
let invalid_restore_mode = RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"),
prefault: true,
memory_restore_mode: MemoryRestoreMode::OnDemand,
net_fds: None,
vfio_fds: None,
iommufd_fd: None,
resume: false,
zone_updates: vec![],
};
assert_eq!(
invalid_restore_mode.validate(&snapshot_vm_config),
Err(ValidationError::InvalidRestorePrefaultWithOnDemand)
);
// It is invalid to submit more than one update for a single zone.
let mut invalid_config_zone_updates = valid_config.clone();
invalid_config_zone_updates.zone_updates = vec![
VmMemoryZoneUpdateData {
id: "id1".to_string(),
host_numa_node: 0,
},
VmMemoryZoneUpdateData {
id: "id1".to_string(),
host_numa_node: 20,
},
];
assert_eq!(
invalid_config_zone_updates.validate(&snapshot_vm_config),
Err(ValidationError::MultipleMemoryZoneUpdates)
);
// It is invalid to submit an update without referring to a memory zone by specifying an ID.
let mut invalid_config_zone_updates = valid_config.clone();
invalid_config_zone_updates.zone_updates = vec![VmMemoryZoneUpdateData {
id: String::new(),
host_numa_node: 0,
}];
assert_eq!(
invalid_config_zone_updates.validate(&snapshot_vm_config),
Err(ValidationError::MemoryZoneUpdatesEmptyId)
);
}
#[test]
fn test_restore_config_vfio_fds_validation() {
// interested in only VmConfig.devices and platform, so set rest to
// default values
let mut snapshot_vm_config = VmConfig {
cpus: CpusConfig::default(),
memory: MemoryConfig::default(),
payload: None,
rate_limit_groups: None,
disks: None,
rng: RngConfig::default(),
generic_vhost_user: None,
balloon: None,
fs: None,
pmem: None,
serial: SerialConfig::default(),
console: ConsoleConfig::default(),
#[cfg(target_arch = "x86_64")]
debug_console: DebugConsoleConfig::default(),
devices: Some(vec![DeviceConfig {
pci_common: PciDeviceCommonConfig {
id: Some("vfio0".to_owned()),
..Default::default()
},
path: Some(PathBuf::from("/sys/bus/pci/devices/0000:01:00.0")),
fd: None,
x_nv_gpudirect_clique: None,
x_exclude_mmap_bars: Vec::new(),
}]),
user_devices: None,
vdpa: None,
vsock: None,
#[cfg(feature = "pvmemcontrol")]
pvmemcontrol: None,
pvpanic: false,
iommu: false,
numa: None,
watchdog: false,
rtc: None,
#[cfg(feature = "guest_debug")]
gdb: false,
pci_segments: None,
platform: Some(PlatformConfig {
iommufd: true,
..platform_fixture()
}),
tpm: None,
preserved_fds: None,
net: None,
landlock_enable: false,
landlock_rules: None,
#[cfg(feature = "ivshmem")]
ivshmem: None,
};
let valid_config = RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"),
prefault: false,
memory_restore_mode: MemoryRestoreMode::Copy,
net_fds: None,
vfio_fds: Some(vec![RestoredVfioConfig {
id: "vfio0".to_string(),
fd: Some(5),
}]),
iommufd_fd: Some(6),
resume: false,
zone_updates: vec![],
};
valid_config.validate(&snapshot_vm_config).unwrap();
let missing_iommufd = RestoreConfig {
iommufd_fd: None,
..valid_config.clone()
};
assert_eq!(
missing_iommufd.validate(&snapshot_vm_config),
Err(ValidationError::VfioFdRequiresIommufdFd)
);
let unknown_id = RestoreConfig {
vfio_fds: Some(vec![RestoredVfioConfig {
id: "missing".to_string(),
fd: Some(5),
}]),
..valid_config.clone()
};
assert_eq!(
unknown_id.validate(&snapshot_vm_config),
Err(ValidationError::RestoreUnknownVfioId("missing".to_string()))
);
let duplicate_id = RestoreConfig {
vfio_fds: Some(vec![
RestoredVfioConfig {
id: "vfio0".to_string(),
fd: Some(5),
},
RestoredVfioConfig {
id: "vfio0".to_string(),
fd: Some(6),
},
]),
..valid_config.clone()
};
assert_eq!(
duplicate_id.validate(&snapshot_vm_config),
Err(ValidationError::IdentifierNotUnique("vfio0".to_string()))
);
// A device saved with an FD must get a replacement FD.
snapshot_vm_config.devices.as_mut().unwrap()[0].path = None;
snapshot_vm_config.devices.as_mut().unwrap()[0].fd = Some(-1);
let no_substitution = RestoreConfig {
vfio_fds: None,
iommufd_fd: None,
..valid_config.clone()
};
assert_eq!(
no_substitution.validate(&snapshot_vm_config),
Err(ValidationError::RestoreMissingVfioFd("vfio0".to_string()))
);
// The iommufd backend must be enabled in the snapshot config.
snapshot_vm_config.platform = Some(platform_fixture());
assert_eq!(
valid_config.validate(&snapshot_vm_config),
Err(ValidationError::IommufdFdRequiresIommufd)
);
}
fn platform_fixture() -> PlatformConfig {
PlatformConfig {
num_pci_segments: MAX_NUM_PCI_SEGMENTS,
iommu_segments: None,
iommu_address_width_bits: MAX_IOMMU_ADDRESS_WIDTH_BITS,
system_serial_number: None,
system_uuid: None,
oem_strings: None,
iommufd: false,
iommufd_fd: None,
vfio_p2p_dma: default_platformconfig_vfio_p2p_dma(),
system_manufacturer: None,
system_product_name: None,
system_version: None,
system_family: None,
system_sku_number: None,
chassis_asset_tag: None,
#[cfg(feature = "tdx")]
tdx: false,
#[cfg(feature = "sev_snp")]
sev_snp: false,
}
}
fn numa_fixture() -> NumaConfig {
NumaConfig {
guest_numa_id: 0,
cpus: None,
distances: None,
device_id: None,
memory_zones: None,
pci_segments: None,
}
}
#[test]
fn test_config_validation() {
let mut valid_config = VmConfig {
cpus: CpusConfig {
boot_vcpus: 1,
max_vcpus: 1,
..Default::default()
},
memory: MemoryConfig {
size: 536_870_912,
mergeable: false,
hotplug_method: HotplugMethod::Acpi,
hotplug_size: None,
hotplugged_size: None,
shared: false,
hugepages: false,
hugepage_size: None,
prefault: false,
reserve: false,
zones: None,
thp: true,
},
payload: Some(PayloadConfig {
kernel: Some(PathBuf::from("/path/to/kernel")),
firmware: None,
cmdline: None,
initramfs: None,
#[cfg(feature = "igvm")]
igvm: None,
#[cfg(feature = "sev_snp")]
host_data: Some(
"243eb7dc1a21129caa91dcbb794922b933baecb5823a377eb431188673288c07".to_string(),
),
#[cfg(feature = "fw_cfg")]
fw_cfg_config: None,
}),
rate_limit_groups: None,
disks: None,
net: None,
rng: RngConfig {
src: PathBuf::from("/dev/urandom"),
pci_common: PciDeviceCommonConfig::default(),
},
balloon: None,
fs: None,
generic_vhost_user: None,
pmem: None,
serial: SerialConfig {
common: CommonConsoleConfig {
file: None,
mode: ConsoleOutputMode::Null,
socket: None,
},
},
console: ConsoleConfig {
common: CommonConsoleConfig {
file: None,
mode: ConsoleOutputMode::Tty,
socket: None,
},
pci_common: PciDeviceCommonConfig::default(),
},
#[cfg(target_arch = "x86_64")]
debug_console: DebugConsoleConfig::default(),
devices: None,
user_devices: None,
vdpa: None,
vsock: None,
#[cfg(feature = "pvmemcontrol")]
pvmemcontrol: None,
pvpanic: false,
iommu: false,
numa: None,
watchdog: false,
rtc: None,
#[cfg(feature = "guest_debug")]
gdb: false,
pci_segments: None,
platform: None,
tpm: None,
preserved_fds: None,
landlock_enable: false,
landlock_rules: None,
#[cfg(feature = "ivshmem")]
ivshmem: None,
};
valid_config.validate().unwrap();
let mut invalid_config = valid_config.clone();
invalid_config.serial.common.mode = ConsoleOutputMode::Tty;
invalid_config.console.common.mode = ConsoleOutputMode::Tty;
valid_config.validate().unwrap();
let mut invalid_config = valid_config.clone();
invalid_config.payload = None;
assert_eq!(
invalid_config.validate(),
Err(ValidationError::PayloadError(
PayloadConfigError::MissingBootitem
))
);
#[cfg(feature = "fw_cfg")]
{
let mut invalid_config = valid_config.clone();
if let Some(payload) = invalid_config.payload.as_mut() {
payload.fw_cfg_config = Some(FwCfgConfig {
e820: true,
kernel: false,
cmdline: false,
initramfs: false,
acpi_tables: true,
items: Some(FwCfgItemList {
item_list: vec![FwCfgItem {
name: "opt/org.test/invalid".to_string(),
file: None,
string: None,
}],
}),
});
}
assert_eq!(
invalid_config.validate(),
Err(ValidationError::PayloadError(
PayloadConfigError::FwCfgInvalidItem("opt/org.test/invalid".to_string())
))
);
}
let mut invalid_config = valid_config.clone();
invalid_config.serial.common.mode = ConsoleOutputMode::File;
invalid_config.serial.common.file = None;
assert_eq!(
invalid_config.validate(),
Err(ValidationError::ConsoleFileMissing)
);
let mut invalid_config = valid_config.clone();
invalid_config.cpus.max_vcpus = 16;
invalid_config.cpus.boot_vcpus = 32;
assert_eq!(
invalid_config.validate(),
Err(ValidationError::CpusMaxLowerThanBoot(16, 32))
);
let mut invalid_config = valid_config.clone();
invalid_config.cpus.max_vcpus = 16;
invalid_config.cpus.boot_vcpus = 16;
invalid_config.cpus.topology = Some(CpuTopology {
threads_per_core: 2,
cores_per_die: 8,
dies_per_package: 1,
packages: 2,
});
assert_eq!(
invalid_config.validate(),
Err(ValidationError::CpuTopologyCount)
);
let mut still_valid_config = valid_config.clone();
still_valid_config.cpus.max_vcpus = 8;
still_valid_config.cpus.boot_vcpus = 8;
still_valid_config.cpus.topology = Some(CpuTopology {
threads_per_core: 1,
cores_per_die: 8,
dies_per_package: 1,
packages: 1,
});
still_valid_config.validate().unwrap();
let mut still_valid_config = valid_config.clone();
still_valid_config.cpus.max_vcpus = 8;
still_valid_config.cpus.boot_vcpus = 8;
still_valid_config.cpus.topology = Some(CpuTopology {
threads_per_core: 2,
cores_per_die: 4,
dies_per_package: 1,
packages: 1,
});
still_valid_config.validate().unwrap();
#[cfg(target_arch = "x86_64")]
{
let mut invalid_config = valid_config.clone();
invalid_config.cpus.max_vcpus = 6;
invalid_config.cpus.boot_vcpus = 6;
invalid_config.cpus.topology = Some(CpuTopology {
threads_per_core: 3,
cores_per_die: 2,
dies_per_package: 1,
packages: 1,
});
assert_eq!(
invalid_config.validate(),
Err(ValidationError::CpuTopologyThreadsPerCore)
);
}
let mut invalid_config = valid_config.clone();
invalid_config.disks = Some(vec![DiskConfig {
vhost_socket: Some("/path/to/sock".to_owned()),
path: Some(PathBuf::from("/path/to/image")),
..disk_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::DiskSocketAndPath)
);
let mut invalid_config = valid_config.clone();
invalid_config.memory.shared = true;
invalid_config.disks = Some(vec![DiskConfig {
path: None,
vhost_user: true,
..disk_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::VhostUserMissingSocket)
);
let mut invalid_config = valid_config.clone();
invalid_config.disks = Some(vec![DiskConfig {
path: None,
vhost_user: true,
vhost_socket: Some("/path/to/sock".to_owned()),
..disk_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::VhostUserRequiresSharedMemory)
);
let mut still_valid_config = valid_config.clone();
still_valid_config.disks = Some(vec![DiskConfig {
path: None,
vhost_user: true,
vhost_socket: Some("/path/to/sock".to_owned()),
..disk_fixture()
}]);
still_valid_config.memory.shared = true;
still_valid_config.validate().unwrap();
// A block queue size that is not a power of 2 is rejected.
let mut invalid_config = valid_config.clone();
invalid_config.disks = Some(vec![DiskConfig {
queue_size: 100,
..disk_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::InvalidQueueSize(100))
);
// A power-of-2 block queue size too small for a usable seg_max is
// rejected with the block-specific error.
let mut invalid_config = valid_config.clone();
invalid_config.disks = Some(vec![DiskConfig {
queue_size: MINIMUM_BLOCK_QUEUE_SIZE,
..disk_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::BlockQueueSizeTooSmall(
MINIMUM_BLOCK_QUEUE_SIZE
))
);
// A net queue size that is not a power of 2 is rejected.
let mut invalid_config = valid_config.clone();
invalid_config.net = Some(vec![NetConfig {
queue_size: 100,
..net_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::InvalidQueueSize(100))
);
let mut invalid_config = valid_config.clone();
invalid_config.net = Some(vec![NetConfig {
vhost_user: true,
..net_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::VhostUserRequiresSharedMemory)
);
let mut invalid_config = valid_config.clone();
invalid_config.memory.shared = true;
invalid_config.net = Some(vec![NetConfig {
vhost_user: true,
..net_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::VhostUserMissingSocket)
);
let mut still_valid_config = valid_config.clone();
still_valid_config.net = Some(vec![NetConfig {
vhost_user: true,
vhost_socket: Some("/path/to/sock".to_owned()),
..net_fixture()
}]);
still_valid_config.memory.shared = true;
still_valid_config.validate().unwrap();
// Test vhost_user with rate limiting for disk
let mut invalid_config = valid_config.clone();
invalid_config.memory.shared = true;
invalid_config.disks = Some(vec![DiskConfig {
path: None,
vhost_user: true,
vhost_socket: Some("/path/to/sock".to_owned()),
rate_limiter_config: Some(RateLimiterConfig::default()),
..disk_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::VhostUserRateLimiterNotSupported)
);
// Test vhost_user with rate_limit_group for disk
let mut invalid_config = valid_config.clone();
invalid_config.memory.shared = true;
invalid_config.disks = Some(vec![DiskConfig {
path: None,
vhost_user: true,
vhost_socket: Some("/path/to/sock".to_owned()),
rate_limit_group: Some("group0".to_string()),
..disk_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::VhostUserRateLimiterNotSupported)
);
// Test vhost_user with rate limiting for net
let mut invalid_config = valid_config.clone();
invalid_config.memory.shared = true;
invalid_config.net = Some(vec![NetConfig {
vhost_user: true,
vhost_socket: Some("/path/to/sock".to_owned()),
rate_limiter_config: Some(RateLimiterConfig::default()),
..net_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::VhostUserRateLimiterNotSupported)
);
let mut invalid_config = valid_config.clone();
invalid_config.net = Some(vec![NetConfig {
fds: Some(vec![0]),
..net_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::VnetReservedFd(0))
);
let mut invalid_config = valid_config.clone();
invalid_config.net = Some(vec![NetConfig {
offload_csum: false,
..net_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::NoHardwareChecksumOffload)
);
let mut invalid_config = valid_config.clone();
invalid_config.net = Some(vec![NetConfig {
ip: None,
mask: Some("255.255.255.0".parse().unwrap()),
..net_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::MaskProvidedWithoutIp)
);
let mut invalid_config = valid_config.clone();
invalid_config.net = Some(vec![NetConfig {
ip: Some("192.1.33.7".parse().unwrap()),
mask: None,
..net_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::IpProvidedWithoutMask)
);
let mut invalid_config = valid_config.clone();
invalid_config.fs = Some(vec![fs_fixture()]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::VhostUserRequiresSharedMemory)
);
let mut still_valid_config = valid_config.clone();
still_valid_config.memory.shared = true;
still_valid_config.validate().unwrap();
let mut still_valid_config = valid_config.clone();
still_valid_config.memory.hugepages = true;
still_valid_config.validate().unwrap();
let mut still_valid_config = valid_config.clone();
still_valid_config.memory.hugepages = true;
still_valid_config.memory.hugepage_size = Some(2 << 20);
still_valid_config.validate().unwrap();
let mut invalid_config = valid_config.clone();
invalid_config.memory.hugepages = false;
invalid_config.memory.hugepage_size = Some(2 << 20);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::HugePageSizeWithoutHugePages)
);
let mut invalid_config = valid_config.clone();
invalid_config.memory.hugepages = true;
invalid_config.memory.hugepage_size = Some(3 << 20);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::InvalidHugePageSize(3 << 20))
);
// Test mergeable and shared validation for global memory
let mut invalid_config = valid_config.clone();
invalid_config.memory.shared = true;
invalid_config.memory.mergeable = true;
assert_eq!(
invalid_config.validate(),
Err(ValidationError::InvalidSharedMemoryWithMergeable)
);
// Test mergeable and shared validation for memory zones
let mut invalid_config = valid_config.clone();
invalid_config.memory.zones = Some(vec![MemoryZoneConfig {
id: "mem0".to_string(),
size: 1 << 30,
shared: true,
mergeable: true,
..Default::default()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::InvalidSharedMemoryWithMergeable)
);
let mut still_valid_config = valid_config.clone();
still_valid_config.platform = Some(platform_fixture());
still_valid_config.validate().unwrap();
let mut invalid_config = valid_config.clone();
invalid_config.platform = Some(PlatformConfig {
num_pci_segments: MAX_NUM_PCI_SEGMENTS + 1,
..platform_fixture()
});
assert_eq!(
invalid_config.validate(),
Err(ValidationError::InvalidNumPciSegments(
MAX_NUM_PCI_SEGMENTS + 1
))
);
let mut still_valid_config = valid_config.clone();
still_valid_config.platform = Some(PlatformConfig {
iommu_segments: Some(Box::new([1, 2, 3])),
..platform_fixture()
});
still_valid_config.validate().unwrap();
let mut invalid_config = valid_config.clone();
invalid_config.platform = Some(PlatformConfig {
iommu_segments: Some(Box::new([
MAX_NUM_PCI_SEGMENTS + 1,
MAX_NUM_PCI_SEGMENTS + 2,
])),
..platform_fixture()
});
assert_eq!(
invalid_config.validate(),
Err(ValidationError::InvalidPciSegment(MAX_NUM_PCI_SEGMENTS + 1))
);
let mut invalid_config = valid_config.clone();
invalid_config.platform = Some(PlatformConfig {
iommu_address_width_bits: MAX_IOMMU_ADDRESS_WIDTH_BITS + 1,
..platform_fixture()
});
assert_eq!(
invalid_config.validate(),
Err(ValidationError::InvalidIommuAddressWidthBits(
MAX_IOMMU_ADDRESS_WIDTH_BITS + 1
))
);
let mut still_valid_config = valid_config.clone();
still_valid_config.platform = Some(PlatformConfig {
iommu_segments: Some(Box::new([1, 2, 3])),
..platform_fixture()
});
still_valid_config.disks = Some(vec![DiskConfig {
pci_common: PciDeviceCommonConfig {
iommu: true,
pci_segment: 1,
..Default::default()
},
..disk_fixture()
}]);
still_valid_config.validate().unwrap();
let mut still_valid_config = valid_config.clone();
still_valid_config.platform = Some(PlatformConfig {
iommu_segments: Some(Box::new([1, 2, 3])),
..platform_fixture()
});
still_valid_config.net = Some(vec![NetConfig {
pci_common: PciDeviceCommonConfig {
iommu: true,
pci_segment: 1,
..Default::default()
},
..net_fixture()
}]);
still_valid_config.validate().unwrap();
let mut still_valid_config = valid_config.clone();
still_valid_config.platform = Some(PlatformConfig {
iommu_segments: Some(Box::new([1, 2, 3])),
..platform_fixture()
});
still_valid_config.pmem = Some(vec![PmemConfig {
pci_common: PciDeviceCommonConfig {
iommu: true,
pci_segment: 1,
..Default::default()
},
..pmem_fixture()
}]);
still_valid_config.validate().unwrap();
let mut still_valid_config = valid_config.clone();
still_valid_config.platform = Some(PlatformConfig {
iommu_segments: Some(Box::new([1, 2, 3])),
..platform_fixture()
});
still_valid_config.devices = Some(vec![DeviceConfig {
pci_common: PciDeviceCommonConfig {
iommu: true,
pci_segment: 1,
..Default::default()
},
..device_fixture()
}]);
still_valid_config.validate().unwrap();
let mut still_valid_config = valid_config.clone();
still_valid_config.platform = Some(PlatformConfig {
iommu_segments: Some(Box::new([1, 2, 3])),
..platform_fixture()
});
still_valid_config.vsock = Some(VsockConfig {
pci_common: PciDeviceCommonConfig {
iommu: true,
pci_segment: 1,
..Default::default()
},
cid: 3,
socket: PathBuf::new(),
});
still_valid_config.validate().unwrap();
let mut invalid_config = valid_config.clone();
invalid_config.platform = Some(PlatformConfig {
iommu_segments: Some(Box::new([1, 2, 3])),
..platform_fixture()
});
invalid_config.disks = Some(vec![DiskConfig {
pci_common: PciDeviceCommonConfig {
iommu: false,
pci_segment: 1,
..Default::default()
},
..disk_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::OnIommuSegment(1))
);
let mut invalid_config = valid_config.clone();
invalid_config.platform = Some(PlatformConfig {
iommu_segments: Some(Box::new([1, 2, 3])),
..platform_fixture()
});
invalid_config.net = Some(vec![NetConfig {
pci_common: PciDeviceCommonConfig {
iommu: false,
pci_segment: 1,
..Default::default()
},
..net_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::OnIommuSegment(1))
);
let mut invalid_config = valid_config.clone();
invalid_config.platform = Some(PlatformConfig {
num_pci_segments: MAX_NUM_PCI_SEGMENTS,
iommu_segments: Some(Box::new([1, 2, 3])),
..platform_fixture()
});
invalid_config.pmem = Some(vec![PmemConfig {
pci_common: PciDeviceCommonConfig {
pci_segment: 1,
..Default::default()
},
..pmem_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::OnIommuSegment(1))
);
let mut invalid_config = valid_config.clone();
invalid_config.platform = Some(PlatformConfig {
num_pci_segments: MAX_NUM_PCI_SEGMENTS,
iommu_segments: Some(Box::new([1, 2, 3])),
..platform_fixture()
});
invalid_config.devices = Some(vec![DeviceConfig {
pci_common: PciDeviceCommonConfig {
pci_segment: 1,
..Default::default()
},
..device_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::OnIommuSegment(1))
);
let mut invalid_config = valid_config.clone();
invalid_config.platform = Some(PlatformConfig {
iommu_segments: Some(Box::new([1, 2, 3])),
..platform_fixture()
});
invalid_config.vsock = Some(VsockConfig {
pci_common: PciDeviceCommonConfig {
pci_segment: 1,
..Default::default()
},
cid: 3,
socket: PathBuf::new(),
});
assert_eq!(
invalid_config.validate(),
Err(ValidationError::OnIommuSegment(1))
);
let mut invalid_config = valid_config.clone();
invalid_config.memory.shared = true;
invalid_config.platform = Some(PlatformConfig {
iommu_segments: Some(Box::new([1, 2, 3])),
..platform_fixture()
});
invalid_config.user_devices = Some(vec![UserDeviceConfig {
pci_common: PciDeviceCommonConfig {
pci_segment: 1,
..Default::default()
},
socket: PathBuf::new(),
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::OnIommuSegment(1))
);
let mut invalid_config = valid_config.clone();
invalid_config.platform = Some(PlatformConfig {
iommu_segments: Some(Box::new([1, 2, 3])),
..platform_fixture()
});
invalid_config.vdpa = Some(vec![VdpaConfig {
pci_common: PciDeviceCommonConfig {
pci_segment: 1,
..Default::default()
},
..vdpa_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::OnIommuSegment(1))
);
let mut invalid_config = valid_config.clone();
invalid_config.memory.shared = true;
invalid_config.platform = Some(PlatformConfig {
iommu_segments: Some(Box::new([1, 2, 3])),
..platform_fixture()
});
invalid_config.fs = Some(vec![FsConfig {
pci_common: PciDeviceCommonConfig {
pci_segment: 1,
..Default::default()
},
..fs_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::OnIommuSegment(1))
);
let mut invalid_config = valid_config.clone();
invalid_config.platform = Some(PlatformConfig {
num_pci_segments: 2,
..platform_fixture()
});
invalid_config.numa = Some(Box::new([
NumaConfig {
guest_numa_id: 0,
cpus: Some(Box::new([0])),
pci_segments: Some(Box::new([1])),
..numa_fixture()
},
NumaConfig {
guest_numa_id: 1,
cpus: Some(Box::new([1])),
pci_segments: Some(Box::new([1])),
..numa_fixture()
},
]));
assert_eq!(
invalid_config.validate(),
Err(ValidationError::PciSegmentReused(1, 0, 1))
);
let mut invalid_config = valid_config.clone();
invalid_config.pci_segments = Some(Box::new([PciSegmentConfig {
pci_segment: 0,
mmio32_aperture_weight: 1,
mmio64_aperture_weight: 0,
}]));
assert_eq!(
invalid_config.validate(),
Err(ValidationError::InvalidPciSegmentApertureWeight(0))
);
let mut invalid_config = valid_config.clone();
invalid_config.pci_segments = Some(Box::new([PciSegmentConfig {
pci_segment: 0,
mmio32_aperture_weight: 0,
mmio64_aperture_weight: 1,
}]));
assert_eq!(
invalid_config.validate(),
Err(ValidationError::InvalidPciSegmentApertureWeight(0))
);
let mut invalid_config = valid_config.clone();
invalid_config.numa = Some(Box::new([
NumaConfig {
guest_numa_id: 0,
cpus: Some(Box::new([0])),
..numa_fixture()
},
NumaConfig {
guest_numa_id: 1,
cpus: Some(Box::new([1])),
pci_segments: Some(Box::new([0])),
..numa_fixture()
},
]));
assert_eq!(
invalid_config.validate(),
Err(ValidationError::DefaultPciSegmentInvalidNode(1))
);
let mut invalid_config = valid_config.clone();
invalid_config.numa = Some(Box::new([
NumaConfig {
guest_numa_id: 0,
cpus: Some(Box::new([0])),
pci_segments: Some(Box::new([0])),
..numa_fixture()
},
NumaConfig {
guest_numa_id: 1,
cpus: Some(Box::new([1])),
pci_segments: Some(Box::new([1])),
..numa_fixture()
},
]));
assert_eq!(
invalid_config.validate(),
Err(ValidationError::InvalidPciSegment(1))
);
let mut invalid_config = valid_config.clone();
invalid_config.disks = Some(vec![DiskConfig {
rate_limit_group: Some("foo".into()),
..disk_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::InvalidRateLimiterGroup)
);
// Test serial length validation
let mut valid_serial_config = valid_config.clone();
valid_serial_config.disks = Some(vec![DiskConfig {
serial: Some("valid_serial".to_string()),
..disk_fixture()
}]);
valid_serial_config.validate().unwrap();
// Test empty string serial (should be valid)
let mut empty_serial_config = valid_config.clone();
empty_serial_config.disks = Some(vec![DiskConfig {
serial: Some(String::new()),
..disk_fixture()
}]);
empty_serial_config.validate().unwrap();
// Test None serial (should be valid)
let mut none_serial_config = valid_config.clone();
none_serial_config.disks = Some(vec![DiskConfig {
serial: None,
..disk_fixture()
}]);
none_serial_config.validate().unwrap();
// Test maximum length serial (exactly VIRTIO_BLK_ID_BYTES)
let max_serial = "a".repeat(VIRTIO_BLK_ID_BYTES as usize);
let mut max_serial_config = valid_config.clone();
max_serial_config.disks = Some(vec![DiskConfig {
serial: Some(max_serial),
..disk_fixture()
}]);
max_serial_config.validate().unwrap();
// Test serial length exceeding VIRTIO_BLK_ID_BYTES
let long_serial = "a".repeat(VIRTIO_BLK_ID_BYTES as usize + 1);
let mut invalid_serial_config = valid_config.clone();
invalid_serial_config.disks = Some(vec![DiskConfig {
serial: Some(long_serial.clone()),
..disk_fixture()
}]);
assert_eq!(
invalid_serial_config.validate(),
Err(ValidationError::InvalidSerialLength(
long_serial.len(),
VIRTIO_BLK_ID_BYTES as usize
))
);
let mut still_valid_config = valid_config.clone();
still_valid_config.devices = Some(vec![
DeviceConfig {
path: Some("/device1".into()),
..device_fixture()
},
DeviceConfig {
path: Some("/device2".into()),
..device_fixture()
},
]);
still_valid_config.validate().unwrap();
let mut invalid_config = valid_config.clone();
invalid_config.devices = Some(vec![
DeviceConfig {
path: Some("/device1".into()),
..device_fixture()
},
DeviceConfig {
path: Some("/device1".into()),
..device_fixture()
},
]);
invalid_config.validate().unwrap_err();
// An fd-backed DeviceConfig is only valid with
// externally-supplied fd is provided for iommufd too
let mut fd_valid_config = valid_config.clone();
fd_valid_config.platform = Some(PlatformConfig {
iommufd: true,
iommufd_fd: Some(8),
..platform_fixture()
});
fd_valid_config.devices = Some(vec![DeviceConfig {
path: None,
fd: Some(7),
..device_fixture()
}]);
fd_valid_config.validate().unwrap();
let mut fd_without_iommufd_fd = fd_valid_config.clone();
fd_without_iommufd_fd.platform = Some(PlatformConfig {
iommufd: true,
iommufd_fd: None,
..platform_fixture()
});
assert!(matches!(
fd_without_iommufd_fd.validate(),
Err(ValidationError::VfioFdRequiresIommufdFd),
));
// iommufd_fd without iommufd=on is rejected at PlatformConfig::validate.
let mut iommufd_fd_without_iommufd = valid_config.clone();
iommufd_fd_without_iommufd.platform = Some(PlatformConfig {
iommufd: false,
iommufd_fd: Some(8),
..platform_fixture()
});
assert!(matches!(
iommufd_fd_without_iommufd.validate(),
Err(ValidationError::IommufdFdRequiresIommufd),
));
// Exactly one of path and fd must be set.
let mut both_path_and_fd = fd_valid_config.clone();
both_path_and_fd.devices = Some(vec![DeviceConfig {
path: Some("/device1".into()),
fd: Some(7),
..device_fixture()
}]);
assert!(matches!(
both_path_and_fd.validate(),
Err(ValidationError::VfioDeviceBothPathAndFd),
));
let mut neither_path_nor_fd = fd_valid_config.clone();
neither_path_nor_fd.devices = Some(vec![DeviceConfig {
path: None,
fd: None,
..device_fixture()
}]);
assert!(matches!(
neither_path_nor_fd.validate(),
Err(ValidationError::VfioDeviceNeitherPathNorFd),
));
#[cfg(feature = "sev_snp")]
{
// Payload with empty host data
let mut config_with_no_host_data = valid_config.clone();
config_with_no_host_data.payload = Some(PayloadConfig {
kernel: Some(PathBuf::from("/path/to/kernel")),
firmware: None,
cmdline: None,
initramfs: None,
#[cfg(feature = "igvm")]
igvm: None,
#[cfg(feature = "sev_snp")]
host_data: Some(String::new()),
#[cfg(feature = "fw_cfg")]
fw_cfg_config: None,
});
config_with_no_host_data.validate().unwrap_err();
// Payload with no host data provided
let mut valid_config_with_no_host_data = valid_config.clone();
valid_config_with_no_host_data.payload = Some(PayloadConfig {
kernel: Some(PathBuf::from("/path/to/kernel")),
firmware: None,
cmdline: None,
initramfs: None,
#[cfg(feature = "igvm")]
igvm: None,
#[cfg(feature = "sev_snp")]
host_data: None,
#[cfg(feature = "fw_cfg")]
fw_cfg_config: None,
});
valid_config_with_no_host_data.validate().unwrap();
// Payload with invalid host data length i.e less than 64
let mut config_with_invalid_host_data = valid_config.clone();
config_with_invalid_host_data.payload = Some(PayloadConfig {
kernel: Some(PathBuf::from("/path/to/kernel")),
firmware: None,
cmdline: None,
initramfs: None,
#[cfg(feature = "igvm")]
igvm: None,
#[cfg(feature = "sev_snp")]
host_data: Some(
"243eb7dc1a21129caa91dcbb794922b933baecb5823a377eb43118867328".to_string(),
),
#[cfg(feature = "fw_cfg")]
fw_cfg_config: None,
});
config_with_invalid_host_data.validate().unwrap_err();
}
// x_nv_gpudirect_clique with vfio_p2p_dma=off should fail
let mut invalid_config = valid_config.clone();
invalid_config.platform = Some(PlatformConfig {
vfio_p2p_dma: false,
..platform_fixture()
});
invalid_config.devices = Some(vec![DeviceConfig {
x_nv_gpudirect_clique: Some(0),
..device_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::GpuDirectCliqueRequiresP2pDma)
);
// x_nv_gpudirect_clique with vfio_p2p_dma=on should pass
let mut still_valid_config = valid_config.clone();
still_valid_config.platform = Some(PlatformConfig {
vfio_p2p_dma: true,
..platform_fixture()
});
still_valid_config.devices = Some(vec![DeviceConfig {
x_nv_gpudirect_clique: Some(0),
..device_fixture()
}]);
still_valid_config.validate().unwrap();
// x_nv_gpudirect_clique with no platform config (default p2p_dma=on) should pass
let mut still_valid_config = valid_config.clone();
still_valid_config.devices = Some(vec![DeviceConfig {
x_nv_gpudirect_clique: Some(0),
..device_fixture()
}]);
still_valid_config.validate().unwrap();
// x_exclude_mmap_bars only accepts PCI BAR indices 0 through 5
let mut invalid_config = valid_config.clone();
invalid_config.devices = Some(vec![DeviceConfig {
x_exclude_mmap_bars: vec![6],
..device_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::InvalidDeviceExcludeMmapBar(6))
);
let mut still_valid_config = valid_config.clone();
// SAFETY: Safe as the file was just opened
let fd1 = unsafe { libc::dup(File::open("/dev/null").unwrap().as_raw_fd()) };
// SAFETY: Safe as the file was just opened
let fd2 = unsafe { libc::dup(File::open("/dev/null").unwrap().as_raw_fd()) };
// SAFETY: safe as both FDs are valid
unsafe {
still_valid_config.add_preserved_fds(vec![fd1, fd2]);
}
let _still_valid_config = still_valid_config.clone();
// Valid BDF test
let mut still_valid_config = valid_config.clone();
still_valid_config.disks = Some(vec![DiskConfig {
pci_common: PciDeviceCommonConfig {
pci_device_id: Some(8),
..Default::default()
},
..disk_fixture()
}]);
still_valid_config.validate().unwrap();
// Invalid BDF - Same ID as Root device
let mut invalid_config = valid_config.clone();
invalid_config.disks = Some(vec![DiskConfig {
pci_common: PciDeviceCommonConfig {
pci_device_id: Some(pci::PCI_ROOT_DEVICE_ID),
..Default::default()
},
..disk_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::ReservedPciDeviceId(
pci::PCI_ROOT_DEVICE_ID
))
);
// Invalid BDF - Out of range
let mut invalid_config = valid_config.clone();
invalid_config.disks = Some(vec![DiskConfig {
pci_common: PciDeviceCommonConfig {
pci_device_id: Some(pci::NUM_DEVICE_IDS + 1),
..Default::default()
},
..disk_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::InvalidPciDeviceId(pci::NUM_DEVICE_IDS + 1))
);
// Invalid balloon BDF - Same ID as Root device
let mut invalid_config = valid_config.clone();
invalid_config.balloon = Some(BalloonConfig {
pci_common: PciDeviceCommonConfig {
pci_device_id: Some(pci::PCI_ROOT_DEVICE_ID),
..Default::default()
},
size: 0,
deflate_on_oom: false,
free_page_reporting: false,
});
assert_eq!(
invalid_config.validate(),
Err(ValidationError::ReservedPciDeviceId(
pci::PCI_ROOT_DEVICE_ID
))
);
// Invalid balloon ID - Duplicate identifier
let mut invalid_config = valid_config.clone();
invalid_config.balloon = Some(BalloonConfig {
pci_common: PciDeviceCommonConfig {
id: Some("test0".to_string()),
..Default::default()
},
size: 0,
deflate_on_oom: false,
free_page_reporting: false,
});
invalid_config.disks = Some(vec![DiskConfig {
pci_common: PciDeviceCommonConfig {
id: Some("test0".to_string()),
..Default::default()
},
..disk_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::IdentifierNotUnique("test0".to_string()))
);
// Invalid console BDF - Same ID as Root device
let mut invalid_config = valid_config.clone();
invalid_config.console.pci_common.pci_device_id = Some(pci::PCI_ROOT_DEVICE_ID);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::ReservedPciDeviceId(
pci::PCI_ROOT_DEVICE_ID
))
);
// Invalid console BDF - Out of range
let mut invalid_config = valid_config.clone();
invalid_config.console.pci_common.pci_device_id = Some(pci::NUM_DEVICE_IDS + 1);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::InvalidPciDeviceId(pci::NUM_DEVICE_IDS + 1))
);
// Invalid console ID - Duplicate identifier
let mut invalid_config = valid_config.clone();
invalid_config.console.pci_common.id = Some("test0".to_string());
invalid_config.disks = Some(vec![DiskConfig {
pci_common: PciDeviceCommonConfig {
id: Some("test0".to_string()),
..Default::default()
},
..disk_fixture()
}]);
assert_eq!(
invalid_config.validate(),
Err(ValidationError::IdentifierNotUnique("test0".to_string()))
);
}
#[test]
fn test_landlock_parsing() -> Result<()> {
// should not be empty
LandlockConfig::parse("").unwrap_err();
// access should not be empty
LandlockConfig::parse("path=/dir/path1").unwrap_err();
LandlockConfig::parse("path=/dir/path1,access=rwr").unwrap_err();
assert_eq!(
LandlockConfig::parse("path=/dir/path1,access=rw")?,
LandlockConfig {
path: PathBuf::from("/dir/path1"),
access: "rw".to_string(),
}
);
Ok(())
}
#[test]
#[cfg(feature = "fw_cfg")]
fn test_fw_cfg_config_item_list_parsing() -> Result<()> {
// Empty list
FwCfgConfig::parse("items=[]").unwrap_err();
// Missing closing bracket
FwCfgConfig::parse("items=[name=opt/org.test/fw_cfg_test_item,file=/tmp/fw_cfg_test_item")
.unwrap_err();
// Single file Item
assert_eq!(
FwCfgConfig::parse(
"items=[name=opt/org.test/fw_cfg_test_item,file=/tmp/fw_cfg_test_item]"
)?,
FwCfgConfig {
items: Some(FwCfgItemList {
item_list: vec![FwCfgItem {
name: "opt/org.test/fw_cfg_test_item".to_string(),
file: Some(PathBuf::from("/tmp/fw_cfg_test_item")),
string: None,
}]
}),
..Default::default()
},
);
// Multiple file Items
assert_eq!(
FwCfgConfig::parse(
"items=[name=opt/org.test/fw_cfg_test_item,file=/tmp/fw_cfg_test_item:name=opt/org.test/fw_cfg_test_item2,file=/tmp/fw_cfg_test_item2]"
)?,
FwCfgConfig {
items: Some(FwCfgItemList {
item_list: vec![
FwCfgItem {
name: "opt/org.test/fw_cfg_test_item".to_string(),
file: Some(PathBuf::from("/tmp/fw_cfg_test_item")),
string: None,
},
FwCfgItem {
name: "opt/org.test/fw_cfg_test_item2".to_string(),
file: Some(PathBuf::from("/tmp/fw_cfg_test_item2")),
string: None,
}
]
}),
..Default::default()
},
);
// Single string Item (for OVMF MMIO64 config, GPU CC passthrough, etc.)
assert_eq!(
FwCfgConfig::parse("items=[name=opt/ovmf/X-PciMmio64Mb,string=262144]")?,
FwCfgConfig {
items: Some(FwCfgItemList {
item_list: vec![FwCfgItem {
name: "opt/ovmf/X-PciMmio64Mb".to_string(),
file: None,
string: Some("262144".to_string()),
}]
}),
..Default::default()
},
);
// Mixed file and string Items
assert_eq!(
FwCfgConfig::parse(
"items=[name=opt/org.test/fw_cfg_test_item,file=/tmp/fw_cfg_test_item:name=opt/ovmf/X-PciMmio64Mb,string=262144]"
)?,
FwCfgConfig {
items: Some(FwCfgItemList {
item_list: vec![
FwCfgItem {
name: "opt/org.test/fw_cfg_test_item".to_string(),
file: Some(PathBuf::from("/tmp/fw_cfg_test_item")),
string: None,
},
FwCfgItem {
name: "opt/ovmf/X-PciMmio64Mb".to_string(),
file: None,
string: Some("262144".to_string()),
}
]
}),
..Default::default()
},
);
// Missing both file and string parses OK but fails validation
let missing_content =
FwCfgConfig::parse("items=[name=opt/org.test/missing_content]").unwrap();
assert_eq!(
missing_content.items.as_ref().unwrap().item_list[0].file,
None
);
assert_eq!(
missing_content.items.as_ref().unwrap().item_list[0].string,
None
);
// Both file and string parses OK but fails validation
let both = FwCfgConfig::parse("items=[name=opt/org.test/both,file=/tmp/test,string=test]")
.unwrap();
assert!(both.items.as_ref().unwrap().item_list[0].file.is_some());
assert!(both.items.as_ref().unwrap().item_list[0].string.is_some());
Ok(())
}
}