// Copyright © 2019 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 // use clap::ArgMatches; use net_util::MacAddr; use option_parser::{ ByteSized, IntegerList, OptionParser, OptionParserError, StringList, Toggle, Tuple, }; use serde::{Deserialize, Serialize}; use std::collections::{BTreeSet, HashMap}; use std::convert::From; use std::fmt; use std::net::Ipv4Addr; use std::path::PathBuf; use std::result; use std::str::FromStr; use thiserror::Error; use virtio_devices::{RateLimiterConfig, TokenBucketConfig}; pub const DEFAULT_VCPUS: u8 = 1; pub const DEFAULT_MEMORY_MB: u64 = 512; // When booting with PVH boot the maximum physical addressable size // is a 46 bit address space even when the host supports with 5-level // paging. pub const DEFAULT_MAX_PHYS_BITS: u8 = 46; pub const DEFAULT_RNG_SOURCE: &str = "/dev/urandom"; pub const DEFAULT_NUM_QUEUES_VUNET: usize = 2; pub const DEFAULT_QUEUE_SIZE_VUNET: u16 = 256; pub const DEFAULT_NUM_QUEUES_VUBLK: usize = 1; pub const DEFAULT_QUEUE_SIZE_VUBLK: u16 = 128; pub const DEFAULT_NUM_PCI_SEGMENTS: u16 = 1; const MAX_NUM_PCI_SEGMENTS: u16 = 16; /// Errors associated with VM configuration parameters. #[derive(Debug, Error)] pub enum Error { /// Filesystem tag is missing ParseFsTagMissing, /// Filesystem socket is missing ParseFsSockMissing, /// Missing persistent memory file parameter. ParsePmemFileMissing, /// Missing vsock socket path parameter. ParseVsockSockMissing, /// Missing vsock cid parameter. ParseVsockCidMissing, /// Missing restore source_url parameter. ParseRestoreSourceUrlMissing, /// Error parsing CPU options ParseCpus(OptionParserError), /// Invalid CPU features InvalidCpuFeatures(String), /// Error parsing memory options ParseMemory(OptionParserError), /// Error parsing memory zone options ParseMemoryZone(OptionParserError), /// Missing 'id' from memory zone ParseMemoryZoneIdMissing, /// Error parsing disk options ParseDisk(OptionParserError), /// Error parsing network options ParseNetwork(OptionParserError), /// Error parsing RNG options ParseRng(OptionParserError), /// Error parsing balloon options ParseBalloon(OptionParserError), /// Error parsing filesystem parameters ParseFileSystem(OptionParserError), /// Error parsing persistent memory parameters ParsePersistentMemory(OptionParserError), /// Failed parsing console ParseConsole(OptionParserError), /// No mode given for console ParseConsoleInvalidModeGiven, /// Failed parsing device parameters ParseDevice(OptionParserError), /// Missing path from device, ParseDevicePathMissing, /// Failed parsing vsock parameters ParseVsock(OptionParserError), /// Failed parsing restore parameters ParseRestore(OptionParserError), /// Failed parsing SGX EPC parameters #[cfg(target_arch = "x86_64")] ParseSgxEpc(OptionParserError), /// Missing 'id' from SGX EPC section #[cfg(target_arch = "x86_64")] ParseSgxEpcIdMissing, /// Failed parsing NUMA parameters ParseNuma(OptionParserError), /// Failed validating configuration Validation(ValidationError), #[cfg(feature = "tdx")] /// Failed parsing TDX config ParseTdx(OptionParserError), #[cfg(feature = "tdx")] /// No TDX firmware FirmwarePathMissing, /// Failed parsing userspace device ParseUserDevice(OptionParserError), /// Missing socket for userspace device ParseUserDeviceSocketMissing, /// Failed parsing platform parameters ParsePlatform(OptionParserError), /// Failed parsing vDPA device ParseVdpa(OptionParserError), /// Missing path for vDPA device ParseVdpaPathMissing, } #[derive(Debug, PartialEq, Eq, Error)] pub enum ValidationError { /// Both console and serial are tty. DoubleTtyMode, /// No kernel specified KernelMissing, /// Missing file value for console ConsoleFileMissing, /// Max is less than boot CpusMaxLowerThanBoot, /// Both socket and path specified DiskSocketAndPath, /// Using vhost user requires shared memory VhostUserRequiresSharedMemory, /// No socket provided for vhost_use VhostUserMissingSocket, /// Trying to use IOMMU without PCI IommuUnsupported, /// Trying to use VFIO without PCI VfioUnsupported, /// CPU topology count doesn't match max CpuTopologyCount, /// One part of the CPU topology was zero CpuTopologyZeroPart, #[cfg(target_arch = "aarch64")] /// Dies per package must be 1 CpuTopologyDiesPerPackage, /// Virtio needs a min of 2 queues VnetQueueLowerThan2, /// The input queue number for virtio_net must match the number of input fds VnetQueueFdMismatch, /// Using reserved fd VnetReservedFd, /// Hugepages not turned on HugePageSizeWithoutHugePages, /// Huge page size is not power of 2 InvalidHugePageSize(u64), /// CPU Hotplug is not permitted with TDX #[cfg(feature = "tdx")] TdxNoCpuHotplug, /// Insuffient vCPUs for queues TooManyQueues, /// Need shared memory for vfio-user UserDevicesRequireSharedMemory, /// Memory zone is reused across NUMA nodes MemoryZoneReused(String, u32, u32), /// Invalid number of PCI segments InvalidNumPciSegments(u16), /// Invalid PCI segment id InvalidPciSegment(u16), /// Balloon too big BalloonLargerThanRam(u64, u64), /// On a IOMMU segment but not behind IOMMU OnIommuSegment(u16), // On a IOMMU segment but IOMMU not suported IommuNotSupportedOnSegment(u16), // Identifier is not unique IdentifierNotUnique(String), /// Invalid identifier InvalidIdentifier(String), /// Placing the device behind a virtual IOMMU is not supported IommuNotSupported, /// Duplicated device path (device added twice) DuplicateDevicePath(String), } type ValidationResult = std::result::Result; impl fmt::Display for ValidationError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use self::ValidationError::*; match self { DoubleTtyMode => write!(f, "Console mode tty specified for both serial and console"), KernelMissing => write!(f, "No kernel specified"), ConsoleFileMissing => write!(f, "Path missing when using file console mode"), CpusMaxLowerThanBoot => write!(f, "Max CPUs lower than boot CPUs"), DiskSocketAndPath => write!(f, "Disk path and vhost socket both provided"), VhostUserRequiresSharedMemory => { write!(f, "Using vhost-user requires using shared memory") } VhostUserMissingSocket => write!(f, "No socket provided when using vhost-user"), IommuUnsupported => write!(f, "Using an IOMMU without PCI support is unsupported"), VfioUnsupported => write!(f, "Using VFIO without PCI support is unsupported"), CpuTopologyZeroPart => write!(f, "No part of the CPU topology can be zero"), CpuTopologyCount => write!( f, "Product of CPU topology parts does not match maximum vCPUs" ), #[cfg(target_arch = "aarch64")] CpuTopologyDiesPerPackage => write!(f, "Dies per package must be 1"), VnetQueueLowerThan2 => write!(f, "Number of queues to virtio_net less than 2"), VnetQueueFdMismatch => write!( f, "Number of queues to virtio_net does not match the number of input FDs" ), VnetReservedFd => write!(f, "Reserved fd number (<= 2)"), HugePageSizeWithoutHugePages => { write!(f, "Huge page size specified but huge pages not enabled") } InvalidHugePageSize(s) => { write!(f, "Huge page size is not power of 2: {}", s) } #[cfg(feature = "tdx")] TdxNoCpuHotplug => { write!(f, "CPU hotplug is not permitted with TDX") } TooManyQueues => { write!(f, "Number of vCPUs is insufficient for number of queues") } UserDevicesRequireSharedMemory => { write!(f, "Using user devices requires using shared memory") } MemoryZoneReused(s, u1, u2) => { write!( f, "Memory zone: {} belongs to multiple NUMA nodes {} and {}", s, u1, u2 ) } InvalidNumPciSegments(n) => { write!( f, "Number of PCI segments ({}) not in range of 1 to {}", n, MAX_NUM_PCI_SEGMENTS ) } InvalidPciSegment(pci_segment) => { write!(f, "Invalid PCI segment id: {}", pci_segment) } BalloonLargerThanRam(balloon_size, ram_size) => { write!( f, "Ballon size ({}) greater than RAM ({})", balloon_size, ram_size ) } OnIommuSegment(pci_segment) => { write!( f, "Device is on an IOMMU PCI segment ({}) but not placed behind IOMMU", pci_segment ) } IommuNotSupportedOnSegment(pci_segment) => { write!( f, "Device is on an IOMMU PCI segment ({}) but does not support being placed behind IOMMU", pci_segment ) } IdentifierNotUnique(s) => { write!(f, "Identifier {} is not unique", s) } InvalidIdentifier(s) => { write!(f, "Identifier {} is invalid", s) } IommuNotSupported => { write!(f, "Device does not support being placed behind IOMMU") } DuplicateDevicePath(p) => write!(f, "Duplicated device path: {}", p), } } } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use self::Error::*; match self { ParseConsole(o) => write!(f, "Error parsing --console: {}", o), ParseConsoleInvalidModeGiven => { write!(f, "Error parsing --console: invalid console mode given") } ParseCpus(o) => write!(f, "Error parsing --cpus: {}", o), InvalidCpuFeatures(o) => write!(f, "Invalid feature in --cpus features list: {}", o), ParseDevice(o) => write!(f, "Error parsing --device: {}", o), ParseDevicePathMissing => write!(f, "Error parsing --device: path missing"), ParseFileSystem(o) => write!(f, "Error parsing --fs: {}", o), ParseFsSockMissing => write!(f, "Error parsing --fs: socket missing"), ParseFsTagMissing => write!(f, "Error parsing --fs: tag missing"), ParsePersistentMemory(o) => write!(f, "Error parsing --pmem: {}", o), ParsePmemFileMissing => write!(f, "Error parsing --pmem: file missing"), ParseVsock(o) => write!(f, "Error parsing --vsock: {}", o), ParseVsockCidMissing => write!(f, "Error parsing --vsock: cid missing"), ParseVsockSockMissing => write!(f, "Error parsing --vsock: socket missing"), ParseMemory(o) => write!(f, "Error parsing --memory: {}", o), ParseMemoryZone(o) => write!(f, "Error parsing --memory-zone: {}", o), ParseMemoryZoneIdMissing => write!(f, "Error parsing --memory-zone: id missing"), ParseNetwork(o) => write!(f, "Error parsing --net: {}", o), ParseDisk(o) => write!(f, "Error parsing --disk: {}", o), ParseRng(o) => write!(f, "Error parsing --rng: {}", o), ParseBalloon(o) => write!(f, "Error parsing --balloon: {}", o), ParseRestore(o) => write!(f, "Error parsing --restore: {}", o), #[cfg(target_arch = "x86_64")] ParseSgxEpc(o) => write!(f, "Error parsing --sgx-epc: {}", o), #[cfg(target_arch = "x86_64")] ParseSgxEpcIdMissing => write!(f, "Error parsing --sgx-epc: id missing"), ParseNuma(o) => write!(f, "Error parsing --numa: {}", o), ParseRestoreSourceUrlMissing => { write!(f, "Error parsing --restore: source_url missing") } ParseUserDeviceSocketMissing => { write!(f, "Error parsing --user-device: socket missing") } ParseUserDevice(o) => write!(f, "Error parsing --user-device: {}", o), Validation(v) => write!(f, "Error validating configuration: {}", v), #[cfg(feature = "tdx")] ParseTdx(o) => write!(f, "Error parsing --tdx: {}", o), #[cfg(feature = "tdx")] FirmwarePathMissing => write!(f, "TDX firmware missing"), ParsePlatform(o) => write!(f, "Error parsing --platform: {}", o), ParseVdpa(o) => write!(f, "Error parsing --vdpa: {}", o), ParseVdpaPathMissing => write!(f, "Error parsing --vdpa: path missing"), } } } pub fn add_to_config(items: &mut Option>, item: T) { if let Some(items) = items { items.push(item); } else { *items = Some(vec![item]); } } pub type Result = result::Result; pub struct VmParams<'a> { pub cpus: &'a str, pub memory: &'a str, pub memory_zones: Option>, pub firmware: Option<&'a str>, pub kernel: Option<&'a str>, pub initramfs: Option<&'a str>, pub cmdline: Option<&'a str>, pub disks: Option>, pub net: Option>, pub rng: &'a str, pub balloon: Option<&'a str>, pub fs: Option>, pub pmem: Option>, pub serial: &'a str, pub console: &'a str, pub devices: Option>, pub user_devices: Option>, pub vdpa: Option>, pub vsock: Option<&'a str>, #[cfg(target_arch = "x86_64")] pub sgx_epc: Option>, pub numa: Option>, pub watchdog: bool, #[cfg(feature = "tdx")] pub tdx: Option<&'a str>, #[cfg(feature = "gdb")] pub gdb: bool, pub platform: 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.value_of("cpus").unwrap(); let memory = args.value_of("memory").unwrap(); let memory_zones: Option> = args.values_of("memory-zone").map(|x| x.collect()); let rng = args.value_of("rng").unwrap(); let serial = args.value_of("serial").unwrap(); let firmware = args.value_of("firmware"); let kernel = args.value_of("kernel"); let initramfs = args.value_of("initramfs"); let cmdline = args.value_of("cmdline"); let disks: Option> = args.values_of("disk").map(|x| x.collect()); let net: Option> = args.values_of("net").map(|x| x.collect()); let console = args.value_of("console").unwrap(); let balloon = args.value_of("balloon"); let fs: Option> = args.values_of("fs").map(|x| x.collect()); let pmem: Option> = args.values_of("pmem").map(|x| x.collect()); let devices: Option> = args.values_of("device").map(|x| x.collect()); let user_devices: Option> = args.values_of("user-device").map(|x| x.collect()); let vdpa: Option> = args.values_of("vdpa").map(|x| x.collect()); let vsock: Option<&str> = args.value_of("vsock"); #[cfg(target_arch = "x86_64")] let sgx_epc: Option> = args.values_of("sgx-epc").map(|x| x.collect()); let numa: Option> = args.values_of("numa").map(|x| x.collect()); let watchdog = args.is_present("watchdog"); let platform = args.value_of("platform"); #[cfg(feature = "tdx")] let tdx = args.value_of("tdx"); #[cfg(feature = "gdb")] let gdb = args.is_present("gdb"); VmParams { cpus, memory, memory_zones, firmware, kernel, initramfs, cmdline, disks, net, rng, balloon, fs, pmem, serial, console, devices, user_devices, vdpa, vsock, #[cfg(target_arch = "x86_64")] sgx_epc, numa, watchdog, #[cfg(feature = "tdx")] tdx, #[cfg(feature = "gdb")] gdb, platform, } } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] pub enum HotplugMethod { Acpi, VirtioMem, } impl Default for HotplugMethod { fn default() -> Self { HotplugMethod::Acpi } } #[derive(Debug)] pub enum ParseHotplugMethodError { InvalidValue(String), } impl FromStr for HotplugMethod { type Err = ParseHotplugMethodError; fn from_str(s: &str) -> std::result::Result { match s.to_lowercase().as_str() { "acpi" => Ok(HotplugMethod::Acpi), "virtio-mem" => Ok(HotplugMethod::VirtioMem), _ => Err(ParseHotplugMethodError::InvalidValue(s.to_owned())), } } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct CpuAffinity { pub vcpu: u8, pub host_cpus: Vec, } #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] pub struct CpuFeatures { #[cfg(all(feature = "amx", target_arch = "x86_64"))] pub amx: bool, } pub enum CpuTopologyParseError { InvalidValue(String), } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct CpuTopology { pub threads_per_core: u8, pub cores_per_die: u8, pub dies_per_package: u8, pub packages: u8, } impl FromStr for CpuTopology { type Err = CpuTopologyParseError; fn from_str(s: &str) -> std::result::Result { 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) } } fn default_cpuconfig_max_phys_bits() -> u8 { DEFAULT_MAX_PHYS_BITS } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct CpusConfig { pub boot_vcpus: u8, pub max_vcpus: u8, #[serde(default)] pub topology: Option, #[serde(default)] pub kvm_hyperv: bool, #[serde(default = "default_cpuconfig_max_phys_bits")] pub max_phys_bits: u8, #[serde(default)] pub affinity: Option>, #[serde(default)] pub features: CpuFeatures, } impl CpusConfig { pub fn parse(cpus: &str) -> Result { let mut parser = OptionParser::new(); parser .add("boot") .add("max") .add("topology") .add("kvm_hyperv") .add("max_phys_bits") .add("affinity") .add("features"); parser.parse(cpus).map_err(Error::ParseCpus)?; let boot_vcpus: u8 = parser .convert("boot") .map_err(Error::ParseCpus)? .unwrap_or(DEFAULT_VCPUS); let max_vcpus: u8 = 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::("kvm_hyperv") .map_err(Error::ParseCpus)? .unwrap_or(Toggle(false)) .0; let max_phys_bits = parser .convert::("max_phys_bits") .map_err(Error::ParseCpus)? .unwrap_or(DEFAULT_MAX_PHYS_BITS); let affinity = parser .convert::>>("affinity") .map_err(Error::ParseCpus)? .map(|v| { v.0.iter() .map(|(e1, e2)| CpuAffinity { vcpu: *e1, host_cpus: e2.clone(), }) .collect() }); let features_list = parser .convert::("features") .map_err(Error::ParseCpus)? .unwrap_or_default(); // Some ugliness here as the features being checked might be disabled // at compile time causing the below allow and the need to specify the // ref type in the match. // The issue will go away once kvm_hyperv is moved under the features // list as it will always be checked for. #[allow(unused_mut)] let mut features = CpuFeatures::default(); for s in features_list.0 { match >::as_ref(&s) { #[cfg(all(feature = "amx", target_arch = "x86_64"))] "amx" => { features.amx = true; Ok(()) } _ => Err(Error::InvalidCpuFeatures(s)), }?; } Ok(CpusConfig { boot_vcpus, max_vcpus, topology, kvm_hyperv, max_phys_bits, affinity, features, }) } } impl Default for CpusConfig { fn default() -> Self { CpusConfig { boot_vcpus: DEFAULT_VCPUS, max_vcpus: DEFAULT_VCPUS, topology: None, kvm_hyperv: false, max_phys_bits: DEFAULT_MAX_PHYS_BITS, affinity: None, features: CpuFeatures::default(), } } } fn default_platformconfig_num_pci_segments() -> u16 { DEFAULT_NUM_PCI_SEGMENTS } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct PlatformConfig { #[serde(default = "default_platformconfig_num_pci_segments")] pub num_pci_segments: u16, #[serde(default)] pub iommu_segments: Option>, #[serde(default)] pub serial_number: Option, #[serde(default)] pub uuid: Option, #[serde(default)] pub oem_strings: Option>, } impl PlatformConfig { pub fn parse(platform: &str) -> Result { let mut parser = OptionParser::new(); parser.add("num_pci_segments"); parser.add("iommu_segments"); parser.add("serial_number"); parser.add("uuid"); parser.add("oem_strings"); 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::("iommu_segments") .map_err(Error::ParsePlatform)? .map(|v| v.0.iter().map(|e| *e as u16).collect()); let serial_number = parser .convert("serial_number") .map_err(Error::ParsePlatform)?; let uuid = parser.convert("uuid").map_err(Error::ParsePlatform)?; let oem_strings = parser .convert::("oem_strings") .map_err(Error::ParsePlatform)? .map(|v| v.0); Ok(PlatformConfig { num_pci_segments, iommu_segments, serial_number, uuid, oem_strings, }) } 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)); } } } Ok(()) } } impl Default for PlatformConfig { fn default() -> Self { PlatformConfig { num_pci_segments: DEFAULT_NUM_PCI_SEGMENTS, iommu_segments: None, serial_number: None, uuid: None, oem_strings: None, } } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct MemoryZoneConfig { pub id: String, pub size: u64, #[serde(default)] pub file: Option, #[serde(default)] pub shared: bool, #[serde(default)] pub hugepages: bool, #[serde(default)] pub hugepage_size: Option, #[serde(default)] pub host_numa_node: Option, #[serde(default)] pub hotplug_size: Option, #[serde(default)] pub hotplugged_size: Option, #[serde(default)] pub prefault: bool, } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct MemoryConfig { pub size: u64, #[serde(default)] pub mergeable: bool, #[serde(default)] pub hotplug_method: HotplugMethod, #[serde(default)] pub hotplug_size: Option, #[serde(default)] pub hotplugged_size: Option, #[serde(default)] pub shared: bool, #[serde(default)] pub hugepages: bool, #[serde(default)] pub hugepage_size: Option, #[serde(default)] pub prefault: bool, #[serde(default)] pub zones: Option>, } impl MemoryConfig { pub fn parse(memory: &str, memory_zones: Option>) -> Result { 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"); parser.parse(memory).map_err(Error::ParseMemory)?; let size = parser .convert::("size") .map_err(Error::ParseMemory)? .unwrap_or(ByteSized(DEFAULT_MEMORY_MB << 20)) .0; let mergeable = parser .convert::("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::("hotplug_size") .map_err(Error::ParseMemory)? .map(|v| v.0); let hotplugged_size = parser .convert::("hotplugged_size") .map_err(Error::ParseMemory)? .map(|v| v.0); let shared = parser .convert::("shared") .map_err(Error::ParseMemory)? .unwrap_or(Toggle(false)) .0; let hugepages = parser .convert::("hugepages") .map_err(Error::ParseMemory)? .unwrap_or(Toggle(false)) .0; let hugepage_size = parser .convert::("hugepage_size") .map_err(Error::ParseMemory)? .map(|v| v.0); let prefault = parser .convert::("prefault") .map_err(Error::ParseMemory)? .unwrap_or(Toggle(false)) .0; let zones: Option> = 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"); parser.parse(memory_zone).map_err(Error::ParseMemoryZone)?; let id = parser.get("id").ok_or(Error::ParseMemoryZoneIdMissing)?; let size = parser .convert::("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::("shared") .map_err(Error::ParseMemoryZone)? .unwrap_or(Toggle(false)) .0; let hugepages = parser .convert::("hugepages") .map_err(Error::ParseMemoryZone)? .unwrap_or(Toggle(false)) .0; let hugepage_size = parser .convert::("hugepage_size") .map_err(Error::ParseMemoryZone)? .map(|v| v.0); let host_numa_node = parser .convert::("host_numa_node") .map_err(Error::ParseMemoryZone)?; let hotplug_size = parser .convert::("hotplug_size") .map_err(Error::ParseMemoryZone)? .map(|v| v.0); let hotplugged_size = parser .convert::("hotplugged_size") .map_err(Error::ParseMemoryZone)? .map(|v| v.0); let prefault = parser .convert::("prefault") .map_err(Error::ParseMemoryZone)? .unwrap_or(Toggle(false)) .0; zones.push(MemoryZoneConfig { id, size, file, shared, hugepages, hugepage_size, host_numa_node, hotplug_size, hotplugged_size, prefault, }); } Some(zones) } else { None }; Ok(MemoryConfig { size, mergeable, hotplug_method, hotplug_size, hotplugged_size, shared, hugepages, hugepage_size, prefault, zones, }) } pub fn total_size(&self) -> u64 { let mut size = self.size; if let Some(hotplugged_size) = self.hotplugged_size { size += hotplugged_size; } if let Some(zones) = &self.zones { for zone in zones.iter() { size += zone.size; if let Some(hotplugged_size) = zone.hotplugged_size { size += hotplugged_size; } } } size } } impl Default for MemoryConfig { fn default() -> Self { MemoryConfig { size: DEFAULT_MEMORY_MB << 20, mergeable: false, hotplug_method: HotplugMethod::Acpi, hotplug_size: None, hotplugged_size: None, shared: false, hugepages: false, hugepage_size: None, prefault: false, zones: None, } } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct KernelConfig { pub path: PathBuf, } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct InitramfsConfig { pub path: PathBuf, } #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] pub struct CmdlineConfig { pub args: String, } impl CmdlineConfig { pub fn parse(cmdline: Option<&str>) -> Result { let args = cmdline .map(std::string::ToString::to_string) .unwrap_or_else(String::new); Ok(CmdlineConfig { args }) } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct DiskConfig { pub path: Option, #[serde(default)] pub readonly: bool, #[serde(default)] pub direct: bool, #[serde(default)] pub iommu: bool, #[serde(default = "default_diskconfig_num_queues")] pub num_queues: usize, #[serde(default = "default_diskconfig_queue_size")] pub queue_size: u16, #[serde(default)] pub vhost_user: bool, pub vhost_socket: Option, #[serde(default)] pub rate_limiter_config: Option, #[serde(default)] pub id: Option, // For testing use only. Not exposed in API. #[serde(default)] pub disable_io_uring: bool, #[serde(default)] pub pci_segment: u16, } fn default_diskconfig_num_queues() -> usize { DEFAULT_NUM_QUEUES_VUBLK } fn default_diskconfig_queue_size() -> u16 { DEFAULT_QUEUE_SIZE_VUBLK } impl Default for DiskConfig { fn default() -> Self { Self { path: None, readonly: false, direct: false, iommu: false, num_queues: default_diskconfig_num_queues(), queue_size: default_diskconfig_queue_size(), vhost_user: false, vhost_socket: None, id: None, disable_io_uring: false, rate_limiter_config: None, pci_segment: 0, } } } impl DiskConfig { pub const SYNTAX: &'static str = "Disk parameters \ \"path=,readonly=on|off,direct=on|off,iommu=on|off,\ num_queues=,queue_size=,\ vhost_user=on|off,socket=,\ bw_size=,bw_one_time_burst=,bw_refill_time=,\ ops_size=,ops_one_time_burst=,ops_refill_time=,\ id=,pci_segment=\""; pub fn parse(disk: &str) -> Result { let mut parser = OptionParser::new(); parser .add("path") .add("readonly") .add("direct") .add("iommu") .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("id") .add("_disable_io_uring") .add("pci_segment"); parser.parse(disk).map_err(Error::ParseDisk)?; let path = parser.get("path").map(PathBuf::from); let readonly = parser .convert::("readonly") .map_err(Error::ParseDisk)? .unwrap_or(Toggle(false)) .0; let direct = parser .convert::("direct") .map_err(Error::ParseDisk)? .unwrap_or(Toggle(false)) .0; let iommu = parser .convert::("iommu") .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::("vhost_user") .map_err(Error::ParseDisk)? .unwrap_or(Toggle(false)) .0; let vhost_socket = parser.get("socket"); let id = parser.get("id"); let disable_io_uring = parser .convert::("_disable_io_uring") .map_err(Error::ParseDisk)? .unwrap_or(Toggle(false)) .0; let pci_segment = parser .convert("pci_segment") .map_err(Error::ParseDisk)? .unwrap_or_default(); 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 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 }; Ok(DiskConfig { path, readonly, direct, iommu, num_queues, queue_size, vhost_user, vhost_socket, rate_limiter_config, id, disable_io_uring, pci_segment, }) } pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> { if self.num_queues > vm_config.cpus.boot_vcpus as usize { return Err(ValidationError::TooManyQueues); } if self.vhost_user && self.iommu { return Err(ValidationError::IommuNotSupported); } if let Some(platform_config) = vm_config.platform.as_ref() { if self.pci_segment >= platform_config.num_pci_segments { return Err(ValidationError::InvalidPciSegment(self.pci_segment)); } if let Some(iommu_segments) = platform_config.iommu_segments.as_ref() { if iommu_segments.contains(&self.pci_segment) && !self.iommu { return Err(ValidationError::OnIommuSegment(self.pci_segment)); } } } Ok(()) } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub enum VhostMode { Client, Server, } impl Default for VhostMode { fn default() -> Self { VhostMode::Client } } #[derive(Debug)] pub enum ParseVhostModeError { InvalidValue(String), } impl FromStr for VhostMode { type Err = ParseVhostModeError; fn from_str(s: &str) -> std::result::Result { match s.to_lowercase().as_str() { "client" => Ok(VhostMode::Client), "server" => Ok(VhostMode::Server), _ => Err(ParseVhostModeError::InvalidValue(s.to_owned())), } } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct NetConfig { #[serde(default = "default_netconfig_tap")] pub tap: Option, #[serde(default = "default_netconfig_ip")] pub ip: Ipv4Addr, #[serde(default = "default_netconfig_mask")] pub mask: Ipv4Addr, #[serde(default = "default_netconfig_mac")] pub mac: MacAddr, #[serde(default)] pub host_mac: Option, #[serde(default)] pub iommu: bool, #[serde(default = "default_netconfig_num_queues")] pub num_queues: usize, #[serde(default = "default_netconfig_queue_size")] pub queue_size: u16, #[serde(default)] pub vhost_user: bool, pub vhost_socket: Option, #[serde(default)] pub vhost_mode: VhostMode, #[serde(default)] pub id: Option, #[serde(default)] pub fds: Option>, #[serde(default)] pub rate_limiter_config: Option, #[serde(default)] pub pci_segment: u16, } fn default_netconfig_tap() -> Option { None } fn default_netconfig_ip() -> Ipv4Addr { Ipv4Addr::new(192, 168, 249, 1) } fn default_netconfig_mask() -> Ipv4Addr { Ipv4Addr::new(255, 255, 255, 0) } fn default_netconfig_mac() -> MacAddr { MacAddr::local_random() } fn default_netconfig_num_queues() -> usize { DEFAULT_NUM_QUEUES_VUNET } fn default_netconfig_queue_size() -> u16 { DEFAULT_QUEUE_SIZE_VUNET } impl Default for NetConfig { fn default() -> Self { Self { tap: default_netconfig_tap(), ip: default_netconfig_ip(), mask: default_netconfig_mask(), mac: default_netconfig_mac(), host_mac: None, iommu: false, num_queues: default_netconfig_num_queues(), queue_size: default_netconfig_queue_size(), vhost_user: false, vhost_socket: None, vhost_mode: VhostMode::Client, id: None, fds: None, rate_limiter_config: None, pci_segment: 0, } } } impl NetConfig { pub const SYNTAX: &'static str = "Network parameters \ \"tap=,ip=,mask=,mac=,fd=,iommu=on|off,\ num_queues=,queue_size=,id=,\ vhost_user=,socket=,vhost_mode=client|server,\ bw_size=,bw_one_time_burst=,bw_refill_time=,\ ops_size=,ops_one_time_burst=,ops_refill_time=,pci_segment=\""; pub fn parse(net: &str) -> Result { let mut parser = OptionParser::new(); parser .add("tap") .add("ip") .add("mask") .add("mac") .add("host_mac") .add("iommu") .add("queue_size") .add("num_queues") .add("vhost_user") .add("socket") .add("vhost_mode") .add("id") .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("pci_segment"); parser.parse(net).map_err(Error::ParseNetwork)?; let tap = parser.get("tap"); let ip = parser .convert("ip") .map_err(Error::ParseNetwork)? .unwrap_or_else(default_netconfig_ip); let mask = parser .convert("mask") .map_err(Error::ParseNetwork)? .unwrap_or_else(default_netconfig_mask); 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 iommu = parser .convert::("iommu") .map_err(Error::ParseNetwork)? .unwrap_or(Toggle(false)) .0; 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::("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 id = parser.get("id"); let fds = parser .convert::("fd") .map_err(Error::ParseNetwork)? .map(|v| v.0.iter().map(|e| *e as i32).collect()); let pci_segment = parser .convert("pci_segment") .map_err(Error::ParseNetwork)? .unwrap_or_default(); 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 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 config = NetConfig { tap, ip, mask, mac, host_mac, iommu, num_queues, queue_size, vhost_user, vhost_socket, vhost_mode, id, fds, rate_limiter_config, pci_segment, }; Ok(config) } pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> { if self.num_queues < 2 { return Err(ValidationError::VnetQueueLowerThan2); } if self.fds.is_some() && self.fds.as_ref().unwrap().len() * 2 != self.num_queues { return Err(ValidationError::VnetQueueFdMismatch); } if let Some(fds) = self.fds.as_ref() { for fd in fds { if *fd <= 2 { return Err(ValidationError::VnetReservedFd); } } } if (self.num_queues / 2) > vm_config.cpus.boot_vcpus as usize { return Err(ValidationError::TooManyQueues); } if self.vhost_user && self.iommu { return Err(ValidationError::IommuNotSupported); } if let Some(platform_config) = vm_config.platform.as_ref() { if self.pci_segment >= platform_config.num_pci_segments { return Err(ValidationError::InvalidPciSegment(self.pci_segment)); } if let Some(iommu_segments) = platform_config.iommu_segments.as_ref() { if iommu_segments.contains(&self.pci_segment) && !self.iommu { return Err(ValidationError::OnIommuSegment(self.pci_segment)); } } } Ok(()) } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct RngConfig { pub src: PathBuf, #[serde(default)] pub iommu: bool, } impl RngConfig { pub fn parse(rng: &str) -> Result { let mut parser = OptionParser::new(); parser.add("src").add("iommu"); parser.parse(rng).map_err(Error::ParseRng)?; let src = PathBuf::from( parser .get("src") .unwrap_or_else(|| DEFAULT_RNG_SOURCE.to_owned()), ); let iommu = parser .convert::("iommu") .map_err(Error::ParseRng)? .unwrap_or(Toggle(false)) .0; Ok(RngConfig { src, iommu }) } } impl Default for RngConfig { fn default() -> Self { RngConfig { src: PathBuf::from(DEFAULT_RNG_SOURCE), iommu: false, } } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct BalloonConfig { pub size: u64, /// Option to deflate the balloon in case the guest is out of memory. #[serde(default)] pub deflate_on_oom: bool, /// Option to enable free page reporting from the guest. #[serde(default)] pub free_page_reporting: bool, } impl BalloonConfig { pub const SYNTAX: &'static str = "Balloon parameters \"size=,deflate_on_oom=on|off,\ free_page_reporting=on|off\""; pub fn parse(balloon: &str) -> Result { let mut parser = OptionParser::new(); parser.add("size"); parser.add("deflate_on_oom"); parser.add("free_page_reporting"); parser.parse(balloon).map_err(Error::ParseBalloon)?; let size = parser .convert::("size") .map_err(Error::ParseBalloon)? .map(|v| v.0) .unwrap_or(0); let deflate_on_oom = parser .convert::("deflate_on_oom") .map_err(Error::ParseBalloon)? .unwrap_or(Toggle(false)) .0; let free_page_reporting = parser .convert::("free_page_reporting") .map_err(Error::ParseBalloon)? .unwrap_or(Toggle(false)) .0; Ok(BalloonConfig { size, deflate_on_oom, free_page_reporting, }) } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct FsConfig { pub tag: String, pub socket: PathBuf, #[serde(default = "default_fsconfig_num_queues")] pub num_queues: usize, #[serde(default = "default_fsconfig_queue_size")] pub queue_size: u16, #[serde(default)] pub id: Option, #[serde(default)] pub pci_segment: u16, } fn default_fsconfig_num_queues() -> usize { 1 } fn default_fsconfig_queue_size() -> u16 { 1024 } impl Default for FsConfig { fn default() -> Self { Self { tag: "".to_owned(), socket: PathBuf::new(), num_queues: default_fsconfig_num_queues(), queue_size: default_fsconfig_queue_size(), id: None, pci_segment: 0, } } } impl FsConfig { pub const SYNTAX: &'static str = "virtio-fs parameters \ \"tag=,socket=,num_queues=,\ queue_size=,id=,pci_segment=\""; pub fn parse(fs: &str) -> Result { let mut parser = OptionParser::new(); parser .add("tag") .add("queue_size") .add("num_queues") .add("socket") .add("id") .add("pci_segment"); parser.parse(fs).map_err(Error::ParseFileSystem)?; let tag = parser.get("tag").ok_or(Error::ParseFsTagMissing)?; 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 id = parser.get("id"); let pci_segment = parser .convert("pci_segment") .map_err(Error::ParseFileSystem)? .unwrap_or_default(); Ok(FsConfig { tag, socket, num_queues, queue_size, id, pci_segment, }) } pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> { if self.num_queues > vm_config.cpus.boot_vcpus as usize { return Err(ValidationError::TooManyQueues); } if let Some(platform_config) = vm_config.platform.as_ref() { if self.pci_segment >= platform_config.num_pci_segments { return Err(ValidationError::InvalidPciSegment(self.pci_segment)); } if let Some(iommu_segments) = platform_config.iommu_segments.as_ref() { if iommu_segments.contains(&self.pci_segment) { return Err(ValidationError::IommuNotSupportedOnSegment( self.pci_segment, )); } } } Ok(()) } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] pub struct PmemConfig { pub file: PathBuf, #[serde(default)] pub size: Option, #[serde(default)] pub iommu: bool, #[serde(default)] pub discard_writes: bool, #[serde(default)] pub id: Option, #[serde(default)] pub pci_segment: u16, } impl PmemConfig { pub const SYNTAX: &'static str = "Persistent memory parameters \ \"file=,size=,iommu=on|off,\ discard_writes=on|off,id=,pci_segment=\""; pub fn parse(pmem: &str) -> Result { let mut parser = OptionParser::new(); parser .add("size") .add("file") .add("iommu") .add("discard_writes") .add("id") .add("pci_segment"); parser.parse(pmem).map_err(Error::ParsePersistentMemory)?; let file = PathBuf::from(parser.get("file").ok_or(Error::ParsePmemFileMissing)?); let size = parser .convert::("size") .map_err(Error::ParsePersistentMemory)? .map(|v| v.0); let iommu = parser .convert::("iommu") .map_err(Error::ParsePersistentMemory)? .unwrap_or(Toggle(false)) .0; let discard_writes = parser .convert::("discard_writes") .map_err(Error::ParsePersistentMemory)? .unwrap_or(Toggle(false)) .0; let id = parser.get("id"); let pci_segment = parser .convert("pci_segment") .map_err(Error::ParsePersistentMemory)? .unwrap_or_default(); Ok(PmemConfig { file, size, iommu, discard_writes, id, pci_segment, }) } pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> { if let Some(platform_config) = vm_config.platform.as_ref() { if self.pci_segment >= platform_config.num_pci_segments { return Err(ValidationError::InvalidPciSegment(self.pci_segment)); } if let Some(iommu_segments) = platform_config.iommu_segments.as_ref() { if iommu_segments.contains(&self.pci_segment) && !self.iommu { return Err(ValidationError::OnIommuSegment(self.pci_segment)); } } } Ok(()) } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub enum ConsoleOutputMode { Off, Pty, Tty, File, Null, } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct ConsoleConfig { #[serde(default = "default_consoleconfig_file")] pub file: Option, pub mode: ConsoleOutputMode, #[serde(default)] pub iommu: bool, } fn default_consoleconfig_file() -> Option { None } impl ConsoleConfig { pub fn parse(console: &str) -> Result { let mut parser = OptionParser::new(); parser .add_valueless("off") .add_valueless("pty") .add_valueless("tty") .add_valueless("null") .add("file") .add("iommu"); parser.parse(console).map_err(Error::ParseConsole)?; let mut file: Option = default_consoleconfig_file(); 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); } let iommu = parser .convert::("iommu") .map_err(Error::ParseConsole)? .unwrap_or(Toggle(false)) .0; Ok(Self { file, mode, iommu }) } pub fn default_serial() -> Self { ConsoleConfig { file: None, mode: ConsoleOutputMode::Null, iommu: false, } } pub fn default_console() -> Self { ConsoleConfig { file: None, mode: ConsoleOutputMode::Tty, iommu: false, } } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] pub struct DeviceConfig { pub path: PathBuf, #[serde(default)] pub iommu: bool, #[serde(default)] pub id: Option, #[serde(default)] pub pci_segment: u16, } impl DeviceConfig { pub const SYNTAX: &'static str = "Direct device assignment parameters \"path=,iommu=on|off,id=,pci_segment=\""; pub fn parse(device: &str) -> Result { let mut parser = OptionParser::new(); parser.add("path").add("id").add("iommu").add("pci_segment"); parser.parse(device).map_err(Error::ParseDevice)?; let path = parser .get("path") .map(PathBuf::from) .ok_or(Error::ParseDevicePathMissing)?; let iommu = parser .convert::("iommu") .map_err(Error::ParseDevice)? .unwrap_or(Toggle(false)) .0; let id = parser.get("id"); let pci_segment = parser .convert::("pci_segment") .map_err(Error::ParseDevice)? .unwrap_or_default(); Ok(DeviceConfig { path, iommu, id, pci_segment, }) } pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> { if let Some(platform_config) = vm_config.platform.as_ref() { if self.pci_segment >= platform_config.num_pci_segments { return Err(ValidationError::InvalidPciSegment(self.pci_segment)); } if let Some(iommu_segments) = platform_config.iommu_segments.as_ref() { if iommu_segments.contains(&self.pci_segment) && !self.iommu { return Err(ValidationError::OnIommuSegment(self.pci_segment)); } } } Ok(()) } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] pub struct UserDeviceConfig { pub socket: PathBuf, #[serde(default)] pub id: Option, #[serde(default)] pub pci_segment: u16, } impl UserDeviceConfig { pub const SYNTAX: &'static str = "Userspace device socket=,id=,pci_segment=\""; pub fn parse(user_device: &str) -> Result { let mut parser = OptionParser::new(); parser.add("socket").add("id").add("pci_segment"); parser.parse(user_device).map_err(Error::ParseUserDevice)?; let socket = parser .get("socket") .map(PathBuf::from) .ok_or(Error::ParseUserDeviceSocketMissing)?; let id = parser.get("id"); let pci_segment = parser .convert::("pci_segment") .map_err(Error::ParseUserDevice)? .unwrap_or_default(); Ok(UserDeviceConfig { socket, id, pci_segment, }) } pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> { if let Some(platform_config) = vm_config.platform.as_ref() { if self.pci_segment >= platform_config.num_pci_segments { return Err(ValidationError::InvalidPciSegment(self.pci_segment)); } if let Some(iommu_segments) = platform_config.iommu_segments.as_ref() { if iommu_segments.contains(&self.pci_segment) { return Err(ValidationError::IommuNotSupportedOnSegment( self.pci_segment, )); } } } Ok(()) } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] pub struct VdpaConfig { pub path: PathBuf, #[serde(default = "default_vdpaconfig_num_queues")] pub num_queues: usize, #[serde(default)] pub iommu: bool, #[serde(default)] pub id: Option, #[serde(default)] pub pci_segment: u16, } fn default_vdpaconfig_num_queues() -> usize { 1 } impl VdpaConfig { pub const SYNTAX: &'static str = "vDPA device \ \"path=,num_queues=,iommu=on|off,\ id=,pci_segment=\""; pub fn parse(vdpa: &str) -> Result { let mut parser = OptionParser::new(); parser .add("path") .add("num_queues") .add("iommu") .add("id") .add("pci_segment"); parser.parse(vdpa).map_err(Error::ParseVdpa)?; 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); let iommu = parser .convert::("iommu") .map_err(Error::ParseVdpa)? .unwrap_or(Toggle(false)) .0; let id = parser.get("id"); let pci_segment = parser .convert("pci_segment") .map_err(Error::ParseVdpa)? .unwrap_or_default(); Ok(VdpaConfig { path, num_queues, iommu, id, pci_segment, }) } pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> { if let Some(platform_config) = vm_config.platform.as_ref() { if self.pci_segment >= platform_config.num_pci_segments { return Err(ValidationError::InvalidPciSegment(self.pci_segment)); } if let Some(iommu_segments) = platform_config.iommu_segments.as_ref() { if iommu_segments.contains(&self.pci_segment) && !self.iommu { return Err(ValidationError::OnIommuSegment(self.pci_segment)); } } } Ok(()) } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] pub struct VsockConfig { pub cid: u64, pub socket: PathBuf, #[serde(default)] pub iommu: bool, #[serde(default)] pub id: Option, #[serde(default)] pub pci_segment: u16, } impl VsockConfig { pub const SYNTAX: &'static str = "Virtio VSOCK parameters \ \"cid=,socket=,iommu=on|off,id=,pci_segment=\""; pub fn parse(vsock: &str) -> Result { let mut parser = OptionParser::new(); parser .add("socket") .add("cid") .add("iommu") .add("id") .add("pci_segment"); parser.parse(vsock).map_err(Error::ParseVsock)?; let socket = parser .get("socket") .map(PathBuf::from) .ok_or(Error::ParseVsockSockMissing)?; let iommu = parser .convert::("iommu") .map_err(Error::ParseVsock)? .unwrap_or(Toggle(false)) .0; let cid = parser .convert("cid") .map_err(Error::ParseVsock)? .ok_or(Error::ParseVsockCidMissing)?; let id = parser.get("id"); let pci_segment = parser .convert("pci_segment") .map_err(Error::ParseVsock)? .unwrap_or_default(); Ok(VsockConfig { cid, socket, iommu, id, pci_segment, }) } pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> { if let Some(platform_config) = vm_config.platform.as_ref() { if self.pci_segment >= platform_config.num_pci_segments { return Err(ValidationError::InvalidPciSegment(self.pci_segment)); } if let Some(iommu_segments) = platform_config.iommu_segments.as_ref() { if iommu_segments.contains(&self.pci_segment) && !self.iommu { return Err(ValidationError::OnIommuSegment(self.pci_segment)); } } } Ok(()) } } #[cfg(feature = "tdx")] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] pub struct TdxConfig { pub firmware: PathBuf, } #[cfg(feature = "tdx")] impl TdxConfig { pub fn parse(tdx: &str) -> Result { let mut parser = OptionParser::new(); parser.add("firmware"); parser.parse(tdx).map_err(Error::ParseTdx)?; let firmware = parser .get("firmware") .map(PathBuf::from) .ok_or(Error::FirmwarePathMissing)?; Ok(TdxConfig { firmware }) } } #[cfg(target_arch = "x86_64")] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] pub struct SgxEpcConfig { pub id: String, #[serde(default)] pub size: u64, #[serde(default)] pub prefault: bool, } #[cfg(target_arch = "x86_64")] impl SgxEpcConfig { pub const SYNTAX: &'static str = "SGX EPC parameters \ \"id=,size=,prefault=on|off\""; pub fn parse(sgx_epc: &str) -> Result { let mut parser = OptionParser::new(); parser.add("id").add("size").add("prefault"); parser.parse(sgx_epc).map_err(Error::ParseSgxEpc)?; let id = parser.get("id").ok_or(Error::ParseSgxEpcIdMissing)?; let size = parser .convert::("size") .map_err(Error::ParseSgxEpc)? .unwrap_or(ByteSized(0)) .0; let prefault = parser .convert::("prefault") .map_err(Error::ParseSgxEpc)? .unwrap_or(Toggle(false)) .0; Ok(SgxEpcConfig { id, size, prefault }) } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] pub struct NumaDistance { #[serde(default)] pub destination: u32, #[serde(default)] pub distance: u8, } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] pub struct NumaConfig { #[serde(default)] pub guest_numa_id: u32, #[serde(default)] pub cpus: Option>, #[serde(default)] pub distances: Option>, #[serde(default)] pub memory_zones: Option>, #[cfg(target_arch = "x86_64")] #[serde(default)] pub sgx_epc_sections: Option>, } impl NumaConfig { pub const SYNTAX: &'static str = "Settings related to a given NUMA node \ \"guest_numa_id=,cpus=,distances=,\ memory_zones=,sgx_epc_sections=\""; pub fn parse(numa: &str) -> Result { let mut parser = OptionParser::new(); parser .add("guest_numa_id") .add("cpus") .add("distances") .add("memory_zones") .add("sgx_epc_sections"); parser.parse(numa).map_err(Error::ParseNuma)?; let guest_numa_id = parser .convert::("guest_numa_id") .map_err(Error::ParseNuma)? .unwrap_or(0); let cpus = parser .convert::("cpus") .map_err(Error::ParseNuma)? .map(|v| v.0.iter().map(|e| *e as u8).collect()); let distances = parser .convert::>("distances") .map_err(Error::ParseNuma)? .map(|v| { v.0.iter() .map(|(e1, e2)| NumaDistance { destination: *e1 as u32, distance: *e2 as u8, }) .collect() }); let memory_zones = parser .convert::("memory_zones") .map_err(Error::ParseNuma)? .map(|v| v.0); #[cfg(target_arch = "x86_64")] let sgx_epc_sections = parser .convert::("sgx_epc_sections") .map_err(Error::ParseNuma)? .map(|v| v.0); Ok(NumaConfig { guest_numa_id, cpus, distances, memory_zones, #[cfg(target_arch = "x86_64")] sgx_epc_sections, }) } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] pub struct RestoreConfig { pub source_url: PathBuf, #[serde(default)] pub prefault: bool, } impl RestoreConfig { pub const SYNTAX: &'static str = "Restore from a VM snapshot. \ \nRestore parameters \"source_url=,prefault=on|off\" \ \n`source_url` should be a valid URL (e.g file:///foo/bar or tcp://192.168.1.10/foo) \ \n`prefault` brings memory pages in when enabled (disabled by default)"; pub fn parse(restore: &str) -> Result { let mut parser = OptionParser::new(); parser.add("source_url").add("prefault"); 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::("prefault") .map_err(Error::ParseRestore)? .unwrap_or(Toggle(false)) .0; Ok(RestoreConfig { source_url, prefault, }) } } #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct PayloadConfig { #[serde(default)] pub firmware: Option, #[serde(default)] pub kernel: Option, #[serde(default)] pub cmdline: Option, #[serde(default)] pub initramfs: Option, } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct VmConfig { #[serde(default)] pub cpus: CpusConfig, #[serde(default)] pub memory: MemoryConfig, pub kernel: Option, #[serde(default)] pub initramfs: Option, #[serde(default)] pub cmdline: CmdlineConfig, #[serde(default)] pub payload: Option, pub disks: Option>, pub net: Option>, #[serde(default)] pub rng: RngConfig, pub balloon: Option, pub fs: Option>, pub pmem: Option>, #[serde(default = "ConsoleConfig::default_serial")] pub serial: ConsoleConfig, #[serde(default = "ConsoleConfig::default_console")] pub console: ConsoleConfig, pub devices: Option>, pub user_devices: Option>, pub vdpa: Option>, pub vsock: Option, #[serde(default)] pub iommu: bool, #[cfg(target_arch = "x86_64")] pub sgx_epc: Option>, pub numa: Option>, #[serde(default)] pub watchdog: bool, #[cfg(feature = "tdx")] pub tdx: Option, #[cfg(feature = "gdb")] pub gdb: bool, pub platform: Option, } impl VmConfig { fn validate_identifier( id_list: &mut BTreeSet, id: &Option, ) -> 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(()) } // 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> { let mut id_list = BTreeSet::new(); if self.kernel.is_some() { warn!("The \"VmConfig\" members \"kernel\", \"cmdline\" and \"initramfs\" are deprecated. Use \"payload\" member instead."); self.payload = Some(PayloadConfig { kernel: self.kernel.take().map(|k| k.path), cmdline: if self.cmdline.args.is_empty() { None } else { Some(self.cmdline.args.drain(..).collect()) }, initramfs: self.initramfs.take().map(|i| i.path), ..Default::default() }) } #[cfg(not(feature = "tdx"))] self.payload .as_ref() .ok_or(ValidationError::KernelMissing)?; #[cfg(feature = "tdx")] { let tdx_enabled = self.tdx.is_some(); if !tdx_enabled && self.payload.is_none() { return Err(ValidationError::KernelMissing); } if tdx_enabled && (self.cpus.max_vcpus != self.cpus.boot_vcpus) { return Err(ValidationError::TdxNoCpuHotplug); } } if self.console.mode == ConsoleOutputMode::Tty && self.serial.mode == ConsoleOutputMode::Tty { return Err(ValidationError::DoubleTtyMode); } if self.console.mode == ConsoleOutputMode::File && self.console.file.is_none() { return Err(ValidationError::ConsoleFileMissing); } if self.serial.mode == ConsoleOutputMode::File && self.serial.file.is_none() { return Err(ValidationError::ConsoleFileMissing); } if self.cpus.max_vcpus < self.cpus.boot_vcpus { return Err(ValidationError::CpusMaxLowerThanBoot); } 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.memory.shared { return Err(ValidationError::VhostUserRequiresSharedMemory); } if disk.vhost_user && disk.vhost_socket.is_none() { return Err(ValidationError::VhostUserMissingSocket); } disk.validate(self)?; self.iommu |= disk.iommu; Self::validate_identifier(&mut id_list, &disk.id)?; } } if let Some(nets) = &self.net { for net in nets { if net.vhost_user && !self.memory.shared { return Err(ValidationError::VhostUserRequiresSharedMemory); } net.validate(self)?; self.iommu |= net.iommu; Self::validate_identifier(&mut id_list, &net.id)?; } } if let Some(fses) = &self.fs { if !fses.is_empty() && !self.memory.shared { return Err(ValidationError::VhostUserRequiresSharedMemory); } for fs in fses { fs.validate(self)?; Self::validate_identifier(&mut id_list, &fs.id)?; } } if let Some(pmems) = &self.pmem { for pmem in pmems { pmem.validate(self)?; self.iommu |= pmem.iommu; Self::validate_identifier(&mut id_list, &pmem.id)?; } } self.iommu |= self.rng.iommu; self.iommu |= self.console.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); } // The setting of dies doesen'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 = t.threads_per_core * t.cores_per_die * t.dies_per_package * t.packages; 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 let Some(user_devices) = &self.user_devices { if !user_devices.is_empty() && !self.memory.shared { return Err(ValidationError::UserDevicesRequireSharedMemory); } for user_device in user_devices { user_device.validate(self)?; Self::validate_identifier(&mut id_list, &user_device.id)?; } } if let Some(vdpa_devices) = &self.vdpa { for vdpa_device in vdpa_devices { vdpa_device.validate(self)?; self.iommu |= vdpa_device.iommu; Self::validate_identifier(&mut id_list, &vdpa_device.id)?; } } if let Some(balloon) = &self.balloon { let mut ram_size = self.memory.size; if let Some(zones) = &self.memory.zones { for zone in zones { ram_size += zone.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 !device_paths.insert(device.path.to_string_lossy()) { return Err(ValidationError::DuplicateDevicePath( device.path.to_string_lossy().to_string(), )); } device.validate(self)?; self.iommu |= device.iommu; Self::validate_identifier(&mut id_list, &device.id)?; } } if let Some(vsock) = &self.vsock { vsock.validate(self)?; self.iommu |= vsock.iommu; Self::validate_identifier(&mut id_list, &vsock.id)?; } if let Some(numa) = &self.numa { let mut used_numa_node_memory_zones = HashMap::new(); for numa_node in numa.iter() { for memory_zone in numa_node.memory_zones.clone().unwrap().iter() { if !used_numa_node_memory_zones.contains_key(memory_zone) { used_numa_node_memory_zones .insert(memory_zone.to_string(), numa_node.guest_numa_id); } else { return Err(ValidationError::MemoryZoneReused( memory_zone.to_string(), *used_numa_node_memory_zones.get(memory_zone).unwrap(), 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))?; } } #[cfg(target_arch = "x86_64")] if let Some(sgx_epcs) = &self.sgx_epc { for sgx_epc in sgx_epcs.iter() { let id = sgx_epc.id.clone(); Self::validate_identifier(&mut id_list, &Some(id))?; } } 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(); Ok(id_list) } pub fn parse(vm_params: VmParams) -> Result { let mut disks: Option> = 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); } let mut net: Option> = 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 balloon: Option = None; if let Some(balloon_params) = &vm_params.balloon { balloon = Some(BalloonConfig::parse(balloon_params)?); } let mut fs: Option> = 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 pmem: Option> = 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 = ConsoleConfig::parse(vm_params.serial)?; let mut devices: Option> = 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> = 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> = 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 = None; if let Some(vs) = &vm_params.vsock { let vsock_config = VsockConfig::parse(vs)?; vsock = Some(vsock_config); } let platform = vm_params.platform.map(PlatformConfig::parse).transpose()?; #[cfg(target_arch = "x86_64")] let mut sgx_epc: Option> = None; #[cfg(target_arch = "x86_64")] { if let Some(sgx_epc_list) = &vm_params.sgx_epc { let mut sgx_epc_config_list = Vec::new(); for item in sgx_epc_list.iter() { let sgx_epc_config = SgxEpcConfig::parse(item)?; sgx_epc_config_list.push(sgx_epc_config); } sgx_epc = Some(sgx_epc_config_list); } } let mut numa: Option> = 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); } let payload = if vm_params.kernel.is_some() || vm_params.firmware.is_some() { 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), }) } else { None }; #[cfg(feature = "tdx")] let tdx = vm_params.tdx.map(TdxConfig::parse).transpose()?; #[cfg(feature = "gdb")] let gdb = vm_params.gdb; let mut config = VmConfig { cpus: CpusConfig::parse(vm_params.cpus)?, memory: MemoryConfig::parse(vm_params.memory, vm_params.memory_zones)?, kernel: None, initramfs: None, cmdline: CmdlineConfig::default(), payload, disks, net, rng, balloon, fs, pmem, serial, console, devices, user_devices, vdpa, vsock, iommu: false, // updated in VmConfig::validate() #[cfg(target_arch = "x86_64")] sgx_epc, numa, watchdog: vm_params.watchdog, #[cfg(feature = "tdx")] tdx, #[cfg(feature = "gdb")] gdb, platform, }; config.validate().map_err(Error::Validation)?; Ok(config) } } #[cfg(test)] mod tests { 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() } ); assert!(CpusConfig::parse("boot=8,topology=2:2:1").is_err()); assert!(CpusConfig::parse("boot=8,topology=2:2:1:x").is_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(vec![ CpuAffinity { vcpu: 0, host_cpus: vec![0, 2], }, CpuAffinity { vcpu: 1, host_cpus: vec![1, 3], } ]), ..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() } ); Ok(()) } #[test] fn test_disk_parsing() -> Result<()> { assert_eq!( DiskConfig::parse("path=/path/to_file")?, DiskConfig { path: Some(PathBuf::from("/path/to_file")), ..Default::default() } ); assert_eq!( DiskConfig::parse("path=/path/to_file,id=mydisk0")?, DiskConfig { path: Some(PathBuf::from("/path/to_file")), id: Some("mydisk0".to_owned()), ..Default::default() } ); assert_eq!( DiskConfig::parse("vhost_user=true,socket=/tmp/sock")?, DiskConfig { vhost_socket: Some(String::from("/tmp/sock")), vhost_user: true, ..Default::default() } ); assert_eq!( DiskConfig::parse("path=/path/to_file,iommu=on")?, DiskConfig { path: Some(PathBuf::from("/path/to_file")), iommu: true, ..Default::default() } ); assert_eq!( DiskConfig::parse("path=/path/to_file,iommu=on,queue_size=256")?, DiskConfig { path: Some(PathBuf::from("/path/to_file")), iommu: true, queue_size: 256, ..Default::default() } ); assert_eq!( DiskConfig::parse("path=/path/to_file,iommu=on,queue_size=256,num_queues=4")?, DiskConfig { path: Some(PathBuf::from("/path/to_file")), iommu: true, queue_size: 256, num_queues: 4, ..Default::default() } ); assert_eq!( DiskConfig::parse("path=/path/to_file,direct=on")?, DiskConfig { path: Some(PathBuf::from("/path/to_file")), direct: true, ..Default::default() } ); assert_eq!( DiskConfig::parse("path=/path/to_file")?, DiskConfig { path: Some(PathBuf::from("/path/to_file")), ..Default::default() } ); assert_eq!( DiskConfig::parse("path=/path/to_file")?, DiskConfig { path: Some(PathBuf::from("/path/to_file")), ..Default::default() } ); Ok(()) } #[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")?, NetConfig { mac: MacAddr::parse_str("de:ad:be:ef:12:34").unwrap(), host_mac: Some(MacAddr::parse_str("12:34:de:ad:be:ef").unwrap()), ..Default::default() } ); assert_eq!( NetConfig::parse("mac=de:ad:be:ef:12:34,host_mac=12:34:de:ad:be:ef,id=mynet0")?, NetConfig { mac: MacAddr::parse_str("de:ad:be:ef:12:34").unwrap(), host_mac: Some(MacAddr::parse_str("12:34:de:ad:be:ef").unwrap()), id: Some("mynet0".to_owned()), ..Default::default() } ); 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 { mac: MacAddr::parse_str("de:ad:be:ef:12:34").unwrap(), host_mac: Some(MacAddr::parse_str("12:34:de:ad:be:ef").unwrap()), tap: Some("tap0".to_owned()), ip: "192.168.100.1".parse().unwrap(), mask: "255.255.255.128".parse().unwrap(), ..Default::default() } ); 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 { mac: MacAddr::parse_str("de:ad:be:ef:12:34").unwrap(), host_mac: Some(MacAddr::parse_str("12:34:de:ad:be:ef").unwrap()), vhost_user: true, vhost_socket: Some("/tmp/sock".to_owned()), ..Default::default() } ); 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 { mac: MacAddr::parse_str("de:ad:be:ef:12:34").unwrap(), host_mac: Some(MacAddr::parse_str("12:34:de:ad:be:ef").unwrap()), num_queues: 4, queue_size: 1024, iommu: true, ..Default::default() } ); assert_eq!( NetConfig::parse("mac=de:ad:be:ef:12:34,fd=[3,7],num_queues=4")?, NetConfig { mac: MacAddr::parse_str("de:ad:be:ef:12:34").unwrap(), fds: Some(vec![3, 7]), num_queues: 4, ..Default::default() } ); 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")?, RngConfig { src: PathBuf::from("/dev/random"), iommu: true, } ); assert_eq!( RngConfig::parse("iommu=on")?, RngConfig { iommu: true, ..Default::default() } ); Ok(()) } #[test] fn test_parse_fs() -> Result<()> { // "tag" and "socket" must be supplied assert!(FsConfig::parse("").is_err()); assert!(FsConfig::parse("tag=mytag").is_err()); assert!(FsConfig::parse("socket=/tmp/sock").is_err()); assert_eq!( FsConfig::parse("tag=mytag,socket=/tmp/sock")?, FsConfig { socket: PathBuf::from("/tmp/sock"), tag: "mytag".to_owned(), ..Default::default() } ); assert_eq!( FsConfig::parse("tag=mytag,socket=/tmp/sock")?, FsConfig { socket: PathBuf::from("/tmp/sock"), tag: "mytag".to_owned(), ..Default::default() } ); assert_eq!( FsConfig::parse("tag=mytag,socket=/tmp/sock,num_queues=4,queue_size=1024")?, FsConfig { socket: PathBuf::from("/tmp/sock"), tag: "mytag".to_owned(), num_queues: 4, queue_size: 1024, ..Default::default() } ); Ok(()) } #[test] fn test_pmem_parsing() -> Result<()> { // Must always give a file and size assert!(PmemConfig::parse("").is_err()); assert!(PmemConfig::parse("size=128M").is_err()); assert_eq!( PmemConfig::parse("file=/tmp/pmem,size=128M")?, PmemConfig { file: PathBuf::from("/tmp/pmem"), size: Some(128 << 20), ..Default::default() } ); assert_eq!( PmemConfig::parse("file=/tmp/pmem,size=128M,id=mypmem0")?, PmemConfig { file: PathBuf::from("/tmp/pmem"), size: Some(128 << 20), id: Some("mypmem0".to_owned()), ..Default::default() } ); assert_eq!( PmemConfig::parse("file=/tmp/pmem,size=128M,iommu=on,discard_writes=on")?, PmemConfig { file: PathBuf::from("/tmp/pmem"), size: Some(128 << 20), discard_writes: true, iommu: true, ..Default::default() } ); Ok(()) } #[test] fn test_console_parsing() -> Result<()> { assert!(ConsoleConfig::parse("").is_err()); assert!(ConsoleConfig::parse("badmode").is_err()); assert_eq!( ConsoleConfig::parse("off")?, ConsoleConfig { mode: ConsoleOutputMode::Off, iommu: false, file: None, } ); assert_eq!( ConsoleConfig::parse("pty")?, ConsoleConfig { mode: ConsoleOutputMode::Pty, iommu: false, file: None, } ); assert_eq!( ConsoleConfig::parse("tty")?, ConsoleConfig { mode: ConsoleOutputMode::Tty, iommu: false, file: None, } ); assert_eq!( ConsoleConfig::parse("null")?, ConsoleConfig { mode: ConsoleOutputMode::Null, iommu: false, file: None, } ); assert_eq!( ConsoleConfig::parse("file=/tmp/console")?, ConsoleConfig { mode: ConsoleOutputMode::File, iommu: false, file: Some(PathBuf::from("/tmp/console")) } ); assert_eq!( ConsoleConfig::parse("null,iommu=on")?, ConsoleConfig { mode: ConsoleOutputMode::Null, iommu: true, file: None, } ); assert_eq!( ConsoleConfig::parse("file=/tmp/console,iommu=on")?, ConsoleConfig { mode: ConsoleOutputMode::File, iommu: true, file: Some(PathBuf::from("/tmp/console")) } ); Ok(()) } #[test] fn test_device_parsing() -> Result<()> { // Device must have a path provided assert!(DeviceConfig::parse("").is_err()); assert_eq!( DeviceConfig::parse("path=/path/to/device")?, DeviceConfig { path: PathBuf::from("/path/to/device"), id: None, iommu: false, ..Default::default() } ); assert_eq!( DeviceConfig::parse("path=/path/to/device,iommu=on")?, DeviceConfig { path: PathBuf::from("/path/to/device"), id: None, iommu: true, ..Default::default() } ); assert_eq!( DeviceConfig::parse("path=/path/to/device,iommu=on,id=mydevice0")?, DeviceConfig { path: PathBuf::from("/path/to/device"), id: Some("mydevice0".to_owned()), iommu: true, ..Default::default() } ); Ok(()) } #[test] fn test_vdpa_parsing() -> Result<()> { // path is required assert!(VdpaConfig::parse("").is_err()); assert_eq!( VdpaConfig::parse("path=/dev/vhost-vdpa")?, VdpaConfig { path: PathBuf::from("/dev/vhost-vdpa"), num_queues: 1, id: None, ..Default::default() } ); assert_eq!( VdpaConfig::parse("path=/dev/vhost-vdpa,num_queues=2,id=my_vdpa")?, VdpaConfig { path: PathBuf::from("/dev/vhost-vdpa"), num_queues: 2, id: Some("my_vdpa".to_owned()), ..Default::default() } ); Ok(()) } #[test] fn test_vsock_parsing() -> Result<()> { // socket and cid is required assert!(VsockConfig::parse("").is_err()); assert_eq!( VsockConfig::parse("socket=/tmp/sock,cid=1")?, VsockConfig { cid: 1, socket: PathBuf::from("/tmp/sock"), iommu: false, id: None, ..Default::default() } ); assert_eq!( VsockConfig::parse("socket=/tmp/sock,cid=1,iommu=on")?, VsockConfig { cid: 1, socket: PathBuf::from("/tmp/sock"), iommu: true, id: None, ..Default::default() } ); Ok(()) } #[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, zones: None, }, kernel: None, cmdline: CmdlineConfig { args: String::default(), }, initramfs: None, payload: Some(PayloadConfig { kernel: Some(PathBuf::from("/path/to/kernel")), ..Default::default() }), disks: None, net: None, rng: RngConfig { src: PathBuf::from("/dev/urandom"), iommu: false, }, balloon: None, fs: None, pmem: None, serial: ConsoleConfig { file: None, mode: ConsoleOutputMode::Null, iommu: false, }, console: ConsoleConfig { file: None, mode: ConsoleOutputMode::Tty, iommu: false, }, devices: None, user_devices: None, vdpa: None, vsock: None, iommu: false, #[cfg(target_arch = "x86_64")] sgx_epc: None, numa: None, watchdog: false, #[cfg(feature = "tdx")] tdx: None, #[cfg(feature = "gdb")] gdb: false, platform: None, }; assert!(valid_config.validate().is_ok()); let mut invalid_config = valid_config.clone(); invalid_config.serial.mode = ConsoleOutputMode::Tty; invalid_config.console.mode = ConsoleOutputMode::Tty; assert_eq!( invalid_config.validate(), Err(ValidationError::DoubleTtyMode) ); let mut invalid_config = valid_config.clone(); invalid_config.payload = None; assert_eq!( invalid_config.validate(), Err(ValidationError::KernelMissing) ); let mut invalid_config = valid_config.clone(); invalid_config.serial.mode = ConsoleOutputMode::File; invalid_config.serial.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) ); 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 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")), ..Default::default() }]); 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 { vhost_user: true, ..Default::default() }]); assert_eq!( invalid_config.validate(), Err(ValidationError::VhostUserMissingSocket) ); let mut invalid_config = valid_config.clone(); invalid_config.disks = Some(vec![DiskConfig { vhost_user: true, vhost_socket: Some("/path/to/sock".to_owned()), ..Default::default() }]); assert_eq!( invalid_config.validate(), Err(ValidationError::VhostUserRequiresSharedMemory) ); let mut still_valid_config = valid_config.clone(); still_valid_config.disks = Some(vec![DiskConfig { vhost_user: true, vhost_socket: Some("/path/to/sock".to_owned()), ..Default::default() }]); still_valid_config.memory.shared = true; assert!(still_valid_config.validate().is_ok()); let mut invalid_config = valid_config.clone(); invalid_config.net = Some(vec![NetConfig { vhost_user: true, ..Default::default() }]); assert_eq!( invalid_config.validate(), Err(ValidationError::VhostUserRequiresSharedMemory) ); 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()), ..Default::default() }]); still_valid_config.memory.shared = true; assert!(still_valid_config.validate().is_ok()); let mut invalid_config = valid_config.clone(); invalid_config.net = Some(vec![NetConfig { fds: Some(vec![0]), ..Default::default() }]); assert_eq!( invalid_config.validate(), Err(ValidationError::VnetReservedFd) ); let mut invalid_config = valid_config.clone(); invalid_config.fs = Some(vec![FsConfig { ..Default::default() }]); assert_eq!( invalid_config.validate(), Err(ValidationError::VhostUserRequiresSharedMemory) ); let mut still_valid_config = valid_config.clone(); still_valid_config.memory.shared = true; assert!(still_valid_config.validate().is_ok()); let mut still_valid_config = valid_config.clone(); still_valid_config.memory.hugepages = true; assert!(still_valid_config.validate().is_ok()); let mut still_valid_config = valid_config.clone(); still_valid_config.memory.hugepages = true; still_valid_config.memory.hugepage_size = Some(2 << 20); assert!(still_valid_config.validate().is_ok()); 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)) ); let mut still_valid_config = valid_config.clone(); still_valid_config.platform = Some(PlatformConfig { num_pci_segments: 16, ..Default::default() }); assert!(still_valid_config.validate().is_ok()); let mut invalid_config = valid_config.clone(); invalid_config.platform = Some(PlatformConfig { num_pci_segments: 17, ..Default::default() }); assert_eq!( invalid_config.validate(), Err(ValidationError::InvalidNumPciSegments(17)) ); let mut still_valid_config = valid_config.clone(); still_valid_config.platform = Some(PlatformConfig { num_pci_segments: 16, iommu_segments: Some(vec![1, 2, 3]), ..Default::default() }); assert!(still_valid_config.validate().is_ok()); let mut invalid_config = valid_config.clone(); invalid_config.platform = Some(PlatformConfig { num_pci_segments: 16, iommu_segments: Some(vec![17, 18]), ..Default::default() }); assert_eq!( invalid_config.validate(), Err(ValidationError::InvalidPciSegment(17)) ); let mut still_valid_config = valid_config.clone(); still_valid_config.platform = Some(PlatformConfig { num_pci_segments: 16, iommu_segments: Some(vec![1, 2, 3]), ..Default::default() }); still_valid_config.disks = Some(vec![DiskConfig { iommu: true, pci_segment: 1, ..Default::default() }]); assert!(still_valid_config.validate().is_ok()); let mut still_valid_config = valid_config.clone(); still_valid_config.platform = Some(PlatformConfig { num_pci_segments: 16, iommu_segments: Some(vec![1, 2, 3]), ..Default::default() }); still_valid_config.net = Some(vec![NetConfig { iommu: true, pci_segment: 1, ..Default::default() }]); assert!(still_valid_config.validate().is_ok()); let mut still_valid_config = valid_config.clone(); still_valid_config.platform = Some(PlatformConfig { num_pci_segments: 16, iommu_segments: Some(vec![1, 2, 3]), ..Default::default() }); still_valid_config.pmem = Some(vec![PmemConfig { iommu: true, pci_segment: 1, ..Default::default() }]); assert!(still_valid_config.validate().is_ok()); let mut still_valid_config = valid_config.clone(); still_valid_config.platform = Some(PlatformConfig { num_pci_segments: 16, iommu_segments: Some(vec![1, 2, 3]), ..Default::default() }); still_valid_config.devices = Some(vec![DeviceConfig { iommu: true, pci_segment: 1, ..Default::default() }]); assert!(still_valid_config.validate().is_ok()); let mut still_valid_config = valid_config.clone(); still_valid_config.platform = Some(PlatformConfig { num_pci_segments: 16, iommu_segments: Some(vec![1, 2, 3]), ..Default::default() }); still_valid_config.vsock = Some(VsockConfig { iommu: true, pci_segment: 1, ..Default::default() }); assert!(still_valid_config.validate().is_ok()); let mut invalid_config = valid_config.clone(); invalid_config.platform = Some(PlatformConfig { num_pci_segments: 16, iommu_segments: Some(vec![1, 2, 3]), ..Default::default() }); invalid_config.disks = Some(vec![DiskConfig { iommu: false, pci_segment: 1, ..Default::default() }]); assert_eq!( invalid_config.validate(), Err(ValidationError::OnIommuSegment(1)) ); let mut invalid_config = valid_config.clone(); invalid_config.platform = Some(PlatformConfig { num_pci_segments: 16, iommu_segments: Some(vec![1, 2, 3]), ..Default::default() }); invalid_config.net = Some(vec![NetConfig { iommu: false, pci_segment: 1, ..Default::default() }]); assert_eq!( invalid_config.validate(), Err(ValidationError::OnIommuSegment(1)) ); let mut invalid_config = valid_config.clone(); invalid_config.platform = Some(PlatformConfig { num_pci_segments: 16, iommu_segments: Some(vec![1, 2, 3]), ..Default::default() }); invalid_config.pmem = Some(vec![PmemConfig { iommu: false, pci_segment: 1, ..Default::default() }]); assert_eq!( invalid_config.validate(), Err(ValidationError::OnIommuSegment(1)) ); let mut invalid_config = valid_config.clone(); invalid_config.platform = Some(PlatformConfig { num_pci_segments: 16, iommu_segments: Some(vec![1, 2, 3]), ..Default::default() }); invalid_config.devices = Some(vec![DeviceConfig { iommu: false, pci_segment: 1, ..Default::default() }]); assert_eq!( invalid_config.validate(), Err(ValidationError::OnIommuSegment(1)) ); let mut invalid_config = valid_config.clone(); invalid_config.platform = Some(PlatformConfig { num_pci_segments: 16, iommu_segments: Some(vec![1, 2, 3]), ..Default::default() }); invalid_config.vsock = Some(VsockConfig { iommu: false, pci_segment: 1, ..Default::default() }); 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 { num_pci_segments: 16, iommu_segments: Some(vec![1, 2, 3]), ..Default::default() }); invalid_config.user_devices = Some(vec![UserDeviceConfig { pci_segment: 1, ..Default::default() }]); assert_eq!( invalid_config.validate(), Err(ValidationError::IommuNotSupportedOnSegment(1)) ); let mut invalid_config = valid_config.clone(); invalid_config.platform = Some(PlatformConfig { num_pci_segments: 16, iommu_segments: Some(vec![1, 2, 3]), ..Default::default() }); invalid_config.vdpa = Some(vec![VdpaConfig { pci_segment: 1, ..Default::default() }]); 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 { num_pci_segments: 16, iommu_segments: Some(vec![1, 2, 3]), ..Default::default() }); invalid_config.fs = Some(vec![FsConfig { pci_segment: 1, ..Default::default() }]); assert_eq!( invalid_config.validate(), Err(ValidationError::IommuNotSupportedOnSegment(1)) ); let mut still_valid_config = valid_config.clone(); still_valid_config.devices = Some(vec![ DeviceConfig { path: "/device1".into(), ..Default::default() }, DeviceConfig { path: "/device2".into(), ..Default::default() }, ]); assert!(still_valid_config.validate().is_ok()); let mut invalid_config = valid_config; invalid_config.devices = Some(vec![ DeviceConfig { path: "/device1".into(), ..Default::default() }, DeviceConfig { path: "/device1".into(), ..Default::default() }, ]); assert!(invalid_config.validate().is_err()); } }