mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
The generic vhost-user device took its virtio device type on the command line via the `virtio_id` parameter, but the same value is called `device_type` in the API and the resulting config struct. This irregularity was due to churn during the review process, `device_type` was the intended name. Accept `device_type` on the command line and keep `virtio_id` as a deprecated alias that logs a warning. The alias will then be removed in a later release. Fixes: #8545 Assisted-by: Claude:Opus-4.8 Signed-off-by: Rob Bradford <rbradford@meta.com>
6664 lines
230 KiB
Rust
6664 lines
230 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,
|
|
};
|
|
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),
|
|
/// 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,
|
|
}
|
|
|
|
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::<Tuple<u32, Vec<usize>>>("affinity")
|
|
.map_err(Error::ParseCpus)?
|
|
.map(|v| {
|
|
v.0.iter()
|
|
.map(|(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::<Tuple<u16, Vec<usize>>>("queue_affinity")
|
|
.map_err(Error::ParseDisk)?
|
|
.map(|v| {
|
|
v.0.iter()
|
|
.map(|(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::<Tuple<u64, u64>>("distances")
|
|
.map_err(Error::ParseNuma)?
|
|
.map(|v| {
|
|
v.0.iter()
|
|
.map(|(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, 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 resume: bool,
|
|
}
|
|
|
|
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>,resume=true|false\" \
|
|
\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 `resume` controls whether the VM will be directly resumed after restore ";
|
|
|
|
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("resume");
|
|
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::<Tuple<String, Vec<u64>>>("net_fds")
|
|
.map_err(Error::ParseRestore)?
|
|
.map(|v| {
|
|
v.0.iter()
|
|
.map(|(id, fds)| RestoredNetConfig {
|
|
id: id.clone(),
|
|
num_fds: fds.len(),
|
|
fds: Some(fds.iter().map(|e| *e as i32).collect()),
|
|
})
|
|
.collect()
|
|
});
|
|
let resume = parser
|
|
.convert::<Toggle>("resume")
|
|
.map_err(Error::ParseRestore)?
|
|
.unwrap_or(Toggle(false))
|
|
.0;
|
|
|
|
Ok(RestoreConfig {
|
|
source_url,
|
|
prefault,
|
|
memory_restore_mode,
|
|
net_fds,
|
|
resume,
|
|
})
|
|
}
|
|
|
|
// 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(),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
if !restored_net_with_fds.is_empty() {
|
|
warn!("Ignoring unused 'net_fds' for VM restore.");
|
|
}
|
|
|
|
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,
|
|
resume: false,
|
|
}
|
|
);
|
|
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]),
|
|
}
|
|
]),
|
|
resume: false,
|
|
}
|
|
);
|
|
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,
|
|
resume: false,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
RestoreConfig::parse("source_url=/path/to/snapshot,resume=on")?,
|
|
RestoreConfig {
|
|
source_url: PathBuf::from("/path/to/snapshot"),
|
|
prefault: false,
|
|
memory_restore_mode: MemoryRestoreMode::Copy,
|
|
net_fds: None,
|
|
resume: true,
|
|
}
|
|
);
|
|
// 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();
|
|
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
|
|
);
|
|
}
|
|
|
|
#[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]),
|
|
},
|
|
]),
|
|
resume: false,
|
|
};
|
|
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,
|
|
resume: false,
|
|
};
|
|
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,
|
|
resume: false,
|
|
};
|
|
assert_eq!(
|
|
invalid_restore_mode.validate(&snapshot_vm_config),
|
|
Err(ValidationError::InvalidRestorePrefaultWithOnDemand)
|
|
);
|
|
}
|
|
|
|
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(())
|
|
}
|
|
}
|