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