cloud-hypervisor/vmm/src/config.rs

3234 lines
107 KiB
Rust
Raw Normal View History

// Copyright © 2019 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
//
pub use crate::vm_config::*;
use clap::ArgMatches;
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::path::PathBuf;
use std::result;
use std::str::FromStr;
use thiserror::Error;
use virtio_devices::{RateLimiterConfig, TokenBucketConfig};
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,
/// Failed parsing TPM device
ParseTpm(OptionParserError),
/// Missing path for TPM device
ParseTpmPathMissing,
}
#[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,
/// Missing firmware for TDX
#[cfg(feature = "tdx")]
TdxFirmwareMissing,
/// 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),
/// Provided MTU is lower than what the VIRTIO specification expects
InvalidMtu(u16),
}
type ValidationResult<T> = std::result::Result<T, ValidationError>;
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 or huge pages"
)
}
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")
}
#[cfg(feature = "tdx")]
TdxFirmwareMissing => {
write!(f, "No TDX firmware specified")
}
TooManyQueues => {
write!(f, "Number of vCPUs is insufficient for number of queues")
}
UserDevicesRequireSharedMemory => {
write!(
f,
"Using user devices requires using shared memory or huge pages"
)
}
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),
&InvalidMtu(mtu) => {
write!(
f,
"Provided MTU {} is lower than 1280 (expected by VIRTIO specification)",
mtu
)
}
}
}
}
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"),
ParseTpm(o) => write!(f, "Error parsing --tpm: {}", o),
ParseTpmPathMissing => write!(f, "Error parsing --tpm: path missing"),
}
}
}
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]);
}
}
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 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 pmem: Option<Vec<&'a str>>,
pub serial: &'a str,
pub 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(target_arch = "x86_64")]
pub sgx_epc: Option<Vec<&'a str>>,
pub numa: Option<Vec<&'a str>>,
pub watchdog: bool,
#[cfg(feature = "guest_debug")]
pub gdb: bool,
pub platform: Option<&'a str>,
pub tpm: 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 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();
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 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(target_arch = "x86_64")]
let sgx_epc: Option<Vec<&str>> = args
.get_many::<String>("sgx-epc")
.map(|x| x.map(|y| y as &str).collect());
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 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);
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 = "guest_debug")]
gdb,
platform,
tpm,
}
}
}
#[derive(Debug)]
pub enum ParseHotplugMethodError {
InvalidValue(String),
}
impl FromStr for HotplugMethod {
type Err = ParseHotplugMethodError;
fn from_str(s: &str) -> std::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 CpuTopologyParseError {
InvalidValue(String),
}
impl FromStr for CpuTopology {
type Err = CpuTopologyParseError;
fn from_str(s: &str) -> std::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");
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::<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<u8, Vec<u8>>>("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::<StringList>("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 <std::string::String as AsRef<str>>::as_ref(&s) {
#[cfg(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 PlatformConfig {
pub fn parse(platform: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add("num_pci_segments")
.add("iommu_segments")
.add("serial_number")
.add("uuid")
.add("oem_strings");
#[cfg(feature = "tdx")]
parser.add("tdx");
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 serial_number = parser
.convert("serial_number")
.map_err(Error::ParsePlatform)?;
let uuid = parser.convert("uuid").map_err(Error::ParsePlatform)?;
let oem_strings = parser
.convert::<StringList>("oem_strings")
.map_err(Error::ParsePlatform)?
.map(|v| v.0);
#[cfg(feature = "tdx")]
let tdx = parser
.convert::<Toggle>("tdx")
.map_err(Error::ParsePlatform)?
.unwrap_or(Toggle(false))
.0;
Ok(PlatformConfig {
num_pci_segments,
iommu_segments,
serial_number,
uuid,
oem_strings,
#[cfg(feature = "tdx")]
tdx,
})
}
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 MemoryConfig {
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("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 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");
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;
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,
thp,
})
}
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 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>\"";
pub fn parse(disk: &str) -> Result<Self> {
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::<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 iommu = parser
.convert::<Toggle>("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::<Toggle>("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::<Toggle>("_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(Debug)]
pub enum ParseVhostModeError {
InvalidValue(String),
}
impl FromStr for VhostMode {
type Err = ParseVhostModeError;
fn from_str(s: &str) -> std::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>\"";
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("mtu")
.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 mtu = parser.convert("mtu").map_err(Error::ParseNetwork)?;
let iommu = parser
.convert::<Toggle>("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::<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 id = parser.get("id");
let fds = parser
.convert::<IntegerList>("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,
mtu,
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));
}
}
}
if let Some(mtu) = self.mtu {
if mtu < virtio_devices::net::MIN_MTU {
return Err(ValidationError::InvalidMtu(mtu));
}
}
Ok(())
}
}
impl RngConfig {
pub fn parse(rng: &str) -> Result<Self> {
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::<Toggle>("iommu")
.map_err(Error::ParseRng)?
.unwrap_or(Toggle(false))
.0;
Ok(RngConfig { src, iommu })
}
}
impl BalloonConfig {
pub const SYNTAX: &'static str =
"Balloon parameters \"size=<balloon_size>,deflate_on_oom=on|off,\
free_page_reporting=on|off\"";
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.parse(balloon).map_err(Error::ParseBalloon)?;
let size = parser
.convert::<ByteSized>("size")
.map_err(Error::ParseBalloon)?
.map(|v| v.0)
.unwrap_or(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;
Ok(BalloonConfig {
size,
deflate_on_oom,
free_page_reporting,
})
}
}
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>\"";
pub fn parse(fs: &str) -> Result<Self> {
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(())
}
}
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>\"";
pub fn parse(pmem: &str) -> Result<Self> {
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::<ByteSized>("size")
.map_err(Error::ParsePersistentMemory)?
.map(|v| v.0);
let iommu = parser
.convert::<Toggle>("iommu")
.map_err(Error::ParsePersistentMemory)?
.unwrap_or(Toggle(false))
.0;
let discard_writes = parser
.convert::<Toggle>("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(())
}
}
impl ConsoleConfig {
pub fn parse(console: &str) -> Result<Self> {
let mut parser = OptionParser::new();
parser
.add_valueless("off")
Enable pty console Add the ability for cloud-hypervisor to create, manage and monitor a pty for serial and/or console I/O from a user. The reasoning for having cloud-hypervisor create the ptys is so that clients, libvirt for example, could exit and later re-open the pty without causing I/O issues. If the clients were responsible for creating the pty, when they exit the main pty fd would close and cause cloud-hypervisor to get I/O errors on writes. Ideally the main and subordinate pty fds would be kept in the main vmm's Vm structure. However, because the device manager owns parsing the configuration for the serial and console devices, the information is instead stored in new fields under the DeviceManager structure directly. From there hooking up the main fd is intended to look as close to handling stdin and stdout on the tty as possible (there is some future work ahead for perhaps moving support for the pty into the vmm_sys_utils crate). The main fd is used for reading user input and writing to output of the Vm device. The subordinate fd is used to setup raw mode and it is kept open in order to avoid I/O errors when clients open and close the pty device. The ability to handle multiple inputs as part of this change is intentional. The current code allows serial and console ptys to be created and both be used as input. There was an implementation gap though with the queue_input_bytes needing to be modified so the pty handlers for serial and console could access the methods on the serial and console structures directly. Without this change only a single input source could be processed as the console would switch based on its input type (this is still valid for tty and isn't otherwise modified). Signed-off-by: William Douglas <william.r.douglas@gmail.com>
2021-01-14 03:03:53 +00:00
.add_valueless("pty")
.add_valueless("tty")
.add_valueless("null")
.add("file")
.add("iommu");
parser.parse(console).map_err(Error::ParseConsole)?;
let mut file: Option<PathBuf> = default_consoleconfig_file();
let mut mode: ConsoleOutputMode = ConsoleOutputMode::Off;
if parser.is_set("off") {
Enable pty console Add the ability for cloud-hypervisor to create, manage and monitor a pty for serial and/or console I/O from a user. The reasoning for having cloud-hypervisor create the ptys is so that clients, libvirt for example, could exit and later re-open the pty without causing I/O issues. If the clients were responsible for creating the pty, when they exit the main pty fd would close and cause cloud-hypervisor to get I/O errors on writes. Ideally the main and subordinate pty fds would be kept in the main vmm's Vm structure. However, because the device manager owns parsing the configuration for the serial and console devices, the information is instead stored in new fields under the DeviceManager structure directly. From there hooking up the main fd is intended to look as close to handling stdin and stdout on the tty as possible (there is some future work ahead for perhaps moving support for the pty into the vmm_sys_utils crate). The main fd is used for reading user input and writing to output of the Vm device. The subordinate fd is used to setup raw mode and it is kept open in order to avoid I/O errors when clients open and close the pty device. The ability to handle multiple inputs as part of this change is intentional. The current code allows serial and console ptys to be created and both be used as input. There was an implementation gap though with the queue_input_bytes needing to be modified so the pty handlers for serial and console could access the methods on the serial and console structures directly. Without this change only a single input source could be processed as the console would switch based on its input type (this is still valid for tty and isn't otherwise modified). Signed-off-by: William Douglas <william.r.douglas@gmail.com>
2021-01-14 03:03:53 +00:00
} 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::<Toggle>("iommu")
.map_err(Error::ParseConsole)?
.unwrap_or(Toggle(false))
.0;
Ok(Self { file, mode, iommu })
}
}
impl DeviceConfig {
pub const SYNTAX: &'static str =
"Direct device assignment parameters \"path=<device_path>,iommu=on|off,id=<device_id>,pci_segment=<segment_id>\"";
pub fn parse(device: &str) -> Result<Self> {
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::<Toggle>("iommu")
.map_err(Error::ParseDevice)?
.unwrap_or(Toggle(false))
.0;
let id = parser.get("id");
let pci_segment = parser
.convert::<u16>("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(())
}
}
impl UserDeviceConfig {
pub const SYNTAX: &'static str =
"Userspace device socket=<socket_path>,id=<device_id>,pci_segment=<segment_id>\"";
pub fn parse(user_device: &str) -> Result<Self> {
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::<u16>("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(())
}
}
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>\"";
pub fn parse(vdpa: &str) -> Result<Self> {
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::<Toggle>("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(())
}
}
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>\"";
pub fn parse(vsock: &str) -> Result<Self> {
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::<Toggle>("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(target_arch = "x86_64")]
impl SgxEpcConfig {
pub const SYNTAX: &'static str = "SGX EPC parameters \
\"id=<epc_section_identifier>,size=<epc_section_size>,prefault=on|off\"";
pub fn parse(sgx_epc: &str) -> Result<Self> {
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::<ByteSized>("size")
.map_err(Error::ParseSgxEpc)?
.unwrap_or(ByteSized(0))
.0;
let prefault = parser
.convert::<Toggle>("prefault")
.map_err(Error::ParseSgxEpc)?
.unwrap_or(Toggle(false))
.0;
Ok(SgxEpcConfig { id, size, prefault })
}
}
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>,\
memory_zones=<list_of_memory_zones>,sgx_epc_sections=<list_of_sgx_epc_sections>\"";
pub fn parse(numa: &str) -> Result<Self> {
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::<u32>("guest_numa_id")
.map_err(Error::ParseNuma)?
.unwrap_or(0);
let cpus = parser
.convert::<IntegerList>("cpus")
.map_err(Error::ParseNuma)?
.map(|v| v.0.iter().map(|e| *e as u8).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 memory_zones = parser
.convert::<StringList>("memory_zones")
.map_err(Error::ParseNuma)?
.map(|v| v.0);
#[cfg(target_arch = "x86_64")]
let sgx_epc_sections = parser
.convert::<StringList>("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=<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<Self> {
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::<Toggle>("prefault")
.map_err(Error::ParseRestore)?
.unwrap_or(Toggle(false))
.0;
Ok(RestoreConfig {
source_url,
prefault,
})
}
}
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 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();
self.payload
.as_ref()
.ok_or(ValidationError::KernelMissing)?;
#[cfg(feature = "tdx")]
{
let tdx_enabled = self.platform.as_ref().map(|p| p.tdx).unwrap_or(false);
// 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);
}
}
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.backed_by_shared_memory() {
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.backed_by_shared_memory() {
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.backed_by_shared_memory() {
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.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.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<Self> {
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);
}
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 balloon: Option<BalloonConfig> = None;
if let Some(balloon_params) = &vm_params.balloon {
balloon = Some(BalloonConfig::parse(balloon_params)?);
}
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 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 = ConsoleConfig::parse(vm_params.serial)?;
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 platform = vm_params.platform.map(PlatformConfig::parse).transpose()?;
#[cfg(target_arch = "x86_64")]
let mut sgx_epc: Option<Vec<SgxEpcConfig>> = 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<Vec<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);
}
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
};
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 config = VmConfig {
cpus: CpusConfig::parse(vm_params.cpus)?,
memory: MemoryConfig::parse(vm_params.memory, vm_params.memory_zones)?,
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 = "guest_debug")]
gdb,
platform,
tpm,
};
config.validate().map_err(Error::Validation)?;
Ok(config)
}
#[cfg(feature = "tdx")]
pub fn is_tdx_enabled(&self) -> bool {
self.platform.as_ref().map(|p| p.tdx).unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
use net_util::MacAddr;
#[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,
}
);
Enable pty console Add the ability for cloud-hypervisor to create, manage and monitor a pty for serial and/or console I/O from a user. The reasoning for having cloud-hypervisor create the ptys is so that clients, libvirt for example, could exit and later re-open the pty without causing I/O issues. If the clients were responsible for creating the pty, when they exit the main pty fd would close and cause cloud-hypervisor to get I/O errors on writes. Ideally the main and subordinate pty fds would be kept in the main vmm's Vm structure. However, because the device manager owns parsing the configuration for the serial and console devices, the information is instead stored in new fields under the DeviceManager structure directly. From there hooking up the main fd is intended to look as close to handling stdin and stdout on the tty as possible (there is some future work ahead for perhaps moving support for the pty into the vmm_sys_utils crate). The main fd is used for reading user input and writing to output of the Vm device. The subordinate fd is used to setup raw mode and it is kept open in order to avoid I/O errors when clients open and close the pty device. The ability to handle multiple inputs as part of this change is intentional. The current code allows serial and console ptys to be created and both be used as input. There was an implementation gap though with the queue_input_bytes needing to be modified so the pty handlers for serial and console could access the methods on the serial and console structures directly. Without this change only a single input source could be processed as the console would switch based on its input type (this is still valid for tty and isn't otherwise modified). Signed-off-by: William Douglas <william.r.douglas@gmail.com>
2021-01-14 03:03:53 +00:00
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_tpm_parsing() -> Result<()> {
// path is required
assert!(TpmConfig::parse("").is_err());
assert_eq!(
TpmConfig::parse("socket=/var/run/tpm.sock")?,
TpmConfig {
socket: PathBuf::from("/var/run/tpm.sock"),
}
);
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,
thp: true,
},
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 = "guest_debug")]
gdb: false,
platform: None,
tpm: 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());
}
}