1 // Copyright © 2019 Intel Corporation 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 // 5 6 #[cfg(test)] 7 mod test_util; 8 9 use std::fs::File; 10 use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; 11 use std::sync::mpsc::channel; 12 use std::sync::Mutex; 13 use std::{env, io}; 14 15 use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command}; 16 use event_monitor::event; 17 use libc::EFD_NONBLOCK; 18 use log::{warn, LevelFilter}; 19 use option_parser::OptionParser; 20 use seccompiler::SeccompAction; 21 use signal_hook::consts::SIGSYS; 22 use thiserror::Error; 23 #[cfg(feature = "dbus_api")] 24 use vmm::api::dbus::{dbus_api_graceful_shutdown, DBusApiOptions}; 25 use vmm::api::http::http_api_graceful_shutdown; 26 use vmm::api::ApiAction; 27 use vmm::config::{RestoreConfig, VmParams}; 28 use vmm::landlock::{Landlock, LandlockError}; 29 use vmm::vm_config; 30 #[cfg(target_arch = "x86_64")] 31 use vmm::vm_config::SgxEpcConfig; 32 use vmm::vm_config::{ 33 BalloonConfig, DeviceConfig, DiskConfig, FsConfig, LandlockConfig, NetConfig, NumaConfig, 34 PciSegmentConfig, PmemConfig, RateLimiterGroupConfig, TpmConfig, UserDeviceConfig, VdpaConfig, 35 VmConfig, VsockConfig, 36 }; 37 use vmm_sys_util::eventfd::EventFd; 38 use vmm_sys_util::signal::block_signal; 39 40 #[cfg(feature = "dhat-heap")] 41 #[global_allocator] 42 static ALLOC: dhat::Alloc = dhat::Alloc; 43 44 #[derive(Error, Debug)] 45 enum Error { 46 #[error("Failed to create API EventFd")] 47 CreateApiEventFd(#[source] std::io::Error), 48 #[cfg(feature = "guest_debug")] 49 #[error("Failed to create Debug EventFd")] 50 CreateDebugEventFd(#[source] std::io::Error), 51 #[error("Failed to create exit EventFd")] 52 CreateExitEventFd(#[source] std::io::Error), 53 #[error("Failed to open hypervisor interface (is hypervisor interface available?)")] 54 CreateHypervisor(#[source] hypervisor::HypervisorError), 55 #[error("Failed to start the VMM thread")] 56 StartVmmThread(#[source] vmm::Error), 57 #[error("Error parsing config")] 58 ParsingConfig(#[source] vmm::config::Error), 59 #[error("Error creating VM")] 60 VmCreate(#[source] vmm::api::ApiError), 61 #[error("Error booting VM")] 62 VmBoot(#[source] vmm::api::ApiError), 63 #[error("Error restoring VM")] 64 VmRestore(#[source] vmm::api::ApiError), 65 #[error("Error parsing restore")] 66 ParsingRestore(#[source] vmm::config::Error), 67 #[error("Failed to join on VMM thread: {0:?}")] 68 ThreadJoin(std::boxed::Box<dyn std::any::Any + std::marker::Send>), 69 #[error("VMM thread exited with error")] 70 VmmThread(#[source] vmm::Error), 71 #[error("Error parsing --api-socket")] 72 ParsingApiSocket(#[source] std::num::ParseIntError), 73 #[error("Error parsing --event-monitor")] 74 ParsingEventMonitor(#[source] option_parser::OptionParserError), 75 #[cfg(feature = "dbus_api")] 76 #[error("`--dbus-object-path` option isn't provided")] 77 MissingDBusObjectPath, 78 #[cfg(feature = "dbus_api")] 79 #[error("`--dbus-service-name` option isn't provided")] 80 MissingDBusServiceName, 81 #[error("Error parsing --event-monitor: path or fd required")] 82 BareEventMonitor, 83 #[error("Error doing event monitor I/O")] 84 EventMonitorIo(#[source] std::io::Error), 85 #[error("Event monitor thread failed")] 86 EventMonitorThread(#[source] vmm::Error), 87 #[cfg(feature = "guest_debug")] 88 #[error("Error parsing --gdb")] 89 ParsingGdb(#[source] option_parser::OptionParserError), 90 #[cfg(feature = "guest_debug")] 91 #[error("Error parsing --gdb: path required")] 92 BareGdb, 93 #[error("Error creating log file")] 94 LogFileCreation(#[source] std::io::Error), 95 #[error("Error setting up logger")] 96 LoggerSetup(#[source] log::SetLoggerError), 97 #[error("Failed to gracefully shutdown http api")] 98 HttpApiShutdown(#[source] vmm::Error), 99 #[error("Failed to create Landlock object")] 100 CreateLandlock(#[source] LandlockError), 101 #[error("Failed to apply Landlock")] 102 ApplyLandlock(#[source] LandlockError), 103 } 104 105 #[derive(Error, Debug)] 106 enum FdTableError { 107 #[error("Failed to create event fd")] 108 CreateEventFd(#[source] std::io::Error), 109 #[error("Failed to obtain file limit")] 110 GetRLimit(#[source] std::io::Error), 111 #[error("Error calling fcntl with F_GETFD")] 112 GetFd(#[source] std::io::Error), 113 #[error("Failed to duplicate file handle")] 114 Dup2(#[source] std::io::Error), 115 } 116 117 struct Logger { 118 output: Mutex<Box<dyn std::io::Write + Send>>, 119 start: std::time::Instant, 120 } 121 122 impl log::Log for Logger { 123 fn enabled(&self, _metadata: &log::Metadata) -> bool { 124 true 125 } 126 127 fn log(&self, record: &log::Record) { 128 if !self.enabled(record.metadata()) { 129 return; 130 } 131 132 let now = std::time::Instant::now(); 133 let duration = now.duration_since(self.start); 134 135 if record.file().is_some() && record.line().is_some() { 136 write!( 137 *(*(self.output.lock().unwrap())), 138 "cloud-hypervisor: {:.6?}: <{}> {}:{}:{} -- {}\r\n", 139 duration, 140 std::thread::current().name().unwrap_or("anonymous"), 141 record.level(), 142 record.file().unwrap(), 143 record.line().unwrap(), 144 record.args() 145 ) 146 } else { 147 write!( 148 *(*(self.output.lock().unwrap())), 149 "cloud-hypervisor: {:.6?}: <{}> {}:{} -- {}\r\n", 150 duration, 151 std::thread::current().name().unwrap_or("anonymous"), 152 record.level(), 153 record.target(), 154 record.args() 155 ) 156 } 157 .ok(); 158 } 159 fn flush(&self) {} 160 } 161 162 fn prepare_default_values() -> (String, String, String) { 163 (default_vcpus(), default_memory(), default_rng()) 164 } 165 166 fn default_vcpus() -> String { 167 format!( 168 "boot={},max_phys_bits={}", 169 vm_config::DEFAULT_VCPUS, 170 vm_config::DEFAULT_MAX_PHYS_BITS 171 ) 172 } 173 174 fn default_memory() -> String { 175 format!("size={}M", vm_config::DEFAULT_MEMORY_MB) 176 } 177 178 fn default_rng() -> String { 179 format!("src={}", vm_config::DEFAULT_RNG_SOURCE) 180 } 181 182 /// Returns all [`Arg`]s in alphabetical order. This is the order used in the 183 /// `--help` output. 184 fn get_cli_options_sorted( 185 default_vcpus: String, 186 default_memory: String, 187 default_rng: String, 188 ) -> Box<[Arg]> { 189 [ 190 Arg::new("api-socket") 191 .long("api-socket") 192 .help("HTTP API socket (UNIX domain socket): path=</path/to/a/file> or fd=<fd>.") 193 .num_args(1) 194 .group("vmm-config"), 195 Arg::new("balloon") 196 .long("balloon") 197 .help(BalloonConfig::SYNTAX) 198 .num_args(1) 199 .group("vm-config"), 200 Arg::new("cmdline") 201 .long("cmdline") 202 .help("Kernel command line") 203 .num_args(1) 204 .group("vm-config"), Arg::new("console") 205 .long("console") 206 .help( 207 "Control (virtio) console: \"off|null|pty|tty|file=</path/to/a/file>,iommu=on|off\"", 208 ) 209 .default_value("tty") 210 .group("vm-config"), 211 Arg::new("cpus") 212 .long("cpus") 213 .help( 214 "boot=<boot_vcpus>,max=<max_vcpus>,\ 215 topology=<threads_per_core>:<cores_per_die>:<dies_per_package>:<packages>,\ 216 kvm_hyperv=on|off,max_phys_bits=<maximum_number_of_physical_bits>,\ 217 affinity=<list_of_vcpus_with_their_associated_cpuset>,\ 218 features=<list_of_features_to_enable>", 219 ) 220 .default_value(default_vcpus) 221 .group("vm-config"), 222 #[cfg(target_arch = "x86_64")] 223 Arg::new("debug-console") 224 .long("debug-console") 225 .help("Debug console: off|pty|tty|file=</path/to/a/file>,iobase=<port in hex>") 226 .default_value("off,iobase=0xe9") 227 .group("vm-config"), 228 #[cfg(feature = "dbus_api")] 229 Arg::new("dbus-service-name") 230 .long("dbus-service-name") 231 .help("Well known name of the device") 232 .num_args(1) 233 .group("vmm-config"), 234 #[cfg(feature = "dbus_api")] 235 Arg::new("dbus-object-path") 236 .long("dbus-object-path") 237 .help("Object path to serve the dbus interface") 238 .num_args(1) 239 .group("vmm-config"), 240 #[cfg(feature = "dbus_api")] 241 Arg::new("dbus-system-bus") 242 .long("dbus-system-bus") 243 .action(ArgAction::SetTrue) 244 .help("Use the system bus instead of a session bus") 245 .num_args(0) 246 .group("vmm-config"), 247 Arg::new("device") 248 .long("device") 249 .help(DeviceConfig::SYNTAX) 250 .num_args(1..) 251 .group("vm-config"), 252 Arg::new("disk") 253 .long("disk") 254 .help(DiskConfig::SYNTAX) 255 .num_args(1..) 256 .group("vm-config"), 257 Arg::new("event-monitor") 258 .long("event-monitor") 259 .help("File to report events on: path=</path/to/a/file> or fd=<fd>") 260 .num_args(1) 261 .group("vmm-config"), 262 Arg::new("firmware") 263 .long("firmware") 264 .help("Path to firmware that is loaded in an architectural specific way") 265 .num_args(1) 266 .group("vm-payload"), 267 Arg::new("fs") 268 .long("fs") 269 .help(FsConfig::SYNTAX) 270 .num_args(1..) 271 .group("vm-config"), 272 #[cfg(feature = "guest_debug")] 273 Arg::new("gdb") 274 .long("gdb") 275 .help("GDB socket (UNIX domain socket): path=</path/to/a/file>") 276 .num_args(1) 277 .group("vmm-config"), 278 #[cfg(feature = "igvm")] 279 Arg::new("igvm") 280 .long("igvm") 281 .help("Path to IGVM file to load.") 282 .num_args(1) 283 .group("vm-payload"), 284 #[cfg(feature = "sev_snp")] 285 Arg::new("host-data") 286 .long("host-data") 287 .help("Host specific data to SEV SNP guest") 288 .num_args(1) 289 .group("vm-config"), 290 Arg::new("initramfs") 291 .long("initramfs") 292 .help("Path to initramfs image") 293 .num_args(1) 294 .group("vm-config"), 295 Arg::new("kernel") 296 .long("kernel") 297 .help( 298 "Path to kernel to load. This may be a kernel or firmware that supports a PVH \ 299 entry point (e.g. vmlinux) or architecture equivalent", 300 ) 301 .num_args(1) 302 .group("vm-payload"), 303 Arg::new("landlock") 304 .long("landlock") 305 .num_args(0) 306 .help( 307 "enable/disable Landlock.", 308 ) 309 .action(ArgAction::SetTrue) 310 .default_value("false") 311 .group("vm-config"), 312 Arg::new("landlock-rules") 313 .long("landlock-rules") 314 .help(LandlockConfig::SYNTAX) 315 .num_args(1..) 316 .group("vm-config"), 317 Arg::new("log-file") 318 .long("log-file") 319 .help("Log file. Standard error is used if not specified") 320 .num_args(1) 321 .group("logging"), 322 Arg::new("memory") 323 .long("memory") 324 .help( 325 "Memory parameters \ 326 \"size=<guest_memory_size>,mergeable=on|off,shared=on|off,\ 327 hugepages=on|off,hugepage_size=<hugepage_size>,\ 328 hotplug_method=acpi|virtio-mem,\ 329 hotplug_size=<hotpluggable_memory_size>,\ 330 hotplugged_size=<hotplugged_memory_size>,\ 331 prefault=on|off,thp=on|off\"", 332 ) 333 .default_value(default_memory) 334 .group("vm-config"), 335 Arg::new("memory-zone") 336 .long("memory-zone") 337 .help( 338 "User defined memory zone parameters \ 339 \"size=<guest_memory_region_size>,file=<backing_file>,\ 340 shared=on|off,\ 341 hugepages=on|off,hugepage_size=<hugepage_size>,\ 342 host_numa_node=<node_id>,\ 343 id=<zone_identifier>,hotplug_size=<hotpluggable_memory_size>,\ 344 hotplugged_size=<hotplugged_memory_size>,\ 345 prefault=on|off\"", 346 ) 347 .num_args(1..) 348 .group("vm-config"), 349 Arg::new("net") 350 .long("net") 351 .help(NetConfig::SYNTAX) 352 .num_args(1..) 353 .group("vm-config"), 354 Arg::new("numa") 355 .long("numa") 356 .help(NumaConfig::SYNTAX) 357 .num_args(1..) 358 .group("vm-config"), 359 Arg::new("pci-segment") 360 .long("pci-segment") 361 .help(PciSegmentConfig::SYNTAX) 362 .num_args(1..) 363 .group("vm-config"), 364 Arg::new("platform") 365 .long("platform") 366 .help( 367 "num_pci_segments=<num_pci_segments>,iommu_segments=<list_of_segments>,iommu_address_width=<bits>,serial_number=<dmi_device_serial_number>,uuid=<dmi_device_uuid>,oem_strings=<list_of_strings>" 368 ) 369 .num_args(1) 370 .group("vm-config"), 371 Arg::new("pmem") 372 .long("pmem") 373 .help(PmemConfig::SYNTAX) 374 .num_args(1..) 375 .group("vm-config"), 376 #[cfg(feature = "pvmemcontrol")] 377 Arg::new("pvmemcontrol") 378 .long("pvmemcontrol") 379 .help("Pvmemcontrol device") 380 .num_args(0) 381 .action(ArgAction::SetTrue) 382 .group("vm-config"), 383 Arg::new("pvpanic") 384 .long("pvpanic") 385 .help("Enable pvpanic device") 386 .num_args(0) 387 .action(ArgAction::SetTrue) 388 .group("vm-config"), 389 Arg::new("rate-limit-group") 390 .long("rate-limit-group") 391 .help(RateLimiterGroupConfig::SYNTAX) 392 .num_args(1..) 393 .group("vm-config"), 394 Arg::new("restore") 395 .long("restore") 396 .help(RestoreConfig::SYNTAX) 397 .num_args(1) 398 .group("vmm-config"), 399 Arg::new("rng") 400 .long("rng") 401 .help( 402 "Random number generator parameters \"src=<entropy_source_path>,iommu=on|off\"", 403 ) 404 .default_value(default_rng) 405 .group("vm-config"), 406 Arg::new("seccomp") 407 .long("seccomp") 408 .num_args(1) 409 .value_parser(["true", "false", "log"]) 410 .default_value("true"), 411 Arg::new("serial") 412 .long("serial") 413 .help("Control serial port: off|null|pty|tty|file=</path/to/a/file>|socket=</path/to/a/file>") 414 .default_value("null") 415 .group("vm-config"), 416 #[cfg(target_arch = "x86_64")] 417 Arg::new("sgx-epc") 418 .long("sgx-epc") 419 .help(SgxEpcConfig::SYNTAX) 420 .num_args(1..) 421 .group("vm-config"), 422 Arg::new("tpm") 423 .long("tpm") 424 .num_args(1) 425 .help(TpmConfig::SYNTAX) 426 .group("vm-config"), 427 Arg::new("user-device") 428 .long("user-device") 429 .help(UserDeviceConfig::SYNTAX) 430 .num_args(1..) 431 .group("vm-config"), 432 Arg::new("v") 433 .short('v') 434 .action(ArgAction::Count) 435 .help("Sets the level of debugging output") 436 .group("logging"), 437 Arg::new("vdpa") 438 .long("vdpa") 439 .help(VdpaConfig::SYNTAX) 440 .num_args(1..) 441 .group("vm-config"), 442 Arg::new("version") 443 .short('V') 444 .long("version") 445 .action(ArgAction::SetTrue) 446 .help("Print version") 447 .num_args(0), 448 Arg::new("vsock") 449 .long("vsock") 450 .help(VsockConfig::SYNTAX) 451 .num_args(1) 452 .group("vm-config"), 453 Arg::new("watchdog") 454 .long("watchdog") 455 .help("Enable virtio-watchdog") 456 .num_args(0) 457 .action(ArgAction::SetTrue) 458 .group("vm-config"), 459 ].to_vec().into_boxed_slice() 460 } 461 462 /// Creates the CLI definition of Cloud Hypervisor. 463 fn create_app(default_vcpus: String, default_memory: String, default_rng: String) -> Command { 464 let groups = [ 465 ArgGroup::new("vm-config") 466 .multiple(true) 467 .requires("vm-payload"), 468 ArgGroup::new("vmm-config").multiple(true), 469 ArgGroup::new("logging").multiple(true), 470 ArgGroup::new("vm-payload").multiple(true), 471 ]; 472 473 let args = get_cli_options_sorted(default_vcpus, default_memory, default_rng); 474 475 Command::new("cloud-hypervisor") 476 // 'BUILD_VERSION' is set by the build script 'build.rs' at 477 // compile time 478 .author(env!("CARGO_PKG_AUTHORS")) 479 .about("Launch a cloud-hypervisor VMM.") 480 .arg_required_else_help(true) 481 .groups(groups) 482 .args(args) 483 } 484 485 fn start_vmm(cmd_arguments: ArgMatches) -> Result<Option<String>, Error> { 486 let log_level = match cmd_arguments.get_count("v") { 487 0 => LevelFilter::Warn, 488 1 => LevelFilter::Info, 489 2 => LevelFilter::Debug, 490 _ => LevelFilter::Trace, 491 }; 492 493 let log_file: Box<dyn std::io::Write + Send> = if let Some(ref file) = 494 cmd_arguments.get_one::<String>("log-file") 495 { 496 Box::new(std::fs::File::create(std::path::Path::new(file)).map_err(Error::LogFileCreation)?) 497 } else { 498 Box::new(std::io::stderr()) 499 }; 500 501 log::set_boxed_logger(Box::new(Logger { 502 output: Mutex::new(log_file), 503 start: std::time::Instant::now(), 504 })) 505 .map(|()| log::set_max_level(log_level)) 506 .map_err(Error::LoggerSetup)?; 507 508 let (api_socket_path, api_socket_fd) = 509 if let Some(socket_config) = cmd_arguments.get_one::<String>("api-socket") { 510 let mut parser = OptionParser::new(); 511 parser.add("path").add("fd"); 512 parser.parse(socket_config).unwrap_or_default(); 513 514 if let Some(fd) = parser.get("fd") { 515 ( 516 None, 517 Some(fd.parse::<RawFd>().map_err(Error::ParsingApiSocket)?), 518 ) 519 } else if let Some(path) = parser.get("path") { 520 (Some(path), None) 521 } else { 522 ( 523 cmd_arguments 524 .get_one::<String>("api-socket") 525 .map(|s| s.to_string()), 526 None, 527 ) 528 } 529 } else { 530 (None, None) 531 }; 532 533 let (api_request_sender, api_request_receiver) = channel(); 534 let api_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::CreateApiEventFd)?; 535 536 let api_request_sender_clone = api_request_sender.clone(); 537 let seccomp_action = if let Some(seccomp_value) = cmd_arguments.get_one::<String>("seccomp") { 538 match seccomp_value as &str { 539 "true" => SeccompAction::Trap, 540 "false" => SeccompAction::Allow, 541 "log" => SeccompAction::Log, 542 val => { 543 // The user providing an invalid value will be rejected 544 panic!("Invalid parameter {val} for \"--seccomp\" flag"); 545 } 546 } 547 } else { 548 SeccompAction::Trap 549 }; 550 551 if seccomp_action == SeccompAction::Trap { 552 // SAFETY: We only using signal_hook for managing signals and only execute signal 553 // handler safe functions (writing to stderr) and manipulating signals. 554 unsafe { 555 signal_hook::low_level::register(signal_hook::consts::SIGSYS, || { 556 eprint!( 557 "\n==== Possible seccomp violation ====\n\ 558 Try running with `strace -ff` to identify the cause and open an issue: \ 559 https://github.com/cloud-hypervisor/cloud-hypervisor/issues/new\n" 560 ); 561 signal_hook::low_level::emulate_default_handler(SIGSYS).unwrap(); 562 }) 563 } 564 .map_err(|e| eprintln!("Error adding SIGSYS signal handler: {e}")) 565 .ok(); 566 } 567 568 // SAFETY: Trivially safe. 569 unsafe { 570 libc::signal(libc::SIGCHLD, libc::SIG_IGN); 571 } 572 573 // Before we start any threads, mask the signals we'll be 574 // installing handlers for, to make sure they only ever run on the 575 // dedicated signal handling thread we'll start in a bit. 576 for sig in &vmm::vm::Vm::HANDLED_SIGNALS { 577 if let Err(e) = block_signal(*sig) { 578 eprintln!("Error blocking signals: {e}"); 579 } 580 } 581 582 for sig in &vmm::Vmm::HANDLED_SIGNALS { 583 if let Err(e) = block_signal(*sig) { 584 eprintln!("Error blocking signals: {e}"); 585 } 586 } 587 588 let hypervisor = hypervisor::new().map_err(Error::CreateHypervisor)?; 589 590 #[cfg(feature = "guest_debug")] 591 let gdb_socket_path = if let Some(gdb_config) = cmd_arguments.get_one::<String>("gdb") { 592 let mut parser = OptionParser::new(); 593 parser.add("path"); 594 parser.parse(gdb_config).map_err(Error::ParsingGdb)?; 595 596 if parser.is_set("path") { 597 Some(std::path::PathBuf::from(parser.get("path").unwrap())) 598 } else { 599 return Err(Error::BareGdb); 600 } 601 } else { 602 None 603 }; 604 #[cfg(feature = "guest_debug")] 605 let debug_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::CreateDebugEventFd)?; 606 #[cfg(feature = "guest_debug")] 607 let vm_debug_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::CreateDebugEventFd)?; 608 609 let exit_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::CreateExitEventFd)?; 610 let landlock_enable = cmd_arguments.get_flag("landlock"); 611 612 #[allow(unused_mut)] 613 let mut event_monitor = cmd_arguments 614 .get_one::<String>("event-monitor") 615 .as_ref() 616 .map(|monitor_config| { 617 let mut parser = OptionParser::new(); 618 parser.add("path").add("fd"); 619 parser 620 .parse(monitor_config) 621 .map_err(Error::ParsingEventMonitor)?; 622 623 if parser.is_set("fd") { 624 let fd = parser 625 .convert("fd") 626 .map_err(Error::ParsingEventMonitor)? 627 .unwrap(); 628 // SAFETY: fd is valid 629 Ok(Some(unsafe { File::from_raw_fd(fd) })) 630 } else if parser.is_set("path") { 631 Ok(Some( 632 std::fs::OpenOptions::new() 633 .write(true) 634 .create(true) 635 .truncate(true) 636 .open(parser.get("path").unwrap()) 637 .map_err(Error::EventMonitorIo)?, 638 )) 639 } else { 640 Err(Error::BareEventMonitor) 641 } 642 }) 643 .transpose()? 644 .map(|event_monitor_file| { 645 event_monitor::set_monitor(event_monitor_file).map_err(Error::EventMonitorIo) 646 }) 647 .transpose()?; 648 649 #[cfg(feature = "dbus_api")] 650 let dbus_options = match ( 651 cmd_arguments.get_one::<String>("dbus-service-name"), 652 cmd_arguments.get_one::<String>("dbus-object-path"), 653 ) { 654 (Some(name), Some(path)) => { 655 // monitor is either set (file based) or not. 656 // if it's not set, create one without file support. 657 let mut monitor = match event_monitor.take() { 658 Some(monitor) => monitor, 659 None => event_monitor::set_monitor(None).map_err(Error::EventMonitorIo)?, 660 }; 661 let options = DBusApiOptions { 662 service_name: name.to_string(), 663 object_path: path.to_string(), 664 system_bus: cmd_arguments.get_flag("dbus-system-bus"), 665 event_monitor_rx: monitor.subscribe(), 666 }; 667 668 event_monitor = Some(monitor); 669 Ok(Some(options)) 670 } 671 (Some(_), None) => Err(Error::MissingDBusObjectPath), 672 (None, Some(_)) => Err(Error::MissingDBusServiceName), 673 (None, None) => Ok(None), 674 }?; 675 676 if let Some(monitor) = event_monitor { 677 vmm::start_event_monitor_thread( 678 monitor, 679 &seccomp_action, 680 landlock_enable, 681 hypervisor.hypervisor_type(), 682 exit_evt.try_clone().unwrap(), 683 ) 684 .map_err(Error::EventMonitorThread)?; 685 } 686 687 event!("vmm", "starting"); 688 689 let vmm_thread_handle = vmm::start_vmm_thread( 690 vmm::VmmVersionInfo::new(env!("BUILD_VERSION"), env!("CARGO_PKG_VERSION")), 691 &api_socket_path, 692 api_socket_fd, 693 #[cfg(feature = "dbus_api")] 694 dbus_options, 695 api_evt.try_clone().unwrap(), 696 api_request_sender_clone, 697 api_request_receiver, 698 #[cfg(feature = "guest_debug")] 699 gdb_socket_path, 700 #[cfg(feature = "guest_debug")] 701 debug_evt.try_clone().unwrap(), 702 #[cfg(feature = "guest_debug")] 703 vm_debug_evt.try_clone().unwrap(), 704 exit_evt.try_clone().unwrap(), 705 &seccomp_action, 706 hypervisor, 707 landlock_enable, 708 ) 709 .map_err(Error::StartVmmThread)?; 710 711 let r: Result<(), Error> = (|| { 712 #[cfg(feature = "igvm")] 713 let payload_present = cmd_arguments.contains_id("kernel") 714 || cmd_arguments.contains_id("firmware") 715 || cmd_arguments.contains_id("igvm"); 716 #[cfg(not(feature = "igvm"))] 717 let payload_present = 718 cmd_arguments.contains_id("kernel") || cmd_arguments.contains_id("firmware"); 719 720 if payload_present { 721 let vm_params = VmParams::from_arg_matches(&cmd_arguments); 722 let vm_config = VmConfig::parse(vm_params).map_err(Error::ParsingConfig)?; 723 724 // Create and boot the VM based off the VM config we just built. 725 let sender = api_request_sender.clone(); 726 vmm::api::VmCreate 727 .send( 728 api_evt.try_clone().unwrap(), 729 api_request_sender, 730 Box::new(vm_config), 731 ) 732 .map_err(Error::VmCreate)?; 733 vmm::api::VmBoot 734 .send(api_evt.try_clone().unwrap(), sender, ()) 735 .map_err(Error::VmBoot)?; 736 } else if let Some(restore_params) = cmd_arguments.get_one::<String>("restore") { 737 vmm::api::VmRestore 738 .send( 739 api_evt.try_clone().unwrap(), 740 api_request_sender, 741 RestoreConfig::parse(restore_params).map_err(Error::ParsingRestore)?, 742 ) 743 .map_err(Error::VmRestore)?; 744 } 745 746 Ok(()) 747 })(); 748 749 if r.is_err() { 750 if let Err(e) = exit_evt.write(1) { 751 warn!("writing to exit EventFd: {e}"); 752 } 753 } 754 755 if landlock_enable { 756 Landlock::new() 757 .map_err(Error::CreateLandlock)? 758 .restrict_self() 759 .map_err(Error::ApplyLandlock)?; 760 } 761 762 vmm_thread_handle 763 .thread_handle 764 .join() 765 .map_err(Error::ThreadJoin)? 766 .map_err(Error::VmmThread)?; 767 768 if let Some(api_handle) = vmm_thread_handle.http_api_handle { 769 http_api_graceful_shutdown(api_handle).map_err(Error::HttpApiShutdown)? 770 } 771 772 #[cfg(feature = "dbus_api")] 773 if let Some(chs) = vmm_thread_handle.dbus_shutdown_chs { 774 dbus_api_graceful_shutdown(chs); 775 } 776 777 r.map(|_| api_socket_path) 778 } 779 780 // This is a best-effort solution to the latency induced by the RCU 781 // synchronization that happens in the kernel whenever the file descriptor table 782 // fills up. 783 // The table has initially 64 entries on amd64 and every time it fills up, a new 784 // table is created, double the size of the current one, and the entries are 785 // copied to the new table. The filesystem code that does this uses 786 // synchronize_rcu() to ensure all preexisting RCU read-side critical sections 787 // have completed: 788 // 789 // https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/fs/file.c?h=v6.9.1#n162 790 // 791 // Rust programs that create lots of file handles or use 792 // {File,EventFd}::try_clone() to share them are impacted by this issue. This 793 // behavior is quite noticeable in the snapshot restore scenario, the latency is 794 // a big chunk of the total time required to start cloud-hypervisor and restore 795 // the snapshot. 796 // 797 // The kernel has an optimization in code, where it doesn't call 798 // synchronize_rcu() if there is only one thread in the process. We can take 799 // advantage of this optimization by expanding the descriptor table at 800 // application start, when it has only one thread. 801 // 802 // The code tries to resize the table to an adequate size for most use cases, 803 // 4096, this way we avoid any expansion that might take place later. 804 fn expand_fdtable() -> Result<(), FdTableError> { 805 let mut limits = libc::rlimit { 806 rlim_cur: 0, 807 rlim_max: 0, 808 }; 809 810 // SAFETY: FFI call with valid arguments 811 if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limits) } < 0 { 812 return Err(FdTableError::GetRLimit(io::Error::last_os_error())); 813 } 814 815 let table_size = if limits.rlim_cur == libc::RLIM_INFINITY { 816 4096 817 } else { 818 std::cmp::min(limits.rlim_cur, 4096) as libc::c_int 819 }; 820 821 // The first 3 handles are stdin, stdout, stderr. We don't want to touch 822 // any of them. 823 if table_size <= 3 { 824 return Ok(()); 825 } 826 827 let dummy_evt = EventFd::new(0).map_err(FdTableError::CreateEventFd)?; 828 829 // Test if the file descriptor is empty 830 // SAFETY: FFI call with valid arguments 831 let flags: i32 = unsafe { libc::fcntl(table_size - 1, libc::F_GETFD) }; 832 if flags >= 0 { 833 // Nothing to do, the table is already big enough 834 return Ok(()); 835 } 836 837 let err = io::Error::last_os_error(); 838 if err.raw_os_error() != Some(libc::EBADF) { 839 return Err(FdTableError::GetFd(err)); 840 } 841 // SAFETY: FFI call with valid arguments 842 if unsafe { libc::dup2(dummy_evt.as_raw_fd(), table_size - 1) } < 0 { 843 return Err(FdTableError::Dup2(io::Error::last_os_error())); 844 } 845 // SAFETY: FFI call, trivially 846 unsafe { libc::close(table_size - 1) }; 847 848 Ok(()) 849 } 850 851 fn main() { 852 #[cfg(all(feature = "tdx", feature = "sev_snp"))] 853 compile_error!("Feature 'tdx' and 'sev_snp' are mutually exclusive."); 854 #[cfg(all(feature = "sev_snp", not(target_arch = "x86_64")))] 855 compile_error!("Feature 'sev_snp' needs target 'x86_64'"); 856 857 #[cfg(feature = "dhat-heap")] 858 let _profiler = dhat::Profiler::new_heap(); 859 860 // Ensure all created files (.e.g sockets) are only accessible by this user 861 // SAFETY: trivially safe 862 let _ = unsafe { libc::umask(0o077) }; 863 864 let (default_vcpus, default_memory, default_rng) = prepare_default_values(); 865 let cmd_arguments = create_app(default_vcpus, default_memory, default_rng).get_matches(); 866 867 if cmd_arguments.get_flag("version") { 868 println!("{} {}", env!("CARGO_BIN_NAME"), env!("BUILD_VERSION")); 869 870 if cmd_arguments.get_count("v") != 0 { 871 println!("Enabled features: {:?}", vmm::feature_list()); 872 } 873 874 return; 875 } 876 877 if let Err(e) = expand_fdtable() { 878 warn!("Error expanding FD table: {e}"); 879 } 880 881 let exit_code = match start_vmm(cmd_arguments) { 882 Ok(path) => { 883 path.map(|s| std::fs::remove_file(s).ok()); 884 0 885 } 886 Err(e) => { 887 eprintln!("{e}"); 888 1 889 } 890 }; 891 892 #[cfg(feature = "dhat-heap")] 893 drop(_profiler); 894 895 std::process::exit(exit_code); 896 } 897 898 #[cfg(test)] 899 mod unit_tests { 900 use std::path::PathBuf; 901 902 use vmm::config::VmParams; 903 #[cfg(target_arch = "x86_64")] 904 use vmm::vm_config::DebugConsoleConfig; 905 use vmm::vm_config::{ 906 ConsoleConfig, ConsoleOutputMode, CpuFeatures, CpusConfig, HotplugMethod, MemoryConfig, 907 PayloadConfig, RngConfig, VmConfig, 908 }; 909 910 use crate::test_util::assert_args_sorted; 911 use crate::{create_app, get_cli_options_sorted, prepare_default_values}; 912 913 fn get_vm_config_from_vec(args: &[&str]) -> VmConfig { 914 let (default_vcpus, default_memory, default_rng) = prepare_default_values(); 915 let cmd_arguments = 916 create_app(default_vcpus, default_memory, default_rng).get_matches_from(args); 917 let vm_params = VmParams::from_arg_matches(&cmd_arguments); 918 919 VmConfig::parse(vm_params).unwrap() 920 } 921 922 fn compare_vm_config_cli_vs_json( 923 cli: &[&str], 924 openapi: &str, 925 equal: bool, 926 ) -> (VmConfig, VmConfig) { 927 let cli_vm_config = get_vm_config_from_vec(cli); 928 let openapi_vm_config: VmConfig = serde_json::from_str(openapi).unwrap(); 929 930 if equal { 931 assert_eq!(cli_vm_config, openapi_vm_config); 932 } else { 933 assert_ne!(cli_vm_config, openapi_vm_config); 934 } 935 936 (cli_vm_config, openapi_vm_config) 937 } 938 939 #[test] 940 fn test_valid_vm_config_default() { 941 let cli = vec!["cloud-hypervisor", "--kernel", "/path/to/kernel"]; 942 let openapi = r#"{ "payload": {"kernel": "/path/to/kernel"} }"#; 943 944 // First we check we get identical VmConfig structures. 945 let (result_vm_config, _) = compare_vm_config_cli_vs_json(&cli, openapi, true); 946 947 // As a second step, we validate all the default values. 948 let expected_vm_config = VmConfig { 949 cpus: CpusConfig { 950 boot_vcpus: 1, 951 max_vcpus: 1, 952 topology: None, 953 kvm_hyperv: false, 954 max_phys_bits: 46, 955 affinity: None, 956 features: CpuFeatures::default(), 957 }, 958 memory: MemoryConfig { 959 size: 536_870_912, 960 mergeable: false, 961 hotplug_method: HotplugMethod::Acpi, 962 hotplug_size: None, 963 hotplugged_size: None, 964 shared: false, 965 hugepages: false, 966 hugepage_size: None, 967 prefault: false, 968 zones: None, 969 thp: true, 970 }, 971 payload: Some(PayloadConfig { 972 kernel: Some(PathBuf::from("/path/to/kernel")), 973 firmware: None, 974 cmdline: None, 975 initramfs: None, 976 #[cfg(feature = "igvm")] 977 igvm: None, 978 #[cfg(feature = "sev_snp")] 979 host_data: None, 980 }), 981 rate_limit_groups: None, 982 disks: None, 983 net: None, 984 rng: RngConfig { 985 src: PathBuf::from("/dev/urandom"), 986 iommu: false, 987 }, 988 balloon: None, 989 fs: None, 990 pmem: None, 991 serial: ConsoleConfig { 992 file: None, 993 mode: ConsoleOutputMode::Null, 994 iommu: false, 995 socket: None, 996 }, 997 console: ConsoleConfig { 998 file: None, 999 mode: ConsoleOutputMode::Tty, 1000 iommu: false, 1001 socket: None, 1002 }, 1003 #[cfg(target_arch = "x86_64")] 1004 debug_console: DebugConsoleConfig::default(), 1005 devices: None, 1006 user_devices: None, 1007 vdpa: None, 1008 vsock: None, 1009 pvpanic: false, 1010 #[cfg(feature = "pvmemcontrol")] 1011 pvmemcontrol: None, 1012 iommu: false, 1013 #[cfg(target_arch = "x86_64")] 1014 sgx_epc: None, 1015 numa: None, 1016 watchdog: false, 1017 #[cfg(feature = "guest_debug")] 1018 gdb: false, 1019 pci_segments: None, 1020 platform: None, 1021 tpm: None, 1022 preserved_fds: None, 1023 landlock_enable: false, 1024 landlock_rules: None, 1025 }; 1026 1027 assert_eq!(expected_vm_config, result_vm_config); 1028 } 1029 1030 #[test] 1031 fn test_valid_vm_config_cpus() { 1032 [ 1033 ( 1034 vec![ 1035 "cloud-hypervisor", 1036 "--kernel", 1037 "/path/to/kernel", 1038 "--cpus", 1039 "boot=1", 1040 ], 1041 r#"{ 1042 "payload": {"kernel": "/path/to/kernel"}, 1043 "cpus": {"boot_vcpus": 1, "max_vcpus": 1} 1044 }"#, 1045 true, 1046 ), 1047 ( 1048 vec![ 1049 "cloud-hypervisor", 1050 "--kernel", 1051 "/path/to/kernel", 1052 "--cpus", 1053 "boot=1,max=3", 1054 ], 1055 r#"{ 1056 "payload": {"kernel": "/path/to/kernel"}, 1057 "cpus": {"boot_vcpus": 1, "max_vcpus": 3} 1058 }"#, 1059 true, 1060 ), 1061 ( 1062 vec![ 1063 "cloud-hypervisor", 1064 "--kernel", 1065 "/path/to/kernel", 1066 "--cpus", 1067 "boot=2,max=4", 1068 ], 1069 r#"{ 1070 "payload": {"kernel": "/path/to/kernel"}, 1071 "cpus": {"boot_vcpus": 1, "max_vcpus": 3} 1072 }"#, 1073 false, 1074 ), 1075 ] 1076 .iter() 1077 .for_each(|(cli, openapi, equal)| { 1078 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1079 }); 1080 } 1081 1082 #[test] 1083 fn test_valid_vm_config_memory() { 1084 vec![ 1085 ( 1086 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1073741824"], 1087 r#"{ 1088 "payload": {"kernel": "/path/to/kernel"}, 1089 "memory": {"size": 1073741824} 1090 }"#, 1091 true, 1092 ), 1093 ( 1094 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1G"], 1095 r#"{ 1096 "payload": {"kernel": "/path/to/kernel"}, 1097 "memory": {"size": 1073741824} 1098 }"#, 1099 true, 1100 ), 1101 ( 1102 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1G,mergeable=on"], 1103 r#"{ 1104 "payload": {"kernel": "/path/to/kernel"}, 1105 "memory": {"size": 1073741824, "mergeable": true} 1106 }"#, 1107 true, 1108 ), 1109 ( 1110 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1G,mergeable=off"], 1111 r#"{ 1112 "payload": {"kernel": "/path/to/kernel"}, 1113 "memory": {"size": 1073741824, "mergeable": false} 1114 }"#, 1115 true, 1116 ), 1117 ( 1118 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1G,mergeable=on"], 1119 r#"{ 1120 "payload": {"kernel": "/path/to/kernel"}, 1121 "memory": {"size": 1073741824, "mergeable": false} 1122 }"#, 1123 false, 1124 ), 1125 ( 1126 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1G,hotplug_size=1G"], 1127 r#"{ 1128 "payload": {"kernel": "/path/to/kernel"}, 1129 "memory": {"size": 1073741824, "hotplug_method": "Acpi", "hotplug_size": 1073741824} 1130 }"#, 1131 true, 1132 ), 1133 ( 1134 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1G,hotplug_method=virtio-mem,hotplug_size=1G"], 1135 r#"{ 1136 "payload": {"kernel": "/path/to/kernel"}, 1137 "memory": {"size": 1073741824, "hotplug_method": "VirtioMem", "hotplug_size": 1073741824} 1138 }"#, 1139 true, 1140 ), 1141 ] 1142 .iter() 1143 .for_each(|(cli, openapi, equal)| { 1144 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1145 }); 1146 } 1147 1148 #[test] 1149 fn test_valid_vm_config_kernel() { 1150 [( 1151 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel"], 1152 r#"{ 1153 "payload": {"kernel": "/path/to/kernel"} 1154 }"#, 1155 true, 1156 )] 1157 .iter() 1158 .for_each(|(cli, openapi, equal)| { 1159 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1160 }); 1161 } 1162 1163 #[test] 1164 fn test_valid_vm_config_cmdline() { 1165 [( 1166 vec![ 1167 "cloud-hypervisor", 1168 "--kernel", 1169 "/path/to/kernel", 1170 "--cmdline", 1171 "arg1=foo arg2=bar", 1172 ], 1173 r#"{ 1174 "payload": {"kernel": "/path/to/kernel", "cmdline": "arg1=foo arg2=bar"} 1175 }"#, 1176 true, 1177 )] 1178 .iter() 1179 .for_each(|(cli, openapi, equal)| { 1180 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1181 }); 1182 } 1183 1184 #[test] 1185 fn test_valid_vm_config_disks() { 1186 [ 1187 ( 1188 vec![ 1189 "cloud-hypervisor", 1190 "--kernel", 1191 "/path/to/kernel", 1192 "--disk", 1193 "path=/path/to/disk/1", 1194 "path=/path/to/disk/2", 1195 ], 1196 r#"{ 1197 "payload": {"kernel": "/path/to/kernel"}, 1198 "disks": [ 1199 {"path": "/path/to/disk/1"}, 1200 {"path": "/path/to/disk/2"} 1201 ] 1202 }"#, 1203 true, 1204 ), 1205 ( 1206 vec![ 1207 "cloud-hypervisor", 1208 "--kernel", 1209 "/path/to/kernel", 1210 "--disk", 1211 "path=/path/to/disk/1", 1212 "path=/path/to/disk/2", 1213 ], 1214 r#"{ 1215 "payload": {"kernel": "/path/to/kernel"}, 1216 "disks": [ 1217 {"path": "/path/to/disk/1"} 1218 ] 1219 }"#, 1220 false, 1221 ), 1222 ( 1223 vec![ 1224 "cloud-hypervisor", 1225 "--kernel", 1226 "/path/to/kernel", 1227 "--memory", 1228 "shared=true", 1229 "--disk", 1230 "vhost_user=true,socket=/tmp/sock1", 1231 ], 1232 r#"{ 1233 "payload": {"kernel": "/path/to/kernel"}, 1234 "memory" : { "shared": true, "size": 536870912 }, 1235 "disks": [ 1236 {"vhost_user":true, "vhost_socket":"/tmp/sock1"} 1237 ] 1238 }"#, 1239 true, 1240 ), 1241 ( 1242 vec![ 1243 "cloud-hypervisor", 1244 "--kernel", 1245 "/path/to/kernel", 1246 "--memory", 1247 "shared=true", 1248 "--disk", 1249 "vhost_user=true,socket=/tmp/sock1", 1250 ], 1251 r#"{ 1252 "payload": {"kernel": "/path/to/kernel"}, 1253 "memory" : { "shared": true, "size": 536870912 }, 1254 "disks": [ 1255 {"vhost_user":true, "vhost_socket":"/tmp/sock1"} 1256 ] 1257 }"#, 1258 true, 1259 ), 1260 ( 1261 vec![ 1262 "cloud-hypervisor", 1263 "--kernel", 1264 "/path/to/kernel", 1265 "--disk", 1266 "path=/path/to/disk/1,rate_limit_group=group0", 1267 "path=/path/to/disk/2,rate_limit_group=group0", 1268 "--rate-limit-group", 1269 "id=group0,bw_size=1000,bw_refill_time=100", 1270 ], 1271 r#"{ 1272 "payload": {"kernel": "/path/to/kernel"}, 1273 "disks": [ 1274 {"path": "/path/to/disk/1", "rate_limit_group": "group0"}, 1275 {"path": "/path/to/disk/2", "rate_limit_group": "group0"} 1276 ], 1277 "rate_limit_groups": [ 1278 {"id": "group0", "rate_limiter_config": {"bandwidth": {"size": 1000, "one_time_burst": 0, "refill_time": 100}}} 1279 ] 1280 }"#, 1281 true, 1282 ), 1283 ] 1284 .iter() 1285 .for_each(|(cli, openapi, equal)| { 1286 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1287 }); 1288 } 1289 1290 #[test] 1291 fn test_valid_vm_config_net() { 1292 vec![ 1293 // This test is expected to fail because the default MAC address is 1294 // randomly generated. There's no way we can have twice the same 1295 // default value. 1296 ( 1297 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--net", "mac="], 1298 r#"{ 1299 "payload": {"kernel": "/path/to/kernel"}, 1300 "net": [] 1301 }"#, 1302 false, 1303 ), 1304 ( 1305 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--net", "mac=12:34:56:78:90:ab,host_mac=34:56:78:90:ab:cd"], 1306 r#"{ 1307 "payload": {"kernel": "/path/to/kernel"}, 1308 "net": [ 1309 {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd"} 1310 ] 1311 }"#, 1312 true, 1313 ), 1314 ( 1315 vec![ 1316 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1317 "--net", 1318 "mac=12:34:56:78:90:ab,host_mac=34:56:78:90:ab:cd,tap=tap0", 1319 ], 1320 r#"{ 1321 "payload": {"kernel": "/path/to/kernel"}, 1322 "net": [ 1323 {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd", "tap": "tap0"} 1324 ] 1325 }"#, 1326 true, 1327 ), 1328 ( 1329 vec![ 1330 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1331 "--net", 1332 "mac=12:34:56:78:90:ab,host_mac=34:56:78:90:ab:cd,tap=tap0,ip=1.2.3.4", 1333 ], 1334 r#"{ 1335 "payload": {"kernel": "/path/to/kernel"}, 1336 "net": [ 1337 {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd", "tap": "tap0", "ip": "1.2.3.4"} 1338 ] 1339 }"#, 1340 true, 1341 ), 1342 ( 1343 vec![ 1344 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1345 "--net", 1346 "mac=12:34:56:78:90:ab,host_mac=34:56:78:90:ab:cd,tap=tap0,ip=1.2.3.4,mask=5.6.7.8", 1347 ], 1348 r#"{ 1349 "payload": {"kernel": "/path/to/kernel"}, 1350 "net": [ 1351 {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd", "tap": "tap0", "ip": "1.2.3.4", "mask": "5.6.7.8"} 1352 ] 1353 }"#, 1354 true, 1355 ), 1356 ( 1357 vec![ 1358 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1359 "--cpus", "boot=2", 1360 "--net", 1361 "mac=12:34:56:78:90:ab,host_mac=34:56:78:90:ab:cd,tap=tap0,ip=1.2.3.4,mask=5.6.7.8,num_queues=4", 1362 ], 1363 r#"{ 1364 "payload": {"kernel": "/path/to/kernel"}, 1365 "cpus": {"boot_vcpus": 2, "max_vcpus": 2}, 1366 "net": [ 1367 {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd", "tap": "tap0", "ip": "1.2.3.4", "mask": "5.6.7.8", "num_queues": 4} 1368 ] 1369 }"#, 1370 true, 1371 ), 1372 ( 1373 vec![ 1374 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1375 "--cpus", "boot=2", 1376 "--net", 1377 "mac=12:34:56:78:90:ab,host_mac=34:56:78:90:ab:cd,tap=tap0,ip=1.2.3.4,mask=5.6.7.8,num_queues=4,queue_size=128", 1378 ], 1379 r#"{ 1380 "payload": {"kernel": "/path/to/kernel"}, 1381 "cpus": {"boot_vcpus": 2, "max_vcpus": 2}, 1382 "net": [ 1383 {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd", "tap": "tap0", "ip": "1.2.3.4", "mask": "5.6.7.8", "num_queues": 4, "queue_size": 128} 1384 ] 1385 }"#, 1386 true, 1387 ), 1388 ( 1389 vec![ 1390 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1391 "--net", 1392 "mac=12:34:56:78:90:ab,host_mac=34:56:78:90:ab:cd,tap=tap0,ip=1.2.3.4,mask=5.6.7.8,num_queues=2,queue_size=256", 1393 ], 1394 r#"{ 1395 "payload": {"kernel": "/path/to/kernel"}, 1396 "net": [ 1397 {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd", "tap": "tap0", "ip": "1.2.3.4", "mask": "5.6.7.8"} 1398 ] 1399 }"#, 1400 true, 1401 ), 1402 ( 1403 vec![ 1404 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1405 "--net", 1406 "mac=12:34:56:78:90:ab,host_mac=34:56:78:90:ab:cd,tap=tap0,ip=1.2.3.4,mask=5.6.7.8", 1407 ], 1408 r#"{ 1409 "payload": {"kernel": "/path/to/kernel"}, 1410 "net": [ 1411 {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd", "tap": "tap0", "ip": "1.2.3.4", "mask": "5.6.7.8", "num_queues": 2, "queue_size": 256} 1412 ] 1413 }"#, 1414 true, 1415 ), 1416 #[cfg(target_arch = "x86_64")] 1417 ( 1418 vec![ 1419 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1420 "--net", 1421 "mac=12:34:56:78:90:ab,host_mac=34:56:78:90:ab:cd,tap=tap0,ip=1.2.3.4,mask=5.6.7.8,num_queues=2,queue_size=256,iommu=on", 1422 ], 1423 r#"{ 1424 "payload": {"kernel": "/path/to/kernel"}, 1425 "net": [ 1426 {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd", "tap": "tap0", "ip": "1.2.3.4", "mask": "5.6.7.8", "num_queues": 2, "queue_size": 256, "iommu": true} 1427 ] 1428 }"#, 1429 false, 1430 ), 1431 #[cfg(target_arch = "x86_64")] 1432 ( 1433 vec![ 1434 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1435 "--net", 1436 "mac=12:34:56:78:90:ab,host_mac=34:56:78:90:ab:cd,tap=tap0,ip=1.2.3.4,mask=5.6.7.8,num_queues=2,queue_size=256,iommu=on", 1437 ], 1438 r#"{ 1439 "payload": {"kernel": "/path/to/kernel"}, 1440 "net": [ 1441 {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd", "tap": "tap0", "ip": "1.2.3.4", "mask": "5.6.7.8", "num_queues": 2, "queue_size": 256, "iommu": true} 1442 ], 1443 "iommu": true 1444 }"#, 1445 true, 1446 ), 1447 ( 1448 vec![ 1449 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1450 "--net", 1451 "mac=12:34:56:78:90:ab,host_mac=34:56:78:90:ab:cd,tap=tap0,ip=1.2.3.4,mask=5.6.7.8,num_queues=2,queue_size=256,iommu=off", 1452 ], 1453 r#"{ 1454 "payload": {"kernel": "/path/to/kernel"}, 1455 "net": [ 1456 {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd", "tap": "tap0", "ip": "1.2.3.4", "mask": "5.6.7.8", "num_queues": 2, "queue_size": 256, "iommu": false} 1457 ] 1458 }"#, 1459 true, 1460 ), 1461 ( 1462 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "shared=true", "--net", "mac=12:34:56:78:90:ab,host_mac=34:56:78:90:ab:cd,vhost_user=true,socket=/tmp/sock"], 1463 r#"{ 1464 "payload": {"kernel": "/path/to/kernel"}, 1465 "memory" : { "shared": true, "size": 536870912 }, 1466 "net": [ 1467 {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd", "vhost_user": true, "vhost_socket": "/tmp/sock"} 1468 ] 1469 }"#, 1470 true, 1471 ), 1472 ] 1473 .iter() 1474 .for_each(|(cli, openapi, equal)| { 1475 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1476 }); 1477 } 1478 1479 #[test] 1480 fn test_valid_vm_config_rng() { 1481 [( 1482 vec![ 1483 "cloud-hypervisor", 1484 "--kernel", 1485 "/path/to/kernel", 1486 "--rng", 1487 "src=/path/to/entropy/source", 1488 ], 1489 r#"{ 1490 "payload": {"kernel": "/path/to/kernel"}, 1491 "rng": {"src": "/path/to/entropy/source"} 1492 }"#, 1493 true, 1494 )] 1495 .iter() 1496 .for_each(|(cli, openapi, equal)| { 1497 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1498 }); 1499 } 1500 1501 #[test] 1502 fn test_valid_vm_config_fs() { 1503 [( 1504 vec![ 1505 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1506 "--memory", "shared=true", 1507 "--fs", 1508 "tag=virtiofs1,socket=/path/to/sock1", 1509 "tag=virtiofs2,socket=/path/to/sock2", 1510 ], 1511 r#"{ 1512 "payload": {"kernel": "/path/to/kernel"}, 1513 "memory" : { "shared": true, "size": 536870912 }, 1514 "fs": [ 1515 {"tag": "virtiofs1", "socket": "/path/to/sock1"}, 1516 {"tag": "virtiofs2", "socket": "/path/to/sock2"} 1517 ] 1518 }"#, 1519 true, 1520 ), 1521 ( 1522 vec![ 1523 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1524 "--memory", "shared=true", 1525 "--fs", 1526 "tag=virtiofs1,socket=/path/to/sock1", 1527 "tag=virtiofs2,socket=/path/to/sock2", 1528 ], 1529 r#"{ 1530 "payload": {"kernel": "/path/to/kernel"}, 1531 "memory" : { "shared": true, "size": 536870912 }, 1532 "fs": [ 1533 {"tag": "virtiofs1", "socket": "/path/to/sock1"} 1534 ] 1535 }"#, 1536 false, 1537 ), 1538 ( 1539 vec![ 1540 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1541 "--memory", "shared=true", "--cpus", "boot=4", 1542 "--fs", 1543 "tag=virtiofs1,socket=/path/to/sock1,num_queues=4", 1544 ], 1545 r#"{ 1546 "payload": {"kernel": "/path/to/kernel"}, 1547 "memory" : { "shared": true, "size": 536870912 }, 1548 "cpus": {"boot_vcpus": 4, "max_vcpus": 4}, 1549 "fs": [ 1550 {"tag": "virtiofs1", "socket": "/path/to/sock1", "num_queues": 4} 1551 ] 1552 }"#, 1553 true, 1554 ), 1555 ( 1556 vec![ 1557 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1558 "--memory", "shared=true", "--cpus", "boot=4", 1559 "--fs", 1560 "tag=virtiofs1,socket=/path/to/sock1,num_queues=4,queue_size=128" 1561 ], 1562 r#"{ 1563 "payload": {"kernel": "/path/to/kernel"}, 1564 "memory" : { "shared": true, "size": 536870912 }, 1565 "cpus": {"boot_vcpus": 4, "max_vcpus": 4}, 1566 "fs": [ 1567 {"tag": "virtiofs1", "socket": "/path/to/sock1", "num_queues": 4, "queue_size": 128} 1568 ] 1569 }"#, 1570 true, 1571 )] 1572 .iter() 1573 .for_each(|(cli, openapi, equal)| { 1574 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1575 }); 1576 } 1577 1578 #[test] 1579 fn test_valid_vm_config_pmem() { 1580 [ 1581 ( 1582 vec![ 1583 "cloud-hypervisor", 1584 "--kernel", 1585 "/path/to/kernel", 1586 "--pmem", 1587 "file=/path/to/img/1,size=1G", 1588 "file=/path/to/img/2,size=2G", 1589 ], 1590 r#"{ 1591 "payload": {"kernel": "/path/to/kernel"}, 1592 "pmem": [ 1593 {"file": "/path/to/img/1", "size": 1073741824}, 1594 {"file": "/path/to/img/2", "size": 2147483648} 1595 ] 1596 }"#, 1597 true, 1598 ), 1599 #[cfg(target_arch = "x86_64")] 1600 ( 1601 vec![ 1602 "cloud-hypervisor", 1603 "--kernel", 1604 "/path/to/kernel", 1605 "--pmem", 1606 "file=/path/to/img/1,size=1G,iommu=on", 1607 ], 1608 r#"{ 1609 "payload": {"kernel": "/path/to/kernel"}, 1610 "pmem": [ 1611 {"file": "/path/to/img/1", "size": 1073741824, "iommu": true} 1612 ], 1613 "iommu": true 1614 }"#, 1615 true, 1616 ), 1617 #[cfg(target_arch = "x86_64")] 1618 ( 1619 vec![ 1620 "cloud-hypervisor", 1621 "--kernel", 1622 "/path/to/kernel", 1623 "--pmem", 1624 "file=/path/to/img/1,size=1G,iommu=on", 1625 ], 1626 r#"{ 1627 "payload": {"kernel": "/path/to/kernel"}, 1628 "pmem": [ 1629 {"file": "/path/to/img/1", "size": 1073741824, "iommu": true} 1630 ] 1631 }"#, 1632 false, 1633 ), 1634 ] 1635 .iter() 1636 .for_each(|(cli, openapi, equal)| { 1637 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1638 }); 1639 } 1640 1641 #[cfg(target_arch = "x86_64")] 1642 #[test] 1643 fn test_valid_vm_config_debug_console() { 1644 [( 1645 vec![ 1646 "cloud-hypervisor", 1647 "--kernel", 1648 "/path/to/kernel", 1649 "--debug-console", 1650 "tty,iobase=0xe9", 1651 ], 1652 // 233 == 0xe9 1653 r#"{ 1654 "payload": {"kernel": "/path/to/kernel" }, 1655 "debug_console": {"mode": "Tty", "iobase": 233 } 1656 }"#, 1657 true, 1658 )] 1659 .iter() 1660 .for_each(|(cli, openapi, equal)| { 1661 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1662 }); 1663 } 1664 1665 #[test] 1666 fn test_valid_vm_config_serial_console() { 1667 [ 1668 ( 1669 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel"], 1670 r#"{ 1671 "payload": {"kernel": "/path/to/kernel"}, 1672 "serial": {"mode": "Null"}, 1673 "console": {"mode": "Tty"} 1674 }"#, 1675 true, 1676 ), 1677 ( 1678 vec![ 1679 "cloud-hypervisor", 1680 "--kernel", 1681 "/path/to/kernel", 1682 "--serial", 1683 "null", 1684 "--console", 1685 "tty", 1686 ], 1687 r#"{ 1688 "payload": {"kernel": "/path/to/kernel"} 1689 }"#, 1690 true, 1691 ), 1692 ( 1693 vec![ 1694 "cloud-hypervisor", 1695 "--kernel", 1696 "/path/to/kernel", 1697 "--serial", 1698 "tty", 1699 "--console", 1700 "off", 1701 ], 1702 r#"{ 1703 "payload": {"kernel": "/path/to/kernel"}, 1704 "serial": {"mode": "Tty"}, 1705 "console": {"mode": "Off"} 1706 }"#, 1707 true, 1708 ), 1709 ] 1710 .iter() 1711 .for_each(|(cli, openapi, equal)| { 1712 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1713 }); 1714 } 1715 1716 #[test] 1717 fn test_valid_vm_config_serial_pty_console_pty() { 1718 [ 1719 ( 1720 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel"], 1721 r#"{ 1722 "payload": {"kernel": "/path/to/kernel"}, 1723 "serial": {"mode": "Null"}, 1724 "console": {"mode": "Tty"} 1725 }"#, 1726 true, 1727 ), 1728 ( 1729 vec![ 1730 "cloud-hypervisor", 1731 "--kernel", 1732 "/path/to/kernel", 1733 "--serial", 1734 "null", 1735 "--console", 1736 "tty", 1737 ], 1738 r#"{ 1739 "payload": {"kernel": "/path/to/kernel"} 1740 }"#, 1741 true, 1742 ), 1743 ( 1744 vec![ 1745 "cloud-hypervisor", 1746 "--kernel", 1747 "/path/to/kernel", 1748 "--serial", 1749 "pty", 1750 "--console", 1751 "pty", 1752 ], 1753 r#"{ 1754 "payload": {"kernel": "/path/to/kernel"}, 1755 "serial": {"mode": "Pty"}, 1756 "console": {"mode": "Pty"} 1757 }"#, 1758 true, 1759 ), 1760 ] 1761 .iter() 1762 .for_each(|(cli, openapi, equal)| { 1763 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1764 }); 1765 } 1766 1767 #[test] 1768 #[cfg(target_arch = "x86_64")] 1769 fn test_valid_vm_config_devices() { 1770 vec![ 1771 ( 1772 vec![ 1773 "cloud-hypervisor", 1774 "--kernel", 1775 "/path/to/kernel", 1776 "--device", 1777 "path=/path/to/device/1", 1778 "path=/path/to/device/2", 1779 ], 1780 r#"{ 1781 "payload": {"kernel": "/path/to/kernel"}, 1782 "devices": [ 1783 {"path": "/path/to/device/1"}, 1784 {"path": "/path/to/device/2"} 1785 ] 1786 }"#, 1787 true, 1788 ), 1789 ( 1790 vec![ 1791 "cloud-hypervisor", 1792 "--kernel", 1793 "/path/to/kernel", 1794 "--device", 1795 "path=/path/to/device/1", 1796 "path=/path/to/device/2", 1797 ], 1798 r#"{ 1799 "payload": {"kernel": "/path/to/kernel"}, 1800 "devices": [ 1801 {"path": "/path/to/device/1"} 1802 ] 1803 }"#, 1804 false, 1805 ), 1806 ( 1807 vec![ 1808 "cloud-hypervisor", 1809 "--kernel", 1810 "/path/to/kernel", 1811 "--device", 1812 "path=/path/to/device,iommu=on", 1813 ], 1814 r#"{ 1815 "payload": {"kernel": "/path/to/kernel"}, 1816 "devices": [ 1817 {"path": "/path/to/device", "iommu": true} 1818 ], 1819 "iommu": true 1820 }"#, 1821 true, 1822 ), 1823 ( 1824 vec![ 1825 "cloud-hypervisor", 1826 "--kernel", 1827 "/path/to/kernel", 1828 "--device", 1829 "path=/path/to/device,iommu=on", 1830 ], 1831 r#"{ 1832 "payload": {"kernel": "/path/to/kernel"}, 1833 "devices": [ 1834 {"path": "/path/to/device", "iommu": true} 1835 ] 1836 }"#, 1837 false, 1838 ), 1839 ( 1840 vec![ 1841 "cloud-hypervisor", 1842 "--kernel", 1843 "/path/to/kernel", 1844 "--device", 1845 "path=/path/to/device,iommu=off", 1846 ], 1847 r#"{ 1848 "payload": {"kernel": "/path/to/kernel"}, 1849 "devices": [ 1850 {"path": "/path/to/device", "iommu": false} 1851 ] 1852 }"#, 1853 true, 1854 ), 1855 ] 1856 .iter() 1857 .for_each(|(cli, openapi, equal)| { 1858 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1859 }); 1860 } 1861 1862 #[test] 1863 fn test_valid_vm_config_vdpa() { 1864 [ 1865 ( 1866 vec![ 1867 "cloud-hypervisor", 1868 "--kernel", 1869 "/path/to/kernel", 1870 "--vdpa", 1871 "path=/path/to/device/1", 1872 "path=/path/to/device/2,num_queues=2", 1873 ], 1874 r#"{ 1875 "payload": {"kernel": "/path/to/kernel"}, 1876 "vdpa": [ 1877 {"path": "/path/to/device/1", "num_queues": 1}, 1878 {"path": "/path/to/device/2", "num_queues": 2} 1879 ] 1880 }"#, 1881 true, 1882 ), 1883 ( 1884 vec![ 1885 "cloud-hypervisor", 1886 "--kernel", 1887 "/path/to/kernel", 1888 "--vdpa", 1889 "path=/path/to/device/1", 1890 "path=/path/to/device/2", 1891 ], 1892 r#"{ 1893 "payload": {"kernel": "/path/to/kernel"}, 1894 "vdpa": [ 1895 {"path": "/path/to/device/1"} 1896 ] 1897 }"#, 1898 false, 1899 ), 1900 ] 1901 .iter() 1902 .for_each(|(cli, openapi, equal)| { 1903 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1904 }); 1905 } 1906 1907 #[test] 1908 fn test_valid_vm_config_vsock() { 1909 [ 1910 ( 1911 vec![ 1912 "cloud-hypervisor", 1913 "--kernel", 1914 "/path/to/kernel", 1915 "--vsock", 1916 "cid=123,socket=/path/to/sock/1", 1917 ], 1918 r#"{ 1919 "payload": {"kernel": "/path/to/kernel"}, 1920 "vsock": {"cid": 123, "socket": "/path/to/sock/1"} 1921 }"#, 1922 true, 1923 ), 1924 ( 1925 vec![ 1926 "cloud-hypervisor", 1927 "--kernel", 1928 "/path/to/kernel", 1929 "--vsock", 1930 "cid=124,socket=/path/to/sock/1", 1931 ], 1932 r#"{ 1933 "payload": {"kernel": "/path/to/kernel"}, 1934 "vsock": {"cid": 123, "socket": "/path/to/sock/1"} 1935 }"#, 1936 false, 1937 ), 1938 #[cfg(target_arch = "x86_64")] 1939 ( 1940 vec![ 1941 "cloud-hypervisor", 1942 "--kernel", 1943 "/path/to/kernel", 1944 "--vsock", 1945 "cid=123,socket=/path/to/sock/1,iommu=on", 1946 ], 1947 r#"{ 1948 "payload": {"kernel": "/path/to/kernel"}, 1949 "vsock": {"cid": 123, "socket": "/path/to/sock/1", "iommu": true}, 1950 "iommu": true 1951 }"#, 1952 true, 1953 ), 1954 #[cfg(target_arch = "x86_64")] 1955 ( 1956 vec![ 1957 "cloud-hypervisor", 1958 "--kernel", 1959 "/path/to/kernel", 1960 "--vsock", 1961 "cid=123,socket=/path/to/sock/1,iommu=on", 1962 ], 1963 r#"{ 1964 "payload": {"kernel": "/path/to/kernel"}, 1965 "vsock": {"cid": 123, "socket": "/path/to/sock/1", "iommu": true} 1966 }"#, 1967 false, 1968 ), 1969 ( 1970 vec![ 1971 "cloud-hypervisor", 1972 "--kernel", 1973 "/path/to/kernel", 1974 "--vsock", 1975 "cid=123,socket=/path/to/sock/1,iommu=off", 1976 ], 1977 r#"{ 1978 "payload": {"kernel": "/path/to/kernel"}, 1979 "vsock": {"cid": 123, "socket": "/path/to/sock/1", "iommu": false} 1980 }"#, 1981 true, 1982 ), 1983 ] 1984 .iter() 1985 .for_each(|(cli, openapi, equal)| { 1986 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1987 }); 1988 } 1989 1990 #[test] 1991 fn test_valid_vm_config_tpm_socket() { 1992 [( 1993 vec![ 1994 "cloud-hypervisor", 1995 "--kernel", 1996 "/path/to/kernel", 1997 "--tpm", 1998 "socket=/path/to/tpm/sock", 1999 ], 2000 r#"{ 2001 "payload": {"kernel": "/path/to/kernel"}, 2002 "tpm": {"socket": "/path/to/tpm/sock"} 2003 }"#, 2004 true, 2005 )] 2006 .iter() 2007 .for_each(|(cli, openapi, equal)| { 2008 compare_vm_config_cli_vs_json(cli, openapi, *equal); 2009 }); 2010 } 2011 2012 // TODO the check for the option list being sorted could be moved into the 2013 // getter itself, when the getter becomes a const function. This however 2014 // needs more support by Rust (as of March 2025). 2015 #[test] 2016 fn test_cli_options_sorted() { 2017 let (default_vcpus, default_memory, default_rng) = prepare_default_values(); 2018 let args = get_cli_options_sorted(default_vcpus, default_memory, default_rng); 2019 2020 assert_args_sorted(|| args.iter()) 2021 } 2022 } 2023