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