1 // Copyright © 2019 Intel Corporation 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 // 5 6 use clap::ArgMatches; 7 use net_util::MacAddr; 8 use option_parser::{ 9 ByteSized, IntegerList, OptionParser, OptionParserError, StringList, Toggle, TupleTwoIntegers, 10 }; 11 use std::convert::From; 12 use std::fmt; 13 use std::net::Ipv4Addr; 14 use std::path::PathBuf; 15 use std::result; 16 use std::str::FromStr; 17 18 use virtio_devices::{RateLimiterConfig, TokenBucketConfig}; 19 20 pub const DEFAULT_VCPUS: u8 = 1; 21 pub const DEFAULT_MEMORY_MB: u64 = 512; 22 pub const DEFAULT_RNG_SOURCE: &str = "/dev/urandom"; 23 pub const DEFAULT_NUM_QUEUES_VUNET: usize = 2; 24 pub const DEFAULT_QUEUE_SIZE_VUNET: u16 = 256; 25 pub const DEFAULT_NUM_QUEUES_VUBLK: usize = 1; 26 pub const DEFAULT_QUEUE_SIZE_VUBLK: u16 = 128; 27 28 /// Errors associated with VM configuration parameters. 29 #[derive(Debug)] 30 pub enum Error { 31 /// Filesystem tag is missing 32 ParseFsTagMissing, 33 /// Filesystem socket is missing 34 ParseFsSockMissing, 35 /// Cannot have dax=off along with cache_size parameter. 36 InvalidCacheSizeWithDaxOff, 37 /// Missing persistent memory file parameter. 38 ParsePmemFileMissing, 39 /// Missing vsock socket path parameter. 40 ParseVsockSockMissing, 41 /// Missing vsock cid parameter. 42 ParseVsockCidMissing, 43 /// Missing restore source_url parameter. 44 ParseRestoreSourceUrlMissing, 45 /// Error parsing CPU options 46 ParseCpus(OptionParserError), 47 /// Error parsing memory options 48 ParseMemory(OptionParserError), 49 /// Error parsing memory zone options 50 ParseMemoryZone(OptionParserError), 51 /// Missing 'id' from memory zone 52 ParseMemoryZoneIdMissing, 53 /// Error parsing disk options 54 ParseDisk(OptionParserError), 55 /// Error parsing network options 56 ParseNetwork(OptionParserError), 57 /// Error parsing RNG options 58 ParseRng(OptionParserError), 59 /// Error parsing balloon options 60 ParseBalloon(OptionParserError), 61 /// Error parsing filesystem parameters 62 ParseFileSystem(OptionParserError), 63 /// Error parsing persistent memory parameters 64 ParsePersistentMemory(OptionParserError), 65 /// Failed parsing console 66 ParseConsole(OptionParserError), 67 /// No mode given for console 68 ParseConsoleInvalidModeGiven, 69 /// Failed parsing device parameters 70 ParseDevice(OptionParserError), 71 /// Missing path from device, 72 ParseDevicePathMissing, 73 /// Failed to parse vsock parameters 74 ParseVsock(OptionParserError), 75 /// Failed to parse restore parameters 76 ParseRestore(OptionParserError), 77 /// Failed to parse SGX EPC parameters 78 #[cfg(target_arch = "x86_64")] 79 ParseSgxEpc(OptionParserError), 80 /// Missing 'id' from SGX EPC section 81 #[cfg(target_arch = "x86_64")] 82 ParseSgxEpcIdMissing, 83 /// Failed to parse NUMA parameters 84 ParseNuma(OptionParserError), 85 /// Failed to validate configuration 86 Validation(ValidationError), 87 #[cfg(feature = "tdx")] 88 /// Failed to parse TDX config 89 ParseTdx(OptionParserError), 90 #[cfg(feature = "tdx")] 91 // No TDX firmware 92 FirmwarePathMissing, 93 } 94 95 #[derive(Debug)] 96 pub enum ValidationError { 97 /// Both console and serial are tty. 98 DoubleTtyMode, 99 /// No kernel specified 100 KernelMissing, 101 /// Missing file value for console 102 ConsoleFileMissing, 103 /// Max is less than boot 104 CpusMaxLowerThanBoot, 105 /// Both socket and path specified 106 DiskSocketAndPath, 107 /// Using vhost user requires shared memory 108 VhostUserRequiresSharedMemory, 109 /// No socket provided for vhost_use 110 VhostUserMissingSocket, 111 /// Trying to use IOMMU without PCI 112 IommuUnsupported, 113 /// Trying to use VFIO without PCI 114 VfioUnsupported, 115 /// CPU topology count doesn't match max 116 CpuTopologyCount, 117 /// One part of the CPU topology was zero 118 CpuTopologyZeroPart, 119 /// Virtio needs a min of 2 queues 120 VnetQueueLowerThan2, 121 /// The input queue number for virtio_net must match the number of input fds 122 VnetQueueFdMismatch, 123 /// Using reserved fd 124 VnetReservedFd, 125 // Hugepages not turned on 126 HugePageSizeWithoutHugePages, 127 // Huge page size is not power of 2 128 InvalidHugePageSize(u64), 129 // CPU Hotplug not permitted with TDX 130 #[cfg(feature = "tdx")] 131 TdxNoCpuHotplug, 132 // Specifying kernel not permitted with TDX 133 #[cfg(feature = "tdx")] 134 TdxKernelSpecified, 135 // Insuffient vCPUs for queues 136 TooManyQueues, 137 } 138 139 type ValidationResult<T> = std::result::Result<T, ValidationError>; 140 141 impl fmt::Display for ValidationError { 142 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 143 use self::ValidationError::*; 144 match self { 145 DoubleTtyMode => write!(f, "Console mode tty specified for both serial and console"), 146 KernelMissing => write!(f, "No kernel specified"), 147 ConsoleFileMissing => write!(f, "Path missing when using file console mode"), 148 CpusMaxLowerThanBoot => write!(f, "Max CPUs greater than boot CPUs"), 149 DiskSocketAndPath => write!(f, "Disk path and vhost socket both provided"), 150 VhostUserRequiresSharedMemory => { 151 write!(f, "Using vhost-user requires using shared memory") 152 } 153 VhostUserMissingSocket => write!(f, "No socket provided when using vhost-user"), 154 IommuUnsupported => write!(f, "Using an IOMMU without PCI support is unsupported"), 155 VfioUnsupported => write!(f, "Using VFIO without PCI support is unsupported"), 156 CpuTopologyZeroPart => write!(f, "No part of the CPU topology can be zero"), 157 CpuTopologyCount => write!( 158 f, 159 "Product of CPU topology parts does not match maximum vCPUs" 160 ), 161 VnetQueueLowerThan2 => write!(f, "Number of queues to virtio_net less than 2"), 162 VnetQueueFdMismatch => write!( 163 f, 164 "Number of queues to virtio_net does not match the number of input FDs" 165 ), 166 VnetReservedFd => write!(f, "Reserved fd number (<= 2)"), 167 HugePageSizeWithoutHugePages => { 168 write!(f, "Huge page size specified but huge pages not enabled") 169 } 170 InvalidHugePageSize(s) => { 171 write!(f, "Huge page size is not power of 2: {}", s) 172 } 173 #[cfg(feature = "tdx")] 174 TdxNoCpuHotplug => { 175 write!(f, "CPU hotplug not possible with TDX") 176 } 177 #[cfg(feature = "tdx")] 178 TdxKernelSpecified => { 179 write!(f, "Direct kernel boot not possible with TDX") 180 } 181 TooManyQueues => { 182 write!(f, "Number of vCPUs is insufficient for number of queues") 183 } 184 } 185 } 186 } 187 188 impl fmt::Display for Error { 189 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 190 use self::Error::*; 191 match self { 192 ParseConsole(o) => write!(f, "Error parsing --console: {}", o), 193 ParseConsoleInvalidModeGiven => { 194 write!(f, "Error parsing --console: invalid console mode given") 195 } 196 ParseCpus(o) => write!(f, "Error parsing --cpus: {}", o), 197 198 ParseDevice(o) => write!(f, "Error parsing --device: {}", o), 199 ParseDevicePathMissing => write!(f, "Error parsing --device: path missing"), 200 ParseFileSystem(o) => write!(f, "Error parsing --fs: {}", o), 201 ParseFsSockMissing => write!(f, "Error parsing --fs: socket missing"), 202 ParseFsTagMissing => write!(f, "Error parsing --fs: tag missing"), 203 InvalidCacheSizeWithDaxOff => { 204 write!(f, "Error parsing --fs: cache_size used with dax=on") 205 } 206 ParsePersistentMemory(o) => write!(f, "Error parsing --pmem: {}", o), 207 ParsePmemFileMissing => write!(f, "Error parsing --pmem: file missing"), 208 ParseVsock(o) => write!(f, "Error parsing --vsock: {}", o), 209 ParseVsockCidMissing => write!(f, "Error parsing --vsock: cid missing"), 210 ParseVsockSockMissing => write!(f, "Error parsing --vsock: socket missing"), 211 ParseMemory(o) => write!(f, "Error parsing --memory: {}", o), 212 ParseMemoryZone(o) => write!(f, "Error parsing --memory-zone: {}", o), 213 ParseMemoryZoneIdMissing => write!(f, "Error parsing --memory-zone: id missing"), 214 ParseNetwork(o) => write!(f, "Error parsing --net: {}", o), 215 ParseDisk(o) => write!(f, "Error parsing --disk: {}", o), 216 ParseRng(o) => write!(f, "Error parsing --rng: {}", o), 217 ParseBalloon(o) => write!(f, "Error parsing --balloon: {}", o), 218 ParseRestore(o) => write!(f, "Error parsing --restore: {}", o), 219 #[cfg(target_arch = "x86_64")] 220 ParseSgxEpc(o) => write!(f, "Error parsing --sgx-epc: {}", o), 221 #[cfg(target_arch = "x86_64")] 222 ParseSgxEpcIdMissing => write!(f, "Error parsing --sgx-epc: id missing"), 223 ParseNuma(o) => write!(f, "Error parsing --numa: {}", o), 224 ParseRestoreSourceUrlMissing => { 225 write!(f, "Error parsing --restore: source_url missing") 226 } 227 Validation(v) => write!(f, "Error validating configuration: {}", v), 228 #[cfg(feature = "tdx")] 229 ParseTdx(o) => write!(f, "Error parsing --tdx: {}", o), 230 #[cfg(feature = "tdx")] 231 FirmwarePathMissing => write!(f, "TDX firmware missing"), 232 } 233 } 234 } 235 236 pub type Result<T> = result::Result<T, Error>; 237 238 pub struct VmParams<'a> { 239 pub cpus: &'a str, 240 pub memory: &'a str, 241 pub memory_zones: Option<Vec<&'a str>>, 242 pub kernel: Option<&'a str>, 243 pub initramfs: Option<&'a str>, 244 pub cmdline: Option<&'a str>, 245 pub disks: Option<Vec<&'a str>>, 246 pub net: Option<Vec<&'a str>>, 247 pub rng: &'a str, 248 pub balloon: Option<&'a str>, 249 pub fs: Option<Vec<&'a str>>, 250 pub pmem: Option<Vec<&'a str>>, 251 pub serial: &'a str, 252 pub console: &'a str, 253 pub devices: Option<Vec<&'a str>>, 254 pub vsock: Option<&'a str>, 255 #[cfg(target_arch = "x86_64")] 256 pub sgx_epc: Option<Vec<&'a str>>, 257 pub numa: Option<Vec<&'a str>>, 258 pub watchdog: bool, 259 #[cfg(feature = "tdx")] 260 pub tdx: Option<&'a str>, 261 } 262 263 impl<'a> VmParams<'a> { 264 pub fn from_arg_matches(args: &'a ArgMatches) -> Self { 265 // These .unwrap()s cannot fail as there is a default value defined 266 let cpus = args.value_of("cpus").unwrap(); 267 let memory = args.value_of("memory").unwrap(); 268 let memory_zones: Option<Vec<&str>> = args.values_of("memory-zone").map(|x| x.collect()); 269 let rng = args.value_of("rng").unwrap(); 270 let serial = args.value_of("serial").unwrap(); 271 272 let kernel = args.value_of("kernel"); 273 let initramfs = args.value_of("initramfs"); 274 let cmdline = args.value_of("cmdline"); 275 276 let disks: Option<Vec<&str>> = args.values_of("disk").map(|x| x.collect()); 277 let net: Option<Vec<&str>> = args.values_of("net").map(|x| x.collect()); 278 let console = args.value_of("console").unwrap(); 279 let balloon = args.value_of("balloon"); 280 let fs: Option<Vec<&str>> = args.values_of("fs").map(|x| x.collect()); 281 let pmem: Option<Vec<&str>> = args.values_of("pmem").map(|x| x.collect()); 282 let devices: Option<Vec<&str>> = args.values_of("device").map(|x| x.collect()); 283 let vsock: Option<&str> = args.value_of("vsock"); 284 #[cfg(target_arch = "x86_64")] 285 let sgx_epc: Option<Vec<&str>> = args.values_of("sgx-epc").map(|x| x.collect()); 286 let numa: Option<Vec<&str>> = args.values_of("numa").map(|x| x.collect()); 287 let watchdog = args.is_present("watchdog"); 288 #[cfg(feature = "tdx")] 289 let tdx = args.value_of("tdx"); 290 VmParams { 291 cpus, 292 memory, 293 memory_zones, 294 kernel, 295 initramfs, 296 cmdline, 297 disks, 298 net, 299 rng, 300 balloon, 301 fs, 302 pmem, 303 serial, 304 console, 305 devices, 306 vsock, 307 #[cfg(target_arch = "x86_64")] 308 sgx_epc, 309 numa, 310 watchdog, 311 #[cfg(feature = "tdx")] 312 tdx, 313 } 314 } 315 } 316 317 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 318 pub enum HotplugMethod { 319 Acpi, 320 VirtioMem, 321 } 322 323 impl Default for HotplugMethod { 324 fn default() -> Self { 325 HotplugMethod::Acpi 326 } 327 } 328 329 #[derive(Debug)] 330 pub enum ParseHotplugMethodError { 331 InvalidValue(String), 332 } 333 334 impl FromStr for HotplugMethod { 335 type Err = ParseHotplugMethodError; 336 337 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { 338 match s.to_lowercase().as_str() { 339 "acpi" => Ok(HotplugMethod::Acpi), 340 "virtio-mem" => Ok(HotplugMethod::VirtioMem), 341 _ => Err(ParseHotplugMethodError::InvalidValue(s.to_owned())), 342 } 343 } 344 } 345 346 pub enum CpuTopologyParseError { 347 InvalidValue(String), 348 } 349 350 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 351 pub struct CpuTopology { 352 pub threads_per_core: u8, 353 pub cores_per_die: u8, 354 pub dies_per_package: u8, 355 pub packages: u8, 356 } 357 358 impl FromStr for CpuTopology { 359 type Err = CpuTopologyParseError; 360 361 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { 362 let parts: Vec<&str> = s.split(':').collect(); 363 364 if parts.len() != 4 { 365 return Err(Self::Err::InvalidValue(s.to_owned())); 366 } 367 368 let t = CpuTopology { 369 threads_per_core: parts[0] 370 .parse() 371 .map_err(|_| Self::Err::InvalidValue(s.to_owned()))?, 372 cores_per_die: parts[1] 373 .parse() 374 .map_err(|_| Self::Err::InvalidValue(s.to_owned()))?, 375 dies_per_package: parts[2] 376 .parse() 377 .map_err(|_| Self::Err::InvalidValue(s.to_owned()))?, 378 packages: parts[3] 379 .parse() 380 .map_err(|_| Self::Err::InvalidValue(s.to_owned()))?, 381 }; 382 383 Ok(t) 384 } 385 } 386 387 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 388 pub struct CpusConfig { 389 pub boot_vcpus: u8, 390 pub max_vcpus: u8, 391 #[serde(default)] 392 pub topology: Option<CpuTopology>, 393 #[serde(default)] 394 pub kvm_hyperv: bool, 395 #[serde(default)] 396 pub max_phys_bits: Option<u8>, 397 } 398 399 impl CpusConfig { 400 pub fn parse(cpus: &str) -> Result<Self> { 401 let mut parser = OptionParser::new(); 402 parser 403 .add("boot") 404 .add("max") 405 .add("topology") 406 .add("kvm_hyperv") 407 .add("max_phys_bits"); 408 parser.parse(cpus).map_err(Error::ParseCpus)?; 409 410 let boot_vcpus: u8 = parser 411 .convert("boot") 412 .map_err(Error::ParseCpus)? 413 .unwrap_or(DEFAULT_VCPUS); 414 let max_vcpus: u8 = parser 415 .convert("max") 416 .map_err(Error::ParseCpus)? 417 .unwrap_or(boot_vcpus); 418 let topology = parser.convert("topology").map_err(Error::ParseCpus)?; 419 let kvm_hyperv = parser 420 .convert::<Toggle>("kvm_hyperv") 421 .map_err(Error::ParseCpus)? 422 .unwrap_or(Toggle(false)) 423 .0; 424 let max_phys_bits = parser 425 .convert::<u8>("max_phys_bits") 426 .map_err(Error::ParseCpus)?; 427 428 Ok(CpusConfig { 429 boot_vcpus, 430 max_vcpus, 431 topology, 432 kvm_hyperv, 433 max_phys_bits, 434 }) 435 } 436 } 437 438 impl Default for CpusConfig { 439 fn default() -> Self { 440 CpusConfig { 441 boot_vcpus: DEFAULT_VCPUS, 442 max_vcpus: DEFAULT_VCPUS, 443 topology: None, 444 kvm_hyperv: false, 445 max_phys_bits: None, 446 } 447 } 448 } 449 450 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 451 pub struct MemoryZoneConfig { 452 pub id: String, 453 pub size: u64, 454 #[serde(default)] 455 pub file: Option<PathBuf>, 456 #[serde(default)] 457 pub shared: bool, 458 #[serde(default)] 459 pub hugepages: bool, 460 #[serde(default)] 461 pub hugepage_size: Option<u64>, 462 #[serde(default)] 463 pub host_numa_node: Option<u32>, 464 #[serde(default)] 465 pub hotplug_size: Option<u64>, 466 #[serde(default)] 467 pub hotplugged_size: Option<u64>, 468 } 469 470 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 471 pub struct MemoryConfig { 472 pub size: u64, 473 #[serde(default)] 474 pub mergeable: bool, 475 #[serde(default)] 476 pub hotplug_method: HotplugMethod, 477 #[serde(default)] 478 pub hotplug_size: Option<u64>, 479 #[serde(default)] 480 pub hotplugged_size: Option<u64>, 481 #[serde(default)] 482 pub shared: bool, 483 #[serde(default)] 484 pub hugepages: bool, 485 #[serde(default)] 486 pub hugepage_size: Option<u64>, 487 #[serde(default)] 488 pub zones: Option<Vec<MemoryZoneConfig>>, 489 } 490 491 impl MemoryConfig { 492 pub fn parse(memory: &str, memory_zones: Option<Vec<&str>>) -> Result<Self> { 493 let mut parser = OptionParser::new(); 494 parser 495 .add("size") 496 .add("file") 497 .add("mergeable") 498 .add("hotplug_method") 499 .add("hotplug_size") 500 .add("hotplugged_size") 501 .add("shared") 502 .add("hugepages") 503 .add("hugepage_size"); 504 parser.parse(memory).map_err(Error::ParseMemory)?; 505 506 let size = parser 507 .convert::<ByteSized>("size") 508 .map_err(Error::ParseMemory)? 509 .unwrap_or(ByteSized(DEFAULT_MEMORY_MB << 20)) 510 .0; 511 let mergeable = parser 512 .convert::<Toggle>("mergeable") 513 .map_err(Error::ParseMemory)? 514 .unwrap_or(Toggle(false)) 515 .0; 516 let hotplug_method = parser 517 .convert("hotplug_method") 518 .map_err(Error::ParseMemory)? 519 .unwrap_or_default(); 520 let hotplug_size = parser 521 .convert::<ByteSized>("hotplug_size") 522 .map_err(Error::ParseMemory)? 523 .map(|v| v.0); 524 let hotplugged_size = parser 525 .convert::<ByteSized>("hotplugged_size") 526 .map_err(Error::ParseMemory)? 527 .map(|v| v.0); 528 let shared = parser 529 .convert::<Toggle>("shared") 530 .map_err(Error::ParseMemory)? 531 .unwrap_or(Toggle(false)) 532 .0; 533 let hugepages = parser 534 .convert::<Toggle>("hugepages") 535 .map_err(Error::ParseMemory)? 536 .unwrap_or(Toggle(false)) 537 .0; 538 let hugepage_size = parser 539 .convert::<ByteSized>("hugepage_size") 540 .map_err(Error::ParseMemory)? 541 .map(|v| v.0); 542 543 let zones: Option<Vec<MemoryZoneConfig>> = if let Some(memory_zones) = &memory_zones { 544 let mut zones = Vec::new(); 545 for memory_zone in memory_zones.iter() { 546 let mut parser = OptionParser::new(); 547 parser 548 .add("id") 549 .add("size") 550 .add("file") 551 .add("shared") 552 .add("hugepages") 553 .add("hugepage_size") 554 .add("host_numa_node") 555 .add("hotplug_size") 556 .add("hotplugged_size"); 557 parser.parse(memory_zone).map_err(Error::ParseMemoryZone)?; 558 559 let id = parser.get("id").ok_or(Error::ParseMemoryZoneIdMissing)?; 560 let size = parser 561 .convert::<ByteSized>("size") 562 .map_err(Error::ParseMemoryZone)? 563 .unwrap_or(ByteSized(DEFAULT_MEMORY_MB << 20)) 564 .0; 565 let file = parser.get("file").map(PathBuf::from); 566 let shared = parser 567 .convert::<Toggle>("shared") 568 .map_err(Error::ParseMemoryZone)? 569 .unwrap_or(Toggle(false)) 570 .0; 571 let hugepages = parser 572 .convert::<Toggle>("hugepages") 573 .map_err(Error::ParseMemoryZone)? 574 .unwrap_or(Toggle(false)) 575 .0; 576 let hugepage_size = parser 577 .convert::<ByteSized>("hugepage_size") 578 .map_err(Error::ParseMemoryZone)? 579 .map(|v| v.0); 580 581 let host_numa_node = parser 582 .convert::<u32>("host_numa_node") 583 .map_err(Error::ParseMemoryZone)?; 584 let hotplug_size = parser 585 .convert::<ByteSized>("hotplug_size") 586 .map_err(Error::ParseMemoryZone)? 587 .map(|v| v.0); 588 let hotplugged_size = parser 589 .convert::<ByteSized>("hotplugged_size") 590 .map_err(Error::ParseMemoryZone)? 591 .map(|v| v.0); 592 593 zones.push(MemoryZoneConfig { 594 id, 595 size, 596 file, 597 shared, 598 hugepages, 599 hugepage_size, 600 host_numa_node, 601 hotplug_size, 602 hotplugged_size, 603 }); 604 } 605 Some(zones) 606 } else { 607 None 608 }; 609 610 Ok(MemoryConfig { 611 size, 612 mergeable, 613 hotplug_method, 614 hotplug_size, 615 hotplugged_size, 616 shared, 617 hugepages, 618 hugepage_size, 619 zones, 620 }) 621 } 622 623 pub fn total_size(&self) -> u64 { 624 let mut size = self.size; 625 if let Some(hotplugged_size) = self.hotplugged_size { 626 size += hotplugged_size; 627 } 628 629 if let Some(zones) = &self.zones { 630 for zone in zones.iter() { 631 size += zone.size; 632 if let Some(hotplugged_size) = zone.hotplugged_size { 633 size += hotplugged_size; 634 } 635 } 636 } 637 638 size 639 } 640 } 641 642 impl Default for MemoryConfig { 643 fn default() -> Self { 644 MemoryConfig { 645 size: DEFAULT_MEMORY_MB << 20, 646 mergeable: false, 647 hotplug_method: HotplugMethod::Acpi, 648 hotplug_size: None, 649 hotplugged_size: None, 650 shared: false, 651 hugepages: false, 652 hugepage_size: None, 653 zones: None, 654 } 655 } 656 } 657 658 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 659 pub struct KernelConfig { 660 pub path: PathBuf, 661 } 662 663 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 664 pub struct InitramfsConfig { 665 pub path: PathBuf, 666 } 667 668 #[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] 669 pub struct CmdlineConfig { 670 pub args: String, 671 } 672 673 impl CmdlineConfig { 674 pub fn parse(cmdline: Option<&str>) -> Result<Self> { 675 let args = cmdline 676 .map(std::string::ToString::to_string) 677 .unwrap_or_else(String::new); 678 679 Ok(CmdlineConfig { args }) 680 } 681 } 682 683 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 684 pub struct DiskConfig { 685 pub path: Option<PathBuf>, 686 #[serde(default)] 687 pub readonly: bool, 688 #[serde(default)] 689 pub direct: bool, 690 #[serde(default)] 691 pub iommu: bool, 692 #[serde(default = "default_diskconfig_num_queues")] 693 pub num_queues: usize, 694 #[serde(default = "default_diskconfig_queue_size")] 695 pub queue_size: u16, 696 #[serde(default)] 697 pub vhost_user: bool, 698 pub vhost_socket: Option<String>, 699 #[serde(default = "default_diskconfig_poll_queue")] 700 pub poll_queue: bool, 701 #[serde(default)] 702 pub rate_limiter_config: Option<RateLimiterConfig>, 703 #[serde(default)] 704 pub id: Option<String>, 705 // For testing use only. Not exposed in API. 706 #[serde(default)] 707 pub disable_io_uring: bool, 708 } 709 710 fn default_diskconfig_num_queues() -> usize { 711 DEFAULT_NUM_QUEUES_VUBLK 712 } 713 714 fn default_diskconfig_queue_size() -> u16 { 715 DEFAULT_QUEUE_SIZE_VUBLK 716 } 717 718 fn default_diskconfig_poll_queue() -> bool { 719 true 720 } 721 722 impl Default for DiskConfig { 723 fn default() -> Self { 724 Self { 725 path: None, 726 readonly: false, 727 direct: false, 728 iommu: false, 729 num_queues: default_diskconfig_num_queues(), 730 queue_size: default_diskconfig_queue_size(), 731 vhost_user: false, 732 vhost_socket: None, 733 poll_queue: default_diskconfig_poll_queue(), 734 id: None, 735 disable_io_uring: false, 736 rate_limiter_config: None, 737 } 738 } 739 } 740 741 impl DiskConfig { 742 pub const SYNTAX: &'static str = "Disk parameters \ 743 \"path=<disk_image_path>,readonly=on|off,direct=on|off,iommu=on|off,\ 744 num_queues=<number_of_queues>,queue_size=<size_of_each_queue>,\ 745 vhost_user=on|off,socket=<vhost_user_socket_path>,poll_queue=on|off,\ 746 bw_size=<bytes>,bw_one_time_burst=<bytes>,bw_refill_time=<ms>,\ 747 ops_size=<io_ops>,ops_one_time_burst=<io_ops>,ops_refill_time=<ms>,\ 748 id=<device_id>\""; 749 750 pub fn parse(disk: &str) -> Result<Self> { 751 let mut parser = OptionParser::new(); 752 parser 753 .add("path") 754 .add("readonly") 755 .add("direct") 756 .add("iommu") 757 .add("queue_size") 758 .add("num_queues") 759 .add("vhost_user") 760 .add("socket") 761 .add("poll_queue") 762 .add("bw_size") 763 .add("bw_one_time_burst") 764 .add("bw_refill_time") 765 .add("ops_size") 766 .add("ops_one_time_burst") 767 .add("ops_refill_time") 768 .add("id") 769 .add("_disable_io_uring"); 770 parser.parse(disk).map_err(Error::ParseDisk)?; 771 772 let path = parser.get("path").map(PathBuf::from); 773 let readonly = parser 774 .convert::<Toggle>("readonly") 775 .map_err(Error::ParseDisk)? 776 .unwrap_or(Toggle(false)) 777 .0; 778 let direct = parser 779 .convert::<Toggle>("direct") 780 .map_err(Error::ParseDisk)? 781 .unwrap_or(Toggle(false)) 782 .0; 783 let iommu = parser 784 .convert::<Toggle>("iommu") 785 .map_err(Error::ParseDisk)? 786 .unwrap_or(Toggle(false)) 787 .0; 788 let queue_size = parser 789 .convert("queue_size") 790 .map_err(Error::ParseDisk)? 791 .unwrap_or_else(default_diskconfig_queue_size); 792 let num_queues = parser 793 .convert("num_queues") 794 .map_err(Error::ParseDisk)? 795 .unwrap_or_else(default_diskconfig_num_queues); 796 let vhost_user = parser 797 .convert::<Toggle>("vhost_user") 798 .map_err(Error::ParseDisk)? 799 .unwrap_or(Toggle(false)) 800 .0; 801 let vhost_socket = parser.get("socket"); 802 let poll_queue = parser 803 .convert::<Toggle>("poll_queue") 804 .map_err(Error::ParseDisk)? 805 .unwrap_or_else(|| Toggle(default_diskconfig_poll_queue())) 806 .0; 807 let id = parser.get("id"); 808 let disable_io_uring = parser 809 .convert::<Toggle>("_disable_io_uring") 810 .map_err(Error::ParseDisk)? 811 .unwrap_or(Toggle(false)) 812 .0; 813 let bw_size = parser 814 .convert("bw_size") 815 .map_err(Error::ParseDisk)? 816 .unwrap_or_default(); 817 let bw_one_time_burst = parser 818 .convert("bw_one_time_burst") 819 .map_err(Error::ParseDisk)? 820 .unwrap_or_default(); 821 let bw_refill_time = parser 822 .convert("bw_refill_time") 823 .map_err(Error::ParseDisk)? 824 .unwrap_or_default(); 825 let ops_size = parser 826 .convert("ops_size") 827 .map_err(Error::ParseDisk)? 828 .unwrap_or_default(); 829 let ops_one_time_burst = parser 830 .convert("ops_one_time_burst") 831 .map_err(Error::ParseDisk)? 832 .unwrap_or_default(); 833 let ops_refill_time = parser 834 .convert("ops_refill_time") 835 .map_err(Error::ParseDisk)? 836 .unwrap_or_default(); 837 let bw_tb_config = if bw_size != 0 && bw_refill_time != 0 { 838 Some(TokenBucketConfig { 839 size: bw_size, 840 one_time_burst: Some(bw_one_time_burst), 841 refill_time: bw_refill_time, 842 }) 843 } else { 844 None 845 }; 846 let ops_tb_config = if ops_size != 0 && ops_refill_time != 0 { 847 Some(TokenBucketConfig { 848 size: ops_size, 849 one_time_burst: Some(ops_one_time_burst), 850 refill_time: ops_refill_time, 851 }) 852 } else { 853 None 854 }; 855 let rate_limiter_config = if bw_tb_config.is_some() || ops_tb_config.is_some() { 856 Some(RateLimiterConfig { 857 bandwidth: bw_tb_config, 858 ops: ops_tb_config, 859 }) 860 } else { 861 None 862 }; 863 864 if parser.is_set("poll_queue") && !vhost_user { 865 warn!("poll_queue parameter currently only has effect when used vhost_user=true"); 866 } 867 868 Ok(DiskConfig { 869 path, 870 readonly, 871 direct, 872 iommu, 873 num_queues, 874 queue_size, 875 vhost_user, 876 vhost_socket, 877 poll_queue, 878 rate_limiter_config, 879 id, 880 disable_io_uring, 881 }) 882 } 883 884 pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> { 885 if self.num_queues > vm_config.cpus.boot_vcpus as usize { 886 return Err(ValidationError::TooManyQueues); 887 } 888 889 Ok(()) 890 } 891 } 892 893 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 894 pub enum VhostMode { 895 Client, 896 Server, 897 } 898 899 impl Default for VhostMode { 900 fn default() -> Self { 901 VhostMode::Client 902 } 903 } 904 905 #[derive(Debug)] 906 pub enum ParseVhostModeError { 907 InvalidValue(String), 908 } 909 910 impl FromStr for VhostMode { 911 type Err = ParseVhostModeError; 912 913 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { 914 match s.to_lowercase().as_str() { 915 "client" => Ok(VhostMode::Client), 916 "server" => Ok(VhostMode::Server), 917 _ => Err(ParseVhostModeError::InvalidValue(s.to_owned())), 918 } 919 } 920 } 921 922 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 923 pub struct NetConfig { 924 #[serde(default = "default_netconfig_tap")] 925 pub tap: Option<String>, 926 #[serde(default = "default_netconfig_ip")] 927 pub ip: Ipv4Addr, 928 #[serde(default = "default_netconfig_mask")] 929 pub mask: Ipv4Addr, 930 #[serde(default = "default_netconfig_mac")] 931 pub mac: MacAddr, 932 #[serde(default)] 933 pub host_mac: Option<MacAddr>, 934 #[serde(default)] 935 pub iommu: bool, 936 #[serde(default = "default_netconfig_num_queues")] 937 pub num_queues: usize, 938 #[serde(default = "default_netconfig_queue_size")] 939 pub queue_size: u16, 940 #[serde(default)] 941 pub vhost_user: bool, 942 pub vhost_socket: Option<String>, 943 #[serde(default)] 944 pub vhost_mode: VhostMode, 945 #[serde(default)] 946 pub id: Option<String>, 947 #[serde(default)] 948 pub fds: Option<Vec<i32>>, 949 #[serde(default)] 950 pub rate_limiter_config: Option<RateLimiterConfig>, 951 } 952 953 fn default_netconfig_tap() -> Option<String> { 954 None 955 } 956 957 fn default_netconfig_ip() -> Ipv4Addr { 958 Ipv4Addr::new(192, 168, 249, 1) 959 } 960 961 fn default_netconfig_mask() -> Ipv4Addr { 962 Ipv4Addr::new(255, 255, 255, 0) 963 } 964 965 fn default_netconfig_mac() -> MacAddr { 966 MacAddr::local_random() 967 } 968 969 fn default_netconfig_num_queues() -> usize { 970 DEFAULT_NUM_QUEUES_VUNET 971 } 972 973 fn default_netconfig_queue_size() -> u16 { 974 DEFAULT_QUEUE_SIZE_VUNET 975 } 976 977 impl Default for NetConfig { 978 fn default() -> Self { 979 Self { 980 tap: default_netconfig_tap(), 981 ip: default_netconfig_ip(), 982 mask: default_netconfig_mask(), 983 mac: default_netconfig_mac(), 984 host_mac: None, 985 iommu: false, 986 num_queues: default_netconfig_num_queues(), 987 queue_size: default_netconfig_queue_size(), 988 vhost_user: false, 989 vhost_socket: None, 990 vhost_mode: VhostMode::Client, 991 id: None, 992 fds: None, 993 rate_limiter_config: None, 994 } 995 } 996 } 997 998 impl NetConfig { 999 pub const SYNTAX: &'static str = "Network parameters \ 1000 \"tap=<if_name>,ip=<ip_addr>,mask=<net_mask>,mac=<mac_addr>,fd=<fd1:fd2...>,iommu=on|off,\ 1001 num_queues=<number_of_queues>,queue_size=<size_of_each_queue>,id=<device_id>,\ 1002 vhost_user=<vhost_user_enable>,socket=<vhost_user_socket_path>,vhost_mode=client|server,\ 1003 bw_size=<bytes>,bw_one_time_burst=<bytes>,bw_refill_time=<ms>,\ 1004 ops_size=<io_ops>,ops_one_time_burst=<io_ops>,ops_refill_time=<ms>\""; 1005 1006 pub fn parse(net: &str) -> Result<Self> { 1007 let mut parser = OptionParser::new(); 1008 1009 parser 1010 .add("tap") 1011 .add("ip") 1012 .add("mask") 1013 .add("mac") 1014 .add("host_mac") 1015 .add("iommu") 1016 .add("queue_size") 1017 .add("num_queues") 1018 .add("vhost_user") 1019 .add("socket") 1020 .add("vhost_mode") 1021 .add("id") 1022 .add("fd") 1023 .add("bw_size") 1024 .add("bw_one_time_burst") 1025 .add("bw_refill_time") 1026 .add("ops_size") 1027 .add("ops_one_time_burst") 1028 .add("ops_refill_time"); 1029 parser.parse(net).map_err(Error::ParseNetwork)?; 1030 1031 let tap = parser.get("tap"); 1032 let ip = parser 1033 .convert("ip") 1034 .map_err(Error::ParseNetwork)? 1035 .unwrap_or_else(default_netconfig_ip); 1036 let mask = parser 1037 .convert("mask") 1038 .map_err(Error::ParseNetwork)? 1039 .unwrap_or_else(default_netconfig_mask); 1040 let mac = parser 1041 .convert("mac") 1042 .map_err(Error::ParseNetwork)? 1043 .unwrap_or_else(default_netconfig_mac); 1044 let host_mac = parser.convert("host_mac").map_err(Error::ParseNetwork)?; 1045 let iommu = parser 1046 .convert::<Toggle>("iommu") 1047 .map_err(Error::ParseNetwork)? 1048 .unwrap_or(Toggle(false)) 1049 .0; 1050 let queue_size = parser 1051 .convert("queue_size") 1052 .map_err(Error::ParseNetwork)? 1053 .unwrap_or_else(default_netconfig_queue_size); 1054 let num_queues = parser 1055 .convert("num_queues") 1056 .map_err(Error::ParseNetwork)? 1057 .unwrap_or_else(default_netconfig_num_queues); 1058 let vhost_user = parser 1059 .convert::<Toggle>("vhost_user") 1060 .map_err(Error::ParseNetwork)? 1061 .unwrap_or(Toggle(false)) 1062 .0; 1063 let vhost_socket = parser.get("socket"); 1064 let vhost_mode = parser 1065 .convert("vhost_mode") 1066 .map_err(Error::ParseNetwork)? 1067 .unwrap_or_default(); 1068 let id = parser.get("id"); 1069 let fds = parser 1070 .convert::<IntegerList>("fd") 1071 .map_err(Error::ParseNetwork)? 1072 .map(|v| v.0.iter().map(|e| *e as i32).collect()); 1073 1074 let bw_size = parser 1075 .convert("bw_size") 1076 .map_err(Error::ParseDisk)? 1077 .unwrap_or_default(); 1078 let bw_one_time_burst = parser 1079 .convert("bw_one_time_burst") 1080 .map_err(Error::ParseDisk)? 1081 .unwrap_or_default(); 1082 let bw_refill_time = parser 1083 .convert("bw_refill_time") 1084 .map_err(Error::ParseDisk)? 1085 .unwrap_or_default(); 1086 let ops_size = parser 1087 .convert("ops_size") 1088 .map_err(Error::ParseDisk)? 1089 .unwrap_or_default(); 1090 let ops_one_time_burst = parser 1091 .convert("ops_one_time_burst") 1092 .map_err(Error::ParseDisk)? 1093 .unwrap_or_default(); 1094 let ops_refill_time = parser 1095 .convert("ops_refill_time") 1096 .map_err(Error::ParseDisk)? 1097 .unwrap_or_default(); 1098 let bw_tb_config = if bw_size != 0 && bw_refill_time != 0 { 1099 Some(TokenBucketConfig { 1100 size: bw_size, 1101 one_time_burst: Some(bw_one_time_burst), 1102 refill_time: bw_refill_time, 1103 }) 1104 } else { 1105 None 1106 }; 1107 let ops_tb_config = if ops_size != 0 && ops_refill_time != 0 { 1108 Some(TokenBucketConfig { 1109 size: ops_size, 1110 one_time_burst: Some(ops_one_time_burst), 1111 refill_time: ops_refill_time, 1112 }) 1113 } else { 1114 None 1115 }; 1116 let rate_limiter_config = if bw_tb_config.is_some() || ops_tb_config.is_some() { 1117 Some(RateLimiterConfig { 1118 bandwidth: bw_tb_config, 1119 ops: ops_tb_config, 1120 }) 1121 } else { 1122 None 1123 }; 1124 1125 let config = NetConfig { 1126 tap, 1127 ip, 1128 mask, 1129 mac, 1130 host_mac, 1131 iommu, 1132 num_queues, 1133 queue_size, 1134 vhost_user, 1135 vhost_socket, 1136 vhost_mode, 1137 id, 1138 fds, 1139 rate_limiter_config, 1140 }; 1141 Ok(config) 1142 } 1143 1144 pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> { 1145 if self.num_queues < 2 { 1146 return Err(ValidationError::VnetQueueLowerThan2); 1147 } 1148 1149 if self.fds.is_some() && self.fds.as_ref().unwrap().len() * 2 != self.num_queues { 1150 return Err(ValidationError::VnetQueueFdMismatch); 1151 } 1152 1153 if let Some(fds) = self.fds.as_ref() { 1154 for fd in fds { 1155 if *fd <= 2 { 1156 return Err(ValidationError::VnetReservedFd); 1157 } 1158 } 1159 } 1160 1161 if (self.num_queues / 2) > vm_config.cpus.boot_vcpus as usize { 1162 return Err(ValidationError::TooManyQueues); 1163 } 1164 1165 Ok(()) 1166 } 1167 } 1168 1169 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 1170 pub struct RngConfig { 1171 pub src: PathBuf, 1172 #[serde(default)] 1173 pub iommu: bool, 1174 } 1175 1176 impl RngConfig { 1177 pub fn parse(rng: &str) -> Result<Self> { 1178 let mut parser = OptionParser::new(); 1179 parser.add("src").add("iommu"); 1180 parser.parse(rng).map_err(Error::ParseRng)?; 1181 1182 let src = PathBuf::from( 1183 parser 1184 .get("src") 1185 .unwrap_or_else(|| DEFAULT_RNG_SOURCE.to_owned()), 1186 ); 1187 let iommu = parser 1188 .convert::<Toggle>("iommu") 1189 .map_err(Error::ParseRng)? 1190 .unwrap_or(Toggle(false)) 1191 .0; 1192 1193 Ok(RngConfig { src, iommu }) 1194 } 1195 } 1196 1197 impl Default for RngConfig { 1198 fn default() -> Self { 1199 RngConfig { 1200 src: PathBuf::from(DEFAULT_RNG_SOURCE), 1201 iommu: false, 1202 } 1203 } 1204 } 1205 1206 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 1207 pub struct BalloonConfig { 1208 pub size: u64, 1209 /// Option to deflate the balloon in case the guest is out of memory. 1210 #[serde(default)] 1211 pub deflate_on_oom: bool, 1212 } 1213 1214 impl BalloonConfig { 1215 pub const SYNTAX: &'static str = 1216 "Balloon parameters \"size=<balloon_size>,deflate_on_oom=on|off\""; 1217 1218 pub fn parse(balloon: &str) -> Result<Self> { 1219 let mut parser = OptionParser::new(); 1220 parser.add("size"); 1221 parser.add("deflate_on_oom"); 1222 parser.parse(balloon).map_err(Error::ParseBalloon)?; 1223 1224 let size = parser 1225 .convert::<ByteSized>("size") 1226 .map_err(Error::ParseBalloon)? 1227 .map(|v| v.0) 1228 .unwrap_or(0); 1229 1230 let deflate_on_oom = parser 1231 .convert::<Toggle>("deflate_on_oom") 1232 .map_err(Error::ParseBalloon)? 1233 .unwrap_or(Toggle(false)) 1234 .0; 1235 1236 Ok(BalloonConfig { 1237 size, 1238 deflate_on_oom, 1239 }) 1240 } 1241 } 1242 1243 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 1244 pub struct FsConfig { 1245 pub tag: String, 1246 pub socket: PathBuf, 1247 #[serde(default = "default_fsconfig_num_queues")] 1248 pub num_queues: usize, 1249 #[serde(default = "default_fsconfig_queue_size")] 1250 pub queue_size: u16, 1251 #[serde(default = "default_fsconfig_dax")] 1252 pub dax: bool, 1253 #[serde(default = "default_fsconfig_cache_size")] 1254 pub cache_size: u64, 1255 #[serde(default)] 1256 pub id: Option<String>, 1257 } 1258 1259 fn default_fsconfig_num_queues() -> usize { 1260 1 1261 } 1262 1263 fn default_fsconfig_queue_size() -> u16 { 1264 1024 1265 } 1266 1267 fn default_fsconfig_dax() -> bool { 1268 true 1269 } 1270 1271 fn default_fsconfig_cache_size() -> u64 { 1272 0x0002_0000_0000 1273 } 1274 1275 impl Default for FsConfig { 1276 fn default() -> Self { 1277 Self { 1278 tag: "".to_owned(), 1279 socket: PathBuf::new(), 1280 num_queues: default_fsconfig_num_queues(), 1281 queue_size: default_fsconfig_queue_size(), 1282 dax: default_fsconfig_dax(), 1283 cache_size: default_fsconfig_cache_size(), 1284 id: None, 1285 } 1286 } 1287 } 1288 1289 impl FsConfig { 1290 pub const SYNTAX: &'static str = "virtio-fs parameters \ 1291 \"tag=<tag_name>,socket=<socket_path>,num_queues=<number_of_queues>,\ 1292 queue_size=<size_of_each_queue>,dax=on|off,cache_size=<DAX cache size: \ 1293 default 8Gib>,id=<device_id>\""; 1294 1295 pub fn parse(fs: &str) -> Result<Self> { 1296 let mut parser = OptionParser::new(); 1297 parser 1298 .add("tag") 1299 .add("dax") 1300 .add("cache_size") 1301 .add("queue_size") 1302 .add("num_queues") 1303 .add("socket") 1304 .add("id"); 1305 parser.parse(fs).map_err(Error::ParseFileSystem)?; 1306 1307 let tag = parser.get("tag").ok_or(Error::ParseFsTagMissing)?; 1308 let socket = PathBuf::from(parser.get("socket").ok_or(Error::ParseFsSockMissing)?); 1309 1310 let queue_size = parser 1311 .convert("queue_size") 1312 .map_err(Error::ParseFileSystem)? 1313 .unwrap_or_else(default_fsconfig_queue_size); 1314 let num_queues = parser 1315 .convert("num_queues") 1316 .map_err(Error::ParseFileSystem)? 1317 .unwrap_or_else(default_fsconfig_num_queues); 1318 1319 let dax = parser 1320 .convert::<Toggle>("dax") 1321 .map_err(Error::ParseFileSystem)? 1322 .unwrap_or_else(|| Toggle(default_fsconfig_dax())) 1323 .0; 1324 1325 if parser.is_set("cache_size") && !dax { 1326 return Err(Error::InvalidCacheSizeWithDaxOff); 1327 } 1328 1329 let cache_size = parser 1330 .convert::<ByteSized>("cache_size") 1331 .map_err(Error::ParseFileSystem)? 1332 .unwrap_or_else(|| ByteSized(default_fsconfig_cache_size())) 1333 .0; 1334 1335 let id = parser.get("id"); 1336 1337 Ok(FsConfig { 1338 tag, 1339 socket, 1340 num_queues, 1341 queue_size, 1342 dax, 1343 cache_size, 1344 id, 1345 }) 1346 } 1347 1348 pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> { 1349 if self.num_queues > vm_config.cpus.boot_vcpus as usize { 1350 return Err(ValidationError::TooManyQueues); 1351 } 1352 1353 Ok(()) 1354 } 1355 } 1356 1357 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default)] 1358 pub struct PmemConfig { 1359 pub file: PathBuf, 1360 #[serde(default)] 1361 pub size: Option<u64>, 1362 #[serde(default)] 1363 pub iommu: bool, 1364 #[serde(default)] 1365 pub mergeable: bool, 1366 #[serde(default)] 1367 pub discard_writes: bool, 1368 #[serde(default)] 1369 pub id: Option<String>, 1370 } 1371 1372 impl PmemConfig { 1373 pub const SYNTAX: &'static str = "Persistent memory parameters \ 1374 \"file=<backing_file_path>,size=<persistent_memory_size>,iommu=on|off,\ 1375 mergeable=on|off,discard_writes=on|off,id=<device_id>\""; 1376 pub fn parse(pmem: &str) -> Result<Self> { 1377 let mut parser = OptionParser::new(); 1378 parser 1379 .add("size") 1380 .add("file") 1381 .add("mergeable") 1382 .add("iommu") 1383 .add("discard_writes") 1384 .add("id"); 1385 parser.parse(pmem).map_err(Error::ParsePersistentMemory)?; 1386 1387 let file = PathBuf::from(parser.get("file").ok_or(Error::ParsePmemFileMissing)?); 1388 let size = parser 1389 .convert::<ByteSized>("size") 1390 .map_err(Error::ParsePersistentMemory)? 1391 .map(|v| v.0); 1392 let mergeable = parser 1393 .convert::<Toggle>("mergeable") 1394 .map_err(Error::ParsePersistentMemory)? 1395 .unwrap_or(Toggle(false)) 1396 .0; 1397 let iommu = parser 1398 .convert::<Toggle>("iommu") 1399 .map_err(Error::ParsePersistentMemory)? 1400 .unwrap_or(Toggle(false)) 1401 .0; 1402 let discard_writes = parser 1403 .convert::<Toggle>("discard_writes") 1404 .map_err(Error::ParsePersistentMemory)? 1405 .unwrap_or(Toggle(false)) 1406 .0; 1407 let id = parser.get("id"); 1408 1409 Ok(PmemConfig { 1410 file, 1411 size, 1412 iommu, 1413 mergeable, 1414 discard_writes, 1415 id, 1416 }) 1417 } 1418 } 1419 1420 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 1421 pub enum ConsoleOutputMode { 1422 Off, 1423 Pty, 1424 Tty, 1425 File, 1426 Null, 1427 } 1428 1429 impl ConsoleOutputMode { 1430 pub fn input_enabled(&self) -> bool { 1431 matches!(self, ConsoleOutputMode::Tty | ConsoleOutputMode::Pty) 1432 } 1433 } 1434 1435 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 1436 pub struct ConsoleConfig { 1437 #[serde(default = "default_consoleconfig_file")] 1438 pub file: Option<PathBuf>, 1439 pub mode: ConsoleOutputMode, 1440 #[serde(default)] 1441 pub iommu: bool, 1442 } 1443 1444 fn default_consoleconfig_file() -> Option<PathBuf> { 1445 None 1446 } 1447 1448 impl ConsoleConfig { 1449 pub fn parse(console: &str) -> Result<Self> { 1450 let mut parser = OptionParser::new(); 1451 parser 1452 .add_valueless("off") 1453 .add_valueless("pty") 1454 .add_valueless("tty") 1455 .add_valueless("null") 1456 .add("file") 1457 .add("iommu"); 1458 parser.parse(console).map_err(Error::ParseConsole)?; 1459 1460 let mut file: Option<PathBuf> = default_consoleconfig_file(); 1461 let mut mode: ConsoleOutputMode = ConsoleOutputMode::Off; 1462 1463 if parser.is_set("off") { 1464 } else if parser.is_set("pty") { 1465 mode = ConsoleOutputMode::Pty 1466 } else if parser.is_set("tty") { 1467 mode = ConsoleOutputMode::Tty 1468 } else if parser.is_set("null") { 1469 mode = ConsoleOutputMode::Null 1470 } else if parser.is_set("file") { 1471 mode = ConsoleOutputMode::File; 1472 file = 1473 Some(PathBuf::from(parser.get("file").ok_or( 1474 Error::Validation(ValidationError::ConsoleFileMissing), 1475 )?)); 1476 } else { 1477 return Err(Error::ParseConsoleInvalidModeGiven); 1478 } 1479 let iommu = parser 1480 .convert::<Toggle>("iommu") 1481 .map_err(Error::ParseConsole)? 1482 .unwrap_or(Toggle(false)) 1483 .0; 1484 1485 Ok(Self { file, mode, iommu }) 1486 } 1487 1488 pub fn default_serial() -> Self { 1489 ConsoleConfig { 1490 file: None, 1491 mode: ConsoleOutputMode::Null, 1492 iommu: false, 1493 } 1494 } 1495 1496 pub fn default_console() -> Self { 1497 ConsoleConfig { 1498 file: None, 1499 mode: ConsoleOutputMode::Tty, 1500 iommu: false, 1501 } 1502 } 1503 } 1504 1505 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default)] 1506 pub struct DeviceConfig { 1507 pub path: PathBuf, 1508 #[serde(default)] 1509 pub iommu: bool, 1510 #[serde(default)] 1511 pub id: Option<String>, 1512 } 1513 1514 impl DeviceConfig { 1515 pub const SYNTAX: &'static str = 1516 "Direct device assignment parameters \"path=<device_path>,iommu=on|off,id=<device_id>\""; 1517 pub fn parse(device: &str) -> Result<Self> { 1518 let mut parser = OptionParser::new(); 1519 parser.add("path").add("id").add("iommu"); 1520 parser.parse(device).map_err(Error::ParseDevice)?; 1521 1522 let path = parser 1523 .get("path") 1524 .map(PathBuf::from) 1525 .ok_or(Error::ParseDevicePathMissing)?; 1526 let iommu = parser 1527 .convert::<Toggle>("iommu") 1528 .map_err(Error::ParseDevice)? 1529 .unwrap_or(Toggle(false)) 1530 .0; 1531 let id = parser.get("id"); 1532 Ok(DeviceConfig { path, iommu, id }) 1533 } 1534 } 1535 1536 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default)] 1537 pub struct VsockConfig { 1538 pub cid: u64, 1539 pub socket: PathBuf, 1540 #[serde(default)] 1541 pub iommu: bool, 1542 #[serde(default)] 1543 pub id: Option<String>, 1544 } 1545 1546 impl VsockConfig { 1547 pub const SYNTAX: &'static str = "Virtio VSOCK parameters \ 1548 \"cid=<context_id>,socket=<socket_path>,iommu=on|off,id=<device_id>\""; 1549 pub fn parse(vsock: &str) -> Result<Self> { 1550 let mut parser = OptionParser::new(); 1551 parser.add("socket").add("cid").add("iommu").add("id"); 1552 parser.parse(vsock).map_err(Error::ParseVsock)?; 1553 1554 let socket = parser 1555 .get("socket") 1556 .map(PathBuf::from) 1557 .ok_or(Error::ParseVsockSockMissing)?; 1558 let iommu = parser 1559 .convert::<Toggle>("iommu") 1560 .map_err(Error::ParseVsock)? 1561 .unwrap_or(Toggle(false)) 1562 .0; 1563 let cid = parser 1564 .convert("cid") 1565 .map_err(Error::ParseVsock)? 1566 .ok_or(Error::ParseVsockCidMissing)?; 1567 let id = parser.get("id"); 1568 1569 Ok(VsockConfig { 1570 cid, 1571 socket, 1572 iommu, 1573 id, 1574 }) 1575 } 1576 } 1577 1578 #[cfg(feature = "tdx")] 1579 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default)] 1580 pub struct TdxConfig { 1581 pub firmware: PathBuf, 1582 } 1583 1584 #[cfg(feature = "tdx")] 1585 impl TdxConfig { 1586 pub fn parse(tdx: &str) -> Result<Self> { 1587 let mut parser = OptionParser::new(); 1588 parser.add("firmware"); 1589 parser.parse(tdx).map_err(Error::ParseTdx)?; 1590 let firmware = parser 1591 .get("firmware") 1592 .map(PathBuf::from) 1593 .ok_or(Error::FirmwarePathMissing)?; 1594 Ok(TdxConfig { firmware }) 1595 } 1596 } 1597 1598 #[cfg(target_arch = "x86_64")] 1599 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default)] 1600 pub struct SgxEpcConfig { 1601 pub id: String, 1602 #[serde(default)] 1603 pub size: u64, 1604 #[serde(default)] 1605 pub prefault: bool, 1606 } 1607 1608 #[cfg(target_arch = "x86_64")] 1609 impl SgxEpcConfig { 1610 pub const SYNTAX: &'static str = "SGX EPC parameters \ 1611 \"id=<epc_section_identifier>,size=<epc_section_size>,prefault=on|off\""; 1612 pub fn parse(sgx_epc: &str) -> Result<Self> { 1613 let mut parser = OptionParser::new(); 1614 parser.add("id").add("size").add("prefault"); 1615 parser.parse(sgx_epc).map_err(Error::ParseSgxEpc)?; 1616 1617 let id = parser.get("id").ok_or(Error::ParseSgxEpcIdMissing)?; 1618 let size = parser 1619 .convert::<ByteSized>("size") 1620 .map_err(Error::ParseSgxEpc)? 1621 .unwrap_or(ByteSized(0)) 1622 .0; 1623 let prefault = parser 1624 .convert::<Toggle>("prefault") 1625 .map_err(Error::ParseSgxEpc)? 1626 .unwrap_or(Toggle(false)) 1627 .0; 1628 1629 Ok(SgxEpcConfig { id, size, prefault }) 1630 } 1631 } 1632 1633 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default)] 1634 pub struct NumaDistance { 1635 #[serde(default)] 1636 pub destination: u32, 1637 #[serde(default)] 1638 pub distance: u8, 1639 } 1640 1641 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default)] 1642 pub struct NumaConfig { 1643 #[serde(default)] 1644 pub guest_numa_id: u32, 1645 #[serde(default)] 1646 pub cpus: Option<Vec<u8>>, 1647 #[serde(default)] 1648 pub distances: Option<Vec<NumaDistance>>, 1649 #[serde(default)] 1650 pub memory_zones: Option<Vec<String>>, 1651 #[cfg(target_arch = "x86_64")] 1652 #[serde(default)] 1653 pub sgx_epc_sections: Option<Vec<String>>, 1654 } 1655 1656 impl NumaConfig { 1657 pub const SYNTAX: &'static str = "Settings related to a given NUMA node \ 1658 \"guest_numa_id=<node_id>,cpus=<cpus_id>,distances=<list_of_distances_to_destination_nodes>,\ 1659 memory_zones=<list_of_memory_zones>,sgx_epc_sections=<list_of_sgx_epc_sections>\""; 1660 pub fn parse(numa: &str) -> Result<Self> { 1661 let mut parser = OptionParser::new(); 1662 parser 1663 .add("guest_numa_id") 1664 .add("cpus") 1665 .add("distances") 1666 .add("memory_zones") 1667 .add("sgx_epc_sections"); 1668 parser.parse(numa).map_err(Error::ParseNuma)?; 1669 1670 let guest_numa_id = parser 1671 .convert::<u32>("guest_numa_id") 1672 .map_err(Error::ParseNuma)? 1673 .unwrap_or(0); 1674 let cpus = parser 1675 .convert::<IntegerList>("cpus") 1676 .map_err(Error::ParseNuma)? 1677 .map(|v| v.0.iter().map(|e| *e as u8).collect()); 1678 let distances = parser 1679 .convert::<TupleTwoIntegers>("distances") 1680 .map_err(Error::ParseNuma)? 1681 .map(|v| { 1682 v.0.iter() 1683 .map(|(e1, e2)| NumaDistance { 1684 destination: *e1 as u32, 1685 distance: *e2 as u8, 1686 }) 1687 .collect() 1688 }); 1689 let memory_zones = parser 1690 .convert::<StringList>("memory_zones") 1691 .map_err(Error::ParseNuma)? 1692 .map(|v| v.0); 1693 #[cfg(target_arch = "x86_64")] 1694 let sgx_epc_sections = parser 1695 .convert::<StringList>("sgx_epc_sections") 1696 .map_err(Error::ParseNuma)? 1697 .map(|v| v.0); 1698 1699 Ok(NumaConfig { 1700 guest_numa_id, 1701 cpus, 1702 distances, 1703 memory_zones, 1704 #[cfg(target_arch = "x86_64")] 1705 sgx_epc_sections, 1706 }) 1707 } 1708 } 1709 1710 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default)] 1711 pub struct RestoreConfig { 1712 pub source_url: PathBuf, 1713 #[serde(default)] 1714 pub prefault: bool, 1715 } 1716 1717 impl RestoreConfig { 1718 pub const SYNTAX: &'static str = "Restore from a VM snapshot. \ 1719 \nRestore parameters \"source_url=<source_url>,prefault=on|off\" \ 1720 \n`source_url` should be a valid URL (e.g file:///foo/bar or tcp://192.168.1.10/foo) \ 1721 \n`prefault` brings memory pages in when enabled (disabled by default)"; 1722 pub fn parse(restore: &str) -> Result<Self> { 1723 let mut parser = OptionParser::new(); 1724 parser.add("source_url").add("prefault"); 1725 parser.parse(restore).map_err(Error::ParseRestore)?; 1726 1727 let source_url = parser 1728 .get("source_url") 1729 .map(PathBuf::from) 1730 .ok_or(Error::ParseRestoreSourceUrlMissing)?; 1731 let prefault = parser 1732 .convert::<Toggle>("prefault") 1733 .map_err(Error::ParseRestore)? 1734 .unwrap_or(Toggle(false)) 1735 .0; 1736 1737 Ok(RestoreConfig { 1738 source_url, 1739 prefault, 1740 }) 1741 } 1742 } 1743 1744 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 1745 pub struct VmConfig { 1746 #[serde(default)] 1747 pub cpus: CpusConfig, 1748 #[serde(default)] 1749 pub memory: MemoryConfig, 1750 pub kernel: Option<KernelConfig>, 1751 #[serde(default)] 1752 pub initramfs: Option<InitramfsConfig>, 1753 #[serde(default)] 1754 pub cmdline: CmdlineConfig, 1755 pub disks: Option<Vec<DiskConfig>>, 1756 pub net: Option<Vec<NetConfig>>, 1757 #[serde(default)] 1758 pub rng: RngConfig, 1759 pub balloon: Option<BalloonConfig>, 1760 pub fs: Option<Vec<FsConfig>>, 1761 pub pmem: Option<Vec<PmemConfig>>, 1762 #[serde(default = "ConsoleConfig::default_serial")] 1763 pub serial: ConsoleConfig, 1764 #[serde(default = "ConsoleConfig::default_console")] 1765 pub console: ConsoleConfig, 1766 pub devices: Option<Vec<DeviceConfig>>, 1767 pub vsock: Option<VsockConfig>, 1768 #[serde(default)] 1769 pub iommu: bool, 1770 #[cfg(target_arch = "x86_64")] 1771 pub sgx_epc: Option<Vec<SgxEpcConfig>>, 1772 pub numa: Option<Vec<NumaConfig>>, 1773 #[serde(default)] 1774 pub watchdog: bool, 1775 #[cfg(feature = "tdx")] 1776 pub tdx: Option<TdxConfig>, 1777 } 1778 1779 impl VmConfig { 1780 pub fn validate(&self) -> ValidationResult<()> { 1781 #[cfg(not(feature = "tdx"))] 1782 self.kernel.as_ref().ok_or(ValidationError::KernelMissing)?; 1783 1784 #[cfg(feature = "tdx")] 1785 { 1786 let tdx_enabled = self.tdx.is_some(); 1787 if !tdx_enabled && self.kernel.is_none() { 1788 return Err(ValidationError::KernelMissing); 1789 } 1790 if tdx_enabled && (self.cpus.max_vcpus != self.cpus.boot_vcpus) { 1791 return Err(ValidationError::TdxNoCpuHotplug); 1792 } 1793 if tdx_enabled && self.kernel.is_some() { 1794 return Err(ValidationError::TdxKernelSpecified); 1795 } 1796 } 1797 1798 if self.console.mode == ConsoleOutputMode::Tty && self.serial.mode == ConsoleOutputMode::Tty 1799 { 1800 return Err(ValidationError::DoubleTtyMode); 1801 } 1802 1803 if self.console.mode == ConsoleOutputMode::File && self.console.file.is_none() { 1804 return Err(ValidationError::ConsoleFileMissing); 1805 } 1806 1807 if self.serial.mode == ConsoleOutputMode::File && self.serial.file.is_none() { 1808 return Err(ValidationError::ConsoleFileMissing); 1809 } 1810 1811 if self.cpus.max_vcpus < self.cpus.boot_vcpus { 1812 return Err(ValidationError::CpusMaxLowerThanBoot); 1813 } 1814 1815 if let Some(disks) = &self.disks { 1816 for disk in disks { 1817 if disk.vhost_socket.as_ref().and(disk.path.as_ref()).is_some() { 1818 return Err(ValidationError::DiskSocketAndPath); 1819 } 1820 if disk.vhost_user && !self.memory.shared { 1821 return Err(ValidationError::VhostUserRequiresSharedMemory); 1822 } 1823 if disk.vhost_user && disk.vhost_socket.is_none() { 1824 return Err(ValidationError::VhostUserMissingSocket); 1825 } 1826 disk.validate(self)?; 1827 } 1828 } 1829 1830 if let Some(nets) = &self.net { 1831 for net in nets { 1832 if net.vhost_user && !self.memory.shared { 1833 return Err(ValidationError::VhostUserRequiresSharedMemory); 1834 } 1835 net.validate(self)?; 1836 } 1837 } 1838 1839 if let Some(fses) = &self.fs { 1840 if !fses.is_empty() && !self.memory.shared { 1841 return Err(ValidationError::VhostUserRequiresSharedMemory); 1842 } 1843 for fs in fses { 1844 fs.validate(self)?; 1845 } 1846 } 1847 1848 if let Some(t) = &self.cpus.topology { 1849 if t.threads_per_core == 0 1850 || t.cores_per_die == 0 1851 || t.dies_per_package == 0 1852 || t.packages == 0 1853 { 1854 return Err(ValidationError::CpuTopologyZeroPart); 1855 } 1856 1857 let total = t.threads_per_core * t.cores_per_die * t.dies_per_package * t.packages; 1858 if total != self.cpus.max_vcpus { 1859 return Err(ValidationError::CpuTopologyCount); 1860 } 1861 } 1862 1863 if let Some(hugepage_size) = &self.memory.hugepage_size { 1864 if !self.memory.hugepages { 1865 return Err(ValidationError::HugePageSizeWithoutHugePages); 1866 } 1867 if !hugepage_size.is_power_of_two() { 1868 return Err(ValidationError::InvalidHugePageSize(*hugepage_size)); 1869 } 1870 } 1871 1872 Ok(()) 1873 } 1874 1875 pub fn parse(vm_params: VmParams) -> Result<Self> { 1876 let mut iommu = false; 1877 1878 let mut disks: Option<Vec<DiskConfig>> = None; 1879 if let Some(disk_list) = &vm_params.disks { 1880 let mut disk_config_list = Vec::new(); 1881 for item in disk_list.iter() { 1882 let disk_config = DiskConfig::parse(item)?; 1883 if disk_config.iommu { 1884 iommu = true; 1885 } 1886 disk_config_list.push(disk_config); 1887 } 1888 disks = Some(disk_config_list); 1889 } 1890 1891 let mut net: Option<Vec<NetConfig>> = None; 1892 if let Some(net_list) = &vm_params.net { 1893 let mut net_config_list = Vec::new(); 1894 for item in net_list.iter() { 1895 let net_config = NetConfig::parse(item)?; 1896 if net_config.iommu { 1897 iommu = true; 1898 } 1899 net_config_list.push(net_config); 1900 } 1901 net = Some(net_config_list); 1902 } 1903 1904 let rng = RngConfig::parse(vm_params.rng)?; 1905 if rng.iommu { 1906 iommu = true; 1907 } 1908 1909 let mut balloon: Option<BalloonConfig> = None; 1910 if let Some(balloon_params) = &vm_params.balloon { 1911 balloon = Some(BalloonConfig::parse(balloon_params)?); 1912 } 1913 1914 let mut fs: Option<Vec<FsConfig>> = None; 1915 if let Some(fs_list) = &vm_params.fs { 1916 let mut fs_config_list = Vec::new(); 1917 for item in fs_list.iter() { 1918 fs_config_list.push(FsConfig::parse(item)?); 1919 } 1920 fs = Some(fs_config_list); 1921 } 1922 1923 let mut pmem: Option<Vec<PmemConfig>> = None; 1924 if let Some(pmem_list) = &vm_params.pmem { 1925 let mut pmem_config_list = Vec::new(); 1926 for item in pmem_list.iter() { 1927 let pmem_config = PmemConfig::parse(item)?; 1928 if pmem_config.iommu { 1929 iommu = true; 1930 } 1931 pmem_config_list.push(pmem_config); 1932 } 1933 pmem = Some(pmem_config_list); 1934 } 1935 1936 let console = ConsoleConfig::parse(vm_params.console)?; 1937 if console.iommu { 1938 iommu = true; 1939 } 1940 let serial = ConsoleConfig::parse(vm_params.serial)?; 1941 1942 let mut devices: Option<Vec<DeviceConfig>> = None; 1943 if let Some(device_list) = &vm_params.devices { 1944 let mut device_config_list = Vec::new(); 1945 for item in device_list.iter() { 1946 let device_config = DeviceConfig::parse(item)?; 1947 if device_config.iommu { 1948 iommu = true; 1949 } 1950 device_config_list.push(device_config); 1951 } 1952 devices = Some(device_config_list); 1953 } 1954 1955 let mut vsock: Option<VsockConfig> = None; 1956 if let Some(vs) = &vm_params.vsock { 1957 let vsock_config = VsockConfig::parse(vs)?; 1958 if vsock_config.iommu { 1959 iommu = true; 1960 } 1961 vsock = Some(vsock_config); 1962 } 1963 1964 #[cfg(target_arch = "x86_64")] 1965 let mut sgx_epc: Option<Vec<SgxEpcConfig>> = None; 1966 #[cfg(target_arch = "x86_64")] 1967 { 1968 if let Some(sgx_epc_list) = &vm_params.sgx_epc { 1969 let mut sgx_epc_config_list = Vec::new(); 1970 for item in sgx_epc_list.iter() { 1971 let sgx_epc_config = SgxEpcConfig::parse(item)?; 1972 sgx_epc_config_list.push(sgx_epc_config); 1973 } 1974 sgx_epc = Some(sgx_epc_config_list); 1975 } 1976 } 1977 1978 let mut numa: Option<Vec<NumaConfig>> = None; 1979 if let Some(numa_list) = &vm_params.numa { 1980 let mut numa_config_list = Vec::new(); 1981 for item in numa_list.iter() { 1982 let numa_config = NumaConfig::parse(item)?; 1983 numa_config_list.push(numa_config); 1984 } 1985 numa = Some(numa_config_list); 1986 } 1987 1988 let mut kernel: Option<KernelConfig> = None; 1989 if let Some(k) = vm_params.kernel { 1990 kernel = Some(KernelConfig { 1991 path: PathBuf::from(k), 1992 }); 1993 } 1994 1995 let mut initramfs: Option<InitramfsConfig> = None; 1996 if let Some(k) = vm_params.initramfs { 1997 initramfs = Some(InitramfsConfig { 1998 path: PathBuf::from(k), 1999 }); 2000 } 2001 2002 #[cfg(feature = "tdx")] 2003 let tdx = vm_params.tdx.map(TdxConfig::parse).transpose()?; 2004 2005 let config = VmConfig { 2006 cpus: CpusConfig::parse(vm_params.cpus)?, 2007 memory: MemoryConfig::parse(vm_params.memory, vm_params.memory_zones)?, 2008 kernel, 2009 initramfs, 2010 cmdline: CmdlineConfig::parse(vm_params.cmdline)?, 2011 disks, 2012 net, 2013 rng, 2014 balloon, 2015 fs, 2016 pmem, 2017 serial, 2018 console, 2019 devices, 2020 vsock, 2021 iommu, 2022 #[cfg(target_arch = "x86_64")] 2023 sgx_epc, 2024 numa, 2025 watchdog: vm_params.watchdog, 2026 #[cfg(feature = "tdx")] 2027 tdx, 2028 }; 2029 config.validate().map_err(Error::Validation)?; 2030 Ok(config) 2031 } 2032 } 2033 2034 #[cfg(test)] 2035 mod tests { 2036 use super::*; 2037 2038 #[test] 2039 fn test_option_parser() { 2040 let mut parser = OptionParser::new(); 2041 parser 2042 .add("size") 2043 .add("mergeable") 2044 .add("hotplug_method") 2045 .add("hotplug_size"); 2046 2047 assert!(parser.parse("size=128M,hanging_param").is_err()); 2048 assert!(parser.parse("size=128M,too_many_equals=foo=bar").is_err()); 2049 assert!(parser.parse("size=128M,file=/dev/shm").is_err()); 2050 assert!(parser.parse("size=128M").is_ok()); 2051 2052 assert_eq!(parser.get("size"), Some("128M".to_owned())); 2053 assert!(!parser.is_set("mergeable")); 2054 assert!(parser.is_set("size")); 2055 } 2056 2057 #[test] 2058 fn test_cpu_parsing() -> Result<()> { 2059 assert_eq!(CpusConfig::parse("")?, CpusConfig::default()); 2060 2061 assert_eq!( 2062 CpusConfig::parse("boot=1")?, 2063 CpusConfig { 2064 boot_vcpus: 1, 2065 max_vcpus: 1, 2066 ..Default::default() 2067 } 2068 ); 2069 assert_eq!( 2070 CpusConfig::parse("boot=1,max=2")?, 2071 CpusConfig { 2072 boot_vcpus: 1, 2073 max_vcpus: 2, 2074 ..Default::default() 2075 } 2076 ); 2077 assert_eq!( 2078 CpusConfig::parse("boot=8,topology=2:2:1:2")?, 2079 CpusConfig { 2080 boot_vcpus: 8, 2081 max_vcpus: 8, 2082 topology: Some(CpuTopology { 2083 threads_per_core: 2, 2084 cores_per_die: 2, 2085 dies_per_package: 1, 2086 packages: 2 2087 }), 2088 ..Default::default() 2089 } 2090 ); 2091 2092 assert!(CpusConfig::parse("boot=8,topology=2:2:1").is_err()); 2093 assert!(CpusConfig::parse("boot=8,topology=2:2:1:x").is_err()); 2094 assert_eq!( 2095 CpusConfig::parse("boot=1,kvm_hyperv=on")?, 2096 CpusConfig { 2097 boot_vcpus: 1, 2098 max_vcpus: 1, 2099 kvm_hyperv: true, 2100 ..Default::default() 2101 } 2102 ); 2103 Ok(()) 2104 } 2105 2106 #[test] 2107 fn test_mem_parsing() -> Result<()> { 2108 assert_eq!(MemoryConfig::parse("", None)?, MemoryConfig::default()); 2109 // Default string 2110 assert_eq!( 2111 MemoryConfig::parse("size=512M", None)?, 2112 MemoryConfig::default() 2113 ); 2114 assert_eq!( 2115 MemoryConfig::parse("size=512M,mergeable=on", None)?, 2116 MemoryConfig { 2117 size: 512 << 20, 2118 mergeable: true, 2119 ..Default::default() 2120 } 2121 ); 2122 assert_eq!( 2123 MemoryConfig::parse("mergeable=on", None)?, 2124 MemoryConfig { 2125 mergeable: true, 2126 ..Default::default() 2127 } 2128 ); 2129 assert_eq!( 2130 MemoryConfig::parse("size=1G,mergeable=off", None)?, 2131 MemoryConfig { 2132 size: 1 << 30, 2133 mergeable: false, 2134 ..Default::default() 2135 } 2136 ); 2137 assert_eq!( 2138 MemoryConfig::parse("hotplug_method=acpi", None)?, 2139 MemoryConfig { 2140 ..Default::default() 2141 } 2142 ); 2143 assert_eq!( 2144 MemoryConfig::parse("hotplug_method=acpi,hotplug_size=512M", None)?, 2145 MemoryConfig { 2146 hotplug_size: Some(512 << 20), 2147 ..Default::default() 2148 } 2149 ); 2150 assert_eq!( 2151 MemoryConfig::parse("hotplug_method=virtio-mem,hotplug_size=512M", None)?, 2152 MemoryConfig { 2153 hotplug_size: Some(512 << 20), 2154 hotplug_method: HotplugMethod::VirtioMem, 2155 ..Default::default() 2156 } 2157 ); 2158 assert_eq!( 2159 MemoryConfig::parse("hugepages=on,size=1G,hugepage_size=2M", None)?, 2160 MemoryConfig { 2161 hugepage_size: Some(2 << 20), 2162 size: 1 << 30, 2163 hugepages: true, 2164 ..Default::default() 2165 } 2166 ); 2167 Ok(()) 2168 } 2169 2170 #[test] 2171 fn test_disk_parsing() -> Result<()> { 2172 assert_eq!( 2173 DiskConfig::parse("path=/path/to_file")?, 2174 DiskConfig { 2175 path: Some(PathBuf::from("/path/to_file")), 2176 ..Default::default() 2177 } 2178 ); 2179 assert_eq!( 2180 DiskConfig::parse("path=/path/to_file,id=mydisk0")?, 2181 DiskConfig { 2182 path: Some(PathBuf::from("/path/to_file")), 2183 id: Some("mydisk0".to_owned()), 2184 ..Default::default() 2185 } 2186 ); 2187 assert_eq!( 2188 DiskConfig::parse("vhost_user=true,socket=/tmp/sock")?, 2189 DiskConfig { 2190 vhost_socket: Some(String::from("/tmp/sock")), 2191 vhost_user: true, 2192 ..Default::default() 2193 } 2194 ); 2195 assert_eq!( 2196 DiskConfig::parse("path=/path/to_file,iommu=on")?, 2197 DiskConfig { 2198 path: Some(PathBuf::from("/path/to_file")), 2199 iommu: true, 2200 ..Default::default() 2201 } 2202 ); 2203 assert_eq!( 2204 DiskConfig::parse("path=/path/to_file,iommu=on,queue_size=256")?, 2205 DiskConfig { 2206 path: Some(PathBuf::from("/path/to_file")), 2207 iommu: true, 2208 queue_size: 256, 2209 ..Default::default() 2210 } 2211 ); 2212 assert_eq!( 2213 DiskConfig::parse("path=/path/to_file,iommu=on,queue_size=256,num_queues=4")?, 2214 DiskConfig { 2215 path: Some(PathBuf::from("/path/to_file")), 2216 iommu: true, 2217 queue_size: 256, 2218 num_queues: 4, 2219 ..Default::default() 2220 } 2221 ); 2222 assert_eq!( 2223 DiskConfig::parse("path=/path/to_file,direct=on")?, 2224 DiskConfig { 2225 path: Some(PathBuf::from("/path/to_file")), 2226 direct: true, 2227 ..Default::default() 2228 } 2229 ); 2230 assert_eq!( 2231 DiskConfig::parse("path=/path/to_file")?, 2232 DiskConfig { 2233 path: Some(PathBuf::from("/path/to_file")), 2234 ..Default::default() 2235 } 2236 ); 2237 assert_eq!( 2238 DiskConfig::parse("path=/path/to_file")?, 2239 DiskConfig { 2240 path: Some(PathBuf::from("/path/to_file")), 2241 ..Default::default() 2242 } 2243 ); 2244 assert_eq!( 2245 DiskConfig::parse("path=/path/to_file,poll_queue=false")?, 2246 DiskConfig { 2247 path: Some(PathBuf::from("/path/to_file")), 2248 poll_queue: false, 2249 ..Default::default() 2250 } 2251 ); 2252 assert_eq!( 2253 DiskConfig::parse("path=/path/to_file,poll_queue=true")?, 2254 DiskConfig { 2255 path: Some(PathBuf::from("/path/to_file")), 2256 poll_queue: true, 2257 ..Default::default() 2258 } 2259 ); 2260 2261 Ok(()) 2262 } 2263 2264 #[test] 2265 fn test_net_parsing() -> Result<()> { 2266 // mac address is random 2267 assert_eq!( 2268 NetConfig::parse("mac=de:ad:be:ef:12:34,host_mac=12:34:de:ad:be:ef")?, 2269 NetConfig { 2270 mac: MacAddr::parse_str("de:ad:be:ef:12:34").unwrap(), 2271 host_mac: Some(MacAddr::parse_str("12:34:de:ad:be:ef").unwrap()), 2272 ..Default::default() 2273 } 2274 ); 2275 2276 assert_eq!( 2277 NetConfig::parse("mac=de:ad:be:ef:12:34,host_mac=12:34:de:ad:be:ef,id=mynet0")?, 2278 NetConfig { 2279 mac: MacAddr::parse_str("de:ad:be:ef:12:34").unwrap(), 2280 host_mac: Some(MacAddr::parse_str("12:34:de:ad:be:ef").unwrap()), 2281 id: Some("mynet0".to_owned()), 2282 ..Default::default() 2283 } 2284 ); 2285 2286 assert_eq!( 2287 NetConfig::parse( 2288 "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" 2289 )?, 2290 NetConfig { 2291 mac: MacAddr::parse_str("de:ad:be:ef:12:34").unwrap(), 2292 host_mac: Some(MacAddr::parse_str("12:34:de:ad:be:ef").unwrap()), 2293 tap: Some("tap0".to_owned()), 2294 ip: "192.168.100.1".parse().unwrap(), 2295 mask: "255.255.255.128".parse().unwrap(), 2296 ..Default::default() 2297 } 2298 ); 2299 2300 assert_eq!( 2301 NetConfig::parse( 2302 "mac=de:ad:be:ef:12:34,host_mac=12:34:de:ad:be:ef,vhost_user=true,socket=/tmp/sock" 2303 )?, 2304 NetConfig { 2305 mac: MacAddr::parse_str("de:ad:be:ef:12:34").unwrap(), 2306 host_mac: Some(MacAddr::parse_str("12:34:de:ad:be:ef").unwrap()), 2307 vhost_user: true, 2308 vhost_socket: Some("/tmp/sock".to_owned()), 2309 ..Default::default() 2310 } 2311 ); 2312 2313 assert_eq!( 2314 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")?, 2315 NetConfig { 2316 mac: MacAddr::parse_str("de:ad:be:ef:12:34").unwrap(), 2317 host_mac: Some(MacAddr::parse_str("12:34:de:ad:be:ef").unwrap()), 2318 num_queues: 4, 2319 queue_size: 1024, 2320 iommu: true, 2321 ..Default::default() 2322 } 2323 ); 2324 2325 assert_eq!( 2326 NetConfig::parse("mac=de:ad:be:ef:12:34,fd=3:7,num_queues=4")?, 2327 NetConfig { 2328 mac: MacAddr::parse_str("de:ad:be:ef:12:34").unwrap(), 2329 fds: Some(vec![3, 7]), 2330 num_queues: 4, 2331 ..Default::default() 2332 } 2333 ); 2334 2335 Ok(()) 2336 } 2337 2338 #[test] 2339 fn test_parse_rng() -> Result<()> { 2340 assert_eq!(RngConfig::parse("")?, RngConfig::default()); 2341 assert_eq!( 2342 RngConfig::parse("src=/dev/random")?, 2343 RngConfig { 2344 src: PathBuf::from("/dev/random"), 2345 ..Default::default() 2346 } 2347 ); 2348 assert_eq!( 2349 RngConfig::parse("src=/dev/random,iommu=on")?, 2350 RngConfig { 2351 src: PathBuf::from("/dev/random"), 2352 iommu: true, 2353 } 2354 ); 2355 assert_eq!( 2356 RngConfig::parse("iommu=on")?, 2357 RngConfig { 2358 iommu: true, 2359 ..Default::default() 2360 } 2361 ); 2362 Ok(()) 2363 } 2364 2365 #[test] 2366 fn test_parse_fs() -> Result<()> { 2367 // "tag" and "socket" must be supplied 2368 assert!(FsConfig::parse("").is_err()); 2369 assert!(FsConfig::parse("tag=mytag").is_err()); 2370 assert!(FsConfig::parse("socket=/tmp/sock").is_err()); 2371 assert_eq!( 2372 FsConfig::parse("tag=mytag,socket=/tmp/sock")?, 2373 FsConfig { 2374 socket: PathBuf::from("/tmp/sock"), 2375 tag: "mytag".to_owned(), 2376 ..Default::default() 2377 } 2378 ); 2379 assert_eq!( 2380 FsConfig::parse("tag=mytag,socket=/tmp/sock")?, 2381 FsConfig { 2382 socket: PathBuf::from("/tmp/sock"), 2383 tag: "mytag".to_owned(), 2384 ..Default::default() 2385 } 2386 ); 2387 assert_eq!( 2388 FsConfig::parse("tag=mytag,socket=/tmp/sock,num_queues=4,queue_size=1024")?, 2389 FsConfig { 2390 socket: PathBuf::from("/tmp/sock"), 2391 tag: "mytag".to_owned(), 2392 num_queues: 4, 2393 queue_size: 1024, 2394 ..Default::default() 2395 } 2396 ); 2397 // DAX on -> default cache size 2398 assert_eq!( 2399 FsConfig::parse("tag=mytag,socket=/tmp/sock,dax=on")?, 2400 FsConfig { 2401 socket: PathBuf::from("/tmp/sock"), 2402 tag: "mytag".to_owned(), 2403 dax: true, 2404 cache_size: default_fsconfig_cache_size(), 2405 ..Default::default() 2406 } 2407 ); 2408 assert_eq!( 2409 FsConfig::parse("tag=mytag,socket=/tmp/sock,dax=on,cache_size=4G")?, 2410 FsConfig { 2411 socket: PathBuf::from("/tmp/sock"), 2412 tag: "mytag".to_owned(), 2413 dax: true, 2414 cache_size: 4 << 30, 2415 ..Default::default() 2416 } 2417 ); 2418 // Cache size without DAX is an error 2419 assert!(FsConfig::parse("tag=mytag,socket=/tmp/sock,dax=off,cache_size=4G").is_err()); 2420 Ok(()) 2421 } 2422 2423 #[test] 2424 fn test_pmem_parsing() -> Result<()> { 2425 // Must always give a file and size 2426 assert!(PmemConfig::parse("").is_err()); 2427 assert!(PmemConfig::parse("size=128M").is_err()); 2428 assert_eq!( 2429 PmemConfig::parse("file=/tmp/pmem,size=128M")?, 2430 PmemConfig { 2431 file: PathBuf::from("/tmp/pmem"), 2432 size: Some(128 << 20), 2433 ..Default::default() 2434 } 2435 ); 2436 assert_eq!( 2437 PmemConfig::parse("file=/tmp/pmem,size=128M,id=mypmem0")?, 2438 PmemConfig { 2439 file: PathBuf::from("/tmp/pmem"), 2440 size: Some(128 << 20), 2441 id: Some("mypmem0".to_owned()), 2442 ..Default::default() 2443 } 2444 ); 2445 assert_eq!( 2446 PmemConfig::parse("file=/tmp/pmem,size=128M,iommu=on,mergeable=on,discard_writes=on")?, 2447 PmemConfig { 2448 file: PathBuf::from("/tmp/pmem"), 2449 size: Some(128 << 20), 2450 mergeable: true, 2451 discard_writes: true, 2452 iommu: true, 2453 ..Default::default() 2454 } 2455 ); 2456 2457 Ok(()) 2458 } 2459 2460 #[test] 2461 fn test_console_parsing() -> Result<()> { 2462 assert!(ConsoleConfig::parse("").is_err()); 2463 assert!(ConsoleConfig::parse("badmode").is_err()); 2464 assert_eq!( 2465 ConsoleConfig::parse("off")?, 2466 ConsoleConfig { 2467 mode: ConsoleOutputMode::Off, 2468 iommu: false, 2469 file: None, 2470 } 2471 ); 2472 assert_eq!( 2473 ConsoleConfig::parse("pty")?, 2474 ConsoleConfig { 2475 mode: ConsoleOutputMode::Pty, 2476 iommu: false, 2477 file: None, 2478 } 2479 ); 2480 assert_eq!( 2481 ConsoleConfig::parse("tty")?, 2482 ConsoleConfig { 2483 mode: ConsoleOutputMode::Tty, 2484 iommu: false, 2485 file: None, 2486 } 2487 ); 2488 assert_eq!( 2489 ConsoleConfig::parse("null")?, 2490 ConsoleConfig { 2491 mode: ConsoleOutputMode::Null, 2492 iommu: false, 2493 file: None, 2494 } 2495 ); 2496 assert_eq!( 2497 ConsoleConfig::parse("file=/tmp/console")?, 2498 ConsoleConfig { 2499 mode: ConsoleOutputMode::File, 2500 iommu: false, 2501 file: Some(PathBuf::from("/tmp/console")) 2502 } 2503 ); 2504 assert_eq!( 2505 ConsoleConfig::parse("null,iommu=on")?, 2506 ConsoleConfig { 2507 mode: ConsoleOutputMode::Null, 2508 iommu: true, 2509 file: None, 2510 } 2511 ); 2512 assert_eq!( 2513 ConsoleConfig::parse("file=/tmp/console,iommu=on")?, 2514 ConsoleConfig { 2515 mode: ConsoleOutputMode::File, 2516 iommu: true, 2517 file: Some(PathBuf::from("/tmp/console")) 2518 } 2519 ); 2520 Ok(()) 2521 } 2522 2523 #[test] 2524 fn test_device_parsing() -> Result<()> { 2525 // Device must have a path provided 2526 assert!(DeviceConfig::parse("").is_err()); 2527 assert_eq!( 2528 DeviceConfig::parse("path=/path/to/device")?, 2529 DeviceConfig { 2530 path: PathBuf::from("/path/to/device"), 2531 id: None, 2532 iommu: false 2533 } 2534 ); 2535 2536 assert_eq!( 2537 DeviceConfig::parse("path=/path/to/device,iommu=on")?, 2538 DeviceConfig { 2539 path: PathBuf::from("/path/to/device"), 2540 id: None, 2541 iommu: true 2542 } 2543 ); 2544 2545 assert_eq!( 2546 DeviceConfig::parse("path=/path/to/device,iommu=on,id=mydevice0")?, 2547 DeviceConfig { 2548 path: PathBuf::from("/path/to/device"), 2549 id: Some("mydevice0".to_owned()), 2550 iommu: true 2551 } 2552 ); 2553 2554 Ok(()) 2555 } 2556 2557 #[test] 2558 fn test_vsock_parsing() -> Result<()> { 2559 // socket and cid is required 2560 assert!(VsockConfig::parse("").is_err()); 2561 assert_eq!( 2562 VsockConfig::parse("socket=/tmp/sock,cid=1")?, 2563 VsockConfig { 2564 cid: 1, 2565 socket: PathBuf::from("/tmp/sock"), 2566 iommu: false, 2567 id: None, 2568 } 2569 ); 2570 assert_eq!( 2571 VsockConfig::parse("socket=/tmp/sock,cid=1,iommu=on")?, 2572 VsockConfig { 2573 cid: 1, 2574 socket: PathBuf::from("/tmp/sock"), 2575 iommu: true, 2576 id: None, 2577 } 2578 ); 2579 Ok(()) 2580 } 2581 2582 #[test] 2583 fn test_config_validation() { 2584 let valid_config = VmConfig { 2585 cpus: CpusConfig { 2586 boot_vcpus: 1, 2587 max_vcpus: 1, 2588 ..Default::default() 2589 }, 2590 memory: MemoryConfig { 2591 size: 536_870_912, 2592 mergeable: false, 2593 hotplug_method: HotplugMethod::Acpi, 2594 hotplug_size: None, 2595 hotplugged_size: None, 2596 shared: false, 2597 hugepages: false, 2598 hugepage_size: None, 2599 zones: None, 2600 }, 2601 kernel: Some(KernelConfig { 2602 path: PathBuf::from("/path/to/kernel"), 2603 }), 2604 initramfs: None, 2605 cmdline: CmdlineConfig { 2606 args: String::from(""), 2607 }, 2608 disks: None, 2609 net: None, 2610 rng: RngConfig { 2611 src: PathBuf::from("/dev/urandom"), 2612 iommu: false, 2613 }, 2614 balloon: None, 2615 fs: None, 2616 pmem: None, 2617 serial: ConsoleConfig { 2618 file: None, 2619 mode: ConsoleOutputMode::Null, 2620 iommu: false, 2621 }, 2622 console: ConsoleConfig { 2623 file: None, 2624 mode: ConsoleOutputMode::Tty, 2625 iommu: false, 2626 }, 2627 devices: None, 2628 vsock: None, 2629 iommu: false, 2630 #[cfg(target_arch = "x86_64")] 2631 sgx_epc: None, 2632 numa: None, 2633 watchdog: false, 2634 #[cfg(feature = "tdx")] 2635 tdx: None, 2636 }; 2637 2638 assert!(valid_config.validate().is_ok()); 2639 2640 let mut invalid_config = valid_config.clone(); 2641 invalid_config.serial.mode = ConsoleOutputMode::Tty; 2642 invalid_config.console.mode = ConsoleOutputMode::Tty; 2643 assert!(invalid_config.validate().is_err()); 2644 2645 let mut invalid_config = valid_config.clone(); 2646 invalid_config.kernel = None; 2647 assert!(invalid_config.validate().is_err()); 2648 2649 let mut invalid_config = valid_config.clone(); 2650 invalid_config.serial.mode = ConsoleOutputMode::File; 2651 invalid_config.serial.file = None; 2652 assert!(invalid_config.validate().is_err()); 2653 2654 let mut invalid_config = valid_config.clone(); 2655 invalid_config.cpus.max_vcpus = 16; 2656 invalid_config.cpus.boot_vcpus = 32; 2657 assert!(invalid_config.validate().is_err()); 2658 2659 let mut invalid_config = valid_config.clone(); 2660 invalid_config.cpus.max_vcpus = 16; 2661 invalid_config.cpus.boot_vcpus = 16; 2662 invalid_config.cpus.topology = Some(CpuTopology { 2663 threads_per_core: 2, 2664 cores_per_die: 8, 2665 dies_per_package: 1, 2666 packages: 2, 2667 }); 2668 assert!(invalid_config.validate().is_err()); 2669 2670 let mut invalid_config = valid_config.clone(); 2671 invalid_config.disks = Some(vec![DiskConfig { 2672 vhost_socket: Some("/path/to/sock".to_owned()), 2673 path: Some(PathBuf::from("/path/to/image")), 2674 ..Default::default() 2675 }]); 2676 assert!(invalid_config.validate().is_err()); 2677 2678 let mut invalid_config = valid_config.clone(); 2679 invalid_config.disks = Some(vec![DiskConfig { 2680 vhost_user: true, 2681 ..Default::default() 2682 }]); 2683 assert!(invalid_config.validate().is_err()); 2684 2685 let mut invalid_config = valid_config.clone(); 2686 invalid_config.disks = Some(vec![DiskConfig { 2687 vhost_user: true, 2688 vhost_socket: Some("/path/to/sock".to_owned()), 2689 ..Default::default() 2690 }]); 2691 assert!(invalid_config.validate().is_err()); 2692 2693 let mut still_valid_config = valid_config.clone(); 2694 still_valid_config.disks = Some(vec![DiskConfig { 2695 vhost_user: true, 2696 vhost_socket: Some("/path/to/sock".to_owned()), 2697 ..Default::default() 2698 }]); 2699 still_valid_config.memory.shared = true; 2700 assert!(still_valid_config.validate().is_ok()); 2701 2702 let mut invalid_config = valid_config.clone(); 2703 invalid_config.net = Some(vec![NetConfig { 2704 vhost_user: true, 2705 ..Default::default() 2706 }]); 2707 assert!(invalid_config.validate().is_err()); 2708 2709 let mut still_valid_config = valid_config.clone(); 2710 still_valid_config.net = Some(vec![NetConfig { 2711 vhost_user: true, 2712 vhost_socket: Some("/path/to/sock".to_owned()), 2713 ..Default::default() 2714 }]); 2715 still_valid_config.memory.shared = true; 2716 assert!(still_valid_config.validate().is_ok()); 2717 2718 let mut invalid_config = valid_config.clone(); 2719 invalid_config.net = Some(vec![NetConfig { 2720 fds: Some(vec![0]), 2721 ..Default::default() 2722 }]); 2723 assert!(invalid_config.validate().is_err()); 2724 2725 let mut invalid_config = valid_config.clone(); 2726 invalid_config.fs = Some(vec![FsConfig { 2727 ..Default::default() 2728 }]); 2729 assert!(invalid_config.validate().is_err()); 2730 2731 let mut still_valid_config = valid_config.clone(); 2732 still_valid_config.memory.shared = true; 2733 assert!(still_valid_config.validate().is_ok()); 2734 2735 let mut still_valid_config = valid_config.clone(); 2736 still_valid_config.memory.hugepages = true; 2737 assert!(still_valid_config.validate().is_ok()); 2738 2739 let mut still_valid_config = valid_config.clone(); 2740 still_valid_config.memory.hugepages = true; 2741 still_valid_config.memory.hugepage_size = Some(2 << 20); 2742 assert!(still_valid_config.validate().is_ok()); 2743 2744 let mut invalid_config = valid_config.clone(); 2745 invalid_config.memory.hugepages = false; 2746 invalid_config.memory.hugepage_size = Some(2 << 20); 2747 assert!(invalid_config.validate().is_err()); 2748 2749 let mut invalid_config = valid_config; 2750 invalid_config.memory.hugepages = true; 2751 invalid_config.memory.hugepage_size = Some(3 << 20); 2752 assert!(invalid_config.validate().is_err()); 2753 } 2754 } 2755