1 // Copyright © 2019 Intel Corporation 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 // 5 6 #[macro_use] 7 extern crate event_monitor; 8 9 use argh::FromArgs; 10 use libc::EFD_NONBLOCK; 11 use log::{warn, LevelFilter}; 12 use option_parser::OptionParser; 13 use seccompiler::SeccompAction; 14 use signal_hook::consts::SIGSYS; 15 use std::env; 16 use std::fs::File; 17 use std::os::unix::io::{FromRawFd, RawFd}; 18 use std::sync::mpsc::channel; 19 use std::sync::{Arc, Mutex}; 20 use thiserror::Error; 21 #[cfg(feature = "dbus_api")] 22 use vmm::api::dbus::{dbus_api_graceful_shutdown, DBusApiOptions}; 23 use vmm::config; 24 use vmm_sys_util::eventfd::EventFd; 25 use vmm_sys_util::signal::block_signal; 26 27 #[cfg(feature = "dhat-heap")] 28 #[global_allocator] 29 static ALLOC: dhat::Alloc = dhat::Alloc; 30 31 #[derive(Error, Debug)] 32 enum Error { 33 #[error("Failed to create API EventFd: {0}")] 34 CreateApiEventFd(#[source] std::io::Error), 35 #[cfg(feature = "guest_debug")] 36 #[error("Failed to create Debug EventFd: {0}")] 37 CreateDebugEventFd(#[source] std::io::Error), 38 #[error("Failed to create exit EventFd: {0}")] 39 CreateExitEventFd(#[source] std::io::Error), 40 #[error("Failed to open hypervisor interface (is hypervisor interface available?): {0}")] 41 CreateHypervisor(#[source] hypervisor::HypervisorError), 42 #[error("Failed to start the VMM thread: {0}")] 43 StartVmmThread(#[source] vmm::Error), 44 #[error("Error parsing config: {0}")] 45 ParsingConfig(vmm::config::Error), 46 #[error("Error creating VM: {0:?}")] 47 VmCreate(vmm::api::ApiError), 48 #[error("Error booting VM: {0:?}")] 49 VmBoot(vmm::api::ApiError), 50 #[error("Error restoring VM: {0:?}")] 51 VmRestore(vmm::api::ApiError), 52 #[error("Error parsing restore: {0}")] 53 ParsingRestore(vmm::config::Error), 54 #[error("Failed to join on VMM thread: {0:?}")] 55 ThreadJoin(std::boxed::Box<dyn std::any::Any + std::marker::Send>), 56 #[error("VMM thread exited with error: {0}")] 57 VmmThread(#[source] vmm::Error), 58 #[error("Error parsing --api-socket: {0}")] 59 ParsingApiSocket(std::num::ParseIntError), 60 #[error("Error parsing --event-monitor: {0}")] 61 ParsingEventMonitor(option_parser::OptionParserError), 62 #[cfg(feature = "dbus_api")] 63 #[error("`--dbus-object-path` option isn't provided")] 64 MissingDBusObjectPath, 65 #[cfg(feature = "dbus_api")] 66 #[error("`--dbus-service-name` option isn't provided")] 67 MissingDBusServiceName, 68 #[error("Error parsing --event-monitor: path or fd required")] 69 BareEventMonitor, 70 #[error("Error doing event monitor I/O: {0}")] 71 EventMonitorIo(std::io::Error), 72 #[error("Event monitor thread failed: {0}")] 73 EventMonitorThread(#[source] vmm::Error), 74 #[cfg(feature = "guest_debug")] 75 #[error("Error parsing --gdb: {0}")] 76 ParsingGdb(option_parser::OptionParserError), 77 #[cfg(feature = "guest_debug")] 78 #[error("Error parsing --gdb: path required")] 79 BareGdb, 80 #[error("Error creating log file: {0}")] 81 LogFileCreation(std::io::Error), 82 #[error("Error setting up logger: {0}")] 83 LoggerSetup(log::SetLoggerError), 84 } 85 86 struct Logger { 87 output: Mutex<Box<dyn std::io::Write + Send>>, 88 start: std::time::Instant, 89 } 90 91 impl log::Log for Logger { 92 fn enabled(&self, _metadata: &log::Metadata) -> bool { 93 true 94 } 95 96 fn log(&self, record: &log::Record) { 97 if !self.enabled(record.metadata()) { 98 return; 99 } 100 101 let now = std::time::Instant::now(); 102 let duration = now.duration_since(self.start); 103 104 if record.file().is_some() && record.line().is_some() { 105 write!( 106 *(*(self.output.lock().unwrap())), 107 "cloud-hypervisor: {:.6?}: <{}> {}:{}:{} -- {}\r\n", 108 duration, 109 std::thread::current().name().unwrap_or("anonymous"), 110 record.level(), 111 record.file().unwrap(), 112 record.line().unwrap(), 113 record.args() 114 ) 115 } else { 116 write!( 117 *(*(self.output.lock().unwrap())), 118 "cloud-hypervisor: {:.6?}: <{}> {}:{} -- {}\r\n", 119 duration, 120 std::thread::current().name().unwrap_or("anonymous"), 121 record.level(), 122 record.target(), 123 record.args() 124 ) 125 } 126 .ok(); 127 } 128 fn flush(&self) {} 129 } 130 131 fn default_vcpus() -> String { 132 format!( 133 "boot={},max_phys_bits={}", 134 config::DEFAULT_VCPUS, 135 config::DEFAULT_MAX_PHYS_BITS 136 ) 137 } 138 139 fn default_memory() -> String { 140 format!("size={}M", config::DEFAULT_MEMORY_MB) 141 } 142 143 fn default_rng() -> String { 144 format!("src={}", config::DEFAULT_RNG_SOURCE) 145 } 146 147 #[derive(FromArgs)] 148 /// Launch a cloud-hypervisor VMM. 149 pub struct TopLevel { 150 #[argh(option, long = "cpus", default = "default_vcpus()")] 151 /// boot=<boot_vcpus>, max=<max_vcpus>, topology=<threads_per_core>:<cores_per_die>:<dies_per_package>:<packages>, kvm_hyperv=on|off, max_phys_bits=<maximum_number_of_physical_bits>, affinity=<list_of_vcpus_with_their_associated_cpuset>, features=<list_of_features_to_enable> 152 cpus: String, 153 154 #[argh(option, long = "platform")] 155 /// num_pci_segments=<num_pci_segments>, iommu_segments=<list_of_segments>, serial_number=<dmi_device_serial_number>, uuid=<dmi_device_uuid>, oem_strings=<list_of_strings> 156 platform: Option<String>, 157 158 #[argh(option, long = "memory", default = "default_memory()")] 159 /// size=<guest_memory_size>, mergeable=on|off, shared=on|off, hugepages=on|off, hugepage_size=<hugepage_size>, hotplug_method=acpi|virtio-mem, hotplug_size=<hotpluggable_memory_size>, hotplugged_size=<hotplugged_memory_size>, prefault=on|off, thp=on|off 160 memory: String, 161 162 #[argh(option, long = "memory-zone")] 163 /// size=<guest_memory_region_size>, file=<backing_file>, shared=on|off, hugepages=on|off, hugepage_size=<hugepage_size>, host_numa_node=<node_id>, id=<zone_identifier>, hotplug_size=<hotpluggable_memory_size>, hotplugged_size=<hotplugged_memory_size>, prefault=on|off 164 memory_zone: Vec<String>, 165 166 #[argh(option, long = "firmware")] 167 /// path to firmware that is loaded in an architectural specific way 168 firmware: Option<String>, 169 170 #[argh(option, long = "kernel")] 171 /// path to kernel or firmware that supports a PVH entry point or architecture equivalent 172 kernel: Option<String>, 173 174 #[argh(option, long = "initramfs")] 175 /// path to initramfs image 176 initramfs: Option<String>, 177 178 #[argh(option, long = "cmdline")] 179 /// kernel command line 180 cmdline: Option<String>, 181 182 #[argh(option, long = "disk")] 183 /// path=<disk_image_path>, readonly=on|off, direct=on|off, iommu=on|off, num_queues=<number_of_queues>, queue_size=<size_of_each_queue>, vhost_user=on|off, socket=<vhost_user_socket_path>, bw_size=<bytes>, bw_one_time_burst=<bytes>, bw_refill_time=<ms>, ops_size=<io_ops>, ops_one_time_burst=<io_ops>, ops_refill_time=<ms>, id=<device_id>, pci_segment=<segment_id> 184 disk: Vec<String>, 185 186 #[argh(option, long = "net")] 187 /// tap=<if_name>, ip=<ip_addr>, mask=<net_mask>, mac=<mac_addr>, fd=<fd1,fd2...>, iommu=on|off, num_queues=<number_of_queues>, queue_size=<size_of_each_queue>, id=<device_id>, vhost_user=<vhost_user_enable>, socket=<vhost_user_socket_path>, vhost_mode=client|server, bw_size=<bytes>, bw_one_time_burst=<bytes>, bw_refill_time=<ms>, ops_size=<io_ops>, ops_one_time_burst=<io_ops>, ops_refill_time=<ms>, pci_segment=<segment_id>, offload_tso=on|off, offload_ufo=on|off, offload_csum=on|off 188 net: Vec<String>, 189 190 #[argh(option, long = "rng", default = "default_rng()")] 191 /// src=<entropy_source_path>, iommu=on|off 192 rng: String, 193 194 #[argh(option, long = "balloon")] 195 /// size=<balloon_size>, deflate_on_oom=on|off, free_page_reporting=on|off 196 balloon: Option<String>, 197 198 #[argh(option, long = "fs")] 199 /// tag=<tag_name>, socket=<socket_path>, num_queues=<number_of_queues>, queue_size=<size_of_each_queue>, id=<device_id>, pci_segment=<segment_id> 200 fs: Vec<String>, 201 202 #[argh(option, long = "pmem")] 203 /// file=<backing_file_path>, size=<persistent_memory_size>, iommu=on|off, discard_writes=on|off, id=<device_id>, pci_segment=<segment_id> 204 pmem: Vec<String>, 205 206 #[argh(option, long = "serial", default = "String::from(\"null\")")] 207 /// off|null|pty|tty|file=/path/to/a/file 208 serial: String, 209 210 #[argh(option, long = "console", default = "String::from(\"tty\")")] 211 /// off|null|pty|tty|file=/path/to/a/file, iommu=on|off 212 console: String, 213 214 #[argh(option, long = "device")] 215 /// path=<device_path>, iommu=on|off, id=<device_id>, pci_segment=<segment_id> 216 device: Vec<String>, 217 218 #[argh(option, long = "user-device")] 219 /// socket=<socket_path>, id=<device_id>, pci_segment=<segment_id> 220 user_device: Vec<String>, 221 222 #[argh(option, long = "vdpa")] 223 /// path=<device_path>, num_queues=<number_of_queues>, iommu=on|off, id=<device_id>, pci_segment=<segment_id> 224 vdpa: Vec<String>, 225 226 #[argh(option, long = "vsock")] 227 /// cid=<context_id>, socket=<socket_path>, iommu=on|off, id=<device_id>, pci_segment=<segment_id> 228 vsock: Option<String>, 229 230 #[argh(switch, long = "pvpanic")] 231 /// enable pvpanic device 232 pvpanic: bool, 233 234 #[argh(option, long = "numa")] 235 /// guest_numa_id=<node_id>, cpus=<cpus_id>, distances=<list_of_distances_to_destination_nodes>, memory_zones=<list_of_memory_zones>, sgx_epc_sections=<list_of_sgx_epc_sections> 236 numa: Vec<String>, 237 238 #[argh(switch, long = "watchdog")] 239 /// enable virtio-watchdog 240 watchdog: bool, 241 242 #[argh(switch, short = 'v')] 243 /// set the level of debugging output 244 verbosity: u8, 245 246 #[argh(option, long = "log-file")] 247 /// path to log file 248 log_file: Option<String>, 249 250 #[argh(option, long = "api-socket")] 251 /// path=<path/to/a/file>|fd=<fd> 252 api_socket: Option<String>, 253 254 #[cfg(feature = "dbus_api")] 255 #[argh(option, long = "dbus-service-name")] 256 /// well known name of the service 257 dbus_name: Option<String>, 258 259 #[cfg(feature = "dbus_api")] 260 #[argh(option, long = "dbus-object-path")] 261 /// object path to serve the dbus interface 262 dbus_path: Option<String>, 263 264 #[cfg(feature = "dbus_api")] 265 #[argh(switch, long = "dbus-system-bus")] 266 /// use the system bus instead of a session bus 267 dbus_system_bus: bool, 268 269 #[argh(option, long = "event-monitor")] 270 /// path=<path/to/a/file>|fd=<fd> 271 event_monitor: Option<String>, 272 273 #[argh(option, long = "restore")] 274 /// source_url=<source_url>, prefault=on|off 275 restore: Option<String>, 276 277 #[argh(option, long = "seccomp", default = "String::from(\"true\")")] 278 /// seccomp configuration (true, false or log) 279 seccomp: String, 280 281 #[argh(option, long = "tpm")] 282 /// socket=<path/to/a/socket> 283 tpm: Option<String>, 284 285 #[cfg(target_arch = "x86_64")] 286 #[argh(option, long = "sgx-epc")] 287 /// id=<epc_section_identifier>, size=<epc_section_size>, prefault=on|off 288 sgx_epc: Vec<String>, 289 290 #[cfg(feature = "guest_debug")] 291 #[argh(option, long = "gdb")] 292 /// path=<path/to/a/file> 293 gdb: Option<String>, 294 295 #[argh(switch, short = 'V', long = "version")] 296 /// print version information 297 version: bool, 298 } 299 300 impl TopLevel { 301 fn to_vm_params(&self) -> config::VmParams<'_> { 302 let cpus = &self.cpus; 303 let memory = &self.memory; 304 let memory_zones = if !self.memory_zone.is_empty() { 305 Some(self.memory_zone.iter().map(|x| x.as_str()).collect()) 306 } else { 307 None 308 }; 309 let rng = &self.rng; 310 let serial = &self.serial; 311 let firmware = self.firmware.as_deref(); 312 let kernel = self.kernel.as_deref(); 313 let initramfs = self.initramfs.as_deref(); 314 let cmdline = self.cmdline.as_deref(); 315 316 let disks = if !self.disk.is_empty() { 317 Some(self.disk.iter().map(|x| x.as_str()).collect()) 318 } else { 319 None 320 }; 321 322 let net = if !self.net.is_empty() { 323 Some(self.net.iter().map(|x| x.as_str()).collect()) 324 } else { 325 None 326 }; 327 328 let console = &self.console; 329 let balloon = self.balloon.as_deref(); 330 let fs = if !self.fs.is_empty() { 331 Some(self.fs.iter().map(|x| x.as_str()).collect()) 332 } else { 333 None 334 }; 335 336 let pmem = if !self.pmem.is_empty() { 337 Some(self.pmem.iter().map(|x| x.as_str()).collect()) 338 } else { 339 None 340 }; 341 let devices = if !self.device.is_empty() { 342 Some(self.device.iter().map(|x| x.as_str()).collect()) 343 } else { 344 None 345 }; 346 let user_devices = if !self.user_device.is_empty() { 347 Some(self.user_device.iter().map(|x| x.as_str()).collect()) 348 } else { 349 None 350 }; 351 let vdpa = if !self.vdpa.is_empty() { 352 Some(self.vdpa.iter().map(|x| x.as_str()).collect()) 353 } else { 354 None 355 }; 356 357 let vsock = self.vsock.as_deref(); 358 359 let pvpanic = self.pvpanic; 360 361 #[cfg(target_arch = "x86_64")] 362 let sgx_epc = if !self.sgx_epc.is_empty() { 363 Some(self.sgx_epc.iter().map(|x| x.as_str()).collect()) 364 } else { 365 None 366 }; 367 368 let numa = if !self.numa.is_empty() { 369 Some(self.numa.iter().map(|x| x.as_str()).collect()) 370 } else { 371 None 372 }; 373 let watchdog = self.watchdog; 374 let platform = self.platform.as_deref(); 375 #[cfg(feature = "guest_debug")] 376 let gdb = self.gdb.is_some(); 377 let tpm = self.tpm.as_deref(); 378 379 config::VmParams { 380 cpus, 381 memory, 382 memory_zones, 383 firmware, 384 kernel, 385 initramfs, 386 cmdline, 387 disks, 388 net, 389 rng, 390 balloon, 391 fs, 392 pmem, 393 serial, 394 console, 395 devices, 396 user_devices, 397 vdpa, 398 vsock, 399 pvpanic, 400 #[cfg(target_arch = "x86_64")] 401 sgx_epc, 402 numa, 403 watchdog, 404 #[cfg(feature = "guest_debug")] 405 gdb, 406 platform, 407 tpm, 408 } 409 } 410 } 411 412 fn start_vmm(toplevel: TopLevel) -> Result<Option<String>, Error> { 413 let log_level = match toplevel.verbosity { 414 0 => LevelFilter::Warn, 415 1 => LevelFilter::Info, 416 2 => LevelFilter::Debug, 417 _ => LevelFilter::Trace, 418 }; 419 420 let log_file: Box<dyn std::io::Write + Send> = if let Some(ref file) = toplevel.log_file { 421 Box::new(std::fs::File::create(std::path::Path::new(file)).map_err(Error::LogFileCreation)?) 422 } else { 423 Box::new(std::io::stderr()) 424 }; 425 426 log::set_boxed_logger(Box::new(Logger { 427 output: Mutex::new(log_file), 428 start: std::time::Instant::now(), 429 })) 430 .map(|()| log::set_max_level(log_level)) 431 .map_err(Error::LoggerSetup)?; 432 433 let (api_socket_path, api_socket_fd) = if let Some(ref socket_config) = toplevel.api_socket { 434 let mut parser = OptionParser::new(); 435 parser.add("path").add("fd"); 436 parser.parse(socket_config).unwrap_or_default(); 437 438 if let Some(fd) = parser.get("fd") { 439 ( 440 None, 441 Some(fd.parse::<RawFd>().map_err(Error::ParsingApiSocket)?), 442 ) 443 } else if let Some(path) = parser.get("path") { 444 (Some(path), None) 445 } else { 446 (toplevel.api_socket.as_ref().map(|s| s.to_string()), None) 447 } 448 } else { 449 (None, None) 450 }; 451 452 let (api_request_sender, api_request_receiver) = channel(); 453 let api_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::CreateApiEventFd)?; 454 455 let api_request_sender_clone = api_request_sender.clone(); 456 let seccomp_action = match &toplevel.seccomp as &str { 457 "true" => SeccompAction::Trap, 458 "false" => SeccompAction::Allow, 459 "log" => SeccompAction::Log, 460 val => { 461 // The user providing an invalid value will be rejected 462 panic!("Invalid parameter {val} for \"--seccomp\" flag"); 463 } 464 }; 465 466 if seccomp_action == SeccompAction::Trap { 467 // SAFETY: We only using signal_hook for managing signals and only execute signal 468 // handler safe functions (writing to stderr) and manipulating signals. 469 unsafe { 470 signal_hook::low_level::register(signal_hook::consts::SIGSYS, || { 471 eprint!( 472 "\n==== Possible seccomp violation ====\n\ 473 Try running with `strace -ff` to identify the cause and open an issue: \ 474 https://github.com/cloud-hypervisor/cloud-hypervisor/issues/new\n" 475 ); 476 signal_hook::low_level::emulate_default_handler(SIGSYS).unwrap(); 477 }) 478 } 479 .map_err(|e| eprintln!("Error adding SIGSYS signal handler: {e}")) 480 .ok(); 481 } 482 483 // Before we start any threads, mask the signals we'll be 484 // installing handlers for, to make sure they only ever run on the 485 // dedicated signal handling thread we'll start in a bit. 486 for sig in &vmm::vm::Vm::HANDLED_SIGNALS { 487 if let Err(e) = block_signal(*sig) { 488 eprintln!("Error blocking signals: {e}"); 489 } 490 } 491 492 for sig in &vmm::Vmm::HANDLED_SIGNALS { 493 if let Err(e) = block_signal(*sig) { 494 eprintln!("Error blocking signals: {e}"); 495 } 496 } 497 498 let hypervisor = hypervisor::new().map_err(Error::CreateHypervisor)?; 499 500 #[cfg(feature = "guest_debug")] 501 let gdb_socket_path = if let Some(ref gdb_config) = toplevel.gdb { 502 let mut parser = OptionParser::new(); 503 parser.add("path"); 504 parser.parse(gdb_config).map_err(Error::ParsingGdb)?; 505 506 if parser.is_set("path") { 507 Some(std::path::PathBuf::from(parser.get("path").unwrap())) 508 } else { 509 return Err(Error::BareGdb); 510 } 511 } else { 512 None 513 }; 514 #[cfg(feature = "guest_debug")] 515 let debug_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::CreateDebugEventFd)?; 516 #[cfg(feature = "guest_debug")] 517 let vm_debug_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::CreateDebugEventFd)?; 518 519 let exit_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::CreateExitEventFd)?; 520 521 #[allow(unused_mut)] 522 let mut event_monitor = toplevel 523 .event_monitor 524 .as_ref() 525 .map(|monitor_config| { 526 let mut parser = OptionParser::new(); 527 parser.add("path").add("fd"); 528 parser 529 .parse(monitor_config) 530 .map_err(Error::ParsingEventMonitor)?; 531 532 if parser.is_set("fd") { 533 let fd = parser 534 .convert("fd") 535 .map_err(Error::ParsingEventMonitor)? 536 .unwrap(); 537 // SAFETY: fd is valid 538 Ok(Some(unsafe { File::from_raw_fd(fd) })) 539 } else if parser.is_set("path") { 540 Ok(Some( 541 std::fs::OpenOptions::new() 542 .write(true) 543 .create(true) 544 .open(parser.get("path").unwrap()) 545 .map_err(Error::EventMonitorIo)?, 546 )) 547 } else { 548 Err(Error::BareEventMonitor) 549 } 550 }) 551 .transpose()? 552 .map(|event_monitor_file| { 553 event_monitor::set_monitor(event_monitor_file).map_err(Error::EventMonitorIo) 554 }) 555 .transpose()?; 556 557 #[cfg(feature = "dbus_api")] 558 let dbus_options = match (&toplevel.dbus_name, &toplevel.dbus_path) { 559 (Some(ref name), Some(ref path)) => { 560 // monitor is either set (file based) or not. 561 // if it's not set, create one without file support. 562 let mut monitor = match event_monitor.take() { 563 Some(monitor) => monitor, 564 None => event_monitor::set_monitor(None).map_err(Error::EventMonitorIo)?, 565 }; 566 let options = DBusApiOptions { 567 service_name: name.to_owned(), 568 object_path: path.to_owned(), 569 system_bus: toplevel.dbus_system_bus, 570 event_monitor_rx: monitor.subscribe(), 571 }; 572 573 event_monitor = Some(monitor); 574 Ok(Some(options)) 575 } 576 (Some(_), None) => Err(Error::MissingDBusObjectPath), 577 (None, Some(_)) => Err(Error::MissingDBusServiceName), 578 (None, None) => Ok(None), 579 }?; 580 581 if let Some(monitor) = event_monitor { 582 vmm::start_event_monitor_thread( 583 monitor, 584 &seccomp_action, 585 hypervisor.hypervisor_type(), 586 exit_evt.try_clone().unwrap(), 587 ) 588 .map_err(Error::EventMonitorThread)?; 589 } 590 591 event!("vmm", "starting"); 592 593 let vmm_thread_handle = vmm::start_vmm_thread( 594 vmm::VmmVersionInfo::new(env!("BUILD_VERSION"), env!("CARGO_PKG_VERSION")), 595 &api_socket_path, 596 api_socket_fd, 597 #[cfg(feature = "dbus_api")] 598 dbus_options, 599 api_evt.try_clone().unwrap(), 600 api_request_sender_clone, 601 api_request_receiver, 602 #[cfg(feature = "guest_debug")] 603 gdb_socket_path, 604 #[cfg(feature = "guest_debug")] 605 debug_evt.try_clone().unwrap(), 606 #[cfg(feature = "guest_debug")] 607 vm_debug_evt.try_clone().unwrap(), 608 exit_evt.try_clone().unwrap(), 609 &seccomp_action, 610 hypervisor, 611 ) 612 .map_err(Error::StartVmmThread)?; 613 614 let r: Result<(), Error> = (|| { 615 let payload_present = toplevel.kernel.is_some() || toplevel.firmware.is_some(); 616 617 if payload_present { 618 let vm_params = toplevel.to_vm_params(); 619 let vm_config = config::VmConfig::parse(vm_params).map_err(Error::ParsingConfig)?; 620 621 // Create and boot the VM based off the VM config we just built. 622 let sender = api_request_sender.clone(); 623 vmm::api::vm_create( 624 api_evt.try_clone().unwrap(), 625 api_request_sender, 626 Arc::new(Mutex::new(vm_config)), 627 ) 628 .map_err(Error::VmCreate)?; 629 vmm::api::vm_boot(api_evt.try_clone().unwrap(), sender).map_err(Error::VmBoot)?; 630 } else if let Some(restore_params) = toplevel.restore { 631 vmm::api::vm_restore( 632 api_evt.try_clone().unwrap(), 633 api_request_sender, 634 Arc::new( 635 config::RestoreConfig::parse(&restore_params).map_err(Error::ParsingRestore)?, 636 ), 637 ) 638 .map_err(Error::VmRestore)?; 639 } 640 641 Ok(()) 642 })(); 643 644 if r.is_err() { 645 if let Err(e) = exit_evt.write(1) { 646 warn!("writing to exit EventFd: {e}"); 647 } 648 } 649 650 vmm_thread_handle 651 .thread_handle 652 .join() 653 .map_err(Error::ThreadJoin)? 654 .map_err(Error::VmmThread)?; 655 656 #[cfg(feature = "dbus_api")] 657 if let Some(chs) = vmm_thread_handle.dbus_shutdown_chs { 658 dbus_api_graceful_shutdown(chs); 659 } 660 661 r.map(|_| api_socket_path) 662 } 663 664 fn main() { 665 #[cfg(all(feature = "tdx", feature = "sev_snp"))] 666 compile_error!("Feature 'tdx' and 'sev_snp' are mutually exclusive."); 667 668 #[cfg(feature = "dhat-heap")] 669 let _profiler = dhat::Profiler::new_heap(); 670 671 // Ensure all created files (.e.g sockets) are only accessible by this user 672 // SAFETY: trivially safe 673 let _ = unsafe { libc::umask(0o077) }; 674 675 let toplevel: TopLevel = argh::from_env(); 676 677 if toplevel.version { 678 println!("{} {}", env!("CARGO_BIN_NAME"), env!("BUILD_VERSION")); 679 return; 680 } 681 682 let exit_code = match start_vmm(toplevel) { 683 Ok(path) => { 684 path.map(|s| std::fs::remove_file(s).ok()); 685 0 686 } 687 Err(e) => { 688 eprintln!("{e}"); 689 1 690 } 691 }; 692 693 #[cfg(feature = "dhat-heap")] 694 drop(_profiler); 695 696 std::process::exit(exit_code); 697 } 698 699 #[cfg(test)] 700 mod unit_tests { 701 use crate::config::HotplugMethod; 702 use crate::TopLevel; 703 use std::path::PathBuf; 704 use vmm::config::{ 705 ConsoleConfig, ConsoleOutputMode, CpuFeatures, CpusConfig, MemoryConfig, PayloadConfig, 706 RngConfig, VmConfig, 707 }; 708 709 // Taken from argh 710 fn cmd<'a>(default: &'a str, path: &'a str) -> &'a str { 711 std::path::Path::new(path) 712 .file_name() 713 .and_then(|s| s.to_str()) 714 .unwrap_or(default) 715 } 716 717 // Some code taken from argh since it does not provide a helper to parse arbitrary strings 718 fn get_vm_config_from_vec(args: &[&str]) -> VmConfig { 719 let strings: Vec<String> = args.iter().map(|x| x.to_string()).collect(); 720 let cmd = cmd(&strings[0], &strings[0]); 721 let strs: Vec<&str> = strings.iter().map(|s| s.as_str()).collect(); 722 let toplevel = <TopLevel as argh::FromArgs>::from_args(&[cmd], &strs[1..]).unwrap_or_else( 723 |early_exit| { 724 std::process::exit(match early_exit.status { 725 Ok(()) => { 726 println!("{}", early_exit.output); 727 0 728 } 729 Err(()) => { 730 eprintln!( 731 "{}\nRun {} --help for more information.", 732 early_exit.output, cmd 733 ); 734 1 735 } 736 }) 737 }, 738 ); 739 740 let vm_params = toplevel.to_vm_params(); 741 742 VmConfig::parse(vm_params).unwrap() 743 } 744 745 fn compare_vm_config_cli_vs_json( 746 cli: &[&str], 747 openapi: &str, 748 equal: bool, 749 ) -> (VmConfig, VmConfig) { 750 let cli_vm_config = get_vm_config_from_vec(cli); 751 let openapi_vm_config: VmConfig = serde_json::from_str(openapi).unwrap(); 752 753 if equal { 754 assert_eq!(cli_vm_config, openapi_vm_config); 755 } else { 756 assert_ne!(cli_vm_config, openapi_vm_config); 757 } 758 759 (cli_vm_config, openapi_vm_config) 760 } 761 762 #[test] 763 fn test_valid_vm_config_default() { 764 let cli = vec!["cloud-hypervisor", "--kernel", "/path/to/kernel"]; 765 let openapi = r#"{ "payload": {"kernel": "/path/to/kernel"} }"#; 766 767 // First we check we get identical VmConfig structures. 768 let (result_vm_config, _) = compare_vm_config_cli_vs_json(&cli, openapi, true); 769 770 // As a second step, we validate all the default values. 771 let expected_vm_config = VmConfig { 772 cpus: CpusConfig { 773 boot_vcpus: 1, 774 max_vcpus: 1, 775 topology: None, 776 kvm_hyperv: false, 777 max_phys_bits: 46, 778 affinity: None, 779 features: CpuFeatures::default(), 780 }, 781 memory: MemoryConfig { 782 size: 536_870_912, 783 mergeable: false, 784 hotplug_method: HotplugMethod::Acpi, 785 hotplug_size: None, 786 hotplugged_size: None, 787 shared: false, 788 hugepages: false, 789 hugepage_size: None, 790 prefault: false, 791 zones: None, 792 thp: true, 793 }, 794 payload: Some(PayloadConfig { 795 kernel: Some(PathBuf::from("/path/to/kernel")), 796 ..Default::default() 797 }), 798 disks: None, 799 net: None, 800 rng: RngConfig { 801 src: PathBuf::from("/dev/urandom"), 802 iommu: false, 803 }, 804 balloon: None, 805 fs: None, 806 pmem: None, 807 serial: ConsoleConfig { 808 file: None, 809 mode: ConsoleOutputMode::Null, 810 iommu: false, 811 }, 812 console: ConsoleConfig { 813 file: None, 814 mode: ConsoleOutputMode::Tty, 815 iommu: false, 816 }, 817 devices: None, 818 user_devices: None, 819 vdpa: None, 820 vsock: None, 821 pvpanic: false, 822 iommu: false, 823 #[cfg(target_arch = "x86_64")] 824 sgx_epc: None, 825 numa: None, 826 watchdog: false, 827 #[cfg(feature = "guest_debug")] 828 gdb: false, 829 platform: None, 830 tpm: None, 831 preserved_fds: None, 832 }; 833 834 assert_eq!(expected_vm_config, result_vm_config); 835 } 836 837 #[test] 838 fn test_valid_vm_config_cpus() { 839 [ 840 ( 841 vec![ 842 "cloud-hypervisor", 843 "--kernel", 844 "/path/to/kernel", 845 "--cpus", 846 "boot=1", 847 ], 848 r#"{ 849 "payload": {"kernel": "/path/to/kernel"}, 850 "cpus": {"boot_vcpus": 1, "max_vcpus": 1} 851 }"#, 852 true, 853 ), 854 ( 855 vec![ 856 "cloud-hypervisor", 857 "--kernel", 858 "/path/to/kernel", 859 "--cpus", 860 "boot=1,max=3", 861 ], 862 r#"{ 863 "payload": {"kernel": "/path/to/kernel"}, 864 "cpus": {"boot_vcpus": 1, "max_vcpus": 3} 865 }"#, 866 true, 867 ), 868 ( 869 vec![ 870 "cloud-hypervisor", 871 "--kernel", 872 "/path/to/kernel", 873 "--cpus", 874 "boot=2,max=4", 875 ], 876 r#"{ 877 "payload": {"kernel": "/path/to/kernel"}, 878 "cpus": {"boot_vcpus": 1, "max_vcpus": 3} 879 }"#, 880 false, 881 ), 882 ] 883 .iter() 884 .for_each(|(cli, openapi, equal)| { 885 compare_vm_config_cli_vs_json(cli, openapi, *equal); 886 }); 887 } 888 889 #[test] 890 fn test_valid_vm_config_memory() { 891 vec![ 892 ( 893 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1073741824"], 894 r#"{ 895 "payload": {"kernel": "/path/to/kernel"}, 896 "memory": {"size": 1073741824} 897 }"#, 898 true, 899 ), 900 ( 901 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1G"], 902 r#"{ 903 "payload": {"kernel": "/path/to/kernel"}, 904 "memory": {"size": 1073741824} 905 }"#, 906 true, 907 ), 908 ( 909 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1G,mergeable=on"], 910 r#"{ 911 "payload": {"kernel": "/path/to/kernel"}, 912 "memory": {"size": 1073741824, "mergeable": true} 913 }"#, 914 true, 915 ), 916 ( 917 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1G,mergeable=off"], 918 r#"{ 919 "payload": {"kernel": "/path/to/kernel"}, 920 "memory": {"size": 1073741824, "mergeable": false} 921 }"#, 922 true, 923 ), 924 ( 925 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1G,mergeable=on"], 926 r#"{ 927 "payload": {"kernel": "/path/to/kernel"}, 928 "memory": {"size": 1073741824, "mergeable": false} 929 }"#, 930 false, 931 ), 932 ( 933 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1G,hotplug_size=1G"], 934 r#"{ 935 "payload": {"kernel": "/path/to/kernel"}, 936 "memory": {"size": 1073741824, "hotplug_method": "Acpi", "hotplug_size": 1073741824} 937 }"#, 938 true, 939 ), 940 ( 941 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1G,hotplug_method=virtio-mem,hotplug_size=1G"], 942 r#"{ 943 "payload": {"kernel": "/path/to/kernel"}, 944 "memory": {"size": 1073741824, "hotplug_method": "VirtioMem", "hotplug_size": 1073741824} 945 }"#, 946 true, 947 ), 948 ] 949 .iter() 950 .for_each(|(cli, openapi, equal)| { 951 compare_vm_config_cli_vs_json(cli, openapi, *equal); 952 }); 953 } 954 955 #[test] 956 fn test_valid_vm_config_kernel() { 957 [( 958 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel"], 959 r#"{ 960 "payload": {"kernel": "/path/to/kernel"} 961 }"#, 962 true, 963 )] 964 .iter() 965 .for_each(|(cli, openapi, equal)| { 966 compare_vm_config_cli_vs_json(cli, openapi, *equal); 967 }); 968 } 969 970 #[test] 971 fn test_valid_vm_config_cmdline() { 972 [( 973 vec![ 974 "cloud-hypervisor", 975 "--kernel", 976 "/path/to/kernel", 977 "--cmdline", 978 "arg1=foo arg2=bar", 979 ], 980 r#"{ 981 "payload": {"kernel": "/path/to/kernel", "cmdline": "arg1=foo arg2=bar"} 982 }"#, 983 true, 984 )] 985 .iter() 986 .for_each(|(cli, openapi, equal)| { 987 compare_vm_config_cli_vs_json(cli, openapi, *equal); 988 }); 989 } 990 991 #[test] 992 fn test_valid_vm_config_disks() { 993 [ 994 ( 995 vec![ 996 "cloud-hypervisor", 997 "--kernel", 998 "/path/to/kernel", 999 "--disk", 1000 "path=/path/to/disk/1", 1001 "--disk", 1002 "path=/path/to/disk/2", 1003 ], 1004 r#"{ 1005 "payload": {"kernel": "/path/to/kernel"}, 1006 "disks": [ 1007 {"path": "/path/to/disk/1"}, 1008 {"path": "/path/to/disk/2"} 1009 ] 1010 }"#, 1011 true, 1012 ), 1013 ( 1014 vec![ 1015 "cloud-hypervisor", 1016 "--kernel", 1017 "/path/to/kernel", 1018 "--disk", 1019 "path=/path/to/disk/1", 1020 "--disk", 1021 "path=/path/to/disk/2", 1022 ], 1023 r#"{ 1024 "payload": {"kernel": "/path/to/kernel"}, 1025 "disks": [ 1026 {"path": "/path/to/disk/1"} 1027 ] 1028 }"#, 1029 false, 1030 ), 1031 ( 1032 vec![ 1033 "cloud-hypervisor", 1034 "--kernel", 1035 "/path/to/kernel", 1036 "--memory", 1037 "shared=true", 1038 "--disk", 1039 "vhost_user=true,socket=/tmp/sock1", 1040 ], 1041 r#"{ 1042 "payload": {"kernel": "/path/to/kernel"}, 1043 "memory" : { "shared": true, "size": 536870912 }, 1044 "disks": [ 1045 {"vhost_user":true, "vhost_socket":"/tmp/sock1"} 1046 ] 1047 }"#, 1048 true, 1049 ), 1050 ( 1051 vec![ 1052 "cloud-hypervisor", 1053 "--kernel", 1054 "/path/to/kernel", 1055 "--memory", 1056 "shared=true", 1057 "--disk", 1058 "vhost_user=true,socket=/tmp/sock1", 1059 ], 1060 r#"{ 1061 "payload": {"kernel": "/path/to/kernel"}, 1062 "memory" : { "shared": true, "size": 536870912 }, 1063 "disks": [ 1064 {"vhost_user":true, "vhost_socket":"/tmp/sock1"} 1065 ] 1066 }"#, 1067 true, 1068 ), 1069 ] 1070 .iter() 1071 .for_each(|(cli, openapi, equal)| { 1072 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1073 }); 1074 } 1075 1076 #[test] 1077 fn test_valid_vm_config_net() { 1078 vec![ 1079 // This test is expected to fail because the default MAC address is 1080 // randomly generated. There's no way we can have twice the same 1081 // default value. 1082 ( 1083 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--net", "mac="], 1084 r#"{ 1085 "payload": {"kernel": "/path/to/kernel"}, 1086 "net": [] 1087 }"#, 1088 false, 1089 ), 1090 ( 1091 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--net", "mac=12:34:56:78:90:ab,host_mac=34:56:78:90:ab:cd"], 1092 r#"{ 1093 "payload": {"kernel": "/path/to/kernel"}, 1094 "net": [ 1095 {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd"} 1096 ] 1097 }"#, 1098 true, 1099 ), 1100 ( 1101 vec![ 1102 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1103 "--net", 1104 "mac=12:34:56:78:90:ab,host_mac=34:56:78:90:ab:cd,tap=tap0", 1105 ], 1106 r#"{ 1107 "payload": {"kernel": "/path/to/kernel"}, 1108 "net": [ 1109 {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd", "tap": "tap0"} 1110 ] 1111 }"#, 1112 true, 1113 ), 1114 ( 1115 vec![ 1116 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1117 "--net", 1118 "mac=12:34:56:78:90:ab,host_mac=34:56:78:90:ab:cd,tap=tap0,ip=1.2.3.4", 1119 ], 1120 r#"{ 1121 "payload": {"kernel": "/path/to/kernel"}, 1122 "net": [ 1123 {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd", "tap": "tap0", "ip": "1.2.3.4"} 1124 ] 1125 }"#, 1126 true, 1127 ), 1128 ( 1129 vec![ 1130 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1131 "--net", 1132 "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", 1133 ], 1134 r#"{ 1135 "payload": {"kernel": "/path/to/kernel"}, 1136 "net": [ 1137 {"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"} 1138 ] 1139 }"#, 1140 true, 1141 ), 1142 ( 1143 vec![ 1144 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1145 "--cpus", "boot=2", 1146 "--net", 1147 "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", 1148 ], 1149 r#"{ 1150 "payload": {"kernel": "/path/to/kernel"}, 1151 "cpus": {"boot_vcpus": 2, "max_vcpus": 2}, 1152 "net": [ 1153 {"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} 1154 ] 1155 }"#, 1156 true, 1157 ), 1158 ( 1159 vec![ 1160 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1161 "--cpus", "boot=2", 1162 "--net", 1163 "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", 1164 ], 1165 r#"{ 1166 "payload": {"kernel": "/path/to/kernel"}, 1167 "cpus": {"boot_vcpus": 2, "max_vcpus": 2}, 1168 "net": [ 1169 {"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} 1170 ] 1171 }"#, 1172 true, 1173 ), 1174 ( 1175 vec![ 1176 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1177 "--net", 1178 "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", 1179 ], 1180 r#"{ 1181 "payload": {"kernel": "/path/to/kernel"}, 1182 "net": [ 1183 {"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"} 1184 ] 1185 }"#, 1186 true, 1187 ), 1188 ( 1189 vec![ 1190 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1191 "--net", 1192 "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", 1193 ], 1194 r#"{ 1195 "payload": {"kernel": "/path/to/kernel"}, 1196 "net": [ 1197 {"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} 1198 ] 1199 }"#, 1200 true, 1201 ), 1202 #[cfg(target_arch = "x86_64")] 1203 ( 1204 vec![ 1205 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1206 "--net", 1207 "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", 1208 ], 1209 r#"{ 1210 "payload": {"kernel": "/path/to/kernel"}, 1211 "net": [ 1212 {"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} 1213 ] 1214 }"#, 1215 false, 1216 ), 1217 #[cfg(target_arch = "x86_64")] 1218 ( 1219 vec![ 1220 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1221 "--net", 1222 "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", 1223 ], 1224 r#"{ 1225 "payload": {"kernel": "/path/to/kernel"}, 1226 "net": [ 1227 {"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} 1228 ], 1229 "iommu": true 1230 }"#, 1231 true, 1232 ), 1233 ( 1234 vec![ 1235 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1236 "--net", 1237 "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", 1238 ], 1239 r#"{ 1240 "payload": {"kernel": "/path/to/kernel"}, 1241 "net": [ 1242 {"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} 1243 ] 1244 }"#, 1245 true, 1246 ), 1247 ( 1248 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"], 1249 r#"{ 1250 "payload": {"kernel": "/path/to/kernel"}, 1251 "memory" : { "shared": true, "size": 536870912 }, 1252 "net": [ 1253 {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd", "vhost_user": true, "vhost_socket": "/tmp/sock"} 1254 ] 1255 }"#, 1256 true, 1257 ), 1258 ] 1259 .iter() 1260 .for_each(|(cli, openapi, equal)| { 1261 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1262 }); 1263 } 1264 1265 #[test] 1266 fn test_valid_vm_config_rng() { 1267 [( 1268 vec![ 1269 "cloud-hypervisor", 1270 "--kernel", 1271 "/path/to/kernel", 1272 "--rng", 1273 "src=/path/to/entropy/source", 1274 ], 1275 r#"{ 1276 "payload": {"kernel": "/path/to/kernel"}, 1277 "rng": {"src": "/path/to/entropy/source"} 1278 }"#, 1279 true, 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_fs() { 1289 [( 1290 vec![ 1291 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1292 "--memory", "shared=true", 1293 "--fs", 1294 "tag=virtiofs1,socket=/path/to/sock1", 1295 "--fs", 1296 "tag=virtiofs2,socket=/path/to/sock2", 1297 ], 1298 r#"{ 1299 "payload": {"kernel": "/path/to/kernel"}, 1300 "memory" : { "shared": true, "size": 536870912 }, 1301 "fs": [ 1302 {"tag": "virtiofs1", "socket": "/path/to/sock1"}, 1303 {"tag": "virtiofs2", "socket": "/path/to/sock2"} 1304 ] 1305 }"#, 1306 true, 1307 ), 1308 ( 1309 vec![ 1310 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1311 "--memory", "shared=true", 1312 "--fs", 1313 "tag=virtiofs1,socket=/path/to/sock1", 1314 "--fs", 1315 "tag=virtiofs2,socket=/path/to/sock2", 1316 ], 1317 r#"{ 1318 "payload": {"kernel": "/path/to/kernel"}, 1319 "memory" : { "shared": true, "size": 536870912 }, 1320 "fs": [ 1321 {"tag": "virtiofs1", "socket": "/path/to/sock1"} 1322 ] 1323 }"#, 1324 false, 1325 ), 1326 ( 1327 vec![ 1328 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1329 "--memory", "shared=true", "--cpus", "boot=4", 1330 "--fs", 1331 "tag=virtiofs1,socket=/path/to/sock1,num_queues=4", 1332 ], 1333 r#"{ 1334 "payload": {"kernel": "/path/to/kernel"}, 1335 "memory" : { "shared": true, "size": 536870912 }, 1336 "cpus": {"boot_vcpus": 4, "max_vcpus": 4}, 1337 "fs": [ 1338 {"tag": "virtiofs1", "socket": "/path/to/sock1", "num_queues": 4} 1339 ] 1340 }"#, 1341 true, 1342 ), 1343 ( 1344 vec![ 1345 "cloud-hypervisor", "--kernel", "/path/to/kernel", 1346 "--memory", "shared=true", "--cpus", "boot=4", 1347 "--fs", 1348 "tag=virtiofs1,socket=/path/to/sock1,num_queues=4,queue_size=128" 1349 ], 1350 r#"{ 1351 "payload": {"kernel": "/path/to/kernel"}, 1352 "memory" : { "shared": true, "size": 536870912 }, 1353 "cpus": {"boot_vcpus": 4, "max_vcpus": 4}, 1354 "fs": [ 1355 {"tag": "virtiofs1", "socket": "/path/to/sock1", "num_queues": 4, "queue_size": 128} 1356 ] 1357 }"#, 1358 true, 1359 )] 1360 .iter() 1361 .for_each(|(cli, openapi, equal)| { 1362 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1363 }); 1364 } 1365 1366 #[test] 1367 fn test_valid_vm_config_pmem() { 1368 [ 1369 ( 1370 vec![ 1371 "cloud-hypervisor", 1372 "--kernel", 1373 "/path/to/kernel", 1374 "--pmem", 1375 "file=/path/to/img/1,size=1G", 1376 "--pmem", 1377 "file=/path/to/img/2,size=2G", 1378 ], 1379 r#"{ 1380 "payload": {"kernel": "/path/to/kernel"}, 1381 "pmem": [ 1382 {"file": "/path/to/img/1", "size": 1073741824}, 1383 {"file": "/path/to/img/2", "size": 2147483648} 1384 ] 1385 }"#, 1386 true, 1387 ), 1388 #[cfg(target_arch = "x86_64")] 1389 ( 1390 vec![ 1391 "cloud-hypervisor", 1392 "--kernel", 1393 "/path/to/kernel", 1394 "--pmem", 1395 "file=/path/to/img/1,size=1G,iommu=on", 1396 ], 1397 r#"{ 1398 "payload": {"kernel": "/path/to/kernel"}, 1399 "pmem": [ 1400 {"file": "/path/to/img/1", "size": 1073741824, "iommu": true} 1401 ], 1402 "iommu": true 1403 }"#, 1404 true, 1405 ), 1406 #[cfg(target_arch = "x86_64")] 1407 ( 1408 vec![ 1409 "cloud-hypervisor", 1410 "--kernel", 1411 "/path/to/kernel", 1412 "--pmem", 1413 "file=/path/to/img/1,size=1G,iommu=on", 1414 ], 1415 r#"{ 1416 "payload": {"kernel": "/path/to/kernel"}, 1417 "pmem": [ 1418 {"file": "/path/to/img/1", "size": 1073741824, "iommu": true} 1419 ] 1420 }"#, 1421 false, 1422 ), 1423 ] 1424 .iter() 1425 .for_each(|(cli, openapi, equal)| { 1426 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1427 }); 1428 } 1429 1430 #[test] 1431 fn test_valid_vm_config_serial_console() { 1432 [ 1433 ( 1434 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel"], 1435 r#"{ 1436 "payload": {"kernel": "/path/to/kernel"}, 1437 "serial": {"mode": "Null"}, 1438 "console": {"mode": "Tty"} 1439 }"#, 1440 true, 1441 ), 1442 ( 1443 vec![ 1444 "cloud-hypervisor", 1445 "--kernel", 1446 "/path/to/kernel", 1447 "--serial", 1448 "null", 1449 "--console", 1450 "tty", 1451 ], 1452 r#"{ 1453 "payload": {"kernel": "/path/to/kernel"} 1454 }"#, 1455 true, 1456 ), 1457 ( 1458 vec![ 1459 "cloud-hypervisor", 1460 "--kernel", 1461 "/path/to/kernel", 1462 "--serial", 1463 "tty", 1464 "--console", 1465 "off", 1466 ], 1467 r#"{ 1468 "payload": {"kernel": "/path/to/kernel"}, 1469 "serial": {"mode": "Tty"}, 1470 "console": {"mode": "Off"} 1471 }"#, 1472 true, 1473 ), 1474 ] 1475 .iter() 1476 .for_each(|(cli, openapi, equal)| { 1477 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1478 }); 1479 } 1480 1481 #[test] 1482 fn test_valid_vm_config_serial_pty_console_pty() { 1483 [ 1484 ( 1485 vec!["cloud-hypervisor", "--kernel", "/path/to/kernel"], 1486 r#"{ 1487 "payload": {"kernel": "/path/to/kernel"}, 1488 "serial": {"mode": "Null"}, 1489 "console": {"mode": "Tty"} 1490 }"#, 1491 true, 1492 ), 1493 ( 1494 vec![ 1495 "cloud-hypervisor", 1496 "--kernel", 1497 "/path/to/kernel", 1498 "--serial", 1499 "null", 1500 "--console", 1501 "tty", 1502 ], 1503 r#"{ 1504 "payload": {"kernel": "/path/to/kernel"} 1505 }"#, 1506 true, 1507 ), 1508 ( 1509 vec![ 1510 "cloud-hypervisor", 1511 "--kernel", 1512 "/path/to/kernel", 1513 "--serial", 1514 "pty", 1515 "--console", 1516 "pty", 1517 ], 1518 r#"{ 1519 "payload": {"kernel": "/path/to/kernel"}, 1520 "serial": {"mode": "Pty"}, 1521 "console": {"mode": "Pty"} 1522 }"#, 1523 true, 1524 ), 1525 ] 1526 .iter() 1527 .for_each(|(cli, openapi, equal)| { 1528 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1529 }); 1530 } 1531 1532 #[test] 1533 #[cfg(target_arch = "x86_64")] 1534 fn test_valid_vm_config_devices() { 1535 vec![ 1536 ( 1537 vec![ 1538 "cloud-hypervisor", 1539 "--kernel", 1540 "/path/to/kernel", 1541 "--device", 1542 "path=/path/to/device/1", 1543 "--device", 1544 "path=/path/to/device/2", 1545 ], 1546 r#"{ 1547 "payload": {"kernel": "/path/to/kernel"}, 1548 "devices": [ 1549 {"path": "/path/to/device/1"}, 1550 {"path": "/path/to/device/2"} 1551 ] 1552 }"#, 1553 true, 1554 ), 1555 ( 1556 vec![ 1557 "cloud-hypervisor", 1558 "--kernel", 1559 "/path/to/kernel", 1560 "--device", 1561 "path=/path/to/device/1", 1562 "--device", 1563 "path=/path/to/device/2", 1564 ], 1565 r#"{ 1566 "payload": {"kernel": "/path/to/kernel"}, 1567 "devices": [ 1568 {"path": "/path/to/device/1"} 1569 ] 1570 }"#, 1571 false, 1572 ), 1573 ( 1574 vec![ 1575 "cloud-hypervisor", 1576 "--kernel", 1577 "/path/to/kernel", 1578 "--device", 1579 "path=/path/to/device,iommu=on", 1580 ], 1581 r#"{ 1582 "payload": {"kernel": "/path/to/kernel"}, 1583 "devices": [ 1584 {"path": "/path/to/device", "iommu": true} 1585 ], 1586 "iommu": true 1587 }"#, 1588 true, 1589 ), 1590 ( 1591 vec![ 1592 "cloud-hypervisor", 1593 "--kernel", 1594 "/path/to/kernel", 1595 "--device", 1596 "path=/path/to/device,iommu=on", 1597 ], 1598 r#"{ 1599 "payload": {"kernel": "/path/to/kernel"}, 1600 "devices": [ 1601 {"path": "/path/to/device", "iommu": true} 1602 ] 1603 }"#, 1604 false, 1605 ), 1606 ( 1607 vec![ 1608 "cloud-hypervisor", 1609 "--kernel", 1610 "/path/to/kernel", 1611 "--device", 1612 "path=/path/to/device,iommu=off", 1613 ], 1614 r#"{ 1615 "payload": {"kernel": "/path/to/kernel"}, 1616 "devices": [ 1617 {"path": "/path/to/device", "iommu": false} 1618 ] 1619 }"#, 1620 true, 1621 ), 1622 ] 1623 .iter() 1624 .for_each(|(cli, openapi, equal)| { 1625 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1626 }); 1627 } 1628 1629 #[test] 1630 fn test_valid_vm_config_vdpa() { 1631 [ 1632 ( 1633 vec![ 1634 "cloud-hypervisor", 1635 "--kernel", 1636 "/path/to/kernel", 1637 "--vdpa", 1638 "path=/path/to/device/1", 1639 "--vdpa", 1640 "path=/path/to/device/2,num_queues=2", 1641 ], 1642 r#"{ 1643 "payload": {"kernel": "/path/to/kernel"}, 1644 "vdpa": [ 1645 {"path": "/path/to/device/1", "num_queues": 1}, 1646 {"path": "/path/to/device/2", "num_queues": 2} 1647 ] 1648 }"#, 1649 true, 1650 ), 1651 ( 1652 vec![ 1653 "cloud-hypervisor", 1654 "--kernel", 1655 "/path/to/kernel", 1656 "--vdpa", 1657 "path=/path/to/device/1", 1658 "--vdpa", 1659 "path=/path/to/device/2", 1660 ], 1661 r#"{ 1662 "payload": {"kernel": "/path/to/kernel"}, 1663 "vdpa": [ 1664 {"path": "/path/to/device/1"} 1665 ] 1666 }"#, 1667 false, 1668 ), 1669 ] 1670 .iter() 1671 .for_each(|(cli, openapi, equal)| { 1672 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1673 }); 1674 } 1675 1676 #[test] 1677 fn test_valid_vm_config_vsock() { 1678 [ 1679 ( 1680 vec![ 1681 "cloud-hypervisor", 1682 "--kernel", 1683 "/path/to/kernel", 1684 "--vsock", 1685 "cid=123,socket=/path/to/sock/1", 1686 ], 1687 r#"{ 1688 "payload": {"kernel": "/path/to/kernel"}, 1689 "vsock": {"cid": 123, "socket": "/path/to/sock/1"} 1690 }"#, 1691 true, 1692 ), 1693 ( 1694 vec![ 1695 "cloud-hypervisor", 1696 "--kernel", 1697 "/path/to/kernel", 1698 "--vsock", 1699 "cid=124,socket=/path/to/sock/1", 1700 ], 1701 r#"{ 1702 "payload": {"kernel": "/path/to/kernel"}, 1703 "vsock": {"cid": 123, "socket": "/path/to/sock/1"} 1704 }"#, 1705 false, 1706 ), 1707 #[cfg(target_arch = "x86_64")] 1708 ( 1709 vec![ 1710 "cloud-hypervisor", 1711 "--kernel", 1712 "/path/to/kernel", 1713 "--vsock", 1714 "cid=123,socket=/path/to/sock/1,iommu=on", 1715 ], 1716 r#"{ 1717 "payload": {"kernel": "/path/to/kernel"}, 1718 "vsock": {"cid": 123, "socket": "/path/to/sock/1", "iommu": true}, 1719 "iommu": true 1720 }"#, 1721 true, 1722 ), 1723 #[cfg(target_arch = "x86_64")] 1724 ( 1725 vec![ 1726 "cloud-hypervisor", 1727 "--kernel", 1728 "/path/to/kernel", 1729 "--vsock", 1730 "cid=123,socket=/path/to/sock/1,iommu=on", 1731 ], 1732 r#"{ 1733 "payload": {"kernel": "/path/to/kernel"}, 1734 "vsock": {"cid": 123, "socket": "/path/to/sock/1", "iommu": true} 1735 }"#, 1736 false, 1737 ), 1738 ( 1739 vec![ 1740 "cloud-hypervisor", 1741 "--kernel", 1742 "/path/to/kernel", 1743 "--vsock", 1744 "cid=123,socket=/path/to/sock/1,iommu=off", 1745 ], 1746 r#"{ 1747 "payload": {"kernel": "/path/to/kernel"}, 1748 "vsock": {"cid": 123, "socket": "/path/to/sock/1", "iommu": false} 1749 }"#, 1750 true, 1751 ), 1752 ] 1753 .iter() 1754 .for_each(|(cli, openapi, equal)| { 1755 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1756 }); 1757 } 1758 1759 #[test] 1760 fn test_valid_vm_config_tpm_socket() { 1761 [( 1762 vec![ 1763 "cloud-hypervisor", 1764 "--kernel", 1765 "/path/to/kernel", 1766 "--tpm", 1767 "socket=/path/to/tpm/sock", 1768 ], 1769 r#"{ 1770 "payload": {"kernel": "/path/to/kernel"}, 1771 "tpm": {"socket": "/path/to/tpm/sock"} 1772 }"#, 1773 true, 1774 )] 1775 .iter() 1776 .for_each(|(cli, openapi, equal)| { 1777 compare_vm_config_cli_vs_json(cli, openapi, *equal); 1778 }); 1779 } 1780 } 1781